diff --git a/invoke/parser/context.py b/invoke/parser/context.py index 359e9f9e2..1e036e411 100644 --- a/invoke/parser/context.py +++ b/invoke/parser/context.py @@ -96,6 +96,7 @@ def __init__( self.aliases = aliases for arg in args: self.add_arg(arg) + self.skip_checks = False def __repr__(self) -> str: aliases = "" diff --git a/invoke/parser/parser.py b/invoke/parser/parser.py index 43e95df04..1180d67de 100644 --- a/invoke/parser/parser.py +++ b/invoke/parser/parser.py @@ -296,6 +296,10 @@ def handle(self, token: str) -> None: # 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: + if self._is_token_help(token): + debug("--help passed, skip positional arg") + self._force_help() + return msg = "Context {!r} requires positional args, eating {!r}" debug(msg.format(self.context, token)) self.see_positional_arg(token) @@ -305,12 +309,11 @@ def handle(self, token: str) -> None: # Initial-context flag being given as per-task flag (e.g. --help) elif self.initial and token in self.initial.flags: debug("Saw (initial-context) flag {!r}".format(token)) - flag = self.initial.flags[token] # Special-case for core --help flag: context name is used as value. - if flag.name == "help": - flag.value = self.context.name - msg = "Saw --help in a per-task context, setting task name ({!r}) as its value" # noqa - debug(msg.format(flag.value)) + if self._is_token_help(token): + debug("--help passed") + self._force_help() + return # All others: just enter the 'switch to flag' parser state else: # TODO: handle inverse core flags too? There are none at the @@ -338,15 +341,21 @@ def complete_context(self) -> None: self.context.name if self.context else self.context ) ) - # Ensure all of context's positional args have been given. - if self.context and self.context.missing_positional_args: + if not self.context: + return + + # Ensure all of context's positional args have been given + if ( + not self.context.skip_checks + and self.context.missing_positional_args + ): err = "'{}' did not receive required positional arguments: {}" names = ", ".join( "'{}'".format(x.name) for x in self.context.missing_positional_args ) self.error(err.format(self.context.name, names)) - if self.context and self.context not in self.result: + if self.context not in self.result: self.result.append(self.context) def switch_to_context(self, name: str) -> None: @@ -453,3 +462,18 @@ def see_positional_arg(self, value: Any) -> None: def error(self, msg: str) -> None: raise ParseError(msg, self.context) + + def _is_token_help(self, token: str) -> bool: + try: + flag = self.initial.flags[token] + if flag.name == "help": + return True + except KeyError: + pass + except AttributeError: + pass + return False + + def _force_help(self) -> None: + self.initial.flags["--help"].value = self.context.name + self.context.skip_checks = True diff --git a/invoke/program.py b/invoke/program.py index c7e5cd004..b65a56e84 100644 --- a/invoke/program.py +++ b/invoke/program.py @@ -403,6 +403,8 @@ def run(self, argv: Optional[List[str]] = None, exit: bool = True) -> None: # problems. if isinstance(e, ParseError): print(e, file=sys.stderr) + if e.context and e.context.name: + self.print_task_help(e.context.name) if isinstance(e, Exit) and e.message: print(e.message, file=sys.stderr) if isinstance(e, UnexpectedExit) and e.result.hide: diff --git a/tests/parser_parser.py b/tests/parser_parser.py index c750fd8c7..6db819a78 100644 --- a/tests/parser_parser.py +++ b/tests/parser_parser.py @@ -501,6 +501,45 @@ def core_bool_but_per_task_string(self): assert result[0].args.hide.value is False assert result[1].args.hide.value == "both" + def help_passed_when_task_expects_one_positional_arg(self): + init = Context( + args=[Argument(names=("help", "h"), optional=True)] + ) + task1 = Context( + "mytask", + args=[ + Argument( + names=("name", "n"), kind=str, positional=True + ) + ], + ) + parser = Parser(initial=init, contexts=[task1]) + result = parser.parse_argv(["mytask", "--help"]) + assert result[0].flags["--help"].value == "mytask" + + def help_passed_when_task_expects_multiple_positional_arg(self): + init = Context( + args=[Argument(names=("help", "h"), optional=True)] + ) + task1 = Context( + "mytask", + args=[ + Argument( + names=("pos_arg_one", "o"), + kind=str, + positional=True, + ), + Argument( + names=("pos_arg_two", "t"), + kind=str, + positional=True, + ), + ], + ) + parser = Parser(initial=init, contexts=[task1]) + result = parser.parse_argv(["mytask", "--help"]) + assert result[0].flags["--help"].value == "mytask" + class help_treats_context_name_as_its_value: def by_itself_base_case(self): task1 = Context("mytask") diff --git a/tests/program.py b/tests/program.py index 2d249b1fb..100005fab 100644 --- a/tests/program.py +++ b/tests/program.py @@ -653,6 +653,23 @@ def prints_help_for_task_only(self): for flag in ["-h", "--help"]: expect("-c decorators {} punch".format(flag), out=expected) + def prints_help_if_no_mandatory_arg(self): + expected = """ +Usage: invoke [--core-opts] punch [--options] [other tasks here ...] + +Docstring: + none + +Options: + -h STRING, --why=STRING Motive + -w STRING, --who=STRING Who to punch + +""".lstrip() + expected_error = """ +'punch' did not receive required positional arguments: 'who', 'why' +""".lstrip() + expect("-c decorators punch", out=expected, err=expected_error) + def works_for_unparameterized_tasks(self): expected = """ Usage: invoke [--core-opts] biz [other tasks here ...]