Skip to content

Commit

Permalink
Apply default options for a CLI parent command when a sub command is …
Browse files Browse the repository at this point in the history
…parsed (#4593)

Closes #4580
  • Loading branch information
chalcolith authored Jan 20, 2025
1 parent ff61416 commit 8693c6d
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .release-notes/4593.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Apply default options for a CLI parent command when a sub command is parsed

In the CLI package's parser, a default option for a parent command was ignored when a subcommand was present. This fix makes sure that parents' defaults are applied before handling the sub command.
23 changes: 23 additions & 0 deletions packages/cli/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ actor \nodoc\ Main is TestList
test(_TestBools)
test(_TestChat)
test(_TestDefaults)
test(_TestDefaultWithSub)
test(_TestDuplicate)
test(_TestEnvs)
test(_TestHelp)
Expand Down Expand Up @@ -238,6 +239,17 @@ class \nodoc\ iso _TestDefaults is UnitTest
h.assert_eq[F64](42.0, cmd.option("floato").f64())
h.assert_eq[USize](0, cmd.option("stringso").string_seq().size())

class \nodoc\ iso _TestDefaultWithSub is UnitTest
fun name(): String => "cli/default_with_sub"

fun apply(h: TestHelper) ? =>
let cs = _Fixtures.default_with_sub_spec()?
h.assert_true(cs.is_parent())

let cmd = CommandParser(cs).parse([ "cmd"; "sub" ]) as Command

h.assert_eq[String]("foo", cmd.option("arg").string())

class \nodoc\ iso _TestShortsAdj is UnitTest
fun name(): String => "cli/shorts_adjacent"

Expand Down Expand Up @@ -668,3 +680,14 @@ primitive _Fixtures
ArgSpec.string_seq("args", "Arguments to run.")
])?
])?

fun default_with_sub_spec(): CommandSpec box ? =>
let root = CommandSpec.parent(
"cmd",
"Main command",
[ OptionSpec.string("arg", "an arg" where default' = "foo") ])?
let sub = CommandSpec.leaf("sub", "Sub command")?

root.add_command(sub)?
root.add_help()?
root
38 changes: 30 additions & 8 deletions packages/cli/command_parser.pony
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ class CommandParser
try
match _spec.commands()(token)?
| let cs: CommandSpec box =>
// check args and assign defaults
match _check_args(options, args, envsmap, arg_pos)
| let se: SyntaxError =>
return se
end

return CommandParser._sub(cs, this).
_parse_command(tokens, options, args, envsmap, opt_stop)
end
Expand Down Expand Up @@ -143,6 +149,29 @@ class CommandParser
return Help.general(_root_spec())
end

// check args and assign defaults
match _check_args(options, args, envsmap, arg_pos)
| let se: SyntaxError =>
return se
end

// Specifying only the parent and not a leaf command is an error.
if _spec.is_parent() then
return SyntaxError(_spec.name(), "missing subcommand")
end

// A successfully parsed and populated leaf Command.
Command._create(_spec, _fullname(), consume options, args)

fun _check_args(
options: Map[String,Option] ref,
args: Map[String,Arg] ref,
envsmap: Map[String, String] box,
arg_pos': USize)
: (SyntaxError | USize)
=>
var arg_pos = arg_pos'

// Fill in option values from env or from coded defaults.
for os in _spec.options().values() do
if not options.contains(os.name()) then
Expand Down Expand Up @@ -190,14 +219,7 @@ class CommandParser
end
arg_pos = arg_pos + 1
end

// Specifying only the parent and not a leaf command is an error.
if _spec.is_parent() then
return SyntaxError(_spec.name(), "missing subcommand")
end

// A successfully parsed and populated leaf Command.
Command._create(_spec, _fullname(), consume options, args)
arg_pos

fun _parse_long_option(
token: String,
Expand Down

0 comments on commit 8693c6d

Please sign in to comment.