diff --git a/test/lib/config/cli.py b/test/lib/config/cli.py index d5836cc35..4673fae8e 100644 --- a/test/lib/config/cli.py +++ b/test/lib/config/cli.py @@ -13,8 +13,7 @@ the help shows them without being asked for the command. The unit command is not in the command list the help shows since a test run is what calls it, in a container where only the repository copy is available, rather than something to run by hand. -A command parses into the same configuration as the test run, so an option belongs after the command, which replaces whatever was -given before it.""" +Options may be given before or after the command. The command parser parses all of them.""" #################################################################################################################################### import argparse @@ -157,6 +156,37 @@ def _parser_vm_build(command, parent): result.add_argument("--no-cache", dest="cache", action="store_false", help="build the containers without the cache") +#################################################################################################################################### +def _command_first(arg_list, parser, command_map): + """Move the command to the front of the argument list. + + The command is the first argument that is not an option or an option value. The argument list is unchanged when there is no + command.""" + + # Options that take a value in the main parser or any command parser + option_value = set() + + for parser_option in [parser, *command_map.values()]: + for action in parser_option._actions: + if action.nargs != 0: + option_value.update(action.option_strings) + + index = 0 + + while index < len(arg_list): + arg = arg_list[index] + + if not arg.startswith("-"): + if arg in command_map: + return [arg] + arg_list[:index] + arg_list[index + 1 :] + + break + + index += 2 if arg in option_value else 1 + + return arg_list + + #################################################################################################################################### def cli_parse(arg_list, version): """Build the parser and parse the command line.""" @@ -191,7 +221,8 @@ def cli_parse(arg_list, version): ) code_format.add_argument("--check", action="store_true", help="check the formatting rather than changing it") - result = parser.parse_args(arg_list) + # The command parser parses every option, including those given before the command + result = parser.parse_args(_command_first(arg_list, parser, command.choices)) # Fill in the command a command line with no command was asking for, so nothing downstream has to know it was left out if result.command is None: diff --git a/test/src/module/test/config/cli_test.py b/test/src/module/test/config/cli_test.py index f718b058d..56d30b4f3 100644 --- a/test/src/module/test/config/cli_test.py +++ b/test/src/module/test/config/cli_test.py @@ -160,8 +160,8 @@ def test_cli_test(): assert_true(config.lint_only) - # The command can be named, which is the same run with the same options - config = cli_parse(["test", "--vm=u24", "--module=common/error"], VERSION) + # The command can be named, which is the same run with the same options before or after it + config = cli_parse(["--vm", "u24", "test", "--module=common/error"], VERSION) assert_equal(config.command, "test") assert_equal(config.vm, "u24") @@ -181,7 +181,8 @@ def test_cli_vm_build(): assert_true(config.cache) assert_is_none(config.vm_arch) - config = cli_parse(["vm-build", "--vm=all", "--vm-arch=aarch64", "--no-cache"], VERSION) + # Options before the command + config = cli_parse(["--vm=all", "--vm-arch", "aarch64", "--no-cache", "vm-build"], VERSION) assert_equal(config.vm, "all") assert_equal(config.vm_arch, "aarch64") @@ -201,6 +202,18 @@ def test_cli_error(): assert_equal(status, 2) assert_in("unrecognized arguments: --bogus", output) + # An option before the command that the command does not define + status, output = _cli_parse_exit(["--module=common/error", "vm-build"]) + + assert_equal(status, 2) + assert_in("unrecognized arguments: --module=common/error", output) + + # An argument after the options that is not a command + status, output = _cli_parse_exit(["--vm=u24", "bogus"]) + + assert_equal(status, 2) + assert_in("invalid choice: 'bogus'", output) + # A level outside the log levels is rejected here rather than later when it is converted status, output = _cli_parse_exit(["unit", "common/error", "--log-level=bogus"])