Turn CLI commands into objects, self register subcommands (#935)

Usages of `RootCommand` no longer need to initialize a new instance, nor register subcommands.
This commit is contained in:
Daniel Chao
2025-02-05 09:18:23 -08:00
committed by GitHub
parent aadcccd0fc
commit 9784cd7265
15 changed files with 229 additions and 253 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -17,26 +17,10 @@
package org.pkl.cli package org.pkl.cli
import com.github.ajalt.clikt.core.subcommands
import org.pkl.cli.commands.* import org.pkl.cli.commands.*
import org.pkl.commons.cli.cliMain import org.pkl.commons.cli.cliMain
import org.pkl.core.Release
/** Main method of the Pkl CLI (command-line evaluator and REPL). */ /** Main method of the Pkl CLI (command-line evaluator and REPL). */
internal fun main(args: Array<String>) { internal fun main(args: Array<String>) {
cliMain { cliMain { RootCommand.main(args) }
val version = Release.current().versionInfo()
val helpLink = "${Release.current().documentation().homepage()}pkl-cli/index.html#usage"
RootCommand("pkl", version, helpLink)
.subcommands(
EvalCommand(helpLink),
ReplCommand(helpLink),
ServerCommand(helpLink),
TestCommand(helpLink),
ProjectCommand(helpLink),
DownloadPackageCommand(helpLink),
AnalyzeCommand(helpLink),
)
.main(args)
}
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -25,42 +25,40 @@ import org.pkl.cli.CliImportAnalyzerOptions
import org.pkl.commons.cli.commands.ModulesCommand import org.pkl.commons.cli.commands.ModulesCommand
import org.pkl.commons.cli.commands.single import org.pkl.commons.cli.commands.single
class AnalyzeCommand(helpLink: String) : object AnalyzeCommand :
NoOpCliktCommand( NoOpCliktCommand(
name = "analyze", name = "analyze",
help = "Commands related to static analysis", help = "Commands related to static analysis",
epilog = "For more information, visit $helpLink" epilog = "For more information, visit $helpLink",
) { ) {
init { init {
subcommands(AnalyzeImportsCommand(helpLink)) subcommands(AnalyzeImportsCommand)
} }
}
companion object {
class AnalyzeImportsCommand(helpLink: String) : object AnalyzeImportsCommand :
ModulesCommand( ModulesCommand(
name = "imports", name = "imports",
helpLink = helpLink, helpLink = helpLink,
help = "Prints the graph of modules imported by the input module(s)." help = "Prints the graph of modules imported by the input module(s).",
) { ) {
private val outputPath: Path? by private val outputPath: Path? by
option( option(
names = arrayOf("-o", "--output-path"), names = arrayOf("-o", "--output-path"),
metavar = "<path>", metavar = "<path>",
help = "File path where the output file is placed." help = "File path where the output file is placed.",
) )
.path() .path()
.single() .single()
override fun run() { override fun run() {
val options = val options =
CliImportAnalyzerOptions( CliImportAnalyzerOptions(
base = baseOptions.baseOptions(modules, projectOptions), base = baseOptions.baseOptions(modules, projectOptions),
outputFormat = baseOptions.format, outputFormat = baseOptions.format,
outputPath = outputPath outputPath = outputPath,
) )
CliImportAnalyzer(options).run() CliImportAnalyzer(options).run()
}
}
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.single import org.pkl.commons.cli.commands.single
import org.pkl.core.packages.PackageUri import org.pkl.core.packages.PackageUri
class DownloadPackageCommand(helpLink: String) : object DownloadPackageCommand :
BaseCommand( BaseCommand(
name = "download-package", name = "download-package",
helpLink = helpLink, helpLink = helpLink,
@@ -44,7 +44,7 @@ class DownloadPackageCommand(helpLink: String) :
$ pkl download-package package://example.com/package1@1.0.0 package://example.com/package2@1.0.0 $ pkl download-package package://example.com/package1@1.0.0 package://example.com/package2@1.0.0
``` ```
""" """
.trimIndent() .trimIndent(),
) { ) {
private val projectOptions by ProjectOptions() private val projectOptions by ProjectOptions()
@@ -56,7 +56,7 @@ class DownloadPackageCommand(helpLink: String) :
private val noTransitive: Boolean by private val noTransitive: Boolean by
option( option(
names = arrayOf("--no-transitive"), names = arrayOf("--no-transitive"),
help = "Skip downloading transitive dependencies of a package" help = "Skip downloading transitive dependencies of a package",
) )
.single() .single()
.flag() .flag()
@@ -65,7 +65,7 @@ class DownloadPackageCommand(helpLink: String) :
CliPackageDownloader( CliPackageDownloader(
baseOptions.baseOptions(emptyList(), projectOptions), baseOptions.baseOptions(emptyList(), projectOptions),
packageUris, packageUris,
noTransitive noTransitive,
) )
.run() .run()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -24,17 +24,13 @@ import org.pkl.cli.CliEvaluatorOptions
import org.pkl.commons.cli.commands.ModulesCommand import org.pkl.commons.cli.commands.ModulesCommand
import org.pkl.commons.cli.commands.single import org.pkl.commons.cli.commands.single
class EvalCommand(helpLink: String) : object EvalCommand :
ModulesCommand( ModulesCommand(name = "eval", help = "Render pkl module(s)", helpLink = helpLink) {
name = "eval",
help = "Render pkl module(s)",
helpLink = helpLink,
) {
private val outputPath: String? by private val outputPath: String? by
option( option(
names = arrayOf("-o", "--output-path"), names = arrayOf("-o", "--output-path"),
metavar = "<path>", metavar = "<path>",
help = "File path where the output file is placed." help = "File path where the output file is placed.",
) )
.single() .single()
@@ -43,7 +39,7 @@ class EvalCommand(helpLink: String) :
names = arrayOf("--module-output-separator"), names = arrayOf("--module-output-separator"),
metavar = "<string>", metavar = "<string>",
help = help =
"Separator to use when multiple module outputs are written to the same file. (default: ---)" "Separator to use when multiple module outputs are written to the same file. (default: ---)",
) )
.single() .single()
.default("---") .default("---")
@@ -52,7 +48,7 @@ class EvalCommand(helpLink: String) :
option( option(
names = arrayOf("-x", "--expression"), names = arrayOf("-x", "--expression"),
metavar = "<expression>", metavar = "<expression>",
help = "Expression to be evaluated within the module." help = "Expression to be evaluated within the module.",
) )
.single() .single()
@@ -60,7 +56,7 @@ class EvalCommand(helpLink: String) :
option( option(
names = arrayOf("-m", "--multiple-file-output-path"), names = arrayOf("-m", "--multiple-file-output-path"),
metavar = "<path>", metavar = "<path>",
help = "Directory where a module's multiple file output is placed." help = "Directory where a module's multiple file output is placed.",
) )
.single() .single()
.validate { .validate {
@@ -81,7 +77,7 @@ class EvalCommand(helpLink: String) :
outputFormat = baseOptions.format, outputFormat = baseOptions.format,
moduleOutputSeparator = moduleOutputSeparator, moduleOutputSeparator = moduleOutputSeparator,
multipleFileOutputPath = multipleFileOutputPath, multipleFileOutputPath = multipleFileOutputPath,
expression = expression ?: CliEvaluatorOptions.defaults.expression expression = expression ?: CliEvaluatorOptions.defaults.expression,
) )
CliEvaluator(options).run() CliEvaluator(options).run()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -31,119 +31,117 @@ import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.TestOptions import org.pkl.commons.cli.commands.TestOptions
import org.pkl.commons.cli.commands.single import org.pkl.commons.cli.commands.single
class ProjectCommand(helpLink: String) : private const val NEWLINE = '\u0085'
object ProjectCommand :
NoOpCliktCommand( NoOpCliktCommand(
name = "project", name = "project",
help = "Run commands related to projects", help = "Run commands related to projects",
epilog = "For more information, visit $helpLink" epilog = "For more information, visit $helpLink",
) { ) {
init { init {
subcommands(ResolveCommand(helpLink), PackageCommand(helpLink)) subcommands(ResolveCommand, PackageCommand)
} }
}
companion object {
class ResolveCommand(helpLink: String) : object ResolveCommand :
BaseCommand( BaseCommand(
name = "resolve", name = "resolve",
helpLink = helpLink, helpLink = helpLink,
help = help =
""" """
Resolve dependencies for project(s) Resolve dependencies for project(s)
This command takes the `dependencies` of `PklProject`s, and writes the This command takes the `dependencies` of `PklProject`s, and writes the
resolved versions to `PklProject.deps.json` files. resolved versions to `PklProject.deps.json` files.
Examples: Examples:
``` ```
# Search the current working directory for a project, and resolve its dependencies. # Search the current working directory for a project, and resolve its dependencies.
$ pkl project resolve $ pkl project resolve
# Resolve dependencies for all projects within the `packages/` directory. # Resolve dependencies for all projects within the `packages/` directory.
$ pkl project resolve packages/*/ $ pkl project resolve packages/*/
``` ```
""", """,
) { ) {
private val projectDirs: List<Path> by private val projectDirs: List<Path> by
argument("<dir>", "The project directories to resolve dependencies for").path().multiple() argument("<dir>", "The project directories to resolve dependencies for").path().multiple()
override fun run() { override fun run() {
CliProjectResolver(baseOptions.baseOptions(emptyList()), projectDirs).run() CliProjectResolver(baseOptions.baseOptions(emptyList()), projectDirs).run()
} }
} }
private const val NEWLINE = '\u0085' object PackageCommand :
BaseCommand(
class PackageCommand(helpLink: String) : name = "package",
BaseCommand( helpLink = helpLink,
name = "package", help =
helpLink = helpLink, """
help = Verify package(s), and prepare package artifacts to be published.
"""
Verify package(s), and prepare package artifacts to be published. This command runs a project's api tests, as defined by `apiTests` in `PklProject`.
Additionally, it verifies that all imports resolve to paths that are local to the project.
This command runs a project's api tests, as defined by `apiTests` in `PklProject`.
Additionally, it verifies that all imports resolve to paths that are local to the project. Finally, this command writes the following artifacts into the output directory specified by the output path.
Finally, this command writes the following artifacts into the output directory specified by the output path. - `name@version` - dependency metadata$NEWLINE
- `name@version.sha256` - dependency metadata's SHA-256 checksum$NEWLINE
- `name@version` - dependency metadata$NEWLINE - `name@version.zip` - package archive$NEWLINE
- `name@version.sha256` - dependency metadata's SHA-256 checksum$NEWLINE - `name@version.zip.sha256` - package archive's SHA-256 checksum
- `name@version.zip` - package archive$NEWLINE
- `name@version.zip.sha256` - package archive's SHA-256 checksum The output path option accepts the following placeholders:
The output path option accepts the following placeholders: - %{name}: The display name of the package$NEWLINE
- %{version}: The version of the package
- %{name}: The display name of the package$NEWLINE
- %{version}: The version of the package If a project has local project dependencies, the depended upon project directories must also
be included as arguments to this command.
If a project has local project dependencies, the depended upon project directories must also
be included as arguments to this command. Examples:
Examples: ```
# Search the current working directory for a project, and package it.
``` $ pkl project package
# Search the current working directory for a project, and package it.
$ pkl project package # Package all projects within the `packages/` directory.
$ pkl project package packages/*/
# Package all projects within the `packages/` directory. ```
$ pkl project package packages/*/ """
``` .trimIndent(),
""" ) {
.trimIndent(), private val testOptions by TestOptions()
) {
private val testOptions by TestOptions() private val projectDirs: List<Path> by
argument("<dir>", "The project directories to package").path().multiple()
private val projectDirs: List<Path> by
argument("<dir>", "The project directories to package").path().multiple() private val outputPath: String by
option(
private val outputPath: String by names = arrayOf("--output-path"),
option( help = "The directory to write artifacts to",
names = arrayOf("--output-path"), metavar = "<path>",
help = "The directory to write artifacts to", )
metavar = "<path>" .single()
) .default(".out/%{name}@%{version}")
.single()
.default(".out/%{name}@%{version}") private val skipPublishCheck: Boolean by
option(
private val skipPublishCheck: Boolean by names = arrayOf("--skip-publish-check"),
option( help = "Skip checking if a package has already been published with different contents",
names = arrayOf("--skip-publish-check"), )
help = "Skip checking if a package has already been published with different contents", .single()
) .flag()
.single()
.flag() override fun run() {
CliProjectPackager(
override fun run() { baseOptions.baseOptions(emptyList()),
CliProjectPackager( projectDirs,
baseOptions.baseOptions(emptyList()), testOptions.cliTestOptions,
projectDirs, outputPath,
testOptions.cliTestOptions, skipPublishCheck,
outputPath, )
skipPublishCheck .run()
)
.run()
}
}
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -21,12 +21,8 @@ import org.pkl.cli.CliRepl
import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.ProjectOptions import org.pkl.commons.cli.commands.ProjectOptions
class ReplCommand(helpLink: String) : object ReplCommand :
BaseCommand( BaseCommand(name = "repl", help = "Start a REPL session", helpLink = helpLink) {
name = "repl",
help = "Start a REPL session",
helpLink = helpLink,
) {
private val projectOptions by ProjectOptions() private val projectOptions by ProjectOptions()
override fun run() { override fun run() {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -17,16 +17,20 @@ package org.pkl.cli.commands
import com.github.ajalt.clikt.core.NoOpCliktCommand import com.github.ajalt.clikt.core.NoOpCliktCommand
import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.core.context
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.versionOption import com.github.ajalt.clikt.parameters.options.versionOption
import org.pkl.core.Release
class RootCommand(name: String, version: String, helpLink: String) : internal val helpLink = "${Release.current().documentation.homepage}pkl-cli/index.html#usage"
object RootCommand :
NoOpCliktCommand( NoOpCliktCommand(
name = name, name = "pkl",
printHelpOnEmptyArgs = true, printHelpOnEmptyArgs = true,
epilog = "For more information, visit $helpLink", epilog = "For more information, visit $helpLink",
) { ) {
init { init {
versionOption(version, names = setOf("-v", "--version"), message = { it }) versionOption(Release.current().versionInfo, names = setOf("-v", "--version"), message = { it })
context { context {
correctionSuggestor = { given, possible -> correctionSuggestor = { given, possible ->
@@ -35,5 +39,15 @@ class RootCommand(name: String, version: String, helpLink: String) :
} else possible } else possible
} }
} }
subcommands(
EvalCommand,
ReplCommand,
ServerCommand,
TestCommand,
ProjectCommand,
DownloadPackageCommand,
AnalyzeCommand,
)
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -19,11 +19,11 @@ import com.github.ajalt.clikt.core.CliktCommand
import org.pkl.cli.CliServer import org.pkl.cli.CliServer
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
class ServerCommand(helpLink: String) : object ServerCommand :
CliktCommand( CliktCommand(
name = "server", name = "server",
help = "Run as a server that communicates over standard input/output", help = "Run as a server that communicates over standard input/output",
epilog = "For more information, visit $helpLink" epilog = "For more information, visit $helpLink",
) { ) {
override fun run() { override fun run() {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import org.pkl.commons.cli.commands.BaseOptions
import org.pkl.commons.cli.commands.ProjectOptions import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.TestOptions import org.pkl.commons.cli.commands.TestOptions
class TestCommand(helpLink: String) : object TestCommand :
BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) { BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) {
val modules: List<URI> by val modules: List<URI> by
argument(name = "<modules>", help = "Module paths or URIs to evaluate.") argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
@@ -40,7 +40,7 @@ class TestCommand(helpLink: String) :
override fun run() { override fun run() {
CliTestRunner( CliTestRunner(
options = baseOptions.baseOptions(modules, projectOptions), options = baseOptions.baseOptions(modules, projectOptions),
testOptions = testOptions.cliTestOptions testOptions = testOptions.cliTestOptions,
) )
.run() .run()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.pkl.cli package org.pkl.cli
import com.github.ajalt.clikt.core.BadParameterValue import com.github.ajalt.clikt.core.BadParameterValue
import com.github.ajalt.clikt.core.subcommands
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.createDirectory import kotlin.io.path.createDirectory
import kotlin.io.path.createSymbolicLinkPointingTo import kotlin.io.path.createSymbolicLinkPointingTo
@@ -27,35 +26,34 @@ import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.pkl.cli.commands.AnalyzeCommand
import org.pkl.cli.commands.EvalCommand import org.pkl.cli.commands.EvalCommand
import org.pkl.cli.commands.RootCommand import org.pkl.cli.commands.RootCommand
import org.pkl.commons.writeString import org.pkl.commons.writeString
import org.pkl.core.Release
class CliMainTest { class CliMainTest {
private val evalCmd = EvalCommand("")
private val analyzeCommand = AnalyzeCommand("")
private val cmd = RootCommand("pkl", "pkl version 1", "").subcommands(evalCmd, analyzeCommand)
@Test @Test
fun `duplicate CLI option produces meaningful error message`(@TempDir tempDir: Path) { fun `duplicate CLI option produces meaningful error message`(@TempDir tempDir: Path) {
val inputFile = tempDir.resolve("test.pkl").writeString("").toString() val inputFile = tempDir.resolve("test.pkl").writeString("").toString()
assertThatCode { assertThatCode {
cmd.parse(arrayOf("eval", "--output-path", "path1", "--output-path", "path2", inputFile)) RootCommand.parse(
arrayOf("eval", "--output-path", "path1", "--output-path", "path2", inputFile)
)
} }
.hasMessage("Invalid value for \"--output-path\": Option cannot be repeated") .hasMessage("Invalid value for \"--output-path\": Option cannot be repeated")
assertThatCode { assertThatCode {
cmd.parse(arrayOf("eval", "-o", "path1", "--output-path", "path2", inputFile)) RootCommand.parse(arrayOf("eval", "-o", "path1", "--output-path", "path2", inputFile))
} }
.hasMessage("Invalid value for \"--output-path\": Option cannot be repeated") .hasMessage("Invalid value for \"--output-path\": Option cannot be repeated")
} }
@Test @Test
fun `eval requires at least one file`() { fun `eval requires at least one file`() {
assertThatCode { cmd.parse(arrayOf("eval")) }.hasMessage("""Missing argument "<modules>"""") assertThatCode { RootCommand.parse(arrayOf("eval")) }
.hasMessage("""Missing argument "<modules>"""")
} }
// Can't reliably create symlinks on Windows. // Can't reliably create symlinks on Windows.
@@ -76,7 +74,7 @@ class CliMainTest {
val inputFile = tempDir.resolve("test.pkl").writeString(code).toString() val inputFile = tempDir.resolve("test.pkl").writeString(code).toString()
val outputFile = makeSymdir(tempDir, "out", "linkOut").resolve("test.pkl").toString() val outputFile = makeSymdir(tempDir, "out", "linkOut").resolve("test.pkl").toString()
assertThatCode { cmd.parse(arrayOf("eval", inputFile, "-o", outputFile)) } assertThatCode { RootCommand.parse(arrayOf("eval", inputFile, "-o", outputFile)) }
.doesNotThrowAnyException() .doesNotThrowAnyException()
} }
@@ -87,22 +85,24 @@ class CliMainTest {
val error = val error =
"""Invalid value for "--multiple-file-output-path": Option is mutually exclusive with -o, --output-path and -x, --expression.""" """Invalid value for "--multiple-file-output-path": Option is mutually exclusive with -o, --output-path and -x, --expression."""
assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-x", "x", testIn)) } assertThatCode { RootCommand.parse(arrayOf("eval", "-m", testOut, "-x", "x", testIn)) }
.hasMessage(error) .hasMessage(error)
assertThatCode { cmd.parse(arrayOf("eval", "-m", testOut, "-o", "/tmp/test", testIn)) } assertThatCode { RootCommand.parse(arrayOf("eval", "-m", testOut, "-o", "/tmp/test", testIn)) }
.hasMessage(error) .hasMessage(error)
} }
@Test @Test
fun `showing version works`() { fun `showing version works`() {
assertThatCode { cmd.parse(arrayOf("--version")) }.hasMessage("pkl version 1") assertThatCode { RootCommand.parse(arrayOf("--version")) }
.hasMessage(Release.current().versionInfo)
} }
@Test @Test
fun `file paths get parsed into URIs`(@TempDir tempDir: Path) { fun `file paths get parsed into URIs`(@TempDir tempDir: Path) {
cmd.parse(arrayOf("eval", makeInput(tempDir, "my file.txt"))) RootCommand.parse(arrayOf("eval", makeInput(tempDir, "my file.txt")))
val evalCmd = RootCommand.registeredSubcommands().filterIsInstance<EvalCommand>().first()
val modules = evalCmd.baseOptions.baseOptions(evalCmd.modules).normalizedSourceModules val modules = evalCmd.baseOptions.baseOptions(evalCmd.modules).normalizedSourceModules
assertThat(modules).hasSize(1) assertThat(modules).hasSize(1)
assertThat(modules[0].path).endsWith("my file.txt") assertThat(modules[0].path).endsWith("my file.txt")
@@ -110,7 +110,8 @@ class CliMainTest {
@Test @Test
fun `invalid URIs are not accepted`() { fun `invalid URIs are not accepted`() {
val ex = assertThrows<BadParameterValue> { cmd.parse(arrayOf("eval", "file:my file.txt")) } val ex =
assertThrows<BadParameterValue> { RootCommand.parse(arrayOf("eval", "file:my file.txt")) }
assertThat(ex.message).contains("URI `file:my file.txt` has invalid syntax") assertThat(ex.message).contains("URI `file:my file.txt` has invalid syntax")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -16,18 +16,16 @@
package org.pkl.cli package org.pkl.cli
import com.github.ajalt.clikt.core.MissingArgument import com.github.ajalt.clikt.core.MissingArgument
import com.github.ajalt.clikt.core.subcommands
import java.io.StringWriter import java.io.StringWriter
import java.io.Writer import java.io.Writer
import java.net.URI import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.util.Locale import java.util.*
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.pkl.cli.commands.EvalCommand
import org.pkl.cli.commands.RootCommand import org.pkl.cli.commands.RootCommand
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
@@ -35,7 +33,6 @@ import org.pkl.commons.cli.CliTestOptions
import org.pkl.commons.readString import org.pkl.commons.readString
import org.pkl.commons.toUri import org.pkl.commons.toUri
import org.pkl.commons.writeString import org.pkl.commons.writeString
import org.pkl.core.Release
class CliTestRunnerTest { class CliTestRunnerTest {
@Test @Test
@@ -381,7 +378,7 @@ class CliTestRunnerTest {
val opts = val opts =
CliBaseOptions( CliBaseOptions(
sourceModules = listOf(input.toUri(), input2.toUri()), sourceModules = listOf(input.toUri(), input2.toUri()),
settings = URI("pkl:settings") settings = URI("pkl:settings"),
) )
val testOpts = CliTestOptions(junitDir = tempDir) val testOpts = CliTestOptions(junitDir = tempDir)
val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter) val runner = CliTestRunner(opts, testOpts, noopWriter, noopWriter)
@@ -391,12 +388,7 @@ class CliTestRunnerTest {
@Test @Test
fun `no source modules specified has same message as pkl eval`() { fun `no source modules specified has same message as pkl eval`() {
val e1 = assertThrows<CliException> { CliTestRunner(CliBaseOptions(), CliTestOptions()).run() } val e1 = assertThrows<CliException> { CliTestRunner(CliBaseOptions(), CliTestOptions()).run() }
val e2 = val e2 = assertThrows<MissingArgument> { RootCommand.parse(listOf("eval")) }
assertThrows<MissingArgument> {
val rootCommand =
RootCommand("pkl", Release.current().versionInfo(), "").subcommands(EvalCommand(""))
rootCommand.parse(listOf("eval"))
}
assertThat(e1).hasMessageContaining("Missing argument \"<modules>\"") assertThat(e1).hasMessageContaining("Missing argument \"<modules>\"")
assertThat(e1.message!!.replace("test", "eval")).isEqualTo(e2.helpMessage()) assertThat(e1.message!!.replace("test", "eval")).isEqualTo(e2.helpMessage())
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -28,10 +28,10 @@ import org.pkl.core.Release
/** Main method for the Java code generator CLI. */ /** Main method for the Java code generator CLI. */
internal fun main(args: Array<String>) { internal fun main(args: Array<String>) {
cliMain { PklJavaCodegenCommand().main(args) } cliMain { PklJavaCodegenCommand.main(args) }
} }
class PklJavaCodegenCommand : object PklJavaCodegenCommand :
ModulesCommand( ModulesCommand(
name = "pkl-codegen-java", name = "pkl-codegen-java",
helpLink = Release.current().documentation().homepage(), helpLink = Release.current().documentation().homepage(),
@@ -43,7 +43,7 @@ class PklJavaCodegenCommand :
option( option(
names = arrayOf("-o", "--output-dir"), names = arrayOf("-o", "--output-dir"),
metavar = "<path>", metavar = "<path>",
help = "The directory where generated source code is placed." help = "The directory where generated source code is placed.",
) )
.path() .path()
.default(defaults.outputDir) .default(defaults.outputDir)
@@ -52,7 +52,7 @@ class PklJavaCodegenCommand :
option( option(
names = arrayOf("--indent"), names = arrayOf("--indent"),
metavar = "<chars>", metavar = "<chars>",
help = "The characters to use for indenting generated source code." help = "The characters to use for indenting generated source code.",
) )
.default(defaults.indent) .default(defaults.indent)
@@ -61,21 +61,21 @@ class PklJavaCodegenCommand :
names = arrayOf("--generate-getters"), names = arrayOf("--generate-getters"),
help = help =
"Whether to generate public getter methods and " + "Whether to generate public getter methods and " +
"private final fields instead of public final fields." "private final fields instead of public final fields.",
) )
.flag() .flag()
private val generateJavadoc: Boolean by private val generateJavadoc: Boolean by
option( option(
names = arrayOf("--generate-javadoc"), names = arrayOf("--generate-javadoc"),
help = "Whether to preserve Pkl doc comments by generating corresponding Javadoc comments." help = "Whether to preserve Pkl doc comments by generating corresponding Javadoc comments.",
) )
.flag() .flag()
private val generateSpringBoot: Boolean by private val generateSpringBoot: Boolean by
option( option(
names = arrayOf("--generate-spring-boot"), names = arrayOf("--generate-spring-boot"),
help = "Whether to generate config classes for use with Spring Boot." help = "Whether to generate config classes for use with Spring Boot.",
) )
.flag() .flag()
@@ -83,7 +83,7 @@ class PklJavaCodegenCommand :
option( option(
names = arrayOf("--params-annotation"), names = arrayOf("--params-annotation"),
help = help =
"Fully qualified name of the annotation type to use for annotating constructor parameters with their name." "Fully qualified name of the annotation type to use for annotating constructor parameters with their name.",
) )
.defaultLazy( .defaultLazy(
"`none` if `--generate-spring-boot` is set, `org.pkl.config.java.mapper.Named` otherwise" "`none` if `--generate-spring-boot` is set, `org.pkl.config.java.mapper.Named` otherwise"
@@ -100,13 +100,13 @@ class PklJavaCodegenCommand :
The specified annotation type must be annotated with `@java.lang.annotation.Target(ElementType.TYPE_USE)` The specified annotation type must be annotated with `@java.lang.annotation.Target(ElementType.TYPE_USE)`
or the generated code may not compile. or the generated code may not compile.
""" """
.trimIndent() .trimIndent(),
) )
private val implementSerializable: Boolean by private val implementSerializable: Boolean by
option( option(
names = arrayOf("--implement-serializable"), names = arrayOf("--implement-serializable"),
help = "Whether to generate classes that implement java.io.Serializable." help = "Whether to generate classes that implement java.io.Serializable.",
) )
.flag() .flag()
@@ -121,7 +121,7 @@ class PklJavaCodegenCommand :
With this option, you can override or modify the default names, renaming entire With this option, you can override or modify the default names, renaming entire
classes or just their packages. classes or just their packages.
""" """
.trimIndent() .trimIndent(),
) )
.associate() .associate()
@@ -137,7 +137,7 @@ class PklJavaCodegenCommand :
paramsAnnotation = if (paramsAnnotation == "none") null else paramsAnnotation, paramsAnnotation = if (paramsAnnotation == "none") null else paramsAnnotation,
nonNullAnnotation = nonNullAnnotation, nonNullAnnotation = nonNullAnnotation,
implementSerializable = implementSerializable, implementSerializable = implementSerializable,
renames = renames renames = renames,
) )
CliJavaCodeGenerator(options).run() CliJavaCodeGenerator(options).run()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -31,10 +31,10 @@ import org.pkl.core.Release
/** Main method for the Kotlin code generator CLI. */ /** Main method for the Kotlin code generator CLI. */
internal fun main(args: Array<String>) { internal fun main(args: Array<String>) {
cliMain { PklKotlinCodegenCommand().main(args) } cliMain { PklKotlinCodegenCommand.main(args) }
} }
class PklKotlinCodegenCommand : object PklKotlinCodegenCommand :
ModulesCommand( ModulesCommand(
name = "pkl-codegen-kotlin", name = "pkl-codegen-kotlin",
helpLink = Release.current().documentation().homepage(), helpLink = Release.current().documentation().homepage(),
@@ -46,7 +46,7 @@ class PklKotlinCodegenCommand :
option( option(
names = arrayOf("-o", "--output-dir"), names = arrayOf("-o", "--output-dir"),
metavar = "<path>", metavar = "<path>",
help = "The directory where generated source code is placed." help = "The directory where generated source code is placed.",
) )
.path() .path()
.default(defaults.outputDir) .default(defaults.outputDir)
@@ -55,28 +55,28 @@ class PklKotlinCodegenCommand :
option( option(
names = arrayOf("--indent"), names = arrayOf("--indent"),
metavar = "<chars>", metavar = "<chars>",
help = "The characters to use for indenting generated source code." help = "The characters to use for indenting generated source code.",
) )
.default(defaults.indent) .default(defaults.indent)
private val generateKdoc: Boolean by private val generateKdoc: Boolean by
option( option(
names = arrayOf("--generate-kdoc"), names = arrayOf("--generate-kdoc"),
help = "Whether to preserve Pkl doc comments by generating corresponding KDoc comments." help = "Whether to preserve Pkl doc comments by generating corresponding KDoc comments.",
) )
.flag() .flag()
private val generateSpringboot: Boolean by private val generateSpringboot: Boolean by
option( option(
names = arrayOf("--generate-spring-boot"), names = arrayOf("--generate-spring-boot"),
help = "Whether to generate config classes for use with Spring Boot." help = "Whether to generate config classes for use with Spring Boot.",
) )
.flag() .flag()
private val implementSerializable: Boolean by private val implementSerializable: Boolean by
option( option(
names = arrayOf("--implement-serializable"), names = arrayOf("--implement-serializable"),
help = "Whether to generate classes that implement java.io.Serializable." help = "Whether to generate classes that implement java.io.Serializable.",
) )
.flag() .flag()
@@ -91,7 +91,7 @@ class PklKotlinCodegenCommand :
With this option, you can override or modify the default names, renaming entire With this option, you can override or modify the default names, renaming entire
classes or just their packages. classes or just their packages.
""" """
.trimIndent() .trimIndent(),
) )
.associate() .associate()
@@ -104,7 +104,7 @@ class PklKotlinCodegenCommand :
generateKdoc = generateKdoc, generateKdoc = generateKdoc,
generateSpringBootConfig = generateSpringboot, generateSpringBootConfig = generateSpringboot,
implementSerializable = implementSerializable, implementSerializable = implementSerializable,
renames = renames renames = renames,
) )
CliKotlinCodeGenerator(options).run() CliKotlinCodeGenerator(options).run()
} }

View File

@@ -34,10 +34,10 @@ import org.pkl.core.Release
/** Main method for the Pkldoc CLI. */ /** Main method for the Pkldoc CLI. */
internal fun main(args: Array<String>) { internal fun main(args: Array<String>) {
cliMain { DocCommand().main(args) } cliMain { DocCommand.main(args) }
} }
class DocCommand : object DocCommand :
BaseCommand(name = "pkldoc", helpLink = Release.current().documentation().homepage()) { BaseCommand(name = "pkldoc", helpLink = Release.current().documentation().homepage()) {
private val modules: List<URI> by private val modules: List<URI> by

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. * Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -21,12 +21,9 @@ import org.junit.jupiter.api.assertThrows
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
class CliMainTest { class CliMainTest {
private val docCommand = DocCommand()
@Test @Test
fun `CLI run test`() { fun `CLI run test`() {
val e = assertThrows<CliException> { docCommand.parse(arrayOf("foo", "--output-dir", "/tmp")) } val e = assertThrows<CliException> { DocCommand.parse(arrayOf("foo", "--output-dir", "/tmp")) }
assertThat(e) assertThat(e)
.hasMessageContaining("must contain at least one module named `doc-package-info.pkl`") .hasMessageContaining("must contain at least one module named `doc-package-info.pkl`")
} }