From c7c9670597f33d93af6492debcddab6a5acff78e Mon Sep 17 00:00:00 2001 From: aemous Date: Mon, 31 Aug 2026 17:39:04 -0400 Subject: [PATCH 01/11] Implement --help alias for help subcommand. --- awscli/argparser.py | 42 +++++++++++++++++++++++ awscli/clidriver.py | 2 +- tests/functional/docs/test_help_output.py | 33 ++++++++++++++++++ tests/unit/test_clidriver.py | 3 +- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/awscli/argparser.py b/awscli/argparser.py index 8ddb5f228550..0dbf78c91046 100644 --- a/awscli/argparser.py +++ b/awscli/argparser.py @@ -60,6 +60,46 @@ def choices(self, val): pass + +class _HelpFlagResolver(argparse.ArgumentParser): + """Minimal parser that resolves --help (and abbreviations) to 'help'. + + This is used as a first pass before the real parse so that argparse + handles abbreviation matching (e.g. --hel, --he) 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): + # Suppress errors from this resolver; the real parser will + # report them. + raise ArgParseException(message) + + +_HELP_RESOLVER = _HelpFlagResolver() + + +def _rewrite_help_flag(args): + """Rewrite --help (and abbreviations) to the help positional. + + Uses a minimal argparse parser so that standard abbreviation + matching is applied (e.g. --hel, --he all resolve to --help). + """ + try: + parsed, remaining = _HELP_RESOLVER.parse_known_args(args) + except ArgParseException: + return args + if parsed.help_flag: + remaining.append('help') + return remaining + return args + + class CLIArgParser(argparse.ArgumentParser): Formatter = argparse.RawTextHelpFormatter @@ -81,6 +121,7 @@ def _check_value(self, action, value): raise argparse.ArgumentError(action, '\n'.join(msg)) def parse_known_args(self, args, namespace=None): + args = _rewrite_help_flag(args) parsed, remaining = super().parse_known_args(args, namespace) terminal_encoding = getattr(sys.stdin, 'encoding', 'utf-8') if terminal_encoding is None: @@ -158,6 +199,7 @@ def _build(self, command_table, version_string, argument_table): ) + class ServiceArgParser(CLIArgParser): def __init__(self, operations_table, service_name): super().__init__( diff --git a/awscli/clidriver.py b/awscli/clidriver.py index bc860a4a8bf2..f9ec6f3fb345 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -94,7 +94,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 diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index c627064257fc..de8bd84091fa 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -267,6 +267,39 @@ def test_deprecated_operations_not_documented(self): self.assert_not_contains('put-bucket-notification\n') +class TestHelpFlagOutput(BaseAWSHelpOutputTest): + """Test that --help flag produces the same output as the help subcommand.""" + + def test_top_level_help_flag(self): + self.driver.main(['--help']) + self.assert_contains('***\naws\n***') + self.assert_contains( + 'The AWS Command Line Interface is a unified tool ' + 'to manage your AWS services.' + ) + + def test_service_help_flag(self): + self.driver.main(['ec2', '--help']) + self.assert_contains('***\nec2\n***') + self.assert_contains('===========\nDescription\n===========') + self.assert_contains('* describe-instances') + + def test_operation_help_flag(self): + self.driver.main(['ec2', 'run-instances', '--help']) + self.assert_contains('*************\nrun-instances\n*************') + self.assert_contains('Launches the specified number of instances') + + def test_custom_service_help_flag(self): + self.driver.main(['s3', '--help']) + self.assert_contains('high-level S3 commands') + self.assert_contains('* cp') + + def test_custom_operation_help_flag(self): + self.driver.main(['s3', 'ls', '--help']) + self.assert_contains('List S3 objects') + + + class TestRemoveDeprecatedCommands(BaseAWSHelpOutputTest): def assert_command_does_not_exist(self, service, command): # Basically try to get the help output for the removed diff --git a/tests/unit/test_clidriver.py b/tests/unit/test_clidriver.py index 7635c302e7aa..5aacf1e55839 100644 --- a/tests/unit/test_clidriver.py +++ b/tests/unit/test_clidriver.py @@ -217,6 +217,7 @@ def _generate_auto_prompt_resolve_cases(): Case(['--no-cli-auto-prompt'], 'on-partial', 'off'), Case(['--version'], 'on', 'off'), Case(['help'], 'on', 'off'), + Case(['--help'], 'on', 'off'), ] @@ -834,7 +835,7 @@ def test_help_blurb_in_operation_error_message(self): self.assertIn(HELP_BLURB, self.stderr.getvalue()) def test_help_blurb_in_unknown_argument_error_message(self): - args = ['s3api', 'list-objects', '--help'] + args = ['s3api', 'list-objects', '--unknown-flag-xyz'] driver = create_clidriver(args) rc = driver.main(args) self.assertEqual(rc, 252) From 61a72242d39d9c452854cc924252c3f966430e07 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 14:04:33 -0400 Subject: [PATCH 02/11] Update options and synopsis. --- awscli/examples/global_options.rst | 4 ++++ awscli/examples/global_synopsis.rst | 1 + 2 files changed, 5 insertions(+) diff --git a/awscli/examples/global_options.rst b/awscli/examples/global_options.rst index 20354555fe3c..d537a13bbe1e 100644 --- a/awscli/examples/global_options.rst +++ b/awscli/examples/global_options.rst @@ -115,4 +115,8 @@ * 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..b57876bbe7f4 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 ] +[--help] \ No newline at end of file From f9b31e05d5699a45c20adeea14311f9de53e6375 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 14:09:44 -0400 Subject: [PATCH 03/11] Add new changelog entry. --- .changes/next-release/enhancement-help-27828.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/next-release/enhancement-help-27828.json 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." +} From ae8b30e7e8ef2441d9b24670a38bb52ac91da5c1 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 17:28:37 -0400 Subject: [PATCH 04/11] Re-architect the --help implementation so that we get expected behavior in all cases. --- awscli/argparser.py | 25 ++++----- awscli/clidriver.py | 39 ++++++++++++++ tests/functional/docs/test_help_output.py | 62 ++++++++++++++++++++++- 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/awscli/argparser.py b/awscli/argparser.py index 0dbf78c91046..13ca8d49b8a6 100644 --- a/awscli/argparser.py +++ b/awscli/argparser.py @@ -62,11 +62,10 @@ def choices(self, val): class _HelpFlagResolver(argparse.ArgumentParser): - """Minimal parser that resolves --help (and abbreviations) to 'help'. + """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 (e.g. --hel, --he) consistently with - all other flags. + handles abbreviation matching consistently with all other flags. """ def __init__(self): @@ -76,28 +75,25 @@ def __init__(self): ) def error(self, message): - # Suppress errors from this resolver; the real parser will - # report them. raise ArgParseException(message) _HELP_RESOLVER = _HelpFlagResolver() -def _rewrite_help_flag(args): - """Rewrite --help (and abbreviations) to the help positional. +def detect_help_flag(args): + """Detect and strip --help (and abbreviations) from args. - Uses a minimal argparse parser so that standard abbreviation - matching is applied (e.g. --hel, --he all resolve to --help). + 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. """ try: parsed, remaining = _HELP_RESOLVER.parse_known_args(args) except ArgParseException: - return args - if parsed.help_flag: - remaining.append('help') - return remaining - return args + return args, False + return remaining, parsed.help_flag class CLIArgParser(argparse.ArgumentParser): @@ -121,7 +117,6 @@ def _check_value(self, action, value): raise argparse.ArgumentError(action, '\n'.join(msg)) def parse_known_args(self, args, namespace=None): - args = _rewrite_help_flag(args) parsed, remaining = super().parse_known_args(args, namespace) terminal_encoding = getattr(sys.stdin, 'encoding', 'utf-8') if terminal_encoding is None: diff --git a/awscli/clidriver.py b/awscli/clidriver.py index f9ec6f3fb345..864ccf6e884e 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 ( @@ -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): + # Walk the command tree 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 + remaining_args = list(args) + for arg in args: + if arg.startswith('-'): + continue + if arg in command_table: + current_cmd = command_table[arg] + remaining_args = [a for a in remaining_args if a != arg] + # Try to go one level deeper (service → operation). + command_table = getattr( + current_cmd, 'subcommand_table', {} + ) + else: + # Bare word that isn't a known command — let the real + # parser produce the "invalid choice" error. We append + # the bare word back so the parser sees it. + if current_cmd is not None: + # We matched a command already; delegate to it with + # the invalid token so its parser errors. + remaining_args.append('help') + return current_cmd(remaining_args, 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/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index de8bd84091fa..8935316dc362 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -299,7 +299,6 @@ def test_custom_operation_help_flag(self): self.assert_contains('List S3 objects') - class TestRemoveDeprecatedCommands(BaseAWSHelpOutputTest): def assert_command_does_not_exist(self, service, command): # Basically try to get the help output for the removed @@ -634,3 +633,64 @@ def test_docs_opens_browser( "Opening help file in the default browser." in runner_result.stdout ) mock_open_new_tab.assert_called_once() + + +class TestHelpFlagWithMissingParamValue(BaseAWSHelpOutputTest): + def test_help_flag_takes_priority_over_missing_param_value(self): + # --query is a global arg unknown to the operation parser, so --help + # takes priority and renders operation help. + self.driver.main( + [ + "s3api", + "delete-object", + "--bucket", + "b", + "--key", + "k", + "--query", + "--help", + ] + ) + self.assert_contains('delete-object') + + +class TestHelpFlagEdgeCases(BaseAWSHelpOutputTest): + """Test --help edge cases: abbreviations, invalid commands, and + operation-level params that take values.""" + + def test_help_flag_abbreviation(self): + self.driver.main(['--hel']) + self.assert_contains('***\naws\n***') + + def test_help_flag_with_operation_level_choices_param(self): + # --acl is an operation-level param with constrained choices. + # --help should render help, not error about invalid choice. + self.driver.main(['s3', 'cp', '--acl', '--help']) + self.assert_contains('cp') + + def test_help_flag_with_operation_level_value_param(self): + # --expected-size takes a free-form value. + # --help should render help, not consume 'help' as the value. + self.driver.main(['s3', 'cp', '--expected-size', '--help']) + self.assert_contains('cp') + + def test_help_flag_with_invalid_top_level_command(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['fake-service', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) + + def test_help_flag_with_invalid_service_operation(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['s3api', 'fake-command', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) + + def test_help_flag_with_invalid_custom_command_operation(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['s3', 'fake-command', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) From ce114d8ab9932a8a360c6f7f0aa57eb2b00b4af8 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 18:01:19 -0400 Subject: [PATCH 05/11] Fix edge case. --- awscli/argparser.py | 1 - awscli/clidriver.py | 28 +++++++++++------------ tests/functional/docs/test_help_output.py | 7 ++++++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/awscli/argparser.py b/awscli/argparser.py index 13ca8d49b8a6..57a4b4f3d18d 100644 --- a/awscli/argparser.py +++ b/awscli/argparser.py @@ -194,7 +194,6 @@ def _build(self, command_table, version_string, argument_table): ) - class ServiceArgParser(CLIArgParser): def __init__(self, operations_table, service_name): super().__init__( diff --git a/awscli/clidriver.py b/awscli/clidriver.py index 864ccf6e884e..487acd5a9b93 100644 --- a/awscli/clidriver.py +++ b/awscli/clidriver.py @@ -620,34 +620,34 @@ def main(self, args=None): ) def _route_help(self, args, command_table): - # Walk the command tree 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. + # 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 - remaining_args = list(args) for arg in args: if arg.startswith('-'): continue if arg in command_table: current_cmd = command_table[arg] - remaining_args = [a for a in remaining_args if a != arg] - # Try to go one level deeper (service → operation). 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. We append - # the bare word back so the parser sees it. + # Bare word that isn't a known command — let the + # real parser produce the "invalid choice" error. if current_cmd is not None: - # We matched a command already; delegate to it with - # the invalid token so its parser errors. - remaining_args.append('help') - return current_cmd(remaining_args, None) + current_cmd([arg, 'help'], None) else: parser = self.create_parser(command_table) parser.parse_known_args(args) - return + return if current_cmd is None: return self.create_help_command()([], None) help_cmd = current_cmd.create_help_command() diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index 8935316dc362..a649bb1beb7f 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -674,6 +674,13 @@ def test_help_flag_with_operation_level_value_param(self): self.driver.main(['s3', 'cp', '--expected-size', '--help']) self.assert_contains('cp') + def test_help_flag_with_positional_args(self): + # Positional args should be ignored when --help is present. + self.driver.main( + ['s3', 'cp', 'localfile', 's3://bucket/key', '--help'] + ) + self.assert_contains('cp') + def test_help_flag_with_invalid_top_level_command(self): stderr = StringIO() with mock.patch('sys.stderr', stderr): From f8f80a361657240b90db0bf0087cad3ba5d56daf Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 18:10:12 -0400 Subject: [PATCH 06/11] Reorganize tests. --- tests/functional/docs/test_help_output.py | 127 +++++++++++----------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index a649bb1beb7f..3772b7940d5b 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -298,6 +298,67 @@ def test_custom_operation_help_flag(self): self.driver.main(['s3', 'ls', '--help']) self.assert_contains('List S3 objects') + def test_help_flag_takes_priority_over_missing_param_value(self): + # --query is a global arg unknown to the operation parser, so --help + # takes priority and renders operation help. + self.driver.main( + [ + "s3api", + "delete-object", + "--bucket", + "b", + "--key", + "k", + "--query", + "--help", + ] + ) + self.assert_contains('delete-object') + + def test_help_flag_abbreviation(self): + self.driver.main(['--hel']) + self.assert_contains('***\naws\n***') + + def test_help_flag_with_operation_level_choices_param(self): + # --acl is an operation-level param with constrained choices. + # --help should render help, not error about invalid choice. + self.driver.main(['s3', 'cp', '--acl', '--help']) + self.assert_contains('cp') + + def test_help_flag_with_operation_level_value_param(self): + # --expected-size takes a free-form value. + # --help should render help, not consume 'help' as the value. + self.driver.main(['s3', 'cp', '--expected-size', '--help']) + self.assert_contains('cp') + + def test_help_flag_with_positional_args(self): + # Positional args should be ignored when --help is present. + self.driver.main( + ['s3', 'cp', 'localfile', 's3://bucket/key', '--help'] + ) + self.assert_contains('cp') + + def test_help_flag_with_invalid_top_level_command(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['fake-service', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) + + def test_help_flag_with_invalid_service_operation(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['s3api', 'fake-command', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) + + def test_help_flag_with_invalid_custom_command_operation(self): + stderr = StringIO() + with mock.patch('sys.stderr', stderr): + rc = self.driver.main(['s3', 'fake-command', '--help']) + self.assertEqual(rc, 252) + self.assertIn('Found invalid choice', stderr.getvalue()) + class TestRemoveDeprecatedCommands(BaseAWSHelpOutputTest): def assert_command_does_not_exist(self, service, command): @@ -635,69 +696,3 @@ def test_docs_opens_browser( mock_open_new_tab.assert_called_once() -class TestHelpFlagWithMissingParamValue(BaseAWSHelpOutputTest): - def test_help_flag_takes_priority_over_missing_param_value(self): - # --query is a global arg unknown to the operation parser, so --help - # takes priority and renders operation help. - self.driver.main( - [ - "s3api", - "delete-object", - "--bucket", - "b", - "--key", - "k", - "--query", - "--help", - ] - ) - self.assert_contains('delete-object') - - -class TestHelpFlagEdgeCases(BaseAWSHelpOutputTest): - """Test --help edge cases: abbreviations, invalid commands, and - operation-level params that take values.""" - - def test_help_flag_abbreviation(self): - self.driver.main(['--hel']) - self.assert_contains('***\naws\n***') - - def test_help_flag_with_operation_level_choices_param(self): - # --acl is an operation-level param with constrained choices. - # --help should render help, not error about invalid choice. - self.driver.main(['s3', 'cp', '--acl', '--help']) - self.assert_contains('cp') - - def test_help_flag_with_operation_level_value_param(self): - # --expected-size takes a free-form value. - # --help should render help, not consume 'help' as the value. - self.driver.main(['s3', 'cp', '--expected-size', '--help']) - self.assert_contains('cp') - - def test_help_flag_with_positional_args(self): - # Positional args should be ignored when --help is present. - self.driver.main( - ['s3', 'cp', 'localfile', 's3://bucket/key', '--help'] - ) - self.assert_contains('cp') - - def test_help_flag_with_invalid_top_level_command(self): - stderr = StringIO() - with mock.patch('sys.stderr', stderr): - rc = self.driver.main(['fake-service', '--help']) - self.assertEqual(rc, 252) - self.assertIn('Found invalid choice', stderr.getvalue()) - - def test_help_flag_with_invalid_service_operation(self): - stderr = StringIO() - with mock.patch('sys.stderr', stderr): - rc = self.driver.main(['s3api', 'fake-command', '--help']) - self.assertEqual(rc, 252) - self.assertIn('Found invalid choice', stderr.getvalue()) - - def test_help_flag_with_invalid_custom_command_operation(self): - stderr = StringIO() - with mock.patch('sys.stderr', stderr): - rc = self.driver.main(['s3', 'fake-command', '--help']) - self.assertEqual(rc, 252) - self.assertIn('Found invalid choice', stderr.getvalue()) From cb1193fa9f3535f651d0eba08ae6b7d4dea02ec1 Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 18:13:15 -0400 Subject: [PATCH 07/11] Formatting --- tests/functional/docs/test_help_output.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index 3772b7940d5b..b05bd00cbab0 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -694,5 +694,3 @@ def test_docs_opens_browser( "Opening help file in the default browser." in runner_result.stdout ) mock_open_new_tab.assert_called_once() - - From de6ba42af0553e4f9b5b6c64806904a16ce4c3db Mon Sep 17 00:00:00 2001 From: aemous Date: Wed, 16 Sep 2026 19:24:22 -0400 Subject: [PATCH 08/11] Add help to cli.json for consistency. --- awscli/argparser.py | 8 ++++++++ awscli/data/cli.json | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/awscli/argparser.py b/awscli/argparser.py index 57a4b4f3d18d..a95e2ebaf497 100644 --- a/awscli/argparser.py +++ b/awscli/argparser.py @@ -88,6 +88,14 @@ def detect_help_flag(args): 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) 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.

" } } } From 408e817acce5d1c6854b824a8406afe7493434d5 Mon Sep 17 00:00:00 2001 From: aemous Date: Thu, 17 Sep 2026 09:21:56 -0400 Subject: [PATCH 09/11] Formatting. --- awscli/examples/global_synopsis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli/examples/global_synopsis.rst b/awscli/examples/global_synopsis.rst index b57876bbe7f4..f310cb57eaf2 100644 --- a/awscli/examples/global_synopsis.rst +++ b/awscli/examples/global_synopsis.rst @@ -17,4 +17,4 @@ [--cli-auto-prompt] [--no-cli-auto-prompt] [--cli-error-format ] -[--help] \ No newline at end of file +[--help] From 80f0d9ecb4220c033e704db141af3d94ddbae2b2 Mon Sep 17 00:00:00 2001 From: aemous Date: Thu, 17 Sep 2026 09:30:33 -0400 Subject: [PATCH 10/11] More formatting. --- awscli/examples/global_options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awscli/examples/global_options.rst b/awscli/examples/global_options.rst index d537a13bbe1e..aae2f6b18456 100644 --- a/awscli/examples/global_options.rst +++ b/awscli/examples/global_options.rst @@ -115,8 +115,8 @@ * enhanced + ``--help`` (boolean) Display help for the command/subcommand. - From 33b16dc72b972a1c8ccd2752e17486c117b6f8c3 Mon Sep 17 00:00:00 2001 From: aemous Date: Thu, 17 Sep 2026 09:46:50 -0400 Subject: [PATCH 11/11] Add two new test cases for the help flag. --- tests/functional/docs/test_help_output.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/functional/docs/test_help_output.py b/tests/functional/docs/test_help_output.py index b05bd00cbab0..f36116c1bcb5 100644 --- a/tests/functional/docs/test_help_output.py +++ b/tests/functional/docs/test_help_output.py @@ -338,6 +338,17 @@ def test_help_flag_with_positional_args(self): ) self.assert_contains('cp') + def test_help_flag_before_modeled_operation(self): + # --help before the operation should still show operation help. + self.driver.main(['s3api', '--help', 'put-object']) + self.assert_contains('put-object') + + def test_help_flag_before_modeled_service(self): + # --help before the service should still show the deepest + # recognized command's help. + self.driver.main(['--help', 's3api', 'put-object']) + self.assert_contains('put-object') + def test_help_flag_with_invalid_top_level_command(self): stderr = StringIO() with mock.patch('sys.stderr', stderr):