diff --git a/.changes/next-release/enhancement-help-27828.json b/.changes/next-release/enhancement-help-27828.json new file mode 100644 index 000000000000..dc445887f73d --- /dev/null +++ b/.changes/next-release/enhancement-help-27828.json @@ -0,0 +1,5 @@ +{ + "type": "enhancement", + "category": "help", + "description": "Add a ``--help`` parameter that renders the same help as the ``help`` parameter on every command." +} diff --git a/awscli/argparser.py b/awscli/argparser.py index 8ddb5f228550..a95e2ebaf497 100644 --- a/awscli/argparser.py +++ b/awscli/argparser.py @@ -60,6 +60,50 @@ def choices(self, val): pass + +class _HelpFlagResolver(argparse.ArgumentParser): + """Minimal parser that detects --help (and abbreviations like --hel, --he). + + This is used as a first pass before the real parse so that argparse + handles abbreviation matching consistently with all other flags. + """ + + def __init__(self): + super().__init__(add_help=False) + self.add_argument( + '--help', action='store_true', default=False, dest='help_flag' + ) + + def error(self, message): + raise ArgParseException(message) + + +_HELP_RESOLVER = _HelpFlagResolver() + + +def detect_help_flag(args): + """Detect and strip --help (and abbreviations) from args. + + Returns (remaining_args, help_detected). When --help is present the + flag is removed from the arg list but is *not* replaced with the + positional ``help`` token. Callers are responsible for routing to + the appropriate help rendering. + + --help is intercepted here rather than handled by argparse because + it must take priority over all other argument validation. If --help + were a normal parser argument, a preceding value-taking flag (e.g. + ``--query --help``) could consume it as that flag's value, or the + parser could reject it due to missing required positional args. + By stripping it early, we guarantee that --help always renders help + regardless of what else is in the arg list. + """ + try: + parsed, remaining = _HELP_RESOLVER.parse_known_args(args) + except ArgParseException: + return args, False + return remaining, parsed.help_flag + + class CLIArgParser(argparse.ArgumentParser): Formatter = argparse.RawTextHelpFormatter diff --git a/awscli/clidriver.py b/awscli/clidriver.py index bc860a4a8bf2..487acd5a9b93 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -41,6 +41,7 @@ MainArgParser, ServiceArgParser, SubCommandArgParser, + detect_help_flag, ) from awscli.argprocess import unpack_argument from awscli.arguments import ( @@ -94,7 +95,7 @@ HISTORY_RECORDER = get_global_history_recorder() METADATA_FILENAME = 'metadata.json' INSTALL_FILENAME = 'install.json' -_NO_AUTO_PROMPT_ARGS = ['help', '--version'] +_NO_AUTO_PROMPT_ARGS = ['help', '--help', '--version'] _CLI_AUTO_PROMPT_OPTION = '--cli-auto-prompt' _NO_CLI_AUTO_PROMPT_OPTION = '--no-cli-auto-prompt' # Don't remove this line. The idna encoding @@ -589,8 +590,11 @@ def main(self, args=None): command_table = self._get_command_table() parser = self.create_parser(command_table) self._add_aliases(command_table, parser) + args, help_flag = detect_help_flag(args) parsed_args = None try: + if help_flag: + return self._route_help(args, command_table) # Because _handle_top_level_args emits events, it's possible # that exceptions can be raised, which should have the same # general exception handling logic as calling into the @@ -615,6 +619,41 @@ def main(self, args=None): parsed_globals=parsed_args, ) + def _route_help(self, args, command_table): + # Follow the user's command path (e.g. "s3api delete-object") to + # find the deepest recognized command, then render its help + # directly. This avoids injecting a bare 'help' token that + # could be consumed as a flag value. + current_cmd = None + for arg in args: + if arg.startswith('-'): + continue + if arg in command_table: + current_cmd = command_table[arg] + command_table = getattr( + current_cmd, 'subcommand_table', {} + ) + if not command_table: + # No further subcommands (e.g. we reached an + # operation). Stop scanning so remaining bare + # words (positional param values) aren't + # misinterpreted as commands. + break + else: + # Bare word that isn't a known command — let the + # real parser produce the "invalid choice" error. + if current_cmd is not None: + current_cmd([arg, 'help'], None) + else: + parser = self.create_parser(command_table) + parser.parse_known_args(args) + return + if current_cmd is None: + return self.create_help_command()([], None) + help_cmd = current_cmd.create_help_command() + if help_cmd is not None: + return help_cmd([], None) + def _emit_session_event(self, parsed_args): # This event is guaranteed to run after the session has been # initialized and a profile has been set. This was previously diff --git a/awscli/data/cli.json b/awscli/data/cli.json index 312d46b9e192..d42a14bfb77e 100644 --- a/awscli/data/cli.json +++ b/awscli/data/cli.json @@ -97,6 +97,10 @@ "enhanced" ], "help": "
The formatting style for error output. By default, errors are displayed in enhanced format.
" + }, + "help": { + "action": "store_true", + "help": "Display help for the command/subcommand.
" } } } diff --git a/awscli/examples/global_options.rst b/awscli/examples/global_options.rst index 20354555fe3c..aae2f6b18456 100644 --- a/awscli/examples/global_options.rst +++ b/awscli/examples/global_options.rst @@ -116,3 +116,7 @@ * enhanced +``--help`` (boolean) + + Display help for the command/subcommand. + diff --git a/awscli/examples/global_synopsis.rst b/awscli/examples/global_synopsis.rst index 3e603348debb..f310cb57eaf2 100644 --- a/awscli/examples/global_synopsis.rst +++ b/awscli/examples/global_synopsis.rst @@ -17,3 +17,4 @@ [--cli-auto-prompt] [--no-cli-auto-prompt] [--cli-error-format