Skip to content

Commit f0d71e6

Browse files
committed
Add pkl run execution mode for CLI tools implemented in Pkl
1 parent 817e433 commit f0d71e6

File tree

36 files changed

+4690
-147
lines changed

36 files changed

+4690
-147
lines changed

docs/modules/ROOT/partials/component-attributes.adoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ endif::[]
6868
:uri-pkldoc-example: {uri-pkl-examples-tree}/pkldoc
6969

7070
:uri-stdlib-baseModule: {uri-pkl-stdlib-docs}/base
71+
:uri-stdlib-CommandModule: {uri-pkl-stdlib-docs}/Command
7172
:uri-stdlib-analyzeModule: {uri-pkl-stdlib-docs}/analyze
7273
:uri-stdlib-jsonnetModule: {uri-pkl-stdlib-docs}/jsonnet
7374
:uri-stdlib-reflectModule: {uri-pkl-stdlib-docs}/reflect
@@ -150,6 +151,13 @@ endif::[]
150151
:uri-stdlib-Resource: {uri-stdlib-baseModule}/Resource
151152
:uri-stdlib-outputFiles: {uri-stdlib-baseModule}/ModuleOutput#files
152153
:uri-stdlib-FileOutput: {uri-stdlib-baseModule}/FileOutput
154+
:uri-stdlib-Annotation: {uri-stdlib-baseModule}/Annotation
155+
:uri-stdlib-ConvertProperty: {uri-stdlib-baseModule}/ConvertProperty
156+
:uri-stdlib-Command-Flag: {uri-stdlib-CommandModule}/Flag
157+
:uri-stdlib-Command-BooleanFlag: {uri-stdlib-CommandModule}/BooleanFlag
158+
:uri-stdlib-Command-CountedFlag: {uri-stdlib-CommandModule}/CountedFlag
159+
:uri-stdlib-Command-Argument: {uri-stdlib-CommandModule}/Argument
160+
:uri-stdlib-Command-Import: {uri-stdlib-CommandModule}/Import
153161

154162
:uri-messagepack: https://msgpack.org/index.html
155163
:uri-messagepack-spec: https://github.com/msgpack/msgpack/blob/master/spec.md

docs/modules/language-reference/pages/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3961,6 +3961,7 @@ emailList: List<EmailAddress> // <2>
39613961
<1> equivalent to `email: String(contains("@"))` for type checking purposes
39623962
<2> equivalent to `emailList: List<String(contains("@"))>` for type checking purposes
39633963

3964+
[[nullable-types]]
39643965
==== Nullable Types
39653966

39663967
Class types such as `Bird` (see above) do not admit `null` values.

docs/modules/pkl-cli/pages/index.adoc

Lines changed: 316 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,9 @@ If these are the only failures, the command exits with exit code 10.
489489
Otherwise, failures result in exit code 1.
490490

491491
<modules>::
492-
The absolute or relative URIs of the modules to test. Relative URIs are resolved against the working directory.
492+
The absolute or relative URIs of the modules to test.
493+
The module must extend `pkl:test`.
494+
Relative URIs are resolved against the working directory.
493495

494496
==== Options
495497

@@ -546,6 +548,23 @@ Use `--no-power-assertions` to disable this feature if you prefer simpler output
546548

547549
This command also takes <<common-options, common options>>.
548550

551+
[[command-run]]
552+
=== `pkl run`
553+
554+
*Synopsis:* `pkl run [<options>] [<module>] [<command options>]`
555+
556+
Evaluate a <<cli-tools,CLI command>> defined by `<module>`.
557+
558+
<module>::
559+
The absolute or relative URIs of the command module to run.
560+
The module must extend `pkl:Command`.
561+
Relative URIs are resolved against the working directory.
562+
563+
<command options>::
564+
Additional CLI options and arguments defined by `<module>`.
565+
566+
This command also takes <<common-options, common options>>, but they must be specified before `<module>`.
567+
549568
[[command-repl]]
550569
=== `pkl repl`
551570

@@ -800,7 +819,7 @@ Write the path of files with formatting violations to stdout.
800819
[[common-options]]
801820
=== Common options
802821

803-
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:
822+
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:
804823

805824
include::../../pkl-cli/partials/cli-common-options.adoc[]
806825

@@ -901,6 +920,301 @@ If multiple module outputs are written to the same file, or to standard output,
901920
By default, module outputs are separated with `---`, as in a YAML stream.
902921
The separator can be customized using the `--module-output-separator` option.
903922

923+
[[cli-tools]]
924+
== Implementing CLI Tools
925+
926+
CLI tools can be implemented in Pkl by modules extending the `pkl:Command` module.
927+
With `pkl:Command`, you can define a script in Pkl that is executed by your shell, providing a better CLI experience.
928+
929+
Regular evaluation requires use of xref:language-reference:index.adoc#resources[resources] like properties and evironment variables to provide parameters:
930+
[source,bash]
931+
----
932+
$ pkl eval script.pkl -p username=me -p password=password
933+
----
934+
935+
Commands provide a native, familiar CLI experience:
936+
[source,bash]
937+
----
938+
$ pkl run script.pkl --username=admin --password=hunter2
939+
$ ./script.pkl --username=admin --password=hunter2
940+
----
941+
942+
Pkl commands have a few properties that distinguish them from standard module evaluation:
943+
944+
* 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.
945+
* Commands can dynamically import modules when they are specified as command line options.
946+
* Commands may write to standard output (via `output.text` or `output.bytes`) and the filesystem (via `output.files`) in the same evaluation.
947+
* Command file output may write to any absolute path (not only relative to the `--multiple-file-output-path` option).
948+
** Relative output paths are written relative to the current working directory (or `--working-dir`, if specified).
949+
** Paths of output file are printed to the command's standard error.
950+
951+
IMPORTANT: Users of `pkl run` must be aware of the security implications of this behavior.
952+
Using `pkl eval` prevents accidental overwrites by not allowing absolute paths, but `pkl run` does not offer this protection.
953+
Commands may write to any path the invoking user has permissions to modify.
954+
955+
Commands are implemented as regular modules and declare their supported command line flags and positional arguments using a class with annotated properties.
956+
957+
=== Defining Commands
958+
959+
Commands are defined by creating a module that extends `pkl:Command`:
960+
961+
[source,pkl%tested]
962+
.my-tool.pkl
963+
----
964+
/// This doc comment becomes part of the command's CLI help!
965+
/// Markdown formatting is **allowed!**
966+
extends "pkl:Command"
967+
968+
options: Options // <1>
969+
970+
class Options {
971+
// Define CLI flags/arguments...
972+
}
973+
974+
// Regular module code...
975+
----
976+
<1> Re-declaration of the `options` property's type.
977+
978+
Like `pkl eval`, when a command completes without an evaluation error the process exits successfully (exit code 0).
979+
Commands can return a failure using `throw` (exit code 1), but otherwise may not control the exit code.
980+
981+
Other than the differences listed above, commands behave like any other Pkl module.
982+
For example, there is no way to execute other programs or make arbitrary HTTP requests.
983+
If additional functionality is desired, xref:language-reference:index.adoc#external-readers[external readers] may be used to extends Pkl's capabilities.
984+
985+
=== Command Options
986+
987+
Each property of a command's options class becomes a command line option.
988+
Properties with the `local`, `hidden`, `fixed`, and/or `const` modifiers are not parsed as options
989+
A property's doc comment, if present, becomes the corresponding option's CLI help description.
990+
Doc comments are interpreted as Markdown text and formatted nicely when displayed to users.
991+
Properties must have xref:language-reference:index.adoc#type-annotations[type annotations] to determine how they are parsed.
992+
993+
Properties may be xref:language-reference:index.adoc#annotations[annotated] to influence how they behave:
994+
995+
* Properties annotated with link:{uri-stdlib-Command-Flag}[`@Flag`] become CLI flags named `--<property name>` that accept a value.
996+
* `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.
997+
* `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.
998+
* Properties annotated with link:{uri-stdlib-Command-Argument}[`@Argument`] become positional CLI arguments and are parsed in the order they appear in the class.
999+
* Properties with no annotation are treated the same as `@Flag` with no further customization.
1000+
1001+
Flag options may set a `shortName` property to define a single-character abbreviation (`-<short name>`).
1002+
Flag abbreviations may be combined (e.g. `-a -b -v -v -f some-value` is equivalent to `-abvvf some-value`).
1003+
1004+
A `@Flag` or `@Argument` property's type annotation determines how it is converted from the raw string value:
1005+
1006+
|===
1007+
|Type |Behavior
1008+
1009+
|`String`
1010+
|Value is used verbatim.
1011+
1012+
|`Char`
1013+
|Value is used verbatim but must be exactly one character.
1014+
1015+
|`Boolean`
1016+
|True values: `true`, `t`, `1`, `yes`, `y`, `on`
1017+
1018+
False values: `false`, `f`, `0`, `no`, `n`, `off`
1019+
1020+
|`Number`
1021+
|Value is parsed as an `Int` if possible, otherwise parsed as `Float`.
1022+
1023+
|`Float`
1024+
|Value is parsed as a `Float`.
1025+
1026+
|`Int`
1027+
|Value is parsed as a `Int`.
1028+
1029+
|`Int8`, `Int16`, `Int32`, `UInt`, `UInt8`, `UInt16`, `UInt32`
1030+
|Value is parsed as a `Int` and must be within the type's range.
1031+
1032+
|xref:language-reference:index.adoc#union-types[Union] of xref:language-reference:index.adoc#string-literal-types[string literals]
1033+
|Value is used verbatim but must match a member of the union.
1034+
1035+
|`List<Element>`, `Listing<Element>`, `Set<Element>`
1036+
|Each occurrence of the option becomes an element of the final value.
1037+
1038+
`Element` values are parsed based on the above primitive types.
1039+
1040+
|`Map<Key, Value>`, `Mapping<Key, Value>`
1041+
|Each occurrence of the option becomes an entry of the final value.
1042+
1043+
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.
1044+
1045+
|`Pair<First, Second>`
1046+
|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.
1047+
1048+
|===
1049+
1050+
If a flag that accepts only a single value is provided multiple times, the last occurrence becomes the final value.
1051+
1052+
Only a single positional argument accepting multiple values is permitted per command.
1053+
1054+
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`.
1055+
Properties with default values are also optional.
1056+
Type constraints are evaluated when the command is executed, so additional restrictions on option values are enforced at runtime.
1057+
1058+
==== Custom Option Conversion and Aggregation
1059+
1060+
A property may be annotated with any type if its `@Flag` or `@Argument` annotation sets the `convert` or `transformAll` properties.
1061+
The `convert` property is a xref:language-reference:index.adoc#anonymous-functions[function] that overrides how _each_ raw option value is interpreted.
1062+
The `transformAll` property is a function that overrides how _all_ parsed option values become the final property value.
1063+
1064+
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.
1065+
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.
1066+
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.
1067+
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.
1068+
1069+
[IMPORTANT]
1070+
====
1071+
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.
1072+
Example:
1073+
[source%parsed,{pkl}]
1074+
----
1075+
@Flag {
1076+
convert = (it) -> new Import { uri = it; glob = true }
1077+
multiple = false
1078+
}
1079+
birds: Mapping<String, Bird>
1080+
----
1081+
1082+
If multiple glob patterns values should be accepted and merged, `transformAll` may be used to merge every glob-imported `Mapping`:
1083+
[source%parsed,{pkl}]
1084+
----
1085+
@Flag {
1086+
convert = (it) -> new Import { uri = it; glob = true }
1087+
transformAll =
1088+
(values) -> values.fold(new Mapping {}, (result, element) ->
1089+
(result) { ...element }
1090+
)
1091+
}
1092+
birds: Mapping<String, Bird>
1093+
----
1094+
====
1095+
1096+
=== Subcommands
1097+
1098+
Like many other command line libraries, `pkl:Command` allows building commands into a hierarchy with a root command and subcommands:
1099+
1100+
[source,pkl%tested]
1101+
.my-tool.pkl
1102+
----
1103+
extends "pkl:Command"
1104+
1105+
command {
1106+
subcommands {
1107+
import("subcommand1.pkl")
1108+
import("subcommand2.pkl")
1109+
for (_, subcommand in import*("./subcommands/*.pkl")) {
1110+
subcommand
1111+
}
1112+
}
1113+
}
1114+
----
1115+
1116+
[source,pkl%tested]
1117+
.subcommand1.pkl
1118+
----
1119+
extends "pkl:Command"
1120+
1121+
import "my-tool.pkl"
1122+
1123+
parent: `my-tool` // <1>
1124+
1125+
// Regular module code...
1126+
----
1127+
<1> Optional; asserts that this is a subcommand of `my-tool` and simplifies accessing properties and options of the parent command
1128+
1129+
Each element of `subcommands` must have a unique value for `command.name`.
1130+
1131+
=== Testing Commands
1132+
1133+
Command modules are normal Pkl modules, so they may be imported and used like any other module.
1134+
This is particularly helpful when testing commands, as the command's `options` and `parent` properties can be populated by test code.
1135+
1136+
Testing the above command and subcommand might look like this:
1137+
1138+
[source,pkl%tested]
1139+
----
1140+
amends "pkl:test"
1141+
1142+
import "my-tool.pkl"
1143+
import "subcommand1.pkl"
1144+
1145+
examples {
1146+
["Test my-tool"] {
1147+
(`my-tool`) {
1148+
options {
1149+
// Set my-tool options here...
1150+
}
1151+
}.output.text
1152+
}
1153+
["Test subcommand1"] {
1154+
(subcommand1) {
1155+
parent { // this amends `my-tool`
1156+
options {
1157+
// Set my-tool options here...
1158+
}
1159+
}
1160+
options {
1161+
// Set subcommand options here...
1162+
}
1163+
}.output.text
1164+
}
1165+
}
1166+
----
1167+
1168+
[[commands-as-standalone-scripts]]
1169+
=== Commands as standalone scripts
1170+
1171+
On *nix platforms, Pkl commands can be configured to run as standalone tools that can be invoked without the `pkl run` command.
1172+
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:
1173+
1174+
[source,pkl%parsed]
1175+
----
1176+
#!/usr/bin/env -S pkl run
1177+
----
1178+
1179+
NOTE: The `-S` flag for `env` is required on Linux systems due to a limitation of shebang handling in the Linux kernel.
1180+
While not required on other *nix platforms like macOS, but it should be included for compatibility.
1181+
1182+
==== Shell Completion
1183+
1184+
Like with Pkl's own CLI, <<command-shell-completion, shell completions>> can be generated for standalone scripts.
1185+
1186+
[source,shell]
1187+
----
1188+
# Generate shell completion script for bash
1189+
./my-tool.pkl shell-completion bash
1190+
1191+
# Generate shell completion script for zsh
1192+
./my-tool.pkl shell-completion zsh
1193+
----
1194+
1195+
==== Customizing Completion Candidates
1196+
1197+
`@Flag` and `@Argument` annotations may specify the `completionCandidates` to improve generated shell completions.
1198+
1199+
Valid values include:
1200+
1201+
* A `Listing<String>` of literal string values to offer for completion.
1202+
* The literal string `"path"`, which offers local file paths for completion.
1203+
1204+
Options with a string literal union type implicitly offer the members of the union as completion candidates.
1205+
1206+
=== Flag name ambiguities
1207+
1208+
It is possible for commands to define flags with names or short names that collide with Pkl's own command line options.
1209+
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.
1210+
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.
1211+
1212+
This imposes a limitation around <<commands-as-standalone-scripts,standalone commands>> that prevents users from customizing Pkl evaluator options when they are invoked.
1213+
There are two recommended workarounds for this limitation:
1214+
1215+
* Use a `PklProject` to define evaluator settings instead of doing so on the command line.
1216+
* If the command line must be used, switch to invoking via `pkl run [<flags>] [<root command module>]`.
1217+
9041218
[[repl]]
9051219
== Working with the REPL
9061220

0 commit comments

Comments
 (0)