Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply default options for a CLI parent command when a sub command is parsed #4593

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading