mirror of
https://github.com/apple/pkl.git
synced 2026-04-24 01:08:34 +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 `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 `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.
|
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.
|
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]
|
[IMPORTANT]
|
||||||
====
|
====
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ To learn more about this feature, consult https://github.com/apple/pkl-evolution
|
|||||||
[[cli-framework]]
|
[[cli-framework]]
|
||||||
=== 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.
|
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].
|
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.options.*
|
||||||
import com.github.ajalt.clikt.parameters.types.int
|
import com.github.ajalt.clikt.parameters.types.int
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.net.URI
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.io.path.createParentDirectories
|
import kotlin.io.path.createParentDirectories
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
@@ -159,7 +160,7 @@ constructor(
|
|||||||
)
|
)
|
||||||
.convert {
|
.convert {
|
||||||
try {
|
try {
|
||||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
opt.transformEach.apply(it, workingDirUri)
|
||||||
} catch (e: CommandSpec.Option.BadValue) {
|
} catch (e: CommandSpec.Option.BadValue) {
|
||||||
fail(e.message!!)
|
fail(e.message!!)
|
||||||
} catch (_: CommandSpec.Option.MissingOption) {
|
} catch (_: CommandSpec.Option.MissingOption) {
|
||||||
@@ -168,7 +169,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
.transformAll(opt.defaultValue, opt.showAsRequired) {
|
.transformAll(opt.defaultValue, opt.showAsRequired) {
|
||||||
try {
|
try {
|
||||||
opt.transformAll.apply(it)
|
opt.transformAll.apply(it, workingDirUri)
|
||||||
} catch (e: CommandSpec.Option.BadValue) {
|
} catch (e: CommandSpec.Option.BadValue) {
|
||||||
fail(e.message!!)
|
fail(e.message!!)
|
||||||
} catch (_: CommandSpec.Option.MissingOption) {
|
} catch (_: CommandSpec.Option.MissingOption) {
|
||||||
@@ -201,7 +202,7 @@ constructor(
|
|||||||
)
|
)
|
||||||
.convert {
|
.convert {
|
||||||
try {
|
try {
|
||||||
opt.transformEach.apply(it, runner.options.normalizedWorkingDir.toUri())
|
opt.transformEach.apply(it, workingDirUri)
|
||||||
} catch (e: CommandSpec.Option.BadValue) {
|
} catch (e: CommandSpec.Option.BadValue) {
|
||||||
fail(e.message!!)
|
fail(e.message!!)
|
||||||
} catch (_: CommandSpec.Option.MissingOption) {
|
} catch (_: CommandSpec.Option.MissingOption) {
|
||||||
@@ -210,7 +211,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
.transformAll(if (opt.repeated) -1 else 1, !opt.repeated) {
|
.transformAll(if (opt.repeated) -1 else 1, !opt.repeated) {
|
||||||
try {
|
try {
|
||||||
opt.transformAll.apply(it)
|
opt.transformAll.apply(it, workingDirUri)
|
||||||
} catch (e: CommandSpec.Option.BadValue) {
|
} catch (e: CommandSpec.Option.BadValue) {
|
||||||
fail(e.message!!)
|
fail(e.message!!)
|
||||||
} catch (_: CommandSpec.Option.MissingOption) {
|
} catch (_: CommandSpec.Option.MissingOption) {
|
||||||
@@ -223,6 +224,8 @@ constructor(
|
|||||||
spec.subcommands.forEach { subcommands(SynthesizedRunCommand(it, runner)) }
|
spec.subcommands.forEach { subcommands(SynthesizedRunCommand(it, runner)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val workingDirUri: URI by lazy { runner.options.normalizedWorkingDir.toUri() }
|
||||||
|
|
||||||
override val invokeWithoutSubcommand = true
|
override val invokeWithoutSubcommand = true
|
||||||
|
|
||||||
override val hiddenFromHelp: Boolean = spec.hidden
|
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
|
@Test
|
||||||
fun `convert glob import`() {
|
fun `convert glob import`() {
|
||||||
val moduleUri =
|
val moduleUri =
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public record CommandSpec(
|
|||||||
@Nullable String helpText,
|
@Nullable String helpText,
|
||||||
boolean showAsRequired,
|
boolean showAsRequired,
|
||||||
BiFunction<String, URI, Object> transformEach,
|
BiFunction<String, URI, Object> transformEach,
|
||||||
Function<List<Object>, Object> transformAll,
|
BiFunction<List<Object>, URI, Object> transformAll,
|
||||||
@Nullable CompletionCandidates completionCandidates,
|
@Nullable CompletionCandidates completionCandidates,
|
||||||
@Nullable String shortName,
|
@Nullable String shortName,
|
||||||
String metavar,
|
String metavar,
|
||||||
@@ -134,7 +134,7 @@ public record CommandSpec(
|
|||||||
String name,
|
String name,
|
||||||
@Nullable String helpText,
|
@Nullable String helpText,
|
||||||
BiFunction<String, URI, Object> transformEach,
|
BiFunction<String, URI, Object> transformEach,
|
||||||
Function<List<Object>, Object> transformAll,
|
BiFunction<List<Object>, URI, Object> transformAll,
|
||||||
@Nullable CompletionCandidates completionCandidates,
|
@Nullable CompletionCandidates completionCandidates,
|
||||||
boolean repeated)
|
boolean repeated)
|
||||||
implements Option {
|
implements Option {
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ public final class CommandSpecParser {
|
|||||||
|
|
||||||
private class OptionBehavior {
|
private class OptionBehavior {
|
||||||
private @Nullable BiFunction<String, URI, Object> each;
|
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 Boolean multiple;
|
||||||
private @Nullable String metavar;
|
private @Nullable String metavar;
|
||||||
private @Nullable CommandSpec.CompletionCandidates completionCandidates;
|
private @Nullable CommandSpec.CompletionCandidates completionCandidates;
|
||||||
@@ -471,7 +471,7 @@ public final class CommandSpecParser {
|
|||||||
|
|
||||||
private OptionBehavior(
|
private OptionBehavior(
|
||||||
@Nullable BiFunction<String, URI, Object> each,
|
@Nullable BiFunction<String, URI, Object> each,
|
||||||
@Nullable Function<List<Object>, Object> all,
|
@Nullable BiFunction<List<Object>, URI, Object> all,
|
||||||
@Nullable Boolean multiple,
|
@Nullable Boolean multiple,
|
||||||
@Nullable String metavar,
|
@Nullable String metavar,
|
||||||
@Nullable CommandSpec.CompletionCandidates completionCandidates) {
|
@Nullable CommandSpec.CompletionCandidates completionCandidates) {
|
||||||
@@ -493,7 +493,9 @@ public final class CommandSpecParser {
|
|||||||
annotation == null
|
annotation == null
|
||||||
? null
|
? null
|
||||||
: VmUtils.readMember(annotation, Identifier.TRANSFORM_ALL) instanceof VmFunction func
|
: 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,
|
: null,
|
||||||
annotation == null
|
annotation == null
|
||||||
? null
|
? null
|
||||||
@@ -533,7 +535,7 @@ public final class CommandSpecParser {
|
|||||||
all =
|
all =
|
||||||
!multiple
|
!multiple
|
||||||
? this::allChooseLast
|
? this::allChooseLast
|
||||||
: (values) -> {
|
: (values, workingDirUri) -> {
|
||||||
if (values.isEmpty()) return null;
|
if (values.isEmpty()) return null;
|
||||||
var builder = new VmObjectBuilder();
|
var builder = new VmObjectBuilder();
|
||||||
values.forEach(builder::addElement);
|
values.forEach(builder::addElement);
|
||||||
@@ -547,7 +549,7 @@ public final class CommandSpecParser {
|
|||||||
all =
|
all =
|
||||||
!multiple
|
!multiple
|
||||||
? this::allChooseLast
|
? this::allChooseLast
|
||||||
: (values) -> {
|
: (values, workingDirUri) -> {
|
||||||
if (values.isEmpty()) return null;
|
if (values.isEmpty()) return null;
|
||||||
var builder = new VmObjectBuilder();
|
var builder = new VmObjectBuilder();
|
||||||
values.forEach(
|
values.forEach(
|
||||||
@@ -563,7 +565,7 @@ public final class CommandSpecParser {
|
|||||||
all =
|
all =
|
||||||
!multiple
|
!multiple
|
||||||
? this::allChooseLast
|
? this::allChooseLast
|
||||||
: (values) -> values.isEmpty() ? null : VmList.create(values);
|
: (values, workingDirUri) -> values.isEmpty() ? null : VmList.create(values);
|
||||||
} else if (typeNode instanceof TypeNode.SetTypeNode setTypeNode) {
|
} else if (typeNode instanceof TypeNode.SetTypeNode setTypeNode) {
|
||||||
handleElement(setTypeNode.getElementTypeNode(), prop);
|
handleElement(setTypeNode.getElementTypeNode(), prop);
|
||||||
if (multiple == null) multiple = true;
|
if (multiple == null) multiple = true;
|
||||||
@@ -571,7 +573,7 @@ public final class CommandSpecParser {
|
|||||||
all =
|
all =
|
||||||
!multiple
|
!multiple
|
||||||
? this::allChooseLast
|
? this::allChooseLast
|
||||||
: (values) -> values.isEmpty() ? null : VmSet.create(values);
|
: (values, workingDirUri) -> values.isEmpty() ? null : VmSet.create(values);
|
||||||
} else if (typeNode instanceof TypeNode.MapTypeNode mapTypeNode) {
|
} else if (typeNode instanceof TypeNode.MapTypeNode mapTypeNode) {
|
||||||
handleEntry(mapTypeNode.getKeyTypeNode(), mapTypeNode.getValueTypeNode(), prop);
|
handleEntry(mapTypeNode.getKeyTypeNode(), mapTypeNode.getValueTypeNode(), prop);
|
||||||
if (multiple == null) multiple = true;
|
if (multiple == null) multiple = true;
|
||||||
@@ -579,7 +581,7 @@ public final class CommandSpecParser {
|
|||||||
all =
|
all =
|
||||||
!multiple
|
!multiple
|
||||||
? this::allChooseLast
|
? this::allChooseLast
|
||||||
: (values) -> {
|
: (values, workingDirUri) -> {
|
||||||
if (values.isEmpty()) return null;
|
if (values.isEmpty()) return null;
|
||||||
var builder = VmMap.builder();
|
var builder = VmMap.builder();
|
||||||
values.forEach(
|
values.forEach(
|
||||||
@@ -860,7 +862,7 @@ public final class CommandSpecParser {
|
|||||||
if (metavar == null) metavar = transformKey.getMetavar() + "=" + transformValue.getMetavar();
|
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 (!values.isEmpty()) return values.get(values.size() - 1);
|
||||||
if (isOptional()) return null;
|
if (isOptional()) return null;
|
||||||
throw new MissingOption();
|
throw new MissingOption();
|
||||||
@@ -898,7 +900,7 @@ public final class CommandSpecParser {
|
|||||||
return each;
|
return each;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Function<List<Object>, Object> getAll() {
|
public BiFunction<List<Object>, URI, Object> getAll() {
|
||||||
assert all != null;
|
assert all != null;
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ class Flag extends BaseFlag {
|
|||||||
|
|
||||||
/// Customize the behavior of turning all parsed flag values into the final option value.
|
/// 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:
|
/// If no value is provided, all flag values are transformed according to the option's type:
|
||||||
///
|
///
|
||||||
/// | Type | Behavior |
|
/// | Type | Behavior |
|
||||||
@@ -235,6 +239,10 @@ class Argument extends Annotation {
|
|||||||
|
|
||||||
/// Customize the behavior of turning all parsed flag values into the final option value.
|
/// 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
|
/// If no value is provided, all option values are transformed using the same rules as
|
||||||
/// [Flag.transformAll].
|
/// [Flag.transformAll].
|
||||||
transformAll: ((List<Any>) -> Any)?
|
transformAll: ((List<Any>) -> Any)?
|
||||||
|
|||||||
Reference in New Issue
Block a user