Skip to content

Commit f96d08b

Browse files
committed
Add a "configure" top level command
This command will allow a user to be interactively prompted for values: $ aws configure AWS Access Key ID [****************]: accesskey AWS Secret Access Key [****************]: secretkey Default region name [us-west-2]: us-west-2 Default output format [None]: json This will automatically create an `~/.aws/config` file and write out the values to this file. This can also be used to update existing values. If you run the `configure` command with existing values then the current value will be provided in `[brackets]`, you can hit enter to accept the current value or provide a new value. Care was taken to *not* touch any unchanged data on updates. This means we don't just simply load the parsed config data, manipulate it, the use ConfigParser to write out the new data. This would lose the existing formatting of the file as well as any comments in the config file. If I have a config file like this: aws_access_key_id = myaccesskey aws_secret_access_key = secretkey ; Here's a comment. # Here's a nother comment. ; And another comment output = text ; Use text output instead of json region = us-west-1 And I run `aws configure` and only enter a value for *just* the region, then only the region line will be changed. All the existing comments will be preserved. Internal Changes ================ Part of this change is the introduction of a `BasicCommand` class that makes it much easier to create top level commands. It supports: * Defining documentation on the command object * Defining an argument table on the command object * Automatically managing the argparse creation * Automatically providing a "help" command It doesn't support subcommands (mostly because the "configure" command doesn't have any right now), but I do plan on adding support for subcommands. When this means for plugin writers is that you only have to define a single class to create a new to level command. This required some small changes to `bcdoc` related to pulling up the synopsis/option handlers into the base handler class so that we can have the same look and feel as built the arguments for built in operations.
1 parent d68a5f5 commit f96d08b

9 files changed

Lines changed: 855 additions & 29 deletions

File tree

awscli/argparser.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,40 +90,28 @@ def _build(self, operations_table):
9090
self.add_argument('operation', choices=list(operations_table.keys()))
9191

9292

93-
class OperationArgParser(CLIArgParser):
94-
Formatter = argparse.RawTextHelpFormatter
93+
class ArgTableArgParser(CLIArgParser):
94+
"""CLI arg parser based on an argument table."""
9595
Usage = ("aws [options] <command> <subcommand> [parameters]")
9696

97-
type_map = {
98-
'structure': str,
99-
'map': str,
100-
'timestamp': str,
101-
'list': str,
102-
'string': str,
103-
'float': float,
104-
'integer': str,
105-
'long': int,
106-
'boolean': bool,
107-
'double': float,
108-
'blob': str}
109-
110-
def __init__(self, argument_table, name):
111-
super(OperationArgParser, self).__init__(
97+
def __init__(self, argument_table):
98+
super(ArgTableArgParser, self).__init__(
11299
formatter_class=self.Formatter,
113100
add_help=False,
114101
usage=self.Usage,
115102
conflict_handler='resolve')
116-
self._build(argument_table, name)
103+
self._build(argument_table)
117104

118-
def _build(self, argument_table, name):
105+
def _build(self, argument_table):
119106
for arg_name in argument_table:
120107
argument = argument_table[arg_name]
121108
argument.add_to_parser(self)
122109

123-
def parse_known_args(self, args):
110+
def parse_known_args(self, args, namespace=None):
124111
if len(args) == 1 and args[0] == 'help':
125112
namespace = argparse.Namespace()
126113
namespace.help = 'help'
127114
return namespace, []
128115
else:
129-
return super(OperationArgParser, self).parse_known_args(args)
116+
return super(ArgTableArgParser, self).parse_known_args(
117+
args, namespace)

awscli/clidriver.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from awscli.plugin import load_plugins
2424
from awscli.argparser import MainArgParser
2525
from awscli.argparser import ServiceArgParser
26-
from awscli.argparser import OperationArgParser
26+
from awscli.argparser import ArgTableArgParser
2727
from awscli.help import ProviderHelpCommand
2828
from awscli.help import ServiceHelpCommand
2929
from awscli.help import OperationHelpCommand
@@ -247,6 +247,10 @@ def create_help_command(self):
247247
# help docs.
248248
return None
249249

250+
@property
251+
def arg_table(self):
252+
return {}
253+
250254

251255
class ServiceCommand(CLICommand):
252256
"""A service command for the CLI.
@@ -467,7 +471,7 @@ def _emit(self, name, **kwargs):
467471
return session.emit(name, **kwargs)
468472

469473
def _create_operation_parser(self, arg_table):
470-
parser = OperationArgParser(arg_table, self._name)
474+
parser = ArgTableArgParser(arg_table)
471475
return parser
472476

473477

awscli/customizations/commands.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
from bcdoc.clidocs import CLIDocumentEventHandler
2+
import bcdoc.clidocevents
3+
4+
from awscli.argparser import ArgTableArgParser
5+
from awscli.clidriver import CLICommand
6+
from awscli.arguments import CustomArgument
7+
from awscli.help import HelpCommand
8+
9+
10+
class BasicCommand(CLICommand):
11+
"""Basic top level command with no subcommands.
12+
13+
If you want to create a new command, subclass this and
14+
provide the values documented below.
15+
16+
"""
17+
18+
# This is the name of your command, so if you want to
19+
# create an 'aws mycommand ...' command, the NAME would be
20+
# 'mycommand'
21+
NAME = 'commandname'
22+
# This is the description that will be used for the 'help'
23+
# command.
24+
DESCRIPTION = 'describe the command'
25+
# This is optional, if you are fine with the default synopsis
26+
# (the way all the built in operations are documented) then you
27+
# can leave this empty.
28+
SYNOPSIS = ''
29+
# If you want to provide some hand written examples, you can do
30+
# so here. This is written in RST format. This is optional,
31+
# you don't have to provide any examples, though highly encouraged!
32+
EXAMPLES = ''
33+
# If your command has arguments, you can specify them here. This is
34+
# somewhat of an implementation detail, but this is a list of dicts
35+
# where the dicts match the kwargs of the CustomArgument's __init__.
36+
# For example, if I want to add a '--argument-one' and an
37+
# '--argument-two' command, I'd say:
38+
#
39+
# ARG_TABLE = [
40+
# {'name': 'argument-one', 'help_text': 'This argument does foo bar.',
41+
# 'action': 'store', 'required': False, 'cli_type_name': 'string',},
42+
# {'name': 'argument-two', 'help_text': 'This argument does some other thing.',
43+
# 'action': 'store', 'choices': ['a', 'b', 'c']},
44+
# ]
45+
ARG_TABLE = []
46+
47+
# At this point, the only other thing you have to implement is a _run_main
48+
# method (see the method for more information).
49+
50+
def __init__(self, session):
51+
self._session = session
52+
53+
def __call__(self, args, parsed_globals):
54+
# args is the remaining unparsed args.
55+
# We might be able to parse these args so we need to create
56+
# an arg parser and parse them.
57+
parser = ArgTableArgParser(self.arg_table)
58+
parsed_args = parser.parse_args(args)
59+
if hasattr(parsed_args, 'help'):
60+
self._display_help(parsed_args, parsed_globals)
61+
else:
62+
self._run_main(parsed_args, parsed_globals)
63+
64+
def _run_main(self, parsed_args, parsed_globals):
65+
# Subclasses should implement this method.
66+
# parsed_globals are the parsed global args (things like region,
67+
# profile, output, etc.)
68+
# parsed_args are any arguments you've defined in your ARG_TABLE
69+
# that are parsed. These will come through as whatever you've
70+
# provided as the 'dest' key. Otherwise they default to the
71+
# 'name' key. For example: ARG_TABLE[0] = {"name": "foo-arg", ...}
72+
# can be accessed by ``parsed_args.foo_arg``.
73+
raise NotImlementedError("_run_main")
74+
75+
def _display_help(self, parsed_args, parsed_globals):
76+
help_command = self.create_help_command()
77+
help_command(parsed_args, parsed_globals)
78+
79+
def create_help_command(self):
80+
return BasicHelp(self._session, self, command_table={},
81+
arg_table=self.arg_table)
82+
83+
@property
84+
def arg_table(self):
85+
arg_table = {}
86+
for arg_data in self.ARG_TABLE:
87+
custom_argument = CustomArgument(**arg_data)
88+
arg_table[arg_data['name']] = custom_argument
89+
return arg_table
90+
91+
@classmethod
92+
def add_command(cls, command_table, session, **kwargs):
93+
command_table[cls.NAME] = cls(session)
94+
95+
96+
class BasicHelp(HelpCommand):
97+
event_class = 'command'
98+
99+
def __init__(self, session, command_object, command_table, arg_table,
100+
event_handler_class=None):
101+
super(BasicHelp, self).__init__(session, command_object,
102+
command_table, arg_table)
103+
# This is defined in HelpCommand so we're matching the
104+
# casing here.
105+
if event_handler_class is None:
106+
event_handler_class=BasicDocHandler
107+
self.EventHandlerClass = event_handler_class
108+
109+
# These are public attributes that are mapped from the command
110+
# object. These are used by the BasicDocHandler below.
111+
self.description = command_object.DESCRIPTION
112+
self.synopsis = command_object.SYNOPSIS
113+
self.examples = command_object.EXAMPLES
114+
115+
@property
116+
def name(self):
117+
return self.obj.NAME
118+
119+
def __call__(self, args, parsed_globals):
120+
# Create an event handler for a Provider Document
121+
instance = self.EventHandlerClass(self)
122+
# Now generate all of the events for a Provider document.
123+
# We pass ourselves along so that we can, in turn, get passed
124+
# to all event handlers.
125+
bcdoc.clidocevents.generate_events(self.session, self)
126+
self.renderer.render(self.doc.getvalue())
127+
instance.unregister()
128+
129+
130+
class BasicDocHandler(CLIDocumentEventHandler):
131+
def __init__(self, help_command):
132+
super(BasicDocHandler, self).__init__(help_command)
133+
self.doc = help_command.doc
134+
135+
def doc_description(self, help_command, **kwargs):
136+
self.doc.style.h2('Description')
137+
self.doc.write(help_command.description)
138+
self.doc.style.new_paragraph()
139+
140+
def doc_synopsis_start(self, help_command, **kwargs):
141+
if not help_command.synopsis:
142+
super(BasicDocHandler, self).doc_synopsis_start(
143+
help_command=help_command, **kwargs)
144+
else:
145+
self.doc.style.h2('Synopsis')
146+
self.doc.style.start_codeblock()
147+
self.doc.writeln(help_command.synopsis)
148+
149+
def doc_synopsis_end(self, help_command, **kwargs):
150+
if not help_command.synopsis:
151+
super(BasicDocHandler, self).doc_synopsis_end(
152+
help_command=help_command, **kwargs)
153+
else:
154+
self.doc.style.end_codeblock()
155+
156+
def doc_option_example(self, arg_name, help_command, **kwargs):
157+
pass
158+
159+
def doc_examples(self, help_command, **kwargs):
160+
if help_command.examples:
161+
self.doc.style.h2('Examples')
162+
self.doc.write(help_command.examples)
163+
164+
def doc_subitems_start(self, help_command, **kwargs):
165+
pass
166+
167+
def doc_subitem(self, command_name, help_command, **kwargs):
168+
pass
169+
170+
def doc_subitems_end(self, help_command, **kwargs):
171+
pass

0 commit comments

Comments
 (0)