11/*
2- * Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+ * Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@ import org.pkl.core.CommandSpec
3737import org.pkl.core.EvaluatorBuilder
3838import org.pkl.core.FileOutput
3939import org.pkl.core.ModuleSource.uri
40+ import org.pkl.core.PklException
4041import org.pkl.core.util.IoUtils
4142
4243class CliCommandRunner
@@ -64,14 +65,20 @@ constructor(
6465 val evaluator = builder.build()
6566 evaluator.use {
6667 evaluator.evaluateCommand(uri(normalizedSourceModule)) { spec ->
67- val root = SynthesizedRunCommand (spec, this , options.sourceModules.first().toString())
68- root.subcommands(
69- CompletionCommand (
70- name = " shell-completion" ,
71- help = " Generate a completion script for the given shell" ,
68+ try {
69+ val root = SynthesizedRunCommand (spec, this , options.sourceModules.first().toString())
70+ root.subcommands(
71+ CompletionCommand (
72+ name = " shell-completion" ,
73+ help = " Generate a completion script for the given shell" ,
74+ )
7275 )
73- )
74- root.main(args)
76+ root.parse(args)
77+ } catch (e: PklException ) {
78+ throw e
79+ } catch (e: Exception ) {
80+ throw e.message?.let { PklException (it, e) } ? : PklException (e)
81+ }
7582 }
7683 }
7784 }
@@ -109,7 +116,6 @@ constructor(
109116 checkPathSpec(pathSpec)
110117 val resolvedPath = outputDir.resolve(pathSpec).normalize()
111118 val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath
112- // FIXME: should we validate against options.normalizedRootDir?
113119 val previousOutput = writtenFiles[realPath]
114120 if (previousOutput != null ) {
115121 throw CliException (
@@ -149,39 +155,6 @@ constructor(
149155
150156 override fun help (context : Context ): String = spec.description ? : " "
151157
152- override fun run () {
153- if (currentContext.invokedSubcommand is CompletionCommand ) return
154-
155- val opts =
156- registeredOptions()
157- .mapNotNull {
158- val opt = it as ? OptionWithValues <* , * , * > ? : return @mapNotNull null
159- return @mapNotNull if (
160- it.names.contains(" --help" ) ||
161- (opt.value as ? List <* >)?.isEmpty() ? : false ||
162- (opt.value as ? Map <* , * >)?.isEmpty() ? : false
163- )
164- null
165- else it.names.first().trimStart(' -' ) to opt.value
166- }
167- .toMap() +
168- registeredArguments()
169- .mapNotNull { it as ? ArgumentDelegate <* > }
170- .associateBy({ it.name }, { it.value })
171-
172- val state = spec.apply.apply (opts, currentContext.obj as CommandSpec .State ? )
173- currentContext.obj = state
174-
175- if (currentContext.invokedSubcommand != null ) return
176- if (spec.subcommands.isNotEmpty() && spec.noOp) {
177- throw PrintHelpMessage (currentContext, true , 1 )
178- }
179-
180- val result = state.evaluate()
181- runner.writeOutput(result.outputBytes)
182- runner.writeMultipleFileOutput(result.outputFiles)
183- }
184-
185158 private fun registerFlag (flag : CommandSpec .Flag ) {
186159 val names =
187160 if (flag.shortName == null ) arrayOf(" --${flag.name} " )
@@ -204,12 +177,7 @@ constructor(
204177 else convertValue(it.second, type.valueType, " value" ),
205178 )
206179 }
207- .multiple(
208- default =
209- (flag.defaultValue?.let { default ->
210- (default as Map <* , * >).entries.toList().map { Pair (it.key!! , it.value!! ) }
211- } ? : emptyList())
212- )
180+ .multiple()
213181 .toMap()
214182 }
215183 )
@@ -227,6 +195,39 @@ constructor(
227195 }
228196 )
229197 }
198+
199+ override fun run () {
200+ if (currentContext.invokedSubcommand is CompletionCommand ) return
201+
202+ val opts =
203+ registeredOptions()
204+ .mapNotNull {
205+ val opt = it as ? OptionWithValues <* , * , * > ? : return @mapNotNull null
206+ return @mapNotNull if (
207+ it.names.contains(" --help" ) ||
208+ (opt.value as ? List <* >)?.isEmpty() ? : false ||
209+ (opt.value as ? Map <* , * >)?.isEmpty() ? : false
210+ )
211+ null
212+ else it.names.first().trimStart(' -' ) to opt.value
213+ }
214+ .toMap() +
215+ registeredArguments()
216+ .mapNotNull { it as ? ArgumentDelegate <* > }
217+ .associateBy({ it.name }, { it.value })
218+
219+ val state = spec.apply.apply (opts, currentContext.obj as CommandSpec .State ? )
220+ currentContext.obj = state
221+
222+ if (currentContext.invokedSubcommand != null ) return
223+ if (spec.subcommands.isNotEmpty() && spec.noOp) {
224+ throw PrintHelpMessage (currentContext, true , 1 )
225+ }
226+
227+ val result = state.evaluate()
228+ runner.writeOutput(result.outputBytes)
229+ runner.writeMultipleFileOutput(result.outputFiles)
230+ }
230231 }
231232}
232233
@@ -244,7 +245,7 @@ private fun <InT> NullableOption<InT, InT>.flag(flag: CommandSpec.Flag) =
244245 flag.type is CommandSpec .OptionType .Collection ->
245246 when {
246247 it.isEmpty() && flag.type.isRequired -> throw MissingOption (option)
247- it.isEmpty() && ! flag.type.isRequired -> flag.defaultValue
248+ it.isEmpty() && ! flag.type.isRequired -> it
248249 else ->
249250 if (
250251 (flag.type as CommandSpec .OptionType .Collection ).type ==
@@ -253,9 +254,8 @@ private fun <InT> NullableOption<InT, InT>.flag(flag: CommandSpec.Flag) =
253254 it.toSet()
254255 else it
255256 }
256- flag.defaultValue != null -> it.lastOrNull() ? : flag.defaultValue
257257 flag.type.isRequired -> it.lastOrNull() ? : throw MissingOption (option)
258- else -> it
258+ else -> it.lastOrNull()
259259 }
260260 }
261261 else -> throw CliException (" Unexpected collection flag value type $valueType " )
@@ -300,7 +300,15 @@ fun RawOption.convert(
300300 } else it
301301 return @convert parseFunction.parse(parseArg)
302302 }
303- else convert { convertValue(it, type, null ) }.copy(metavarGetter = { type.toString() })
303+ else
304+ convert {
305+ convertValue(
306+ it,
307+ if (type is CommandSpec .OptionType .Collection ) type.valueType else type,
308+ null ,
309+ )
310+ }
311+ .copy(metavarGetter = { type.toString() })
304312
305313@Suppress(" DuplicatedCode" )
306314fun RawArgument.convert (
@@ -321,7 +329,14 @@ fun RawArgument.convert(
321329 } else it
322330 return @convert parseFunction.parse(parseArg)
323331 }
324- else convert { convertValue(it, type, null ) }
332+ else
333+ convert {
334+ convertValue(
335+ it,
336+ if (type is CommandSpec .OptionType .Collection ) type.valueType else type,
337+ null ,
338+ )
339+ }
325340
326341// helpers for converting primitives/enums
327342
@@ -342,12 +357,12 @@ private val convertValue:
342357 is CommandSpec .OptionType .Primitive ->
343358 when (type.type) {
344359 CommandSpec .OptionType .Primitive .Type .NUMBER ->
345- value.toIntOrNull () ? : value.toDoubleOrNull()
360+ value.toLongOrNull () ? : value.toDoubleOrNull()
346361 CommandSpec .OptionType .Primitive .Type .FLOAT -> value.toDoubleOrNull()
347362 CommandSpec .OptionType .Primitive .Type .INT -> value.toLongOrNull(this , type)
348363 // ranges based on org.pkl.core.stdlib.math.MathNodes
349364 CommandSpec .OptionType .Primitive .Type .INT8 ->
350- value.toLongOrNull(this , type, Byte .MIN_VALUE .toLong().. < Byte .MAX_VALUE )
365+ value.toLongOrNull(this , type, Byte .MIN_VALUE .toLong().. Byte .MAX_VALUE )
351366 CommandSpec .OptionType .Primitive .Type .INT16 ->
352367 value.toLongOrNull(this , type, Short .MIN_VALUE .toLong().. Short .MAX_VALUE )
353368 CommandSpec .OptionType .Primitive .Type .INT32 ->
@@ -381,6 +396,6 @@ private val convertValue:
381396 is CommandSpec .OptionType .Enum ->
382397 if (type.choices.contains(value)) value
383398 else fail(" invalid choice: $value . (choose from ${type.choices.joinToString()} )" )
384- else -> fail(" unsupported ${mapPosition?.let { " map $it " }} type $type " )
399+ else -> fail(" unsupported ${mapPosition?.let { " map $it " } ? : " " } type $type " )
385400 } ? : fail(" $value is not a valid $type " )
386401 }
0 commit comments