mirror of
https://github.com/apple/pkl.git
synced 2026-07-08 14:05:17 +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-pkldoc-example: {uri-pkl-examples-tree}/pkldoc
|
||||||
|
|
||||||
:uri-stdlib-baseModule: {uri-pkl-stdlib-docs}/base
|
: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-analyzeModule: {uri-pkl-stdlib-docs}/analyze
|
||||||
:uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet
|
:uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet
|
||||||
:uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect
|
:uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect
|
||||||
@@ -150,6 +151,13 @@ endif::[]
|
|||||||
:uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource
|
:uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource
|
||||||
:uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files
|
:uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files
|
||||||
:uri-stdlib-FileOutput: {uri-stdlib-baseModule}/FileOutput
|
: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: https://msgpack.org/index.html
|
||||||
:uri-messagepack-spec: https://github.com/msgpack/msgpack/blob/master/spec.md
|
: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
|
<1> equivalent to `email: String(contains("@"))` for type checking purposes
|
||||||
<2> equivalent to `emailList: List<String(contains("@"))>` for type checking purposes
|
<2> equivalent to `emailList: List<String(contains("@"))>` for type checking purposes
|
||||||
|
|
||||||
|
[[nullable-types]]
|
||||||
==== Nullable Types
|
==== Nullable Types
|
||||||
|
|
||||||
Class types such as `Bird` (see above) do not admit `null` values.
|
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.
|
Otherwise, failures result in exit code 1.
|
||||||
|
|
||||||
<modules>::
|
<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
|
==== 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>>.
|
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]]
|
[[command-repl]]
|
||||||
=== `pkl repl`
|
=== `pkl repl`
|
||||||
|
|
||||||
@@ -800,7 +819,7 @@ Write the path of files with formatting violations to stdout.
|
|||||||
[[common-options]]
|
[[common-options]]
|
||||||
=== 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[]
|
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.
|
By default, module outputs are separated with `---`, as in a YAML stream.
|
||||||
The separator can be customized using the `--module-output-separator` option.
|
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]]
|
[[repl]]
|
||||||
== Working with the 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 {
|
private fun Evaluator.evalOutput(moduleSource: ModuleSource): ByteArray {
|
||||||
if (options.expression == null) {
|
if (options.expression == null) {
|
||||||
return evaluateOutputBytes(moduleSource)
|
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) =
|
private fun toModuleSource(uri: URI, reader: InputStream) =
|
||||||
if (uri == VmUtils.REPL_TEXT_URI) {
|
if (uri == VmUtils.REPL_TEXT_URI) {
|
||||||
ModuleSource.create(uri, reader.readAllBytes().toString(StandardCharsets.UTF_8))
|
ModuleSource.create(uri, reader.readAllBytes().toString(StandardCharsets.UTF_8))
|
||||||
@@ -241,14 +223,6 @@ constructor(
|
|||||||
ModuleSource.uri(uri)
|
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
|
* Renders each module's `output.files`, writing each entry as a file into the specified output
|
||||||
* directory.
|
* 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -50,6 +50,7 @@ class RootCommand : NoOpCliktCommand(name = "pkl") {
|
|||||||
DownloadPackageCommand(),
|
DownloadPackageCommand(),
|
||||||
AnalyzeCommand(),
|
AnalyzeCommand(),
|
||||||
FormatterCommand(),
|
FormatterCommand(),
|
||||||
|
RunCommand(),
|
||||||
CompletionCommand(
|
CompletionCommand(
|
||||||
name = "shell-completion",
|
name = "shell-completion",
|
||||||
help = "Generate a completion script for the given shell",
|
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. */
|
/** Base options shared between CLI commands. */
|
||||||
data class CliBaseOptions(
|
data class CliBaseOptions(
|
||||||
/** The source modules to evaluate. Relative URIs are resolved against [workingDir]. */
|
/** 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
|
* The URI patterns that determine which modules can be loaded and evaluated. Patterns are matched
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.pkl.commons.cli
|
package org.pkl.commons.cli
|
||||||
|
|
||||||
|
import com.github.ajalt.clikt.core.CliktError
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@@ -46,6 +47,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
|||||||
proxyAddress?.let(IoUtils::setSystemProxy)
|
proxyAddress?.let(IoUtils::setSystemProxy)
|
||||||
doRun()
|
doRun()
|
||||||
} catch (e: PklException) {
|
} catch (e: PklException) {
|
||||||
|
if (e.cause is CliktError) throw e.cause!!
|
||||||
throw CliException(e.message!!)
|
throw CliException(e.message!!)
|
||||||
} catch (e: CliException) {
|
} catch (e: CliException) {
|
||||||
throw e
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -27,14 +27,16 @@ import org.pkl.core.Release
|
|||||||
|
|
||||||
private val theme = Theme { styles["markdown.code.span"] = TextStyle(bold = true) }
|
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()
|
installMordantMarkdown()
|
||||||
|
|
||||||
versionOption(
|
if (includeVersion) {
|
||||||
Release.current().versionInfo,
|
versionOption(
|
||||||
names = setOf("-v", "--version"),
|
Release.current().versionInfo,
|
||||||
message = { if (commandName == "pkl") it else it.replaceFirst("Pkl", commandName) },
|
names = setOf("-v", "--version"),
|
||||||
)
|
message = { if (commandName == "pkl") it else it.replaceFirst("Pkl", commandName) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
context { terminal = Terminal(theme = theme) }
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
package org.pkl.core;
|
package org.pkl.core;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import org.pkl.core.runtime.VmEvalException;
|
import org.pkl.core.runtime.VmEvalException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -224,6 +225,20 @@ public interface Evaluator extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
TestResults evaluateTest(ModuleSource moduleSource, boolean overwrite);
|
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
|
* 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.
|
* 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.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import org.graalvm.polyglot.Context;
|
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.project.DeclaredDependencies;
|
||||||
import org.pkl.core.resource.ResourceReader;
|
import org.pkl.core.resource.ResourceReader;
|
||||||
import org.pkl.core.runtime.BaseModule;
|
import org.pkl.core.runtime.BaseModule;
|
||||||
|
import org.pkl.core.runtime.CommandSpecParser;
|
||||||
import org.pkl.core.runtime.Identifier;
|
import org.pkl.core.runtime.Identifier;
|
||||||
import org.pkl.core.runtime.ModuleResolver;
|
import org.pkl.core.runtime.ModuleResolver;
|
||||||
import org.pkl.core.runtime.ResourceManager;
|
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.VmException;
|
||||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||||
import org.pkl.core.runtime.VmLanguage;
|
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.VmPklBinaryEncoder;
|
||||||
import org.pkl.core.runtime.VmStackOverflowException;
|
import org.pkl.core.runtime.VmStackOverflowException;
|
||||||
import org.pkl.core.runtime.VmTyped;
|
import org.pkl.core.runtime.VmTyped;
|
||||||
@@ -148,7 +147,7 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
return doEvaluate(
|
return doEvaluate(
|
||||||
moduleSource,
|
moduleSource,
|
||||||
(module) -> {
|
(module) -> {
|
||||||
var output = readModuleOutput(module);
|
var output = VmUtils.readModuleOutput(module);
|
||||||
return VmUtils.readTextProperty(output);
|
return VmUtils.readTextProperty(output);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -157,7 +156,7 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
return doEvaluate(
|
return doEvaluate(
|
||||||
moduleSource,
|
moduleSource,
|
||||||
(module) -> {
|
(module) -> {
|
||||||
var output = readModuleOutput(module);
|
var output = VmUtils.readModuleOutput(module);
|
||||||
var vmBytes = VmUtils.readBytesProperty(output);
|
var vmBytes = VmUtils.readBytesProperty(output);
|
||||||
return vmBytes.export();
|
return vmBytes.export();
|
||||||
});
|
});
|
||||||
@@ -168,7 +167,7 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
return doEvaluate(
|
return doEvaluate(
|
||||||
moduleSource,
|
moduleSource,
|
||||||
(module) -> {
|
(module) -> {
|
||||||
var output = readModuleOutput(module);
|
var output = VmUtils.readModuleOutput(module);
|
||||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||||
if (value instanceof VmValue vmValue) {
|
if (value instanceof VmValue vmValue) {
|
||||||
vmValue.force(false);
|
vmValue.force(false);
|
||||||
@@ -183,20 +182,9 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
return doEvaluate(
|
return doEvaluate(
|
||||||
moduleSource,
|
moduleSource,
|
||||||
(module) -> {
|
(module) -> {
|
||||||
var output = readModuleOutput(module);
|
var output = VmUtils.readModuleOutput(module);
|
||||||
var filesOrNull = VmUtils.readMember(output, Identifier.FILES);
|
return VmUtils.readFilesProperty(
|
||||||
if (filesOrNull instanceof VmNull) {
|
output, (fileOutput) -> new FileOutputImpl(this, fileOutput));
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,10 +227,10 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
var expressionResult =
|
var expressionResult =
|
||||||
switch (expression) {
|
switch (expression) {
|
||||||
case "module" -> module;
|
case "module" -> module;
|
||||||
case "output.text" -> VmUtils.readTextProperty(readModuleOutput(module));
|
case "output.text" -> VmUtils.readTextProperty(VmUtils.readModuleOutput(module));
|
||||||
case "output.value" ->
|
case "output.value" ->
|
||||||
VmUtils.readMember(readModuleOutput(module), Identifier.VALUE);
|
VmUtils.readMember(VmUtils.readModuleOutput(module), Identifier.VALUE);
|
||||||
case "output.bytes" -> VmUtils.readBytesProperty(readModuleOutput(module));
|
case "output.bytes" -> VmUtils.readBytesProperty(VmUtils.readModuleOutput(module));
|
||||||
default ->
|
default ->
|
||||||
VmUtils.evaluateExpression(module, expression, securityManager, moduleResolver);
|
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
|
@Override
|
||||||
public <T> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo) {
|
public <T> T evaluateOutputValueAs(ModuleSource moduleSource, PClassInfo<T> classInfo) {
|
||||||
return doEvaluate(
|
return doEvaluate(
|
||||||
moduleSource,
|
moduleSource,
|
||||||
(module) -> {
|
(module) -> {
|
||||||
var output = readModuleOutput(module);
|
var output = VmUtils.readModuleOutput(module);
|
||||||
var value = VmUtils.readMember(output, Identifier.VALUE);
|
var value = VmUtils.readMember(output, Identifier.VALUE);
|
||||||
var valueClassInfo = VmUtils.getClass(value).getPClassInfo();
|
var valueClassInfo = VmUtils.getClass(value).getPClassInfo();
|
||||||
if (valueClassInfo.equals(classInfo)) {
|
if (valueClassInfo.equals(classInfo)) {
|
||||||
@@ -367,6 +370,9 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
} catch (VmException e) {
|
} catch (VmException e) {
|
||||||
handleTimeout(timeoutTask);
|
handleTimeout(timeoutTask);
|
||||||
throw e.toPklException(frameTransformer, color);
|
throw e.toPklException(frameTransformer, color);
|
||||||
|
} catch (PklException e) {
|
||||||
|
// evaluateCommand can throw PklException from the CLI layer, pass them through
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new PklBugException(e);
|
throw new PklBugException(e);
|
||||||
} catch (ExceptionInInitializerError e) {
|
} catch (ExceptionInInitializerError e) {
|
||||||
@@ -420,32 +426,6 @@ public final class EvaluatorImpl implements Evaluator {
|
|||||||
"evaluationTimedOut", (timeout.getSeconds() + timeout.getNano() / 1_000_000_000d)));
|
"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(
|
private VmException moduleOutputValueTypeMismatch(
|
||||||
VmTyped module, PClassInfo<?> expectedClassInfo, Object value, VmTyped output) {
|
VmTyped module, PClassInfo<?> expectedClassInfo, Object value, VmTyped output) {
|
||||||
var moduleUri = module.getModuleInfo().getModuleKey().getUri();
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -146,6 +146,10 @@ public final class VmModifier {
|
|||||||
return (modifiers & (LOCAL | EXTERNAL | ABSTRACT)) != 0;
|
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) {
|
public static boolean isConstOrFixed(int modifiers) {
|
||||||
return (modifiers & (CONST | FIXED)) != 0;
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -45,6 +45,10 @@ public abstract class AmendModuleNode extends SpecializedObjectLiteralNode {
|
|||||||
this.moduleInfo = moduleInfo;
|
this.moduleInfo = moduleInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ModuleInfo getModuleInfo() {
|
||||||
|
return moduleInfo;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@TruffleBoundary
|
@TruffleBoundary
|
||||||
protected AmendModuleNode copy(ExpressionNode newParentNode) {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
package org.pkl.core.ast.member;
|
package org.pkl.core.ast.member;
|
||||||
|
|
||||||
import com.oracle.truffle.api.source.SourceSection;
|
import com.oracle.truffle.api.source.SourceSection;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.pkl.core.runtime.Identifier;
|
import org.pkl.core.runtime.Identifier;
|
||||||
import org.pkl.core.runtime.VmClass;
|
import org.pkl.core.runtime.VmClass;
|
||||||
@@ -53,6 +54,33 @@ public abstract class ClassMember extends Member {
|
|||||||
return annotations;
|
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. */
|
/** Returns the prototype of the class that declares this member. */
|
||||||
public final VmTyped getOwner() {
|
public final VmTyped getOwner() {
|
||||||
return owner;
|
return owner;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
package org.pkl.core.ast.member;
|
package org.pkl.core.ast.member;
|
||||||
|
|
||||||
import com.oracle.truffle.api.source.SourceSection;
|
import com.oracle.truffle.api.source.SourceSection;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.pkl.core.Member.SourceLocation;
|
import org.pkl.core.Member.SourceLocation;
|
||||||
import org.pkl.core.PClass;
|
import org.pkl.core.PClass;
|
||||||
@@ -54,33 +53,6 @@ public final class ClassProperty extends ClassMember {
|
|||||||
this.initializer = initializer;
|
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() {
|
public VmSet getAllModifierMirrors() {
|
||||||
var mods = 0;
|
var mods = 0;
|
||||||
for (var clazz = getDeclaringClass(); clazz != null; clazz = clazz.getSuperclass()) {
|
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 abstract class TypeNode extends PklNode {
|
||||||
|
|
||||||
|
public interface ClassTypeNode {
|
||||||
|
VmClass getVmClass();
|
||||||
|
}
|
||||||
|
|
||||||
protected TypeNode(SourceSection sourceSection) {
|
protected TypeNode(SourceSection sourceSection) {
|
||||||
super(sourceSection);
|
super(sourceSection);
|
||||||
}
|
}
|
||||||
@@ -402,7 +406,8 @@ public abstract class TypeNode extends PklNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The `module` type for a final module. */
|
/** 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;
|
private final VmClass moduleClass;
|
||||||
|
|
||||||
public FinalModuleTypeNode(SourceSection sourceSection, 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. */
|
/** 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()
|
private final VmClass moduleClass; // only used by getVmClass()
|
||||||
@Child private ExpressionNode getModuleNode;
|
@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
|
* String/Boolean/Int/Float and their supertypes, only `VmValue`s can possibly pass its type
|
||||||
* check.
|
* check.
|
||||||
*/
|
*/
|
||||||
public static final class FinalClassTypeNode extends ObjectSlotTypeNode {
|
public static final class FinalClassTypeNode extends ObjectSlotTypeNode implements ClassTypeNode {
|
||||||
private final VmClass clazz;
|
private final VmClass clazz;
|
||||||
|
|
||||||
public FinalClassTypeNode(SourceSection sourceSection, 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
|
* String/Boolean/Int/Float and their supertypes, only {@link VmValue}s can possibly pass its type
|
||||||
* check.
|
* check.
|
||||||
*/
|
*/
|
||||||
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode {
|
public abstract static class NonFinalClassTypeNode extends ObjectSlotTypeNode
|
||||||
|
implements ClassTypeNode {
|
||||||
protected final VmClass clazz;
|
protected final VmClass clazz;
|
||||||
|
|
||||||
public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
public NonFinalClassTypeNode(SourceSection sourceSection, VmClass clazz) {
|
||||||
@@ -1088,6 +1095,14 @@ public abstract class TypeNode extends PklNode {
|
|||||||
|
|
||||||
return unionDefault;
|
return unionDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getStringLiterals() {
|
||||||
|
return stringLiterals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable String getUnionDefault() {
|
||||||
|
return unionDefault;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
|
public static final class CollectionTypeNode extends ObjectSlotTypeNode {
|
||||||
@@ -1421,6 +1436,10 @@ public abstract class TypeNode extends PklNode {
|
|||||||
return BaseModule.getMapClass();
|
return BaseModule.getMapClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TypeNode getKeyTypeNode() {
|
||||||
|
return keyTypeNode;
|
||||||
|
}
|
||||||
|
|
||||||
public TypeNode getValueTypeNode() {
|
public TypeNode getValueTypeNode() {
|
||||||
return valueTypeNode;
|
return valueTypeNode;
|
||||||
}
|
}
|
||||||
@@ -2131,6 +2150,14 @@ public abstract class TypeNode extends PklNode {
|
|||||||
protected boolean isParametric() {
|
protected boolean isParametric() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TypeNode getFirstTypeNode() {
|
||||||
|
return firstTypeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypeNode getSecondTypeNode() {
|
||||||
|
return secondTypeNode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class VarArgsTypeNode extends ObjectSlotTypeNode {
|
public static class VarArgsTypeNode extends ObjectSlotTypeNode {
|
||||||
@@ -2313,6 +2340,10 @@ public abstract class TypeNode extends PklNode {
|
|||||||
protected final boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
protected final boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) {
|
||||||
return consumer.accept(this);
|
return consumer.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getMask() {
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
|
public static final class UIntTypeAliasTypeNode extends IntMaskSlotTypeNode {
|
||||||
@@ -2505,6 +2536,10 @@ public abstract class TypeNode extends PklNode {
|
|||||||
aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes);
|
aliasedTypeNode = typeAlias.instantiate(typeArgumentNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TypeNode getAliasedTypeNode() {
|
||||||
|
return aliasedTypeNode;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FrameSlotKind getFrameSlotKind() {
|
public FrameSlotKind getFrameSlotKind() {
|
||||||
return aliasedTypeNode.getFrameSlotKind();
|
return aliasedTypeNode.getFrameSlotKind();
|
||||||
@@ -2670,6 +2705,10 @@ public abstract class TypeNode extends PklNode {
|
|||||||
this.constraintNodes = constraintNodes;
|
this.constraintNodes = constraintNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TypeNode getChildTypeNode() {
|
||||||
|
return childNode;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FrameSlotKind getFrameSlotKind() {
|
public FrameSlotKind getFrameSlotKind() {
|
||||||
return childNode.getFrameSlotKind();
|
return childNode.getFrameSlotKind();
|
||||||
|
|||||||
@@ -227,10 +227,26 @@ public final class BaseModule extends StdLibModule {
|
|||||||
return MixinTypeAlias.instance;
|
return MixinTypeAlias.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static VmTypeAlias getUIntTypeAlias() {
|
||||||
|
return UIntTypeAlias.instance;
|
||||||
|
}
|
||||||
|
|
||||||
public static VmTypeAlias getUInt8TypeAlias() {
|
public static VmTypeAlias getUInt8TypeAlias() {
|
||||||
return UInt8TypeAlias.instance;
|
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 {
|
private static final class AnyClass {
|
||||||
static final VmClass instance = loadClass("Any");
|
static final VmClass instance = loadClass("Any");
|
||||||
}
|
}
|
||||||
@@ -403,10 +419,26 @@ public final class BaseModule extends StdLibModule {
|
|||||||
static final VmTypeAlias instance = loadTypeAlias("Int32");
|
static final VmTypeAlias instance = loadTypeAlias("Int32");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class UIntTypeAlias {
|
||||||
|
static final VmTypeAlias instance = loadTypeAlias("UInt");
|
||||||
|
}
|
||||||
|
|
||||||
private static final class UInt8TypeAlias {
|
private static final class UInt8TypeAlias {
|
||||||
static final VmTypeAlias instance = loadTypeAlias("UInt8");
|
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 {
|
private static final class MixinTypeAlias {
|
||||||
static final VmTypeAlias instance = loadTypeAlias("Mixin");
|
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
|
// members of pkl.yaml
|
||||||
public static final Identifier MAX_COLLECTION_ALIASES = get("maxCollectionAliases");
|
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
|
// common in lambdas etc
|
||||||
public static final Identifier IT = get("it");
|
public static final Identifier IT = get("it");
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ public final class ModuleCache {
|
|||||||
return BaseModule.getModule();
|
return BaseModule.getModule();
|
||||||
case "Benchmark":
|
case "Benchmark":
|
||||||
return BenchmarkModule.getModule();
|
return BenchmarkModule.getModule();
|
||||||
|
case "Command":
|
||||||
|
return CommandModule.getModule();
|
||||||
case "jsonnet":
|
case "jsonnet":
|
||||||
return JsonnetModule.getModule();
|
return JsonnetModule.getModule();
|
||||||
case "math":
|
case "math":
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public final class TestRunner {
|
|||||||
var resultsBuilder = new TestResults.Builder(info.getModuleName(), getDisplayUri(info));
|
var resultsBuilder = new TestResults.Builder(info.getModuleName(), getDisplayUri(info));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkAmendsPklTest(testModule);
|
VmUtils.checkAmends(testModule, TestModule.getModule().getVmClass());
|
||||||
} catch (VmException v) {
|
} catch (VmException v) {
|
||||||
var error =
|
var error =
|
||||||
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
|
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, useColor));
|
||||||
@@ -83,17 +83,6 @@ public final class TestRunner {
|
|||||||
return resultsBuilder.build();
|
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) {
|
private TestSectionResults runFacts(VmTyped testModule) {
|
||||||
var facts = VmUtils.readMember(testModule, Identifier.FACTS);
|
var facts = VmUtils.readMember(testModule, Identifier.FACTS);
|
||||||
if (facts instanceof VmNull) return new TestSectionResults(TestSectionName.FACTS, List.of());
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -30,12 +30,14 @@ import java.net.URI;
|
|||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.DecimalFormatSymbols;
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.regex.PatternSyntaxException;
|
import java.util.regex.PatternSyntaxException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.graalvm.polyglot.Context;
|
import org.graalvm.polyglot.Context;
|
||||||
import org.graalvm.polyglot.Engine;
|
import org.graalvm.polyglot.Engine;
|
||||||
import org.organicdesign.fp.collections.ImMap;
|
import org.organicdesign.fp.collections.ImMap;
|
||||||
|
import org.pkl.core.FileOutput;
|
||||||
import org.pkl.core.PClassInfo;
|
import org.pkl.core.PClassInfo;
|
||||||
import org.pkl.core.PObject;
|
import org.pkl.core.PObject;
|
||||||
import org.pkl.core.SecurityManager;
|
import org.pkl.core.SecurityManager;
|
||||||
@@ -179,6 +181,49 @@ public final class VmUtils {
|
|||||||
return (VmBytes) VmUtils.readMember(receiver, Identifier.BYTES);
|
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
|
@TruffleBoundary
|
||||||
public static Object readMember(VmObjectLike receiver, Object memberKey) {
|
public static Object readMember(VmObjectLike receiver, Object memberKey) {
|
||||||
var result = readMemberOrNull(receiver, memberKey);
|
var result = readMemberOrNull(receiver, memberKey);
|
||||||
@@ -855,7 +900,7 @@ public final class VmUtils {
|
|||||||
String expression,
|
String expression,
|
||||||
SecurityManager securityManager,
|
SecurityManager securityManager,
|
||||||
ModuleResolver moduleResolver) {
|
ModuleResolver moduleResolver) {
|
||||||
var syntheticModule = ModuleKeys.synthetic(URI.create(REPL_TEXT), expression);
|
var syntheticModule = ModuleKeys.synthetic(REPL_TEXT_URI, expression);
|
||||||
ResolvedModuleKey resolvedModule;
|
ResolvedModuleKey resolvedModule;
|
||||||
try {
|
try {
|
||||||
resolvedModule = syntheticModule.resolve(securityManager);
|
resolvedModule = syntheticModule.resolve(securityManager);
|
||||||
@@ -870,7 +915,7 @@ public final class VmUtils {
|
|||||||
source.createSection(0, source.getLength()),
|
source.createSection(0, source.getLength()),
|
||||||
VmUtils.unavailableSourceSection(),
|
VmUtils.unavailableSourceSection(),
|
||||||
null,
|
null,
|
||||||
"repl:text",
|
REPL_TEXT,
|
||||||
syntheticModule,
|
syntheticModule,
|
||||||
resolvedModule,
|
resolvedModule,
|
||||||
false);
|
false);
|
||||||
@@ -914,4 +959,20 @@ public final class VmUtils {
|
|||||||
public static String concat(String str1, String str2) {
|
public static String concat(String str1, String str2) {
|
||||||
return str1 + 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -51,14 +51,14 @@ public final class MathNodes {
|
|||||||
public abstract static class minInt8 extends ExternalPropertyNode {
|
public abstract static class minInt8 extends ExternalPropertyNode {
|
||||||
@Specialization
|
@Specialization
|
||||||
protected long eval(VmTyped self) {
|
protected long eval(VmTyped self) {
|
||||||
return -128;
|
return Byte.MIN_VALUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract static class maxInt8 extends ExternalPropertyNode {
|
public abstract static class maxInt8 extends ExternalPropertyNode {
|
||||||
@Specialization
|
@Specialization
|
||||||
protected long eval(VmTyped self) {
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -471,13 +471,15 @@ public final class GlobResolver {
|
|||||||
* Resolves a glob expression.
|
* Resolves a glob expression.
|
||||||
*
|
*
|
||||||
* <p>Each pair is the expanded form of the glob pattern, paired with its resolved absolute URI.
|
* <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
|
@TruffleBoundary
|
||||||
public static Map<String, ResolvedGlobElement> resolveGlob(
|
public static Map<String, ResolvedGlobElement> resolveGlob(
|
||||||
SecurityManager securityManager,
|
SecurityManager securityManager,
|
||||||
ReaderBase reader,
|
ReaderBase reader,
|
||||||
ModuleKey enclosingModuleKey,
|
@Nullable ModuleKey enclosingModuleKey,
|
||||||
URI enclosingUri,
|
@Nullable URI enclosingUri,
|
||||||
String globPattern)
|
String globPattern)
|
||||||
throws IOException,
|
throws IOException,
|
||||||
SecurityManagerException,
|
SecurityManagerException,
|
||||||
@@ -486,6 +488,10 @@ public final class GlobResolver {
|
|||||||
|
|
||||||
var result = new LinkedHashMap<String, ResolvedGlobElement>();
|
var result = new LinkedHashMap<String, ResolvedGlobElement>();
|
||||||
var hasAbsoluteGlob = globPattern.matches("\\w+:.*");
|
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()) {
|
if (reader.hasHierarchicalUris()) {
|
||||||
var splitPattern = splitGlobPatternIntoBaseAndWildcards(reader, globPattern, hasAbsoluteGlob);
|
var splitPattern = splitGlobPatternIntoBaseAndWildcards(reader, globPattern, hasAbsoluteGlob);
|
||||||
@@ -493,7 +499,10 @@ public final class GlobResolver {
|
|||||||
var globParts = splitPattern.second;
|
var globParts = splitPattern.second;
|
||||||
// short-circuit for glob pattern with no wildcards (can only match 0 or 1 element)
|
// short-circuit for glob pattern with no wildcards (can only match 0 or 1 element)
|
||||||
if (globParts.length == 0) {
|
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)) {
|
if (reader.hasElement(securityManager, resolvedUri)) {
|
||||||
result.put(globPattern, new ResolvedGlobElement(globPattern, resolvedUri, true));
|
result.put(globPattern, new ResolvedGlobElement(globPattern, resolvedUri, true));
|
||||||
}
|
}
|
||||||
@@ -501,7 +510,10 @@ public final class GlobResolver {
|
|||||||
}
|
}
|
||||||
URI baseUri;
|
URI baseUri;
|
||||||
try {
|
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) {
|
} catch (URISyntaxException e) {
|
||||||
// assertion: this is only thrown if the pattern starts with a triple-dot import.
|
// 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.
|
// the language will throw an error if glob imports is combined with triple-dots.
|
||||||
|
|||||||
@@ -1076,3 +1076,54 @@ invalidStringBase64=\
|
|||||||
|
|
||||||
characterCodingException=\
|
characterCodingException=\
|
||||||
Invalid bytes for charset "{0}".
|
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:analyze
|
||||||
pkl:base
|
pkl:base
|
||||||
pkl:Benchmark
|
pkl:Benchmark
|
||||||
|
pkl:Command
|
||||||
pkl:DocPackageInfo
|
pkl:DocPackageInfo
|
||||||
pkl:DocsiteInfo
|
pkl:DocsiteInfo
|
||||||
pkl:EvaluatorSettings
|
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.
|
/// A template for writing tests.
|
||||||
///
|
///
|
||||||
/// To write tests, amend this module and define [facts] or [examples] (or both).
|
/// 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" }
|
@ModuleInfo { minPklVersion = "0.31.0" }
|
||||||
open module pkl.test
|
open module pkl.test
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user