mirror of
https://github.com/apple/pkl.git
synced 2026-05-07 07:33:36 +02:00
Allow command transformAll functions to perform imports (#1440)
This commit is contained in:
@@ -1070,10 +1070,10 @@ A property may be annotated with any type if its `@Flag` or `@Argument` annotati
|
||||
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.
|
||||
The `convert` and `transformAll` functions may return an pkldoc:Import[pkl:Command] 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.
|
||||
If the return value of `convert` or `transformAll` is a `List`, `Set`, `Map`, or `Pair`, each contained value (elements and entry keys/values) that are `Import` values are also replaced.
|
||||
|
||||
[IMPORTANT]
|
||||
====
|
||||
|
||||
@@ -99,7 +99,7 @@ To learn more about this feature, consult https://github.com/apple/pkl-evolution
|
||||
[[cli-framework]]
|
||||
=== CLI Framework
|
||||
|
||||
Pkl 0.31 introduces a new framework for implementing CLI tools in Pkl (pr:https://github.com/apple/pkl/pull/1367[], pr:https://github.com/apple/pkl/pull/1431[], pr:https://github.com/apple/pkl/pull/1432[], pr:https://github.com/apple/pkl/pull/1436[]).
|
||||
Pkl 0.31 introduces a new framework for implementing CLI tools in Pkl (pr:https://github.com/apple/pkl/pull/1367[], pr:https://github.com/apple/pkl/pull/1431[], pr:https://github.com/apple/pkl/pull/1432[], pr:https://github.com/apple/pkl/pull/1436[], pr:https://github.com/apple/pkl/pull/1440[]).
|
||||
|
||||
The framework provides a way to build command line tools with user experience idioms that will be immediately familiar to users.
|
||||
CLI tools implemented in Pkl have largely the same capabilities as normal Pkl evaluation (i.e. writing to standard output and files), but this may be extended using xref:language-reference:index.adoc#external-readers[external readers].
|
||||
|
||||
@@ -22,6 +22,7 @@ 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.net.URI
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import kotlin.io.path.exists
|
||||
@@ -159,7 +160,7 @@ constructor(
|
||||
)
|
||||
.convert {
|
||||
try {
|
||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
||||
opt.transformEach.apply(it, workingDirUri)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
@@ -168,7 +169,7 @@ constructor(
|
||||
}
|
||||
.transformAll(opt.defaultValue, opt.showAsRequired) {
|
||||
try {
|
||||
opt.transformAll.apply(it)
|
||||
opt.transformAll.apply(it, workingDirUri)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
@@ -201,7 +202,7 @@ constructor(
|
||||
)
|
||||
.convert {
|
||||
try {
|
||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
||||
opt.transformEach.apply(it, workingDirUri)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
@@ -210,7 +211,7 @@ constructor(
|
||||
}
|
||||
.transformAll(if (opt.repeated) -1 else 1, !opt.repeated) {
|
||||
try {
|
||||
opt.transformAll.apply(it)
|
||||
opt.transformAll.apply(it, workingDirUri)
|
||||
} catch (e: CommandSpec.Option.BadValue) {
|
||||
fail(e.message!!)
|
||||
} catch (_: CommandSpec.Option.MissingOption) {
|
||||
@@ -223,6 +224,8 @@ constructor(
|
||||
spec.subcommands.forEach { subcommands(SynthesizedRunCommand(it, runner)) }
|
||||
}
|
||||
|
||||
val workingDirUri: URI by lazy { runner.options.normalizedWorkingDir.toUri() }
|
||||
|
||||
override val invokeWithoutSubcommand = true
|
||||
|
||||
override val hiddenFromHelp: Boolean = spec.hidden
|
||||
|
||||
@@ -867,6 +867,65 @@ class CliCommandRunnerTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transformAll import`() {
|
||||
val moduleUri =
|
||||
writePklFile(
|
||||
"cmd.pkl",
|
||||
"""
|
||||
extends "pkl:Command"
|
||||
|
||||
options: Options
|
||||
|
||||
output {
|
||||
value = (options) {
|
||||
fromImport {
|
||||
baz = true // assert that imported modules are not forced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
@Flag {
|
||||
convert = (it) -> new Import{ uri = it }
|
||||
transformAll = (values) -> values.firstOrNull ?? new Import { uri = "./default.pkl" }
|
||||
}
|
||||
fromImport: Module
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val importUri =
|
||||
writePklFile(
|
||||
"default.pkl",
|
||||
"""
|
||||
foo = 1
|
||||
bar = "baz"
|
||||
baz: Boolean
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
val output =
|
||||
runToStdout(
|
||||
CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir),
|
||||
emptyList(),
|
||||
)
|
||||
assertThat(output)
|
||||
.isEqualTo(
|
||||
"""
|
||||
fromImport {
|
||||
foo = 1
|
||||
bar = "baz"
|
||||
baz = true
|
||||
}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert glob import`() {
|
||||
val moduleUri =
|
||||
|
||||
@@ -89,7 +89,7 @@ public record CommandSpec(
|
||||
@Nullable String helpText,
|
||||
boolean showAsRequired,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
BiFunction<List<Object>, URI, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
@Nullable String shortName,
|
||||
String metavar,
|
||||
@@ -134,7 +134,7 @@ public record CommandSpec(
|
||||
String name,
|
||||
@Nullable String helpText,
|
||||
BiFunction<String, URI, Object> transformEach,
|
||||
Function<List<Object>, Object> transformAll,
|
||||
BiFunction<List<Object>, URI, Object> transformAll,
|
||||
@Nullable CompletionCandidates completionCandidates,
|
||||
boolean repeated)
|
||||
implements Option {
|
||||
|
||||
@@ -462,7 +462,7 @@ public final class CommandSpecParser {
|
||||
|
||||
private class OptionBehavior {
|
||||
private @Nullable BiFunction<String, URI, Object> each;
|
||||
private @Nullable Function<List<Object>, Object> all;
|
||||
private @Nullable BiFunction<List<Object>, URI, Object> all;
|
||||
private @Nullable Boolean multiple;
|
||||
private @Nullable String metavar;
|
||||
private @Nullable CommandSpec.CompletionCandidates completionCandidates;
|
||||
@@ -471,7 +471,7 @@ public final class CommandSpecParser {
|
||||
|
||||
private OptionBehavior(
|
||||
@Nullable BiFunction<String, URI, Object> each,
|
||||
@Nullable Function<List<Object>, Object> all,
|
||||
@Nullable BiFunction<List<Object>, URI, Object> all,
|
||||
@Nullable Boolean multiple,
|
||||
@Nullable String metavar,
|
||||
@Nullable CommandSpec.CompletionCandidates completionCandidates) {
|
||||
@@ -493,7 +493,9 @@ public final class CommandSpecParser {
|
||||
annotation == null
|
||||
? null
|
||||
: VmUtils.readMember(annotation, Identifier.TRANSFORM_ALL) instanceof VmFunction func
|
||||
? (it) -> handleBadValue(() -> func.apply(VmList.create(it)))
|
||||
? (values, workingDirUri) ->
|
||||
handleBadValue(
|
||||
() -> handleImports(func.apply(VmList.create(values)), workingDirUri))
|
||||
: null,
|
||||
annotation == null
|
||||
? null
|
||||
@@ -533,7 +535,7 @@ public final class CommandSpecParser {
|
||||
all =
|
||||
!multiple
|
||||
? this::allChooseLast
|
||||
: (values) -> {
|
||||
: (values, workingDirUri) -> {
|
||||
if (values.isEmpty()) return null;
|
||||
var builder = new VmObjectBuilder();
|
||||
values.forEach(builder::addElement);
|
||||
@@ -547,7 +549,7 @@ public final class CommandSpecParser {
|
||||
all =
|
||||
!multiple
|
||||
? this::allChooseLast
|
||||
: (values) -> {
|
||||
: (values, workingDirUri) -> {
|
||||
if (values.isEmpty()) return null;
|
||||
var builder = new VmObjectBuilder();
|
||||
values.forEach(
|
||||
@@ -563,7 +565,7 @@ public final class CommandSpecParser {
|
||||
all =
|
||||
!multiple
|
||||
? this::allChooseLast
|
||||
: (values) -> values.isEmpty() ? null : VmList.create(values);
|
||||
: (values, workingDirUri) -> values.isEmpty() ? null : VmList.create(values);
|
||||
} else if (typeNode instanceof TypeNode.SetTypeNode setTypeNode) {
|
||||
handleElement(setTypeNode.getElementTypeNode(), prop);
|
||||
if (multiple == null) multiple = true;
|
||||
@@ -571,7 +573,7 @@ public final class CommandSpecParser {
|
||||
all =
|
||||
!multiple
|
||||
? this::allChooseLast
|
||||
: (values) -> values.isEmpty() ? null : VmSet.create(values);
|
||||
: (values, workingDirUri) -> values.isEmpty() ? null : VmSet.create(values);
|
||||
} else if (typeNode instanceof TypeNode.MapTypeNode mapTypeNode) {
|
||||
handleEntry(mapTypeNode.getKeyTypeNode(), mapTypeNode.getValueTypeNode(), prop);
|
||||
if (multiple == null) multiple = true;
|
||||
@@ -579,7 +581,7 @@ public final class CommandSpecParser {
|
||||
all =
|
||||
!multiple
|
||||
? this::allChooseLast
|
||||
: (values) -> {
|
||||
: (values, workingDirUri) -> {
|
||||
if (values.isEmpty()) return null;
|
||||
var builder = VmMap.builder();
|
||||
values.forEach(
|
||||
@@ -860,7 +862,7 @@ public final class CommandSpecParser {
|
||||
if (metavar == null) metavar = transformKey.getMetavar() + "=" + transformValue.getMetavar();
|
||||
}
|
||||
|
||||
private @Nullable Object allChooseLast(List<Object> values) {
|
||||
private @Nullable Object allChooseLast(List<Object> values, URI workingDirUri) {
|
||||
if (!values.isEmpty()) return values.get(values.size() - 1);
|
||||
if (isOptional()) return null;
|
||||
throw new MissingOption();
|
||||
@@ -898,7 +900,7 @@ public final class CommandSpecParser {
|
||||
return each;
|
||||
}
|
||||
|
||||
public Function<List<Object>, Object> getAll() {
|
||||
public BiFunction<List<Object>, URI, Object> getAll() {
|
||||
assert all != null;
|
||||
return all;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ class Flag extends BaseFlag {
|
||||
|
||||
/// 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 |
|
||||
@@ -235,6 +239,10 @@ class Argument extends Annotation {
|
||||
|
||||
/// 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>) -> Any)?
|
||||
|
||||
Reference in New Issue
Block a user