//===----------------------------------------------------------------------===// // 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.32.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" } /// `connection-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 /// } /// ``` 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 } /// 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. /// /// 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, 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)? /// 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(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 `--`/`--no-`. /// /// 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. /// /// 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, all option values are transformed using the same rules as /// [Flag.transformAll]. transformAll: ((List) -> 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(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 = 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)'"))