-
Notifications
You must be signed in to change notification settings - Fork 4.7k
[v2] Add --help parameter for all CLI commands #10630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "type": "feature", | ||
| "category": "help", | ||
| "description": "Add a ``--help`` parameter that renders the same help as the ``help`` subcommand on every command (provider, service, operation, and custom commands)." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,11 +36,15 @@ | |
| from awscli import __version__ | ||
| from awscli.alias import AliasCommandInjector, AliasLoader | ||
| from awscli.argparser import ( | ||
| ArgParseException, | ||
| ArgTableArgParser, | ||
| FirstPassGlobalArgParser, | ||
| MainArgParser, | ||
| ServiceArgParser, | ||
| SubCommandArgParser, | ||
| first_help_option_index, | ||
| is_help_option_present, | ||
| strip_help_options, | ||
| ) | ||
| from awscli.argprocess import unpack_argument | ||
| from awscli.arguments import ( | ||
|
|
@@ -94,7 +98,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 | ||
|
|
@@ -591,6 +595,16 @@ def main(self, args=None): | |
| self._add_aliases(command_table, parser) | ||
| parsed_args = None | ||
| try: | ||
| # When --help is present but no command is named, render provider | ||
| # help instead of failing on the required ``command`` positional | ||
| # argument. When a command is named, fall through so its own | ||
| # __call__ renders the more specific help. Kept inside the try so a | ||
| # ``--version`` SystemExit(0) from the pre-help slice parse still | ||
| # exits 0. | ||
| if is_help_option_present(args) and not self._names_a_command( | ||
| args, parser | ||
| ): | ||
| args = self._route_to_provider_help(args) | ||
| # 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 +629,44 @@ def main(self, args=None): | |
| parsed_globals=parsed_args, | ||
| ) | ||
|
|
||
| def _names_a_command(self, args, parser): | ||
| # ``parser`` is the provider-level ``MainArgParser`` (the one built in | ||
| # ``main()`` and passed in). Return True if a real command is named | ||
| # before the first --help token. Parsing the pre-help slice with that | ||
| # parser lets argparse consume the VALUES of value-taking global options | ||
| # instead of mistaking them for a command: in ``aws --region ec2 | ||
| # --help`` the ``ec2`` is --region's value, so no command is named and | ||
| # provider help renders. | ||
| head = self._tokens_before_help(args) | ||
| if head == list(args): | ||
| # No --help token present; nothing to decide here. | ||
| return True | ||
| try: | ||
| parsed, _ = parser.parse_known_args(head) | ||
| except ArgParseException: | ||
| # Only ArgParseException (raised by CLIArgParser.error) means "no | ||
| # parseable command", e.g. ``--region ec2`` -> "required: command". | ||
| # We deliberately do not catch broader exceptions so a --version | ||
| # SystemExit and a user KeyboardInterrupt propagate. | ||
| return False | ||
| return getattr(parsed, 'command', None) is not None | ||
|
|
||
| def _tokens_before_help(self, args): | ||
| # The tokens that precede the first --help token. Tokens at or after | ||
| # the first help token are ignored for routing, exactly as the | ||
| # positional ``help`` token ignores everything after it. | ||
| help_index = first_help_option_index(args) | ||
| if help_index is None: | ||
| return list(args) | ||
| return list(args[:help_index]) | ||
|
|
||
| def _route_to_provider_help(self, args): | ||
| # Render provider help: keep the tokens before the first --help and | ||
| # append the ``help`` positional argument, so a trailing command is | ||
| # ignored. | ||
| head = self._tokens_before_help(args) | ||
| return head + ['help'] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we avoid converting the help request into a positional help token here? The parse fails because
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth a regression test on a mutating operation specifically
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Supplying |
||
|
|
||
| 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 | ||
|
|
@@ -737,10 +789,33 @@ def __call__(self, args, parsed_globals): | |
| # we can go ahead and create the parser for it. We | ||
| # can also grab the Service object from botocore. | ||
| service_parser = self.create_parser() | ||
| parsed_args, remaining = service_parser.parse_known_args(args) | ||
| command_table = self._get_command_table() | ||
| # Resolve help intent before binding. If --help is present we still | ||
| # want to route to a specific operation's help when an operation was | ||
| # named (e.g. ``aws ec2 describe-instances --help``); the operation's | ||
| # own __call__ renders it. Only when no operation token is present | ||
| # (e.g. ``aws ec2 --help``) do we render this service's help here. | ||
| if is_help_option_present(args): | ||
| operation = self._find_operation_in_args(args, command_table) | ||
| if operation is None: | ||
| return self.create_help_command()( | ||
| strip_help_options(args), parsed_globals | ||
| ) | ||
| parsed_args, remaining = service_parser.parse_known_args(args) | ||
| return command_table[parsed_args.operation](remaining, parsed_globals) | ||
|
|
||
| def _find_operation_in_args(self, args, command_table): | ||
| # Return the first token before the first --help that names an | ||
| # operation, or None. An operation named after --help is ignored: | ||
| # ``aws ec2 --help describe-instances`` renders EC2 (service) help, the | ||
| # same as ``aws ec2 help describe-instances``. | ||
| help_index = first_help_option_index(args) | ||
| candidates = args if help_index is None else args[:help_index] | ||
| for token in candidates: | ||
| if not token.startswith('-') and token in command_table: | ||
| return token | ||
| return None | ||
|
|
||
| def _create_command_table(self): | ||
| command_table = OrderedDict() | ||
| service_model = self._get_service_model() | ||
|
|
@@ -890,6 +965,21 @@ def _parse_potential_subcommand(self, args, subcommand_table): | |
| return parser.parse_known_args(args) | ||
| return None | ||
|
|
||
| def _subcommand_precedes_help(self, args, maybe_parsed_subcommand): | ||
| # True if the subcommand is named before the first --help token, | ||
| # decided from what the subcommand parser binds on the pre-help slice | ||
| # rather than a value-blind string index. Mirrors | ||
| # ``BasicCommand._subcommand_precedes_help``. | ||
| help_index = first_help_option_index(args) | ||
| if help_index is None: | ||
| return True | ||
| subcommand_name = maybe_parsed_subcommand[1] | ||
| head = args[:help_index] | ||
| parsed_head = self._parse_potential_subcommand( | ||
| head, self.subcommand_table | ||
| ) | ||
| return parsed_head is not None and parsed_head[1] == subcommand_name | ||
|
|
||
| def __call__(self, args, parsed_globals): | ||
| # Once we know we're trying to call a particular operation | ||
| # of a service we can go ahead and load the parameters. | ||
|
|
@@ -907,9 +997,26 @@ def __call__(self, args, parsed_globals): | |
| maybe_parsed_subcommand = self._parse_potential_subcommand( | ||
| args, subcommand_table | ||
| ) | ||
| if maybe_parsed_subcommand is not None: | ||
| # Descend into a parsed subcommand only when it is named before the | ||
| # first --help token, mirroring the guard in ``BasicCommand.__call__``. | ||
| # Otherwise ``--help`` before a subcommand (e.g. a hypothetical | ||
| # ``aws myservice myoperation --help mysubcommand``) would render the | ||
| # subcommand's help instead of this operation's. No operation | ||
| # currently has a subcommand table, so this is a forward-looking | ||
| # robustness check that keeps the two dispatch layers symmetric. | ||
| if ( | ||
| maybe_parsed_subcommand is not None | ||
| and self._subcommand_precedes_help(args, maybe_parsed_subcommand) | ||
| ): | ||
| new_args, subcommand_name = maybe_parsed_subcommand | ||
| return subcommand_table[subcommand_name](new_args, parsed_globals) | ||
| # Resolve --help before binding so a preceding value option (e.g. | ||
| # ``--instance-ids i-123 --help``) cannot swallow it. The positional | ||
| # ``help`` path below still works; this is an additional path. | ||
| if is_help_option_present(args): | ||
| return self.create_help_command()( | ||
| strip_help_options(args), parsed_globals | ||
| ) | ||
| operation_parser = self._create_operation_parser( | ||
| self.arg_table, subcommand_table | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Help detection should stop at the
--end-of-options marker.aws configure set test.key -- --helpnormally treats--helpas the positional value, but this interprets it as a help request and renders command help insteadThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it should behave as other parameters in this aspect. I'll publish a revision