diff --git a/.release-notes/4593.md b/.release-notes/4593.md new file mode 100644 index 0000000000..44becdec2e --- /dev/null +++ b/.release-notes/4593.md @@ -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. diff --git a/packages/cli/_test.pony b/packages/cli/_test.pony index 9fd121c6a9..5c00372bc8 100644 --- a/packages/cli/_test.pony +++ b/packages/cli/_test.pony @@ -10,6 +10,7 @@ actor \nodoc\ Main is TestList test(_TestBools) test(_TestChat) test(_TestDefaults) + test(_TestDefaultWithSub) test(_TestDuplicate) test(_TestEnvs) test(_TestHelp) @@ -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" @@ -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 diff --git a/packages/cli/command_parser.pony b/packages/cli/command_parser.pony index a5acce8eab..41f4e17afc 100644 --- a/packages/cli/command_parser.pony +++ b/packages/cli/command_parser.pony @@ -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 @@ -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 @@ -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,