diff --git a/invoke/parser/parser.py b/invoke/parser/parser.py index 43e95df04..eaec13d79 100644 --- a/invoke/parser/parser.py +++ b/invoke/parser/parser.py @@ -292,17 +292,9 @@ def handle(self, token: str) -> None: ) ) # noqa self.see_value(token) - # Positional args (must come above context-name check in case we still - # need a posarg and the user legitimately wants to give it a value that - # just happens to be a valid context name.) - elif self.context and self.context.missing_positional_args: - msg = "Context {!r} requires positional args, eating {!r}" - debug(msg.format(self.context, token)) - self.see_positional_arg(token) - # New context - elif token in self.contexts: - self.see_context(token) - # Initial-context flag being given as per-task flag (e.g. --help) + # Initial-context flag being given as per-task flag (e.g. --help). + # Must come above positional-args check so that flag-like tokens + # aren't consumed as positional argument values. elif self.initial and token in self.initial.flags: debug("Saw (initial-context) flag {!r}".format(token)) flag = self.initial.flags[token] @@ -318,6 +310,16 @@ def handle(self, token: str) -> None: # default-False 'dedupe') and it's up to us whether we actually # put any in place. self.switch_to_flag(token) + # Positional args (must come above context-name check in case we still + # need a posarg and the user legitimately wants to give it a value that + # just happens to be a valid context name.) + elif self.context and self.context.missing_positional_args: + msg = "Context {!r} requires positional args, eating {!r}" + debug(msg.format(self.context, token)) + self.see_positional_arg(token) + # New context + elif token in self.contexts: + self.see_context(token) # Unknown else: if not self.ignore_unknown: @@ -339,7 +341,18 @@ def complete_context(self) -> None: ) ) # Ensure all of context's positional args have been given. - if self.context and self.context.missing_positional_args: + # Skip validation when --help was requested (positional args aren't + # required just to ask for help). + help_was_requested = ( + self.initial + and "--help" in self.initial.flags + and self.initial.flags["--help"].raw_value is not None + ) + if ( + self.context + and self.context.missing_positional_args + and not help_was_requested + ): err = "'{}' did not receive required positional arguments: {}" names = ", ".join( "'{}'".format(x.name) diff --git a/tests/parser_parser.py b/tests/parser_parser.py index c750fd8c7..63e9e0e97 100644 --- a/tests/parser_parser.py +++ b/tests/parser_parser.py @@ -511,6 +511,18 @@ def by_itself_base_case(self): assert result[0].args.help.value == "mytask" assert "help" not in result[1].args + def not_consumed_as_positional_arg_value(self): + # A task with a required positional arg should still treat + # --help as the initial-context help flag, not eat it as + # the positional arg's value. (GH #982) + pos = Argument("pos", positional=True) + task1 = Context("mytask", args=[pos]) + init = Context(args=[Argument("help", optional=True)]) + parser = Parser(initial=init, contexts=[task1]) + result = parser.parse_argv(["mytask", "--help"]) + assert len(result) == 2 + assert result[0].args.help.value == "mytask" + def other_tokens_afterwards_raise_parse_errors(self): # NOTE: this is because of the special-casing where we supply # the task name as the value when the flag is literally named