1# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"). You 4# may not use this file except in compliance with the License. A copy of 5# the License is located at 6# 7# http://aws.amazon.com/apache2.0/ 8# 9# or in the "license" file accompanying this file. This file is 10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11# ANY KIND, either express or implied. See the License for the specific 12# language governing permissions and limitations under the License. 13import argparse 14import sys 15from awscli.compat import six 16from difflib import get_close_matches 17 18 19AWS_CLI_V2_MESSAGE = ( 20 'Note: AWS CLI version 2, the latest major version ' 21 'of the AWS CLI, is now stable and recommended for general ' 22 'use. For more information, see the AWS CLI version 2 ' 23 'installation instructions at: https://docs.aws.amazon.com/cli/' 24 'latest/userguide/install-cliv2.html' 25) 26 27HELP_BLURB = ( 28 "To see help text, you can run:\n" 29 "\n" 30 " aws help\n" 31 " aws <command> help\n" 32 " aws <command> <subcommand> help\n" 33) 34USAGE = ( 35 "\r%s\n\n" 36 "usage: aws [options] <command> <subcommand> " 37 "[<subcommand> ...] [parameters]\n" 38 "%s" % (AWS_CLI_V2_MESSAGE, HELP_BLURB) 39) 40 41 42class CommandAction(argparse.Action): 43 """Custom action for CLI command arguments 44 45 Allows the choices for the argument to be mutable. The choices 46 are dynamically retrieved from the keys of the referenced command 47 table 48 """ 49 def __init__(self, option_strings, dest, command_table, **kwargs): 50 self.command_table = command_table 51 super(CommandAction, self).__init__( 52 option_strings, dest, choices=self.choices, **kwargs 53 ) 54 55 def __call__(self, parser, namespace, values, option_string=None): 56 setattr(namespace, self.dest, values) 57 58 @property 59 def choices(self): 60 return list(self.command_table.keys()) 61 62 @choices.setter 63 def choices(self, val): 64 # argparse.Action will always try to set this value upon 65 # instantiation, but this value should be dynamically 66 # generated from the command table keys. So make this a 67 # NOOP if argparse.Action tries to set this value. 68 pass 69 70 71class CLIArgParser(argparse.ArgumentParser): 72 Formatter = argparse.RawTextHelpFormatter 73 74 # When displaying invalid choice error messages, 75 # this controls how many options to show per line. 76 ChoicesPerLine = 2 77 78 def _check_value(self, action, value): 79 """ 80 It's probably not a great idea to override a "hidden" method 81 but the default behavior is pretty ugly and there doesn't 82 seem to be any other way to change it. 83 """ 84 # converted value must be one of the choices (if specified) 85 if action.choices is not None and value not in action.choices: 86 msg = ['Invalid choice, valid choices are:\n'] 87 for i in range(len(action.choices))[::self.ChoicesPerLine]: 88 current = [] 89 for choice in action.choices[i:i+self.ChoicesPerLine]: 90 current.append('%-40s' % choice) 91 msg.append(' | '.join(current)) 92 possible = get_close_matches(value, action.choices, cutoff=0.8) 93 if possible: 94 extra = ['\n\nInvalid choice: %r, maybe you meant:\n' % value] 95 for word in possible: 96 extra.append(' * %s' % word) 97 msg.extend(extra) 98 raise argparse.ArgumentError(action, '\n'.join(msg)) 99 100 def parse_known_args(self, args, namespace=None): 101 parsed, remaining = super(CLIArgParser, self).parse_known_args(args, namespace) 102 terminal_encoding = getattr(sys.stdin, 'encoding', 'utf-8') 103 if terminal_encoding is None: 104 # In some cases, sys.stdin won't have an encoding set, 105 # (e.g if it's set to a StringIO). In this case we just 106 # default to utf-8. 107 terminal_encoding = 'utf-8' 108 for arg, value in vars(parsed).items(): 109 if isinstance(value, six.binary_type): 110 setattr(parsed, arg, value.decode(terminal_encoding)) 111 elif isinstance(value, list): 112 encoded = [] 113 for v in value: 114 if isinstance(v, six.binary_type): 115 encoded.append(v.decode(terminal_encoding)) 116 else: 117 encoded.append(v) 118 setattr(parsed, arg, encoded) 119 return parsed, remaining 120 121 122class MainArgParser(CLIArgParser): 123 Formatter = argparse.RawTextHelpFormatter 124 125 def __init__(self, command_table, version_string, 126 description, argument_table, prog=None): 127 super(MainArgParser, self).__init__( 128 formatter_class=self.Formatter, 129 add_help=False, 130 conflict_handler='resolve', 131 description=description, 132 usage=USAGE, 133 prog=prog) 134 self._build(command_table, version_string, argument_table) 135 136 def _create_choice_help(self, choices): 137 help_str = '' 138 for choice in sorted(choices): 139 help_str += '* %s\n' % choice 140 return help_str 141 142 def _build(self, command_table, version_string, argument_table): 143 for argument_name in argument_table: 144 argument = argument_table[argument_name] 145 argument.add_to_parser(self) 146 self.add_argument('--version', action="version", 147 version=version_string, 148 help='Display the version of this tool') 149 self.add_argument('command', action=CommandAction, 150 command_table=command_table) 151 152 153class ServiceArgParser(CLIArgParser): 154 155 def __init__(self, operations_table, service_name): 156 super(ServiceArgParser, self).__init__( 157 formatter_class=argparse.RawTextHelpFormatter, 158 add_help=False, 159 conflict_handler='resolve', 160 usage=USAGE) 161 self._build(operations_table) 162 self._service_name = service_name 163 164 def _build(self, operations_table): 165 self.add_argument('operation', action=CommandAction, 166 command_table=operations_table) 167 168 169class ArgTableArgParser(CLIArgParser): 170 """CLI arg parser based on an argument table.""" 171 172 def __init__(self, argument_table, command_table=None): 173 # command_table is an optional subcommand_table. If it's passed 174 # in, then we'll update the argparse to parse a 'subcommand' argument 175 # and populate the choices field with the command table keys. 176 super(ArgTableArgParser, self).__init__( 177 formatter_class=self.Formatter, 178 add_help=False, 179 usage=USAGE, 180 conflict_handler='resolve') 181 if command_table is None: 182 command_table = {} 183 self._build(argument_table, command_table) 184 185 def _build(self, argument_table, command_table): 186 for arg_name in argument_table: 187 argument = argument_table[arg_name] 188 argument.add_to_parser(self) 189 if command_table: 190 self.add_argument('subcommand', action=CommandAction, 191 command_table=command_table, nargs='?') 192 193 def parse_known_args(self, args, namespace=None): 194 if len(args) == 1 and args[0] == 'help': 195 namespace = argparse.Namespace() 196 namespace.help = 'help' 197 return namespace, [] 198 else: 199 return super(ArgTableArgParser, self).parse_known_args( 200 args, namespace) 201