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