mirror of
https://github.com/apple/pkl.git
synced 2026-05-30 18:40:40 +02:00
SPICE-0025: pkl run CLI framework (#1367)
This commit is contained in:
@@ -68,6 +68,7 @@ endif::[]
|
||||
:uri-pkldoc-example: {uri-pkl-examples-tree}/pkldoc
|
||||
|
||||
:uri-stdlib-baseModule: {uri-pkl-stdlib-docs}/base
|
||||
:uri-stdlib-CommandModule: {uri-pkl-stdlib-docs}/Command
|
||||
:uri-stdlib-analyzeModule: {uri-pkl-stdlib-docs}/analyze
|
||||
:uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet
|
||||
:uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect
|
||||
@@ -150,6 +151,13 @@ endif::[]
|
||||
:uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource
|
||||
:uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files
|
||||
:uri-stdlib-FileOutput: {uri-stdlib-baseModule}/FileOutput
|
||||
:uri-stdlib-Annotation: {uri-stdlib-baseModule}/Annotation
|
||||
:uri-stdlib-ConvertProperty: {uri-stdlib-baseModule}/ConvertProperty
|
||||
:uri-stdlib-Command-Flag: {uri-stdlib-CommandModule}/Flag
|
||||
:uri-stdlib-Command-BooleanFlag: {uri-stdlib-CommandModule}/BooleanFlag
|
||||
:uri-stdlib-Command-CountedFlag: {uri-stdlib-CommandModule}/CountedFlag
|
||||
:uri-stdlib-Command-Argument: {uri-stdlib-CommandModule}/Argument
|
||||
:uri-stdlib-Command-Import: {uri-stdlib-CommandModule}/Import
|
||||
|
||||
:uri-messagepack: https://msgpack.org/index.html
|
||||
:uri-messagepack-spec: https://github.com/msgpack/msgpack/blob/master/spec.md
|
||||
|
||||
@@ -3961,6 +3961,7 @@ emailList: List<EmailAddress> // <2>
|
||||
<1> equivalent to `email: String(contains("@"))` for type checking purposes
|
||||
<2> equivalent to `emailList: List<String(contains("@"))>` for type checking purposes
|
||||
|
||||
[[nullable-types]]
|
||||
==== Nullable Types
|
||||
|
||||
Class types such as `Bird` (see above) do not admit `null` values.
|
||||
|
||||
@@ -489,7 +489,9 @@ If these are the only failures, the command exits with exit code 10.
|
||||
Otherwise, failures result in exit code 1.
|
||||
|
||||
<modules>::
|
||||
The absolute or relative URIs of the modules to test. Relative URIs are resolved against the working directory.
|
||||
The absolute or relative URIs of the modules to test.
|
||||
The module must extend `pkl:test`.
|
||||
Relative URIs are resolved against the working directory.
|
||||
|
||||
==== Options
|
||||
|
||||
@@ -546,6 +548,23 @@ Use `--no-power-assertions` to disable this feature if you prefer simpler output
|
||||
|
||||
This command also takes <<common-options, common options>>.
|
||||
|
||||
[[command-run]]
|
||||
=== `pkl run`
|
||||
|
||||
*Synopsis:* `pkl run [<options>] [<module>] [<command options>]`
|
||||
|
||||
Evaluate a <<cli-tools,CLI command>> defined by `<module>`.
|
||||
|
||||
<module>::
|
||||
The absolute or relative URIs of the command module to run.
|
||||
The module must extend `pkl:Command`.
|
||||
Relative URIs are resolved against the working directory.
|
||||
|
||||
<command options>::
|
||||
Additional CLI options and arguments defined by `<module>`.
|
||||
|
||||
This command also takes <<common-options, common options>>, but they must be specified before `<module>`.
|
||||
|
||||
[[command-repl]]
|
||||
=== `pkl repl`
|
||||
|
||||
@@ -800,7 +819,7 @@ Write the path of files with formatting violations to stdout.
|
||||
[[common-options]]
|
||||
=== Common options
|
||||
|
||||
The <<command-eval>>, <<command-test>>, <<command-repl>>, <<command-project-resolve>>, <<command-project-package>>, <<command-download-package>>, and <<command-analyze-imports>> commands support the following common options:
|
||||
The <<command-eval>>, <<command-test>>, <<command-run>>, <<command-repl>>, <<command-project-resolve>>, <<command-project-package>>, <<command-download-package>>, and <<command-analyze-imports>> commands support the following common options:
|
||||
|
||||
include::../../pkl-cli/partials/cli-common-options.adoc[]
|
||||
|
||||
@@ -901,6 +920,301 @@ If multiple module outputs are written to the same file, or to standard output,
|
||||
By default, module outputs are separated with `---`, as in a YAML stream.
|
||||
The separator can be customized using the `--module-output-separator` option.
|
||||
|
||||
[[cli-tools]]
|
||||
== Implementing CLI Tools
|
||||
|
||||
CLI tools can be implemented in Pkl by modules extending the `pkl:Command` module.
|
||||
With `pkl:Command`, you can define a script in Pkl that is executed by your shell, providing a better CLI experience.
|
||||
|
||||
Regular evaluation requires use of xref:language-reference:index.adoc#resources[resources] like properties and evironment variables to provide parameters:
|
||||
[source,bash]
|
||||
----
|
||||
$ pkl eval script.pkl -p username=me -p password=password
|
||||
----
|
||||
|
||||
Commands provide a native, familiar CLI experience:
|
||||
[source,bash]
|
||||
----
|
||||
$ pkl run script.pkl --username=admin --password=hunter2
|
||||
$ ./script.pkl --username=admin --password=hunter2
|
||||
----
|
||||
|
||||
Pkl commands have a few properties that distinguish them from standard module evaluation:
|
||||
|
||||
* Users provide input to commands using familiar command line idioms, providing a better experience than deriving inputs from xref:language-reference:index.adoc#resources[resources] like external properties or environment variables.
|
||||
* Commands can dynamically import modules when they are specified as command line options.
|
||||
* Commands may write to standard output (via `output.text` or `output.bytes`) and the filesystem (via `output.files`) in the same evaluation.
|
||||
* Command file output may write to any absolute path (not only relative to the `--multiple-file-output-path` option).
|
||||
** Relative output paths are written relative to the current working directory (or `--working-dir`, if specified).
|
||||
** Paths of output file are printed to the command's standard error.
|
||||
|
||||
IMPORTANT: Users of `pkl run` must be aware of the security implications of this behavior.
|
||||
Using `pkl eval` prevents accidental overwrites by not allowing absolute paths, but `pkl run` does not offer this protection.
|
||||
Commands may write to any path the invoking user has permissions to modify.
|
||||
|
||||
Commands are implemented as regular modules and declare their supported command line flags and positional arguments using a class with annotated properties.
|
||||
|
||||
=== Defining Commands
|
||||
|
||||
Commands are defined by creating a module that extends `pkl:Command`:
|
||||
|
||||
[source,pkl%tested]
|
||||
.my-tool.pkl
|
||||
----
|
||||
/// This doc comment becomes part of the command's CLI help!
|
||||
/// Markdown formatting is **allowed!**
|
||||
extends "pkl:Command"
|
||||
|
||||
options: Options // <1>
|
||||
|
||||
class Options {
|
||||
// Define CLI flags/arguments...
|
||||
}
|
||||
|
||||
// Regular module code...
|
||||
----
|
||||
<1> Re-declaration of the `options` property's type.
|
||||
|
||||
Like `pkl eval`, when a command completes without an evaluation error the process exits successfully (exit code 0).
|
||||
Commands can return a failure using `throw` (exit code 1), but otherwise may not control the exit code.
|
||||
|
||||
Other than the differences listed above, commands behave like any other Pkl module.
|
||||
For example, there is no way to execute other programs or make arbitrary HTTP requests.
|
||||
If additional functionality is desired, xref:language-reference:index.adoc#external-readers[external readers] may be used to extends Pkl's capabilities.
|
||||
|
||||
=== Command Options
|
||||
|
||||
Each property of a command's options class becomes a command line option.
|
||||
Properties with the `local`, `hidden`, `fixed`, and/or `const` modifiers are not parsed as options
|
||||
A property's doc comment, if present, becomes the corresponding option's CLI help description.
|
||||
Doc comments are interpreted as Markdown text and formatted nicely when displayed to users.
|
||||
Properties must have xref:language-reference:index.adoc#type-annotations[type annotations] to determine how they are parsed.
|
||||
|
||||
Properties may be xref:language-reference:index.adoc#annotations[annotated] to influence how they behave:
|
||||
|
||||
* Properties annotated with link:{uri-stdlib-Command-Flag}[`@Flag`] become CLI flags named `--<property name>` that accept a value.
|
||||
* `Boolean` properties annotated with link:{uri-stdlib-Command-BooleanFlag}[`@BooleanFlag`] become CLI flags named `--<property name>` and `--no-<property name>` that result in `true` and `false` values, respectively.
|
||||
* `Int` (and type aliases of `Int`) properties annotated with link:{uri-stdlib-Command-CountedFlag}[`@CountedFlag`] become CLI flags named `--<property name>` that produce a value equal to the number of times they are present on the command line.
|
||||
* Properties annotated with link:{uri-stdlib-Command-Argument}[`@Argument`] become positional CLI arguments and are parsed in the order they appear in the class.
|
||||
* Properties with no annotation are treated the same as `@Flag` with no further customization.
|
||||
|
||||
Flag options may set a `shortName` property to define a single-character abbreviation (`-<short name>`).
|
||||
Flag abbreviations may be combined (e.g. `-a -b -v -v -f some-value` is equivalent to `-abvvf some-value`).
|
||||
|
||||
A `@Flag` or `@Argument` property's type annotation determines how it is converted from the raw string value:
|
||||
|
||||
|===
|
||||
|Type |Behavior
|
||||
|
||||
|`String`
|
||||
|Value is used verbatim.
|
||||
|
||||
|`Char`
|
||||
|Value is used verbatim but must be exactly one character.
|
||||
|
||||
|`Boolean`
|
||||
|True values: `true`, `t`, `1`, `yes`, `y`, `on`
|
||||
|
||||
False values: `false`, `f`, `0`, `no`, `n`, `off`
|
||||
|
||||
|`Number`
|
||||
|Value is parsed as an `Int` if possible, otherwise parsed as `Float`.
|
||||
|
||||
|`Float`
|
||||
|Value is parsed as a `Float`.
|
||||
|
||||
|`Int`
|
||||
|Value is parsed as a `Int`.
|
||||
|
||||
|`Int8`, `Int16`, `Int32`, `UInt`, `UInt8`, `UInt16`, `UInt32`
|
||||
|Value is parsed as a `Int` and must be within the type's range.
|
||||
|
||||
|xref:language-reference:index.adoc#union-types[Union] of xref:language-reference:index.adoc#string-literal-types[string literals]
|
||||
|Value is used verbatim but must match a member of the union.
|
||||
|
||||
|`List<Element>`, `Listing<Element>`, `Set<Element>`
|
||||
|Each occurrence of the option becomes an element of the final value.
|
||||
|
||||
`Element` values are parsed based on the above primitive types.
|
||||
|
||||
|`Map<Key, Value>`, `Mapping<Key, Value>`
|
||||
|Each occurrence of the option becomes an entry of the final value.
|
||||
|
||||
Values are split on the first `"="` character; the first part is parsed as `Key` and the second as `Value`, both based on the above primitive types.
|
||||
|
||||
|`Pair<First, Second>`
|
||||
|Value is split on the first `"="` character; the first part is parsed as `First` and the second as `Second`, both based on the above primitive types.
|
||||
|
||||
|===
|
||||
|
||||
If a flag that accepts only a single value is provided multiple times, the last occurrence becomes the final value.
|
||||
|
||||
Only a single positional argument accepting multiple values is permitted per command.
|
||||
|
||||
A property with a xref:language-reference:index.adoc#nullable-types[nullable type] is optional and, if not specified on the command line, will have value `null`.
|
||||
Properties with default values are also optional.
|
||||
Type constraints are evaluated when the command is executed, so additional restrictions on option values are enforced at runtime.
|
||||
|
||||
==== Custom Option Conversion and Aggregation
|
||||
|
||||
A property may be annotated with any type if its `@Flag` or `@Argument` annotation sets the `convert` or `transformAll` properties.
|
||||
The `convert` property is a xref:language-reference:index.adoc#anonymous-functions[function] that overrides how _each_ raw option value is interpreted.
|
||||
The `transformAll` property is a function that overrides how _all_ parsed option values become the final property value.
|
||||
|
||||
The `convert` function may return an link:{uri-stdlib-Command-Import}[`Import`] value that is replaced during option parsing with the actual value of the module specified by its `uri` property.
|
||||
If `glob` is `true`, the replacement value is a `Mapping`; its keys are the _absolute_ URIs of the matched modules and its values are the actual module values.
|
||||
When specifying glob import options on the command line, it is often necessary to quote the value to avoid it being interpreted by the shell.
|
||||
If the return value of `convert` is a `List`, `Set`, `Map`, or `Pair`, each contained value (elements and entry keys/values) that are `Import` values are also replaced.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
If an option has type `Mapping<String, «some module type»>` and should accept a single glob pattern value, the option's annotation must also set `multiple = false` to override the default behavior of `Mapping` options accepting multiple values.
|
||||
Example:
|
||||
[source%parsed,{pkl}]
|
||||
----
|
||||
@Flag {
|
||||
convert = (it) -> new Import { uri = it; glob = true }
|
||||
multiple = false
|
||||
}
|
||||
birds: Mapping<String, Bird>
|
||||
----
|
||||
|
||||
If multiple glob patterns values should be accepted and merged, `transformAll` may be used to merge every glob-imported `Mapping`:
|
||||
[source%parsed,{pkl}]
|
||||
----
|
||||
@Flag {
|
||||
convert = (it) -> new Import { uri = it; glob = true }
|
||||
transformAll =
|
||||
(values) -> values.fold(new Mapping {}, (result, element) ->
|
||||
(result) { ...element }
|
||||
)
|
||||
}
|
||||
birds: Mapping<String, Bird>
|
||||
----
|
||||
====
|
||||
|
||||
=== Subcommands
|
||||
|
||||
Like many other command line libraries, `pkl:Command` allows building commands into a hierarchy with a root command and subcommands:
|
||||
|
||||
[source,pkl%tested]
|
||||
.my-tool.pkl
|
||||
----
|
||||
extends "pkl:Command"
|
||||
|
||||
command {
|
||||
subcommands {
|
||||
import("subcommand1.pkl")
|
||||
import("subcommand2.pkl")
|
||||
for (_, subcommand in import*("./subcommands/*.pkl")) {
|
||||
subcommand
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[source,pkl%tested]
|
||||
.subcommand1.pkl
|
||||
----
|
||||
extends "pkl:Command"
|
||||
|
||||
import "my-tool.pkl"
|
||||
|
||||
parent: `my-tool` // <1>
|
||||
|
||||
// Regular module code...
|
||||
----
|
||||
<1> Optional; asserts that this is a subcommand of `my-tool` and simplifies accessing properties and options of the parent command
|
||||
|
||||
Each element of `subcommands` must have a unique value for `command.name`.
|
||||
|
||||
=== Testing Commands
|
||||
|
||||
Command modules are normal Pkl modules, so they may be imported and used like any other module.
|
||||
This is particularly helpful when testing commands, as the command's `options` and `parent` properties can be populated by test code.
|
||||
|
||||
Testing the above command and subcommand might look like this:
|
||||
|
||||
[source,pkl%tested]
|
||||
----
|
||||
amends "pkl:test"
|
||||
|
||||
import "my-tool.pkl"
|
||||
import "subcommand1.pkl"
|
||||
|
||||
examples {
|
||||
["Test my-tool"] {
|
||||
(`my-tool`) {
|
||||
options {
|
||||
// Set my-tool options here...
|
||||
}
|
||||
}.output.text
|
||||
}
|
||||
["Test subcommand1"] {
|
||||
(subcommand1) {
|
||||
parent { // this amends `my-tool`
|
||||
options {
|
||||
// Set my-tool options here...
|
||||
}
|
||||
}
|
||||
options {
|
||||
// Set subcommand options here...
|
||||
}
|
||||
}.output.text
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[commands-as-standalone-scripts]]
|
||||
=== Commands as standalone scripts
|
||||
|
||||
On *nix platforms, Pkl commands can be configured to run as standalone tools that can be invoked without the `pkl run` command.
|
||||
To achieve this, the command file must be marked executable (i.e. `chmod +x my-tool.pkl`) and a link:https://en.wikipedia.org/wiki/Shebang_(Unix)[shebang comment] must be added on the first line of the file:
|
||||
|
||||
[source,pkl%parsed]
|
||||
----
|
||||
#!/usr/bin/env -S pkl run
|
||||
----
|
||||
|
||||
NOTE: The `-S` flag for `env` is required on Linux systems due to a limitation of shebang handling in the Linux kernel.
|
||||
While not required on other *nix platforms like macOS, but it should be included for compatibility.
|
||||
|
||||
==== Shell Completion
|
||||
|
||||
Like with Pkl's own CLI, <<command-shell-completion, shell completions>> can be generated for standalone scripts.
|
||||
|
||||
[source,shell]
|
||||
----
|
||||
# Generate shell completion script for bash
|
||||
./my-tool.pkl shell-completion bash
|
||||
|
||||
# Generate shell completion script for zsh
|
||||
./my-tool.pkl shell-completion zsh
|
||||
----
|
||||
|
||||
==== Customizing Completion Candidates
|
||||
|
||||
`@Flag` and `@Argument` annotations may specify the `completionCandidates` to improve generated shell completions.
|
||||
|
||||
Valid values include:
|
||||
|
||||
* A `Listing<String>` of literal string values to offer for completion.
|
||||
* The literal string `"path"`, which offers local file paths for completion.
|
||||
|
||||
Options with a string literal union type implicitly offer the members of the union as completion candidates.
|
||||
|
||||
=== Flag name ambiguities
|
||||
|
||||
It is possible for commands to define flags with names or short names that collide with Pkl's own command line options.
|
||||
To avoid ambiguity in parsing these options, all flags for Pkl itself (e.g. `--root-dir`) must be placed before the root command module's URI.
|
||||
Command authors are encouraged to avoid overlapping with Pkl's built-in flags, but this may not always be feasible, especially for single-character abbreviated names.
|
||||
|
||||
This imposes a limitation around <<commands-as-standalone-scripts,standalone commands>> that prevents users from customizing Pkl evaluator options when they are invoked.
|
||||
There are two recommended workarounds for this limitation:
|
||||
|
||||
* Use a `PklProject` to define evaluator settings instead of doing so on the command line.
|
||||
* If the command line must be used, switch to invoking via `pkl run [<flags>] [<root command module>]`.
|
||||
|
||||
[[repl]]
|
||||
== Working with the REPL
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.cli
|
||||
|
||||
import com.github.ajalt.clikt.completion.CompletionCandidates
|
||||
import com.github.ajalt.clikt.completion.CompletionCommand
|
||||
import com.github.ajalt.clikt.core.*
|
||||
import com.github.ajalt.clikt.parameters.arguments.*
|
||||
import com.github.ajalt.clikt.parameters.options.*
|
||||
import com.github.ajalt.clikt.parameters.types.int
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.writeBytes
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.commons.cli.CliCommand
|
||||
import org.pkl.commons.cli.CliException
|
||||
import org.pkl.commons.cli.commands.installCommonOptions
|
||||
import org.pkl.commons.currentWorkingDir
|
||||
import org.pkl.core.Closeables
|
||||
import org.pkl.core.CommandSpec
|
||||
import org.pkl.core.EvaluatorBuilder
|
||||
import org.pkl.core.FileOutput
|
||||
import org.pkl.core.ModuleSource.uri
|
||||
import org.pkl.core.PklBugException
|
||||
import org.pkl.core.PklException
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
class CliCommandRunner
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
private val options: CliBaseOptions,
|
||||
private val args: List<String>,
|
||||
private val outputStream: OutputStream = System.out,
|
||||
private val errStream: OutputStream = System.err,
|
||||
) : CliCommand(options) {
|
||||
|
||||
private val normalizedSourceModule = options.normalizedSourceModules.first()
|
||||
|
||||
override fun doRun() {
|
||||
val builder = evaluatorBuilder()
|
||||
try {
|
||||
evalCmd(builder)
|
||||
} finally {
|
||||
Closeables.closeQuietly(builder.moduleKeyFactories)
|
||||
Closeables.closeQuietly(builder.resourceReaders)
|
||||
}
|
||||
}
|
||||
|
||||
private fun evalCmd(builder: EvaluatorBuilder) {
|
||||
val evaluator = builder.build()
|
||||
evaluator.use {
|
||||
evaluator.evaluateCommand(uri(normalizedSourceModule)) { spec ->
|
||||
try {
|
||||
val root = SynthesizedRunCommand(spec, this, options.sourceModules.first().toString())
|
||||
root.installCommonOptions(includeVersion = false)
|
||||
root.subcommands(
|
||||
CompletionCommand(
|
||||
name = "shell-completion",
|
||||
help = "Generate a completion script for the given shell",
|
||||
)
|
||||
)
|
||||
root.parse(args)
|
||||
} catch (e: PklException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw e.message?.let { PklException(it, e) } ?: PklException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders the comand's `output.bytes`, writing it to the standard output stream. */
|
||||
fun writeOutput(outputBytes: ByteArray) {
|
||||
if (outputBytes.isEmpty()) return
|
||||
outputStream.write(outputBytes)
|
||||
outputStream.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the command's `output.files`, writing each entry as a file.
|
||||
*
|
||||
* File paths are written to the standard error stream.
|
||||
*
|
||||
* Unlike CliEvaluator, command outputs write relative to --working-dir and may write files
|
||||
* anywhere in the filesystem. This is intentionally less sandboxed than `pkl eval` and directly
|
||||
* targets the capabilities of CLI tools written in general purpose languages. Pkl commands should
|
||||
* therefore be treated as untrusted code the way that any other CLI tool would be.
|
||||
*/
|
||||
fun writeMultipleFileOutput(outputFiles: Map<String, FileOutput>) {
|
||||
if (outputFiles.isEmpty()) return
|
||||
|
||||
val writtenFiles = mutableMapOf<Path, String>()
|
||||
val outputDir = options.normalizedWorkingDir
|
||||
if (outputDir.exists() && !outputDir.isDirectory()) {
|
||||
throw CliException("Output path `$outputDir` exists and is not a directory.")
|
||||
}
|
||||
for ((pathSpec, fileOutput) in outputFiles) {
|
||||
checkPathSpec(pathSpec)
|
||||
val resolvedPath = outputDir.resolve(pathSpec).normalize()
|
||||
val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath
|
||||
val previousOutput = writtenFiles[realPath]
|
||||
if (previousOutput != null) {
|
||||
throw CliException(
|
||||
"Output file conflict: `output.files` entries `\"${previousOutput}\"` and `\"$pathSpec\"` resolve to the same file path `$realPath`."
|
||||
)
|
||||
}
|
||||
if (realPath.isDirectory()) {
|
||||
throw CliException(
|
||||
"Output file conflict: `output.files` entry `\"$pathSpec\"` resolves to file path `$realPath`, which is a directory."
|
||||
)
|
||||
}
|
||||
writtenFiles[realPath] = pathSpec
|
||||
realPath.createParentDirectories()
|
||||
realPath.writeBytes(fileOutput.bytes)
|
||||
val displayPath =
|
||||
if (Path.of(pathSpec).isAbsolute) pathSpec
|
||||
else IoUtils.relativize(resolvedPath, currentWorkingDir).toString()
|
||||
errStream.writeText(displayPath + IoUtils.getLineSeparator())
|
||||
errStream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
class SynthesizedRunCommand(
|
||||
private val spec: CommandSpec,
|
||||
private val runner: CliCommandRunner,
|
||||
name: String? = null,
|
||||
) : CliktCommand(name = name ?: spec.name) {
|
||||
init {
|
||||
spec.options.forEach { opt ->
|
||||
when (opt) {
|
||||
is CommandSpec.Flag ->
|
||||
registerOption(
|
||||
option(
|
||||
names = opt.names,
|
||||
help = opt.helpText ?: "",
|
||||
metavar = opt.metavar,
|
||||
hidden = opt.hidden,
|
||||
completionCandidates = opt.completionCandidates?.toClikt(),
|
||||
)
|
||||
.convert {
|
||||
try {
|
||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
throw MissingOption(option)
|
||||
}
|
||||
}
|
||||
.transformAll(opt.defaultValue, opt.showAsRequired) {
|
||||
try {
|
||||
opt.transformAll.apply(it)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
throw MissingOption(option)
|
||||
}
|
||||
}
|
||||
)
|
||||
is CommandSpec.BooleanFlag ->
|
||||
registerOption(
|
||||
if (opt.defaultValue != null)
|
||||
option(names = opt.names, help = opt.helpText ?: "", hidden = opt.hidden)
|
||||
.flag("--no-${opt.name}", default = opt.defaultValue!!)
|
||||
else
|
||||
option(names = opt.names, help = opt.helpText ?: "", hidden = opt.hidden)
|
||||
.nullableFlag("--no-${opt.name}")
|
||||
)
|
||||
is CommandSpec.CountedFlag ->
|
||||
registerOption(
|
||||
option(names = opt.names, help = opt.helpText ?: "", hidden = opt.hidden)
|
||||
.int()
|
||||
.transformValues(0..0) { it.lastOrNull() ?: 1 }
|
||||
.transformAll { it.sum().toLong() }
|
||||
)
|
||||
is CommandSpec.Argument ->
|
||||
registerArgument(
|
||||
argument(
|
||||
opt.name,
|
||||
opt.helpText ?: "",
|
||||
completionCandidates = opt.completionCandidates?.toClikt(),
|
||||
)
|
||||
.convert {
|
||||
try {
|
||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
throw MissingArgument(argument)
|
||||
}
|
||||
}
|
||||
.transformAll(if (opt.repeated) -1 else 1, !opt.repeated) {
|
||||
try {
|
||||
opt.transformAll.apply(it)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
throw MissingArgument(argument)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
spec.subcommands.forEach { subcommands(SynthesizedRunCommand(it, runner)) }
|
||||
}
|
||||
|
||||
override val invokeWithoutSubcommand = true
|
||||
|
||||
override val hiddenFromHelp: Boolean = spec.hidden
|
||||
|
||||
override fun help(context: Context): String = spec.helpText ?: ""
|
||||
|
||||
override fun run() {
|
||||
if (currentContext.invokedSubcommand is CompletionCommand) return
|
||||
|
||||
val opts =
|
||||
registeredOptions()
|
||||
.mapNotNull {
|
||||
val opt = it as? OptionWithValues<*, *, *> ?: return@mapNotNull null
|
||||
return@mapNotNull if (it.names.contains("--help")) null
|
||||
else it.names.first().trimStart('-') to opt.value
|
||||
}
|
||||
.toMap() +
|
||||
registeredArguments()
|
||||
.mapNotNull { it as? ArgumentDelegate<*> }
|
||||
.associateBy({ it.name }, { it.value })
|
||||
|
||||
val state = spec.apply.apply(opts, currentContext.obj as CommandSpec.State?)
|
||||
currentContext.obj = state
|
||||
|
||||
if (currentContext.invokedSubcommand != null) return
|
||||
if (spec.subcommands.isNotEmpty() && spec.noOp) {
|
||||
throw PrintHelpMessage(currentContext, true, 1)
|
||||
}
|
||||
|
||||
val result = state.evaluate()
|
||||
runner.writeOutput(result.outputBytes)
|
||||
runner.writeMultipleFileOutput(result.outputFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun CommandSpec.CompletionCandidates.toClikt(): CompletionCandidates =
|
||||
when (this) {
|
||||
CommandSpec.CompletionCandidates.PATH -> CompletionCandidates.Path
|
||||
is CommandSpec.CompletionCandidates.Fixed -> CompletionCandidates.Fixed(values)
|
||||
else -> throw PklBugException.unreachableCode()
|
||||
}
|
||||
@@ -143,17 +143,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Evaluator.writeOutput(moduleSource: ModuleSource, writeTo: Path): Boolean {
|
||||
if (options.expression == null) {
|
||||
val bytes = evaluateOutputBytes(moduleSource)
|
||||
writeTo.writeBytes(bytes)
|
||||
return bytes.isNotEmpty()
|
||||
}
|
||||
val text = evaluateExpressionString(moduleSource, options.expression)
|
||||
writeTo.writeString(text)
|
||||
return text.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun Evaluator.evalOutput(moduleSource: ModuleSource): ByteArray {
|
||||
if (options.expression == null) {
|
||||
return evaluateOutputBytes(moduleSource)
|
||||
@@ -227,13 +216,6 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun OutputStream.writeText(text: String) = write(text.toByteArray())
|
||||
|
||||
private fun OutputStream.writeLine(text: String) {
|
||||
writeText(text)
|
||||
writeText("\n")
|
||||
}
|
||||
|
||||
private fun toModuleSource(uri: URI, reader: InputStream) =
|
||||
if (uri == VmUtils.REPL_TEXT_URI) {
|
||||
ModuleSource.create(uri, reader.readAllBytes().toString(StandardCharsets.UTF_8))
|
||||
@@ -241,14 +223,6 @@ constructor(
|
||||
ModuleSource.uri(uri)
|
||||
}
|
||||
|
||||
private fun checkPathSpec(pathSpec: String) {
|
||||
val illegal = pathSpec.indexOfFirst { IoUtils.isReservedFilenameChar(it) && it != '/' }
|
||||
if (illegal == -1) {
|
||||
return
|
||||
}
|
||||
throw CliException("Path spec `$pathSpec` contains illegal character `${pathSpec[illegal]}`.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders each module's `output.files`, writing each entry as a file into the specified output
|
||||
* directory.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.cli
|
||||
|
||||
import java.io.OutputStream
|
||||
import org.pkl.commons.cli.CliException
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
fun checkPathSpec(pathSpec: String) {
|
||||
val illegal = pathSpec.indexOfFirst { IoUtils.isReservedFilenameChar(it) && it != '/' }
|
||||
if (illegal == -1) {
|
||||
return
|
||||
}
|
||||
throw CliException("Path spec `$pathSpec` contains illegal character `${pathSpec[illegal]}`.")
|
||||
}
|
||||
|
||||
fun OutputStream.writeText(text: String) = write(text.toByteArray())
|
||||
|
||||
fun OutputStream.writeLine(text: String) {
|
||||
writeText(text)
|
||||
writeText("\n")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -50,6 +50,7 @@ class RootCommand : NoOpCliktCommand(name = "pkl") {
|
||||
DownloadPackageCommand(),
|
||||
AnalyzeCommand(),
|
||||
FormatterCommand(),
|
||||
RunCommand(),
|
||||
CompletionCommand(
|
||||
name = "shell-completion",
|
||||
help = "Generate a completion script for the given shell",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.cli.commands
|
||||
|
||||
import com.github.ajalt.clikt.completion.CompletionCandidates
|
||||
import com.github.ajalt.clikt.core.context
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.arguments.convert
|
||||
import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||
import java.net.URI
|
||||
import org.pkl.cli.CliCommandRunner
|
||||
import org.pkl.commons.cli.commands.BaseCommand
|
||||
import org.pkl.commons.cli.commands.BaseOptions
|
||||
import org.pkl.commons.cli.commands.ProjectOptions
|
||||
|
||||
class RunCommand : BaseCommand(name = "run", helpLink = helpLink) {
|
||||
override val helpString = "Run a Pkl pkl:Command CLI tool"
|
||||
override val treatUnknownOptionsAsArgs = true
|
||||
|
||||
init {
|
||||
context { allowInterspersedArgs = false }
|
||||
}
|
||||
|
||||
val module: URI by
|
||||
argument(name = "module", completionCandidates = CompletionCandidates.Path).convert {
|
||||
BaseOptions.parseModuleName(it)
|
||||
}
|
||||
|
||||
val args: List<String> by argument(name = "args").multiple()
|
||||
|
||||
private val projectOptions by ProjectOptions()
|
||||
|
||||
override fun run() {
|
||||
CliCommandRunner(baseOptions.baseOptions(listOf(module), projectOptions), args).run()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ import org.pkl.core.util.IoUtils
|
||||
/** Base options shared between CLI commands. */
|
||||
data class CliBaseOptions(
|
||||
/** The source modules to evaluate. Relative URIs are resolved against [workingDir]. */
|
||||
private val sourceModules: List<URI> = listOf(),
|
||||
val sourceModules: List<URI> = listOf(),
|
||||
|
||||
/**
|
||||
* The URI patterns that determine which modules can be loaded and evaluated. Patterns are matched
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.pkl.commons.cli
|
||||
|
||||
import com.github.ajalt.clikt.core.CliktError
|
||||
import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@@ -46,6 +47,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
proxyAddress?.let(IoUtils::setSystemProxy)
|
||||
doRun()
|
||||
} catch (e: PklException) {
|
||||
if (e.cause is CliktError) throw e.cause!!
|
||||
throw CliException(e.message!!)
|
||||
} catch (e: CliException) {
|
||||
throw e
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,14 +27,16 @@ import org.pkl.core.Release
|
||||
|
||||
private val theme = Theme { styles["markdown.code.span"] = TextStyle(bold = true) }
|
||||
|
||||
fun <T : BaseCliktCommand<T>> T.installCommonOptions() {
|
||||
fun <T : BaseCliktCommand<T>> T.installCommonOptions(includeVersion: Boolean = true) {
|
||||
installMordantMarkdown()
|
||||
|
||||
versionOption(
|
||||
Release.current().versionInfo,
|
||||
names = setOf("-v", "--version"),
|
||||
message = { if (commandName == "pkl") it else it.replaceFirst("Pkl", commandName) },
|
||||
)
|
||||
if (includeVersion) {
|
||||
versionOption(
|
||||
Release.current().versionInfo,
|
||||
names = setOf("-v", "--version"),
|
||||
message = { if (commandName == "pkl") it else it.replaceFirst("Pkl", commandName) },
|
||||
)
|
||||
}
|
||||
|
||||
context { terminal = Terminal(theme = theme) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public record CommandSpec(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
boolean hidden,
|
||||
boolean noOp,
|
||||
Iterable<Option> options,
|
||||
List<CommandSpec> subcommands,
|
||||
ApplyFunction apply) {
|
||||
|
||||
public sealed interface Option {
|
||||
String name();
|
||||
|
||||
String[] getNames();
|
||||
|
||||
class MissingOption extends RuntimeException {
|
||||
public MissingOption() {}
|
||||
}
|
||||
|
||||
class BadValue extends RuntimeException {
|
||||
public BadValue(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public static BadValue invalid(String value, String type) {
|
||||
return new BadValue(String.format("%s is not a valid %s", value, type));
|
||||
}
|
||||
|
||||
public static BadValue badKeyValue(String value) {
|
||||
return new BadValue(String.format("%s is not a valid key=value pair", value));
|
||||
}
|
||||
|
||||
public static BadValue invalidChoice(String value, List<String> choices) {
|
||||
return new BadValue(
|
||||
String.format(
|
||||
"invalid choice: %s. (choose from %s)", value, String.join(", ", choices)));
|
||||
}
|
||||
|
||||
public static BadValue invalidChoice(String value, String choice) {
|
||||
return new BadValue(String.format("invalid choice: %s. (choose from %s)", value, choice));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static sealed class CompletionCandidates {
|
||||
public static final CompletionCandidates PATH = new StaticCompletionCandidates();
|
||||
|
||||
public static final class Fixed extends CompletionCandidates {
|
||||
private final Set<String> values;
|
||||
|
||||
public Fixed(Set<String> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public Set<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StaticCompletionCandidates extends CompletionCandidates {}
|
||||
}
|
||||
|
||||
public record Flag(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
boolean showAsRequired,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
@Nullable String shortName,
|
||||
String metavar,
|
||||
boolean hidden,
|
||||
@Nullable String defaultValue)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record BooleanFlag(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
@Nullable String shortName,
|
||||
boolean hidden,
|
||||
@Nullable Boolean defaultValue)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record CountedFlag(
|
||||
String name, @Nullable String helpText, @Nullable String shortName, boolean hidden)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return shortName == null
|
||||
? new String[] {"--" + name}
|
||||
: new String[] {"--" + name, "-" + shortName};
|
||||
}
|
||||
}
|
||||
|
||||
public record Argument(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
boolean repeated)
|
||||
implements Option {
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[] {name};
|
||||
}
|
||||
}
|
||||
|
||||
public interface ApplyFunction {
|
||||
State apply(Map<String, Object> options, @Nullable State parent);
|
||||
}
|
||||
|
||||
public record State(Object contents, Function<Object, Result> reify) {
|
||||
public Result evaluate() {
|
||||
return reify.apply(contents);
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(byte[] outputBytes, Map<String, FileOutput> outputFiles) {}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import org.pkl.core.runtime.VmEvalException;
|
||||
|
||||
/**
|
||||
@@ -224,6 +225,20 @@ public interface Evaluator extends AutoCloseable {
|
||||
*/
|
||||
TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite);
|
||||
|
||||
/**
|
||||
* Parses the command module into a spec that describes the CLI options and subcommands.
|
||||
*
|
||||
* <p>This requires that the target module be a {@code "pkl:Command"} instance.
|
||||
*
|
||||
* <p>Unlike other evaluator methods, the resulting {@link CommandSpec} must be handled in a
|
||||
* closure. This is because specs must be applied to parsed CLI options to produce command state
|
||||
* that is eventually evaluated, which must happen in the active context of an evaluator.
|
||||
*
|
||||
* @throws PklException if an error occurs during evaluation
|
||||
* @throws IllegalStateException if this evaluator has already been closed
|
||||
*/
|
||||
void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run);
|
||||
|
||||
/**
|
||||
* Releases all resources held by this evaluator. If an {@code evaluate} method is currently
|
||||
* executing, this method blocks until cancellation of that execution has completed.
|
||||
|
||||
@@ -20,11 +20,11 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import org.graalvm.polyglot.Context;
|
||||
@@ -40,6 +40,7 @@ import org.pkl.core.packages.PackageResolver;
|
||||
import org.pkl.core.project.DeclaredDependencies;
|
||||
import org.pkl.core.resource.ResourceReader;
|
||||
import org.pkl.core.runtime.BaseModule;
|
||||
import org.pkl.core.runtime.CommandSpecParser;
|
||||
import org.pkl.core.runtime.Identifier;
|
||||
import org.pkl.core.runtime.ModuleResolver;
|
||||
import org.pkl.core.runtime.ResourceManager;
|
||||
@@ -48,8 +49,6 @@ import org.pkl.core.runtime.VmContext;
|
||||
import org.pkl.core.runtime.VmException;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.runtime.VmLanguage;
|
||||
import org.pkl.core.runtime.VmMapping;
|
||||
import org.pkl.core.runtime.VmNull;
|
||||
import org.pkl.core.runtime.VmPklBinaryEncoder;
|
||||
import org.pkl.core.runtime.VmStackOverflowException;
|
||||
import org.pkl.core.runtime.VmTyped;
|
||||
@@ -148,7 +147,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
return VmUtils.readTextProperty(output);
|
||||
});
|
||||
}
|
||||
@@ -157,7 +156,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var vmBytes = VmUtils.readBytesProperty(output);
|
||||
return vmBytes.export();
|
||||
});
|
||||
@@ -168,7 +167,7 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||
if (value instanceof VmValue vmValue) {
|
||||
vmValue.force(false);
|
||||
@@ -183,20 +182,9 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var filesOrNull = VmUtils.readMember(output, Identifier.FILES);
|
||||
if (filesOrNull instanceof VmNull) {
|
||||
return Map.of();
|
||||
}
|
||||
var files = (VmMapping) filesOrNull;
|
||||
var result = new LinkedHashMap<String, FileOutput>();
|
||||
files.forceAndIterateMemberValues(
|
||||
(key, member, value) -> {
|
||||
assert member.isEntry();
|
||||
result.put((String) key, new FileOutputImpl(this, (VmTyped) value));
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
return VmUtils.readFilesProperty(
|
||||
output, (fileOutput) -> new FileOutputImpl(this, fileOutput));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,10 +227,10 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
var expressionResult =
|
||||
switch (expression) {
|
||||
case "module" -> module;
|
||||
case "output.text" -> VmUtils.readTextProperty(readModuleOutput(module));
|
||||
case "output.text" -> VmUtils.readTextProperty(VmUtils.readModuleOutput(module));
|
||||
case "output.value" ->
|
||||
VmUtils.readMember(readModuleOutput(module), Identifier.VALUE);
|
||||
case "output.bytes" -> VmUtils.readBytesProperty(readModuleOutput(module));
|
||||
VmUtils.readMember(VmUtils.readModuleOutput(module), Identifier.VALUE);
|
||||
case "output.bytes" -> VmUtils.readBytesProperty(VmUtils.readModuleOutput(module));
|
||||
default ->
|
||||
VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver);
|
||||
};
|
||||
@@ -289,12 +277,27 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateCommand(ModuleSource moduleSource, Consumer<CommandSpec> run) {
|
||||
doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var commandRunner =
|
||||
new CommandSpecParser(
|
||||
moduleResolver,
|
||||
securityManager,
|
||||
(fileOutput) -> new FileOutputImpl(this, fileOutput));
|
||||
run.accept(commandRunner.parse(module));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo) {
|
||||
return doEvaluate(
|
||||
moduleSource,
|
||||
(module) -> {
|
||||
var output = readModuleOutput(module);
|
||||
var output = VmUtils.readModuleOutput(module);
|
||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||
var valueClassInfo = VmUtils.getClass(value).getPClassInfo();
|
||||
if (valueClassInfo.equals(classInfo)) {
|
||||
@@ -367,6 +370,9 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
} catch (VmException e) {
|
||||
handleTimeout(timeoutTask);
|
||||
throw e.toPklException(frameTransformer, color);
|
||||
} catch (PklException e) {
|
||||
// evaluateCommand can throw PklException from the CLI layer, pass them through
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new PklBugException(e);
|
||||
} catch (ExceptionInInitializerError e) {
|
||||
@@ -420,32 +426,6 @@ public final class EvaluatorImpl implements Evaluator {
|
||||
"evaluationTimedOut", (timeout.getSeconds() + timeout.getNano() / 1_000_000_000d)));
|
||||
}
|
||||
|
||||
private VmTyped readModuleOutput(VmTyped module) {
|
||||
var value = VmUtils.readMember(module, Identifier.OUTPUT);
|
||||
if (value instanceof VmTyped typedOutput
|
||||
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
|
||||
return typedOutput;
|
||||
}
|
||||
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
var builder =
|
||||
new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"invalidModuleOutput",
|
||||
"output",
|
||||
PClassInfo.ModuleOutput.getDisplayName(),
|
||||
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
|
||||
moduleUri);
|
||||
var outputMember = module.getMember(Identifier.OUTPUT);
|
||||
assert outputMember != null;
|
||||
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
|
||||
// If `output` was explicitly re-assigned, show that in the stack trace.
|
||||
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
|
||||
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
|
||||
}
|
||||
throw builder.build();
|
||||
}
|
||||
|
||||
private VmException moduleOutputValueTypeMismatch(
|
||||
VmTyped module, PClassInfo<?> expectedClassInfo, Object value, VmTyped output) {
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -146,6 +146,10 @@ public final class VmModifier {
|
||||
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0;
|
||||
}
|
||||
|
||||
public static boolean isLocalOrExternalOrAbstractOrFixedOrConst(int modifiers) {
|
||||
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT | FIXED | CONST)) != 0;
|
||||
}
|
||||
|
||||
public static boolean isConstOrFixed(int modifiers) {
|
||||
return (modifiers & (CONST | FIXED)) != 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,6 +45,10 @@ public abstract class AmendModuleNode extends SpecializedObjectLiteralNode {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public ModuleInfo getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TruffleBoundary
|
||||
protected AmendModuleNode copy(ExpressionNode newParentNode) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.pkl.core.ast.member;
|
||||
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pkl.core.runtime.Identifier;
|
||||
import org.pkl.core.runtime.VmClass;
|
||||
@@ -53,6 +54,33 @@ public abstract class ClassMember extends Member {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public List<VmTyped> getAllAnnotations(boolean ascending) {
|
||||
var annotations = new ArrayList<VmTyped>();
|
||||
|
||||
if (ascending) {
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
|
||||
}
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the prototype of the class that declares this member. */
|
||||
public final VmTyped getOwner() {
|
||||
return owner;
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package org.pkl.core.ast.member;
|
||||
|
||||
import com.oracle.truffle.api.source.SourceSection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.pkl.core.Member.SourceLocation;
|
||||
import org.pkl.core.PClass;
|
||||
@@ -54,33 +53,6 @@ public final class ClassProperty extends ClassMember {
|
||||
this.initializer = initializer;
|
||||
}
|
||||
|
||||
public List<VmTyped> getAllAnnotations(boolean ascending) {
|
||||
var annotations = new ArrayList<VmTyped>();
|
||||
|
||||
if (ascending) {
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doGetAllAnnotationsDescending(getDeclaringClass(), annotations);
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private void doGetAllAnnotationsDescending(VmClass clazz, List<VmTyped> annotations) {
|
||||
if (clazz.getSuperclass() != null) {
|
||||
doGetAllAnnotationsDescending(clazz.getSuperclass(), annotations);
|
||||
}
|
||||
var p = clazz.getDeclaredProperty(getName());
|
||||
if (p != null) {
|
||||
annotations.addAll(p.getAnnotations());
|
||||
}
|
||||
}
|
||||
|
||||
public VmSet getAllModifierMirrors() {
|
||||
var mods = 0;
|
||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
||||
|
||||
@@ -57,6 +57,10 @@ import org.pkl.core.util.Nullable;
|
||||
|
||||
public abstract class TypeNode extends PklNode {
|
||||
|
||||
public interface ClassTypeNode {
|
||||
VmClass getVmClass();
|
||||
}
|
||||
|
||||
protected TypeNode(SourceSection sourceSection) {
|
||||
super(sourceSection);
|
||||
}
|
||||
@@ -402,7 +406,8 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
/** The `module` type for a final module. */
|
||||
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class FinalModuleTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
private final VmClass moduleClass;
|
||||
|
||||
public FinalModuleTypeNode(SourceSection sourceSection, VmClass moduleClass) {
|
||||
@@ -456,7 +461,8 @@ public abstract class TypeNode extends PklNode {
|
||||
}
|
||||
|
||||
/** The `module` type for an open module. */
|
||||
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class NonFinalModuleTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
private final VmClass moduleClass; // only used by getVmClass()
|
||||
@Child private ExpressionNode getModuleNode;
|
||||
|
||||
@@ -641,7 +647,7 @@ public abstract class TypeNode extends PklNode {
|
||||
* String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type
|
||||
* check.
|
||||
*/
|
||||
public static final class FinalClassTypeNode extends ObjectSlotTypeNode {
|
||||
public static final class FinalClassTypeNode extends ObjectSlotTypeNode implements ClassTypeNode {
|
||||
private final VmClass clazz;
|
||||
|
||||
public FinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
||||
@@ -697,7 +703,8 @@ public abstract class TypeNode extends PklNode {
|
||||
* String/Boolean/Int/Float and their supertypes, only {@link VmValue}s can possibly pass its type
|
||||
* check.
|
||||
*/
|
||||
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode {
|
||||
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode
|
||||
implements ClassTypeNode {
|
||||
protected final VmClass clazz;
|
||||
|
||||
public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
||||
@@ -1088,6 +1095,14 @@ public abstract class TypeNode extends PklNode {
|
||||
|
||||
return unionDefault;
|
||||
}
|
||||
|
||||
public Set<String> getStringLiterals() {
|
||||
return stringLiterals;
|
||||
}
|
||||
|
||||
public @Nullable String getUnionDefault() {
|
||||
return unionDefault;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -1421,6 +1436,10 @@ public abstract class TypeNode extends PklNode {
|
||||
return BaseModule.getMapClass();
|
||||
}
|
||||
|
||||
public TypeNode getKeyTypeNode() {
|
||||
return keyTypeNode;
|
||||
}
|
||||
|
||||
public TypeNode getValueTypeNode() {
|
||||
return valueTypeNode;
|
||||
}
|
||||
@@ -2131,6 +2150,14 @@ public abstract class TypeNode extends PklNode {
|
||||
protected boolean isParametric() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public TypeNode getFirstTypeNode() {
|
||||
return firstTypeNode;
|
||||
}
|
||||
|
||||
public TypeNode getSecondTypeNode() {
|
||||
return secondTypeNode;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VarArgsTypeNode extends ObjectSlotTypeNode {
|
||||
@@ -2313,6 +2340,10 @@ public abstract class TypeNode extends PklNode {
|
||||
protected final boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||
return consumer.accept(this);
|
||||
}
|
||||
|
||||
public long getMask() {
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
|
||||
@@ -2505,6 +2536,10 @@ public abstract class TypeNode extends PklNode {
|
||||
aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes);
|
||||
}
|
||||
|
||||
public TypeNode getAliasedTypeNode() {
|
||||
return aliasedTypeNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameSlotKind getFrameSlotKind() {
|
||||
return aliasedTypeNode.getFrameSlotKind();
|
||||
@@ -2670,6 +2705,10 @@ public abstract class TypeNode extends PklNode {
|
||||
this.constraintNodes = constraintNodes;
|
||||
}
|
||||
|
||||
public TypeNode getChildTypeNode() {
|
||||
return childNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FrameSlotKind getFrameSlotKind() {
|
||||
return childNode.getFrameSlotKind();
|
||||
|
||||
@@ -227,10 +227,26 @@ public final class BaseModule extends StdLibModule {
|
||||
return MixinTypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUIntTypeAlias() {
|
||||
return UIntTypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt8TypeAlias() {
|
||||
return UInt8TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt16TypeAlias() {
|
||||
return UInt16TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getUInt32TypeAlias() {
|
||||
return UInt32TypeAlias.instance;
|
||||
}
|
||||
|
||||
public static VmTypeAlias getCharTypeAlias() {
|
||||
return CharTypeAlias.instance;
|
||||
}
|
||||
|
||||
private static final class AnyClass {
|
||||
static final VmClass instance = loadClass("Any");
|
||||
}
|
||||
@@ -403,10 +419,26 @@ public final class BaseModule extends StdLibModule {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Int32");
|
||||
}
|
||||
|
||||
private static final class UIntTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt");
|
||||
}
|
||||
|
||||
private static final class UInt8TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt8");
|
||||
}
|
||||
|
||||
private static final class UInt16TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt16");
|
||||
}
|
||||
|
||||
private static final class UInt32TypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("UInt32");
|
||||
}
|
||||
|
||||
private static final class CharTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Char");
|
||||
}
|
||||
|
||||
private static final class MixinTypeAlias {
|
||||
static final VmTypeAlias instance = loadTypeAlias("Mixin");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import java.net.URI;
|
||||
|
||||
public final class CommandModule extends StdLibModule {
|
||||
static final VmTyped instance = VmUtils.createEmptyModule();
|
||||
|
||||
static {
|
||||
loadModule(URI.create("pkl:Command"), instance);
|
||||
}
|
||||
|
||||
private CommandModule() {}
|
||||
|
||||
public static VmTyped getModule() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static VmClass getCommandInfoClass() {
|
||||
return CommandInfoClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getBaseFlagClass() {
|
||||
return BaseFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getFlagClass() {
|
||||
return FlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getBooleanFlagClass() {
|
||||
return BooleanFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getCountedFlagClass() {
|
||||
return CountedFlagClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getArgumentClass() {
|
||||
return ArgumentClass.instance;
|
||||
}
|
||||
|
||||
public static VmClass getImportClass() {
|
||||
return ImportClass.instance;
|
||||
}
|
||||
|
||||
private static final class CommandInfoClass {
|
||||
static final VmClass instance = loadClass("CommandInfo");
|
||||
}
|
||||
|
||||
private static final class BaseFlagClass {
|
||||
static final VmClass instance = loadClass("BaseFlag");
|
||||
}
|
||||
|
||||
private static final class FlagClass {
|
||||
static final VmClass instance = loadClass("Flag");
|
||||
}
|
||||
|
||||
private static final class BooleanFlagClass {
|
||||
static final VmClass instance = loadClass("BooleanFlag");
|
||||
}
|
||||
|
||||
private static final class CountedFlagClass {
|
||||
static final VmClass instance = loadClass("CountedFlag");
|
||||
}
|
||||
|
||||
private static final class ArgumentClass {
|
||||
static final VmClass instance = loadClass("Argument");
|
||||
}
|
||||
|
||||
private static final class ImportClass {
|
||||
static final VmClass instance = loadClass("Import");
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private static VmClass loadClass(String className) {
|
||||
var theModule = getModule();
|
||||
return (VmClass) VmUtils.readMember(theModule, Identifier.get(className));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -147,6 +147,22 @@ public final class Identifier implements Comparable<Identifier> {
|
||||
// members of pkl.yaml
|
||||
public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases");
|
||||
|
||||
// members of pkl.Command
|
||||
public static final Identifier OPTIONS = get("options");
|
||||
public static final Identifier PARENT = get("parent");
|
||||
public static final Identifier COMMAND = get("command");
|
||||
public static final Identifier DESCRIPTION = get("description");
|
||||
public static final Identifier HIDE = get("hide");
|
||||
public static final Identifier NOOP = get("noOp");
|
||||
public static final Identifier SUBCOMMANDS = get("subcommands");
|
||||
public static final Identifier SHORT_NAME = get("shortName");
|
||||
public static final Identifier METAVAR = get("metavar");
|
||||
public static final Identifier MULTIPLE = get("multiple");
|
||||
public static final Identifier CONVERT = get("convert");
|
||||
public static final Identifier TRANSFORM_ALL = get("transformAll");
|
||||
public static final Identifier GLOB = get("glob");
|
||||
public static final Identifier COMPLETION_CANDIDATES = get("completionCandidates");
|
||||
|
||||
// common in lambdas etc
|
||||
public static final Identifier IT = get("it");
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ public final class ModuleCache {
|
||||
return BaseModule.getModule();
|
||||
case "Benchmark":
|
||||
return BenchmarkModule.getModule();
|
||||
case "Command":
|
||||
return CommandModule.getModule();
|
||||
case "jsonnet":
|
||||
return JsonnetModule.getModule();
|
||||
case "math":
|
||||
|
||||
@@ -69,7 +69,7 @@ public final class TestRunner {
|
||||
var resultsBuilder = new TestResults.Builder(info.getModuleName(), getDisplayUri(info));
|
||||
|
||||
try {
|
||||
checkAmendsPklTest(testModule);
|
||||
VmUtils.checkAmends(testModule, TestModule.getModule().getVmClass());
|
||||
} catch (VmException v) {
|
||||
var error =
|
||||
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
|
||||
@@ -83,17 +83,6 @@ public final class TestRunner {
|
||||
return resultsBuilder.build();
|
||||
}
|
||||
|
||||
private void checkAmendsPklTest(VmTyped value) {
|
||||
var testModuleClass = TestModule.getModule().getVmClass();
|
||||
var moduleClass = value.getVmClass();
|
||||
while (moduleClass != testModuleClass) {
|
||||
moduleClass = moduleClass.getSuperclass();
|
||||
if (moduleClass == null) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value, testModuleClass).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TestSectionResults runFacts(VmTyped testModule) {
|
||||
var facts = VmUtils.readMember(testModule, Identifier.FACTS);
|
||||
if (facts instanceof VmNull) return new TestSectionResults(TestSectionName.FACTS, List.of());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -30,12 +30,14 @@ import java.net.URI;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.graalvm.polyglot.Engine;
|
||||
import org.organicdesign.fp.collections.ImMap;
|
||||
import org.pkl.core.FileOutput;
|
||||
import org.pkl.core.PClassInfo;
|
||||
import org.pkl.core.PObject;
|
||||
import org.pkl.core.SecurityManager;
|
||||
@@ -179,6 +181,49 @@ public final class VmUtils {
|
||||
return (VmBytes) VmUtils.readMember(receiver, Identifier.BYTES);
|
||||
}
|
||||
|
||||
public static VmTyped readModuleOutput(VmTyped module) {
|
||||
var value = VmUtils.readMember(module, Identifier.OUTPUT);
|
||||
if (value instanceof VmTyped typedOutput
|
||||
&& typedOutput.getVmClass().getPClassInfo() == PClassInfo.ModuleOutput) {
|
||||
return typedOutput;
|
||||
}
|
||||
|
||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
||||
var builder =
|
||||
new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"invalidModuleOutput",
|
||||
"output",
|
||||
PClassInfo.ModuleOutput.getDisplayName(),
|
||||
VmUtils.getClass(value).getPClassInfo().getDisplayName(),
|
||||
moduleUri);
|
||||
var outputMember = module.getMember(Identifier.OUTPUT);
|
||||
assert outputMember != null;
|
||||
var uriOfValueMember = outputMember.getSourceSection().getSource().getURI();
|
||||
// If `output` was explicitly re-assigned, show that in the stack trace.
|
||||
if (!uriOfValueMember.equals(PClassInfo.pklBaseUri)) {
|
||||
builder.withSourceSection(outputMember.getBodySection()).withMemberName("output");
|
||||
}
|
||||
throw builder.build();
|
||||
}
|
||||
|
||||
public static Map<String, FileOutput> readFilesProperty(
|
||||
VmObjectLike receiver, Function<VmTyped, FileOutput> fileOutputFactory) {
|
||||
var filesOrNull = VmUtils.readMember(receiver, Identifier.FILES);
|
||||
if (filesOrNull instanceof VmNull) {
|
||||
return Map.of();
|
||||
}
|
||||
var files = (VmMapping) filesOrNull;
|
||||
var result = new LinkedHashMap<String, FileOutput>();
|
||||
files.forceAndIterateMemberValues(
|
||||
(key, member, value) -> {
|
||||
assert member.isEntry();
|
||||
result.put((String) key, fileOutputFactory.apply((VmTyped) value));
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public static Object readMember(VmObjectLike receiver, Object memberKey) {
|
||||
var result = readMemberOrNull(receiver, memberKey);
|
||||
@@ -855,7 +900,7 @@ public final class VmUtils {
|
||||
String expression,
|
||||
SecurityManager securityManager,
|
||||
ModuleResolver moduleResolver) {
|
||||
var syntheticModule = ModuleKeys.synthetic(URI.create(REPL_TEXT), expression);
|
||||
var syntheticModule = ModuleKeys.synthetic(REPL_TEXT_URI, expression);
|
||||
ResolvedModuleKey resolvedModule;
|
||||
try {
|
||||
resolvedModule = syntheticModule.resolve(securityManager);
|
||||
@@ -870,7 +915,7 @@ public final class VmUtils {
|
||||
source.createSection(0, source.getLength()),
|
||||
VmUtils.unavailableSourceSection(),
|
||||
null,
|
||||
"repl:text",
|
||||
REPL_TEXT,
|
||||
syntheticModule,
|
||||
resolvedModule,
|
||||
false);
|
||||
@@ -914,4 +959,20 @@ public final class VmUtils {
|
||||
public static String concat(String str1, String str2) {
|
||||
return str1 + str2;
|
||||
}
|
||||
|
||||
/** Check that a value is a VmTyped and that it inherits from the given class */
|
||||
public static VmTyped checkAmends(Object value, VmClass clazz) {
|
||||
if (!(value instanceof VmTyped typed)) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value, clazz).build();
|
||||
}
|
||||
return checkAmends(typed, clazz);
|
||||
}
|
||||
|
||||
/** Check that a typed value inherits from the given class */
|
||||
public static VmTyped checkAmends(VmTyped value, VmClass clazz) {
|
||||
if (!value.getVmClass().isSubclassOf(clazz)) {
|
||||
throw new VmExceptionBuilder().typeMismatch(value.getVmClass(), clazz).build();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -51,14 +51,14 @@ public final class MathNodes {
|
||||
public abstract static class minInt8 extends ExternalPropertyNode {
|
||||
@Specialization
|
||||
protected long eval(VmTyped self) {
|
||||
return -128;
|
||||
return Byte.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class maxInt8 extends ExternalPropertyNode {
|
||||
@Specialization
|
||||
protected long eval(VmTyped self) {
|
||||
return 127;
|
||||
return Byte.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -471,13 +471,15 @@ public final class GlobResolver {
|
||||
* Resolves a glob expression.
|
||||
*
|
||||
* <p>Each pair is the expanded form of the glob pattern, paired with its resolved absolute URI.
|
||||
*
|
||||
* <p>globPattern must be absolute if enclosing module details are null.
|
||||
*/
|
||||
@TruffleBoundary
|
||||
public static Map<String, ResolvedGlobElement> resolveGlob(
|
||||
SecurityManager securityManager,
|
||||
ReaderBase reader,
|
||||
ModuleKey enclosingModuleKey,
|
||||
URI enclosingUri,
|
||||
@Nullable ModuleKey enclosingModuleKey,
|
||||
@Nullable URI enclosingUri,
|
||||
String globPattern)
|
||||
throws IOException,
|
||||
SecurityManagerException,
|
||||
@@ -486,6 +488,10 @@ public final class GlobResolver {
|
||||
|
||||
var result = new LinkedHashMap<String, ResolvedGlobElement>();
|
||||
var hasAbsoluteGlob = globPattern.matches("\\w+:.*");
|
||||
if ((enclosingModuleKey == null || enclosingUri == null) && !hasAbsoluteGlob) {
|
||||
throw new PklBugException(
|
||||
"GlobResolver.resolveGlob() callers must check that the glob pattern is absolute if calling with null enclosing module info");
|
||||
}
|
||||
|
||||
if (reader.hasHierarchicalUris()) {
|
||||
var splitPattern = splitGlobPatternIntoBaseAndWildcards(reader, globPattern, hasAbsoluteGlob);
|
||||
@@ -493,7 +499,10 @@ public final class GlobResolver {
|
||||
var globParts = splitPattern.second;
|
||||
// short-circuit for glob pattern with no wildcards (can only match 0 or 1 element)
|
||||
if (globParts.length == 0) {
|
||||
var resolvedUri = IoUtils.resolve(reader, enclosingUri, globPattern);
|
||||
var resolvedUri =
|
||||
enclosingUri == null
|
||||
? URI.create(globPattern)
|
||||
: IoUtils.resolve(reader, enclosingUri, globPattern);
|
||||
if (reader.hasElement(securityManager, resolvedUri)) {
|
||||
result.put(globPattern, new ResolvedGlobElement(globPattern, resolvedUri, true));
|
||||
}
|
||||
@@ -501,7 +510,10 @@ public final class GlobResolver {
|
||||
}
|
||||
URI baseUri;
|
||||
try {
|
||||
baseUri = IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
|
||||
baseUri =
|
||||
enclosingModuleKey == null
|
||||
? URI.create(basePath)
|
||||
: IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
|
||||
} catch (URISyntaxException e) {
|
||||
// assertion: this is only thrown if the pattern starts with a triple-dot import.
|
||||
// the language will throw an error if glob imports is combined with triple-dots.
|
||||
|
||||
@@ -1076,3 +1076,54 @@ invalidStringBase64=\
|
||||
|
||||
characterCodingException=\
|
||||
Invalid bytes for charset "{0}".
|
||||
|
||||
commandSubcommandConflict=\
|
||||
Command `{0}` has subcommands with conflicting name "{1}".\n\
|
||||
Elements of `command.subcommands` must have unique `name` values.
|
||||
|
||||
commandMustNotAssignOrAmendProperty=\
|
||||
Commands must not assign or amend property `{0}`.
|
||||
|
||||
commandOptionNoTypeAnnotation=\
|
||||
No type annotation found for `{0}` property.
|
||||
|
||||
commandOptionsTypeNotClass=\
|
||||
Type annotation `{0}` on `options` property in `pkl:Command` subclass must be a class type.
|
||||
|
||||
commandOptionsTypeAbstractClass=\
|
||||
Command options class `{0}` may not be abstract.
|
||||
|
||||
commandOptionBothFlagAndArgument=\
|
||||
Found both `@Flag` and `@Argument` annotations for options property `{0}`.\n\
|
||||
\n\
|
||||
Only one option type may be specified.
|
||||
|
||||
commandOptionTypeNullableWithDefaultValue=\
|
||||
Unexpected option property `{0}` with nullable type and default value.\n\
|
||||
\n\
|
||||
Options with default values must not be nullable.
|
||||
|
||||
commandOptionUnsupportedType=\
|
||||
Command option property `{0}` has unsupported {1}type `{2}`.
|
||||
|
||||
commandOptionUnexpectedDefaultValue=\
|
||||
Unexpected default value for `@{1}` property `{0}`.\n\
|
||||
\n\
|
||||
{1}s may not specify a default value.
|
||||
|
||||
commandArgumentUnexpectedNonRepeatedNullableType=\
|
||||
Unexpected nullable type for non-collection `@Argument` property `{0}`.\n\
|
||||
\n\
|
||||
Arguments may not be nullable.
|
||||
|
||||
commandArgumentsMultipleRepeated=\
|
||||
More than one repeated option annotated with `@Argument` found: `{0}` and `{1}`.\n\
|
||||
\n\
|
||||
Only one repeated argument is permitted per command.
|
||||
|
||||
commandFlagHelpCollision=\
|
||||
Flag option `{0}` may not have name "help" or short name "h".
|
||||
|
||||
commandFlagInvalidType=\
|
||||
Option `{0}` with annotation `@{1}` has invalid type `{2}`.\n\
|
||||
Expected type: `{3}`
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ Available standard library modules:
|
||||
pkl:analyze
|
||||
pkl:base
|
||||
pkl:Benchmark
|
||||
pkl:Command
|
||||
pkl:DocPackageInfo
|
||||
pkl:DocsiteInfo
|
||||
pkl:EvaluatorSettings
|
||||
|
||||
@@ -0,0 +1,791 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.runtime
|
||||
|
||||
import java.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.writeString
|
||||
import org.pkl.core.CommandSpec
|
||||
import org.pkl.core.Evaluator
|
||||
import org.pkl.core.ModuleSource.uri
|
||||
import org.pkl.core.PklException
|
||||
|
||||
class CommandSpecParserTest {
|
||||
companion object {
|
||||
private val renderOptions =
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
import "pkl:Command"
|
||||
|
||||
options: Options
|
||||
|
||||
output {
|
||||
value = options
|
||||
}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
|
||||
private val evaluator = Evaluator.preconfigured()
|
||||
}
|
||||
|
||||
@TempDir private lateinit var tempDir: Path
|
||||
|
||||
private fun writePklFile(fileName: String, contents: String): URI {
|
||||
tempDir.resolve(fileName).createParentDirectories()
|
||||
return tempDir.resolve(fileName).writeString(contents).toUri()
|
||||
}
|
||||
|
||||
private fun parse(moduleUri: URI): CommandSpec {
|
||||
var spec: CommandSpec? = null
|
||||
evaluator.evaluateCommand(uri(moduleUri)) { spec = it }
|
||||
return spec!!
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `command module does not amend pkl_Command`() {
|
||||
val moduleUri = writePklFile("cmd.pkl", "")
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("Expected value of type `pkl.Command`, but got type")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options property assigned`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options = new {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options = ")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `options`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options property amended`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options {")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `options`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parent property assigned`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
parent = new {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("parent = ")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `parent`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parent property amended`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
parent {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("parent {")
|
||||
assertThat(exc.message).contains("Commands must not assign or amend property `parent`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options type annotation does not reference class`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options: "nope" | "try again"
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("options: \"nope\" | \"try again\"")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Type annotation `\"nope\" | \"try again\"` on `options` property in `pkl:Command` subclass must be a class type."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options class is abstract`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
options: Options
|
||||
abstract class Options {}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("abstract class Options {")
|
||||
assertThat(exc.message).contains("Command options class `cmd#Options` may not be abstract.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `command property value does not amend CommandInfo`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
command = new Foo {}
|
||||
class Foo
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("command = new Foo {}")
|
||||
assertThat(exc.message)
|
||||
.contains("Expected value of type `pkl.Command#CommandInfo`, but got type `cmd#Foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `first annotation of the same type wins`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
open class BaseOptions {
|
||||
/// foo in BaseOptions
|
||||
@Flag { shortName = "a" }
|
||||
foo: String
|
||||
|
||||
/// bar in BaseOptions
|
||||
@Flag { shortName = "b" }
|
||||
bar: String
|
||||
}
|
||||
class Options extends BaseOptions {
|
||||
/// bar in Options
|
||||
@Flag { shortName = "x" }
|
||||
bar: String
|
||||
|
||||
/// baz in Options
|
||||
@Flag { shortName = "y" }
|
||||
@CountedFlag { shortName = "z" }
|
||||
baz: Int
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val spec = parse(moduleUri)
|
||||
|
||||
// assert class overrides its superclass
|
||||
assertThat(spec.options.toList()[0]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[0] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("bar")
|
||||
assertThat(this.shortName).isEqualTo("x")
|
||||
assertThat(this.helpText).isEqualTo("bar in Options")
|
||||
}
|
||||
|
||||
// assert first flag annotation wins
|
||||
assertThat(spec.options.toList()[1]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[1] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("baz")
|
||||
assertThat(this.shortName).isEqualTo("y")
|
||||
assertThat(this.helpText).isEqualTo("baz in Options")
|
||||
}
|
||||
|
||||
// assert superclass options are inherited
|
||||
assertThat(spec.options.toList()[2]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[2] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("foo")
|
||||
assertThat(this.shortName).isEqualTo("a")
|
||||
assertThat(this.helpText).isEqualTo("foo in BaseOptions")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `@Flag and @Argument on the same option`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag
|
||||
@Argument
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Found both `@Flag` and `@Argument` annotations for options property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option with no type annotation`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo = \"bar\"")
|
||||
assertThat(exc.message).contains("No type annotation found for `foo` property.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable option with default not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: String? = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String? = \"bar\"")
|
||||
assertThat(exc.message)
|
||||
.contains("Unexpected option property `foo` with nullable type and default value")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option with union type containing non-string-literals`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: "oops" | String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: \"oops\" | String")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported type `\"oops\" | String`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `argument with default not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
foo: String = "bar"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String = \"bar\"")
|
||||
assertThat(exc.message).contains("Unexpected default value for `@Argument` property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable non-collection argument not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
foo: String?
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String?")
|
||||
assertThat(exc.message)
|
||||
.contains("Unexpected nullable type for non-collection `@Argument` property `foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-constant default values result in an optional flag with no default`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: String = "hi"
|
||||
bar: String = foo
|
||||
baz: Map<String, String> = Map()
|
||||
qux: Map<String, String> = baz
|
||||
quux: Int = 5
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val spec = parse(moduleUri)
|
||||
|
||||
assertThat(spec.options.toList()[0]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[0] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("foo")
|
||||
assertThat(this.defaultValue).isEqualTo("hi")
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[1]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[1] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("bar")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[2]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[2] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("baz")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[3]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[3] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("qux")
|
||||
assertThat(this.defaultValue).isNull()
|
||||
}
|
||||
|
||||
assertThat(spec.options.toList()[4]).isInstanceOf(CommandSpec.Flag::class.java)
|
||||
(spec.options.toList()[4] as CommandSpec.Flag).apply {
|
||||
assertThat(this.name).isEqualTo("quux")
|
||||
assertThat(this.defaultValue).isEqualTo("5")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flag with collision on --help`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
help: Boolean
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("help: Boolean")
|
||||
assertThat(exc.message)
|
||||
.contains("Flag option `help` may not have name \"help\" or short name \"h\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flag with collision on -h`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag { shortName = "h" }
|
||||
showHelp: Boolean
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("showHelp: Boolean")
|
||||
assertThat(exc.message)
|
||||
.contains("Flag option `showHelp` may not have name \"help\" or short name \"h\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple arguments with collection types not allowed`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Argument
|
||||
list: List<String>
|
||||
@Argument
|
||||
set: Set<String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("class Options {")
|
||||
assertThat(exc.message)
|
||||
.contains("More than one repeated option annotated with `@Argument` found: `list` and `set`.")
|
||||
assertThat(exc.message).contains("Only one repeated argument is permitted per command.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collection option with collection element type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: List<List<"a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: List<List<\"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported element type `List<\"a\" | \"b\">`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `collection option with map element type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: List<Map<String, "a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: List<Map<String, \"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported element type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with collection value type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<String, List<"a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<String, List<\"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported value type `List<\"a\" | \"b\">`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map value type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<String, Map<String, "a" | "b">>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<String, Map<String, \"a\" | \"b\">>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported value type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with collection key type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<Map<String, \"a\" | \"b\">, String>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported key type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map key type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map<Map<String, \"a\" | \"b\">, String>")
|
||||
assertThat(exc.message)
|
||||
.contains(
|
||||
"Command option property `foo` has unsupported key type `Map<String, \"a\" | \"b\">`."
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with map key type allowed with convert`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@Flag { convert = (it) -> Pair("foo", "a") }
|
||||
foo: Map<Map<String, "a" | "b">, String>
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
assertDoesNotThrow { parse(moduleUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unsupported option type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Foo
|
||||
}
|
||||
class Foo
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Foo")
|
||||
assertThat(exc.message).contains("Command option property `foo` has unsupported type `Foo`.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options constraints in all positions are erased`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
a: String(true)
|
||||
b: String?(true)
|
||||
c: String(true)?
|
||||
d: List<String(true)>
|
||||
e: List<String(true)>(true)
|
||||
f: List<String(true)>(true)?(true)
|
||||
g: (Map<String(true), String(true)>(true)?(true))(true)
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
parse(moduleUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `conflicting subcommand names`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
import "pkl:Command"
|
||||
command {
|
||||
subcommands {
|
||||
new Sub { command { name = "foo" } }
|
||||
new Sub { command { name = "foo" } }
|
||||
}
|
||||
}
|
||||
|
||||
class Sub extends Command
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("Command `cmd` has subcommands with conflicting name \"foo\".")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `list or set option with no type arguments`() {
|
||||
for (type in listOf("List", "Set")) {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd_$type.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: $type
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: $type")
|
||||
assertThat(exc.message)
|
||||
.contains("Command option property `foo` has unsupported type `$type`.")
|
||||
assertThat(exc.message).contains("$type options must provide one type argument.")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `map option with no type arguments`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
foo: Map
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: Map")
|
||||
assertThat(exc.message).contains("Command option property `foo` has unsupported type `Map`.")
|
||||
assertThat(exc.message).contains("Map options must provide two type arguments.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `boolean flag with incorrect type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@BooleanFlag
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Option `foo` with annotation `@BooleanFlag` has invalid type `String`.")
|
||||
assertThat(exc.message).contains("Expected type: `Boolean`")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `counted flag with incorrect type`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
renderOptions +
|
||||
"""
|
||||
class Options {
|
||||
@CountedFlag
|
||||
foo: String
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val exc = assertThrows<PklException> { parse(moduleUri) }
|
||||
assertThat(exc.message).contains("foo: String")
|
||||
assertThat(exc.message)
|
||||
.contains("Option `foo` with annotation `@CountedFlag` has invalid type `String`.")
|
||||
assertThat(exc.message).contains("Expected type: `Int`")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Defines inputs and outputs for CLI commands implemented in Pkl.
|
||||
///
|
||||
/// Modules extending `pkl:Command` may configure [ModuleOutput.text], [ModuleOutput.bytes], and/or
|
||||
/// [ModuleOutput.files] of [output] to influence the effect of the command.
|
||||
///
|
||||
/// Command modules should override [options] and provide their own class declaring options:
|
||||
/// ```
|
||||
/// extends "pkl:Command"
|
||||
///
|
||||
/// options: Options
|
||||
///
|
||||
/// class Options {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
@ModuleInfo { minPklVersion = "0.31.0" }
|
||||
open module pkl.Command
|
||||
|
||||
import "pkl:Command"
|
||||
import "pkl:reflect"
|
||||
|
||||
local commandClass = reflect.Class(getClass())
|
||||
|
||||
/// Command configuration.
|
||||
hidden command: CommandInfo = new {
|
||||
name = // choose the name of the module if the command is a module or the class otherwise
|
||||
if (commandClass.name == "ModuleClass") reflect.Module(module).name else commandClass.name
|
||||
description = commandClass.docComment // works for both classes and module classes
|
||||
}
|
||||
|
||||
/// Command line options.
|
||||
///
|
||||
/// This property is set by the runtime during command execution.
|
||||
/// It must not be amended or overridden by modules or classes extending [Command].
|
||||
///
|
||||
/// Command modules should override this property and provide their own options type.
|
||||
/// The properties of the specified type declare the command line flags and arguments accepted by
|
||||
/// the command.
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// extends "pkl:Command"
|
||||
///
|
||||
/// options: Options
|
||||
///
|
||||
/// class Options {
|
||||
/// /// Maximum number of tries to attempt operation before giving up.
|
||||
/// `max-tries`: UInt = 3
|
||||
///
|
||||
/// /// Duration after which operation will be timed out.
|
||||
/// @Flag { convert = module.convertDuration; metavar = "duration" }
|
||||
/// timeout: Duration = 30.s
|
||||
///
|
||||
/// /// Whether to use cache data locally.
|
||||
/// @BooleanFlag
|
||||
/// cache: Boolean = true
|
||||
///
|
||||
/// /// Log verbosity.
|
||||
/// @CountedFlag { shortName = "v" }
|
||||
/// verbose: Int
|
||||
///
|
||||
/// /// File paths to operate on.
|
||||
/// @Argument { completionCandidates = "paths" }
|
||||
/// path: Listing<String>
|
||||
/// }
|
||||
/// ```
|
||||
hidden options: Typed
|
||||
|
||||
/// The value of the parent command with parsed options.
|
||||
///
|
||||
/// This property is set by the runtime during command execution.
|
||||
/// It must not be amended or overridden by modules or classes extending [Command].
|
||||
///
|
||||
/// The parent command is `null` for root commands.
|
||||
hidden parent: Command?
|
||||
|
||||
/// The value of the root command with parsed options.
|
||||
///
|
||||
/// This property is set by the runtime during command execution.
|
||||
///
|
||||
/// The root command is `null` for root commands.
|
||||
hidden fixed root: Command? = parent?.root ?? parent
|
||||
|
||||
/// Command configuration.
|
||||
class CommandInfo {
|
||||
/// The name of the subcommand.
|
||||
///
|
||||
/// Default value: the name of the module or class extending [Command].
|
||||
name: String
|
||||
|
||||
/// A description of the command; shown in its CLI help.
|
||||
///
|
||||
/// Default value: the doc comment of the module or class extending [Command].
|
||||
description: String?
|
||||
|
||||
/// Hide this command from CLI help.
|
||||
hide: Boolean = false
|
||||
|
||||
/// If this command is executed, return an error and print CLI usage.
|
||||
///
|
||||
/// Only applicable to commands with [subcommands].
|
||||
///
|
||||
/// This is enabled by default when this command has [subcommands].
|
||||
/// Overriding it to `false` will allow this command to be executed directly.
|
||||
noOp: Boolean(implies(!subcommands.isEmpty)) = !subcommands.isEmpty
|
||||
|
||||
/// Child commands.
|
||||
///
|
||||
/// Must have unique [name] values.
|
||||
// NB: not using isDistinctBy constraint because the command runtime can give better errors
|
||||
subcommands: Listing<Command>
|
||||
}
|
||||
|
||||
/// Annotates [options] properties to configure them as named CLI flags.
|
||||
// NB: this should be a sealed class once Pkl supports them
|
||||
abstract class BaseFlag extends Annotation {
|
||||
/// Abbreviated flag name.
|
||||
shortName: Char?
|
||||
|
||||
/// Hide this option from CLI help.
|
||||
hide: Boolean = false
|
||||
}
|
||||
|
||||
/// Annotates an [options] property to configure it as a named CLI flag that accepts a value.
|
||||
class Flag extends BaseFlag {
|
||||
/// Text to use in place of the option value in CLI help.
|
||||
///
|
||||
/// If not specified, the value is derived from the flag's type.
|
||||
metavar: String?
|
||||
|
||||
/// Customize the behavior of parsing the raw option values.
|
||||
///
|
||||
/// When the return value is an [Import] value or a [Pair] member, [List] or [Set] element
|
||||
/// containing an [Import], the URI or glob URI specified by the value is imported and the value
|
||||
/// is replaced with the value of the imported module(s).
|
||||
///
|
||||
/// If no transform is provided, the raw flag value are parsed according to the option's type:
|
||||
///
|
||||
/// | Type | Behavior |
|
||||
/// | -------------------------- | -------- |
|
||||
/// | [String] | Value is used verbatim |
|
||||
/// | [Char] | Value is used verbatim; must be exactly one character |
|
||||
/// | [Boolean] | True values: `true`, `t`, `1`, `yes`, `y`, `on`; False values: `false`, `f`, `0`, `no`, `n`, `off` |
|
||||
/// | [Number] | Value is parsed as an [Int] if possible, otherwise parsed as [Float] |
|
||||
/// | [Float] | Value is parsed as a [Float] |
|
||||
/// | [Int] | Value is parsed as an [Int] |
|
||||
/// | [Int8], [Int16], [Int32], [UInt], [UInt8], [UInt16], [UInt32] | Value is parsed as an [Int] and must be within the type's range |
|
||||
/// | Union of [String] literals | Value is used verbatim; must match a member of the union |
|
||||
/// | [Listing], [List], [Set] | Element values are parsed based on the above primitive types |
|
||||
/// | [Mapping], [Map], [Pair] | Value is split into a [Pair] on the first `"="` and each substring is parsed based on the above primitive types |
|
||||
/// | Other types | An error is thrown; `convert` should be defined explicitly |
|
||||
convert: ((String) -> Any)?
|
||||
|
||||
/// Specifies whether the flag may be specified more than once.
|
||||
///
|
||||
/// If not specified, this is determined based on the option's type.
|
||||
/// Options with type [Listing], [List], [Set], [Mapping], or [Map] are mulitple by default.
|
||||
/// Overriding this behavior generally requires setting [convert] and/or [transformAll].
|
||||
multiple: Boolean?
|
||||
|
||||
/// Customize the behavior of turning all parsed flag values into the final option value.
|
||||
///
|
||||
/// If no value is provided, all flag values are transformed according to the option's type:
|
||||
///
|
||||
/// | Type | Behavior |
|
||||
/// | ----------------- | -------- |
|
||||
/// | [Mapping], [Map] | Each value must be a [Pair], each pair becomes an entry in the result. |
|
||||
/// | [Listing], [List] | Result is all option values in the order specified |
|
||||
/// | [Set] | Result is all unique option values |
|
||||
/// | Other types | Result is the last value specified option value |
|
||||
transformAll: ((List<Any>) -> Any)?
|
||||
|
||||
/// Specify how this flag should be completed in generated shell completions.
|
||||
///
|
||||
/// If set to `"paths"`, the completion candidates will be local file paths.
|
||||
/// If set to a [Listing], the completion candidates will be the specified literal strings.
|
||||
///
|
||||
/// Options with a string literal union type use the members of the union as completion candidates
|
||||
/// by default.
|
||||
completionCandidates: "paths" | *Listing<String>(isDistinct)
|
||||
}
|
||||
|
||||
/// Annotates [Boolean] [options] properties to configure them as named CLI flags that may be
|
||||
/// specified zero or one times.
|
||||
///
|
||||
/// Boolean flags produce a pair of flags in the form `--<name>`/`--no-<name>`.
|
||||
///
|
||||
/// Annotating a property with a type other than [Boolean] is an error.
|
||||
class BooleanFlag extends BaseFlag
|
||||
|
||||
/// Annotates [Integer] [options] properties to configure them as named CLI flags that may be
|
||||
/// specified zero or more times.
|
||||
///
|
||||
/// Counted flags produce a value equal to the number of times a flag is specified.
|
||||
///
|
||||
/// Annotating a property with a type other than [Int] is an error.
|
||||
class CountedFlag extends BaseFlag
|
||||
|
||||
/// Annotates an [options] property to configure it as a positional CLI argument.
|
||||
class Argument extends Annotation {
|
||||
/// Customize the behavior of turning the raw option values string into the final value.
|
||||
///
|
||||
/// When the return value is an [Import] value or a [Pair] member, [List] or [Set] element
|
||||
/// containing an [Import], the URI or glob URI specified by the value is imported and the value
|
||||
/// is replaced with the value of the imported module(s).
|
||||
///
|
||||
/// If no value is provided, each option value is transformed using the same rules as
|
||||
/// [Flag.convert].
|
||||
convert: ((String) -> Any)?
|
||||
|
||||
/// Specifies whether the argument may be specified more than once.
|
||||
///
|
||||
/// If not specified, this is determined based on the option's type.
|
||||
/// Options with type [Listing], [List], [Set], [Mapping], or [Map] are multiple by default.
|
||||
/// Overriding this behavior generally requires setting [convert] and/or [transformAll].
|
||||
///
|
||||
/// Only one argument per command may be multiple.
|
||||
multiple: Boolean?
|
||||
|
||||
/// Customize the behavior of turning all parsed flag values into the final option value.
|
||||
///
|
||||
/// If no value is provided, all option values are transformed using the same rules as
|
||||
/// [Flag.transformAll].
|
||||
transformAll: ((List<Any>) -> Any)?
|
||||
|
||||
/// Specify how this flag should be completed in generated shell completions.
|
||||
///
|
||||
/// If set to `"paths"`, the completion candidates will be local file paths.
|
||||
/// If set to a [Listing], the completion candidates will be the specified literal strings.
|
||||
///
|
||||
/// Options with a string literal union type use the members of the union as completion candidates
|
||||
/// by default.
|
||||
completionCandidates: "paths" | *Listing<String>(isDistinct)
|
||||
}
|
||||
|
||||
/// A value used in [Flag.convert] and [Argument.convert] to declare an option as a dynamic
|
||||
/// import.
|
||||
class Import {
|
||||
/// The absolute URI of the module to import.
|
||||
uri: String
|
||||
|
||||
/// Whether [uri] should be interpreted as a glob pattern.
|
||||
///
|
||||
/// When `false`, the replacement value is the value of the specified module.
|
||||
/// When `true`, the replacement value is a [Mapping] from [String] keys to matched module values.
|
||||
glob: Boolean = false
|
||||
}
|
||||
|
||||
local const quantityRegex = Regex(#"([0-9]+(?:\.[0-9]+)?)\.?([A-Za-z]+)"#)
|
||||
|
||||
local const function parseQuantity(value: String, typeName: String): Pair<Float, String> =
|
||||
let (match = quantityRegex.matchEntire(value))
|
||||
if (match == null)
|
||||
throw("Unable to parse \(typeName) from string '\(value)'")
|
||||
else
|
||||
Pair(match.groups[1].value.toFloat(), match.groups[2].value.toLowerCase())
|
||||
|
||||
/// A convert function for [Duration] values.
|
||||
///
|
||||
/// For use with [Flag.convert] and [Argument.convert].
|
||||
hidden const convertDuration: (String) -> Duration = (value: String) ->
|
||||
let (quantity = parseQuantity(value, "Duration"))
|
||||
let (_unit = quantity.second)
|
||||
let (unit = if (_unit is DurationUnit) _unit else null)
|
||||
quantity.first.toDuration(unit ?? throw("Unable to parse DurationUnit from '\(_unit)'"))
|
||||
|
||||
/// A convert function for [DataSize] values.
|
||||
///
|
||||
/// For use with [Flag.convert] and [Argument.convert].
|
||||
hidden const convertDataSize: (String) -> DataSize = (value: String) ->
|
||||
let (quantity = parseQuantity(value, "DataSize"))
|
||||
let (_unit = quantity.second)
|
||||
let (unit = if (_unit is DataSizeUnit) _unit else null)
|
||||
quantity.first.toDataSize(unit ?? throw("Unable to parse DataSizeUnit from '\(_unit)'"))
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
/// A template for writing tests.
|
||||
///
|
||||
/// To write tests, amend this module and define [facts] or [examples] (or both).
|
||||
/// To run tests, evaluate the amended module.
|
||||
/// To run tests, use the `pkl test` command to evaluate the amended module.
|
||||
@ModuleInfo { minPklVersion = "0.31.0" }
|
||||
open module pkl.test
|
||||
|
||||
|
||||
Reference in New Issue
Block a user