Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/help.py
1566 views
1
# Copyright 2012-2015 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.
13
import logging
14
import os
15
import platform
16
import shlex
17
import sys
18
from subprocess import PIPE, Popen
19
20
from docutils.core import publish_string
21
from docutils.writers import manpage
22
23
from awscli.argparser import ArgTableArgParser
24
from awscli.argprocess import ParamShorthandParser
25
from awscli.bcdoc import docevents
26
from awscli.bcdoc.restdoc import ReSTDocument
27
from awscli.bcdoc.textwriter import TextWriter
28
from awscli.clidocs import (
29
OperationDocumentEventHandler,
30
ProviderDocumentEventHandler,
31
ServiceDocumentEventHandler,
32
TopicDocumentEventHandler,
33
TopicListerDocumentEventHandler,
34
)
35
from awscli.topictags import TopicTagDB
36
from awscli.utils import ignore_ctrl_c
37
38
LOG = logging.getLogger('awscli.help')
39
40
41
class ExecutableNotFoundError(Exception):
42
def __init__(self, executable_name):
43
super().__init__(
44
f'Could not find executable named "{executable_name}"'
45
)
46
47
48
def get_renderer():
49
"""
50
Return the appropriate HelpRenderer implementation for the
51
current platform.
52
"""
53
if platform.system() == 'Windows':
54
return WindowsHelpRenderer()
55
else:
56
return PosixHelpRenderer()
57
58
59
class PagingHelpRenderer:
60
"""
61
Interface for a help renderer.
62
63
The renderer is responsible for displaying the help content on
64
a particular platform.
65
66
"""
67
68
def __init__(self, output_stream=sys.stdout):
69
self.output_stream = output_stream
70
71
PAGER = None
72
_DEFAULT_DOCUTILS_SETTINGS_OVERRIDES = {
73
# The default for line length limit in docutils is 10,000. However,
74
# currently in the documentation, it inlines all possible enums in
75
# the JSON syntax which exceeds this limit for some EC2 commands
76
# and prevents the manpages from being generated.
77
# This is a temporary fix to allow the manpages for these commands
78
# to be rendered. Long term, we should avoid enumerating over all
79
# enums inline for the JSON syntax snippets.
80
'line_length_limit': 50_000
81
}
82
83
def get_pager_cmdline(self):
84
pager = self.PAGER
85
if 'MANPAGER' in os.environ:
86
pager = os.environ['MANPAGER']
87
elif 'PAGER' in os.environ:
88
pager = os.environ['PAGER']
89
return shlex.split(pager)
90
91
def render(self, contents):
92
"""
93
Each implementation of HelpRenderer must implement this
94
render method.
95
"""
96
converted_content = self._convert_doc_content(contents)
97
self._send_output_to_pager(converted_content)
98
99
def _send_output_to_pager(self, output):
100
cmdline = self.get_pager_cmdline()
101
LOG.debug("Running command: %s", cmdline)
102
p = self._popen(cmdline, stdin=PIPE)
103
p.communicate(input=output)
104
105
def _popen(self, *args, **kwargs):
106
return Popen(*args, **kwargs)
107
108
def _convert_doc_content(self, contents):
109
return contents
110
111
112
class PosixHelpRenderer(PagingHelpRenderer):
113
"""
114
Render help content on a Posix-like system. This includes
115
Linux and MacOS X.
116
"""
117
118
PAGER = 'less -R'
119
120
def _convert_doc_content(self, contents):
121
man_contents = publish_string(
122
contents,
123
writer=manpage.Writer(),
124
settings_overrides=self._DEFAULT_DOCUTILS_SETTINGS_OVERRIDES,
125
)
126
if self._exists_on_path('groff'):
127
cmdline = ['groff', '-m', 'man', '-T', 'ascii']
128
elif self._exists_on_path('mandoc'):
129
cmdline = ['mandoc', '-T', 'ascii']
130
else:
131
raise ExecutableNotFoundError('groff or mandoc')
132
LOG.debug("Running command: %s", cmdline)
133
p3 = self._popen(cmdline, stdin=PIPE, stdout=PIPE, stderr=PIPE)
134
output = p3.communicate(input=man_contents)[0]
135
return output
136
137
def _send_output_to_pager(self, output):
138
cmdline = self.get_pager_cmdline()
139
if not self._exists_on_path(cmdline[0]):
140
LOG.debug(
141
"Pager '%s' not found in PATH, printing raw help.", cmdline[0]
142
)
143
self.output_stream.write(output.decode('utf-8') + "\n")
144
self.output_stream.flush()
145
return
146
LOG.debug("Running command: %s", cmdline)
147
with ignore_ctrl_c():
148
# We can't rely on the KeyboardInterrupt from
149
# the CLIDriver being caught because when we
150
# send the output to a pager it will use various
151
# control characters that need to be cleaned
152
# up gracefully. Otherwise if we simply catch
153
# the Ctrl-C and exit, it will likely leave the
154
# users terminals in a bad state and they'll need
155
# to manually run ``reset`` to fix this issue.
156
# Ignoring Ctrl-C solves this issue. It's also
157
# the default behavior of less (you can't ctrl-c
158
# out of a manpage).
159
p = self._popen(cmdline, stdin=PIPE)
160
p.communicate(input=output)
161
162
def _exists_on_path(self, name):
163
# Since we're only dealing with POSIX systems, we can
164
# ignore things like PATHEXT.
165
return any(
166
[
167
os.path.exists(os.path.join(p, name))
168
for p in os.environ.get('PATH', '').split(os.pathsep)
169
]
170
)
171
172
173
class WindowsHelpRenderer(PagingHelpRenderer):
174
"""Render help content on a Windows platform."""
175
176
PAGER = 'more'
177
178
def _convert_doc_content(self, contents):
179
text_output = publish_string(
180
contents,
181
writer=TextWriter(),
182
settings_overrides=self._DEFAULT_DOCUTILS_SETTINGS_OVERRIDES,
183
)
184
return text_output
185
186
def _popen(self, *args, **kwargs):
187
# Also set the shell value to True. To get any of the
188
# piping to a pager to work, we need to use shell=True.
189
kwargs['shell'] = True
190
return Popen(*args, **kwargs)
191
192
193
class HelpCommand:
194
"""
195
HelpCommand Interface
196
---------------------
197
A HelpCommand object acts as the interface between objects in the
198
CLI (e.g. Providers, Services, Operations, etc.) and the documentation
199
system (bcdoc).
200
201
A HelpCommand object wraps the object from the CLI space and provides
202
a consistent interface to critical information needed by the
203
documentation pipeline such as the object's name, description, etc.
204
205
The HelpCommand object is passed to the component of the
206
documentation pipeline that fires documentation events. It is
207
then passed on to each document event handler that has registered
208
for the events.
209
210
All HelpCommand objects contain the following attributes:
211
212
+ ``session`` - A ``botocore`` ``Session`` object.
213
+ ``obj`` - The object that is being documented.
214
+ ``command_table`` - A dict mapping command names to
215
callable objects.
216
+ ``arg_table`` - A dict mapping argument names to callable objects.
217
+ ``doc`` - A ``Document`` object that is used to collect the
218
generated documentation.
219
220
In addition, please note the `properties` defined below which are
221
required to allow the object to be used in the document pipeline.
222
223
Implementations of HelpCommand are provided here for Provider,
224
Service and Operation objects. Other implementations for other
225
types of objects might be needed for customization in plugins.
226
As long as the implementations conform to this basic interface
227
it should be possible to pass them to the documentation system
228
and generate interactive and static help files.
229
"""
230
231
EventHandlerClass = None
232
"""
233
Each subclass should define this class variable to point to the
234
EventHandler class used by this HelpCommand.
235
"""
236
237
def __init__(self, session, obj, command_table, arg_table):
238
self.session = session
239
self.obj = obj
240
if command_table is None:
241
command_table = {}
242
self.command_table = command_table
243
if arg_table is None:
244
arg_table = {}
245
self.arg_table = arg_table
246
self._subcommand_table = {}
247
self._related_items = []
248
self.renderer = get_renderer()
249
self.doc = ReSTDocument(target='man')
250
251
@property
252
def event_class(self):
253
"""
254
Return the ``event_class`` for this object.
255
256
The ``event_class`` is used by the documentation pipeline
257
when generating documentation events. For the event below::
258
259
doc-title.<event_class>.<name>
260
261
The document pipeline would use this property to determine
262
the ``event_class`` value.
263
"""
264
pass
265
266
@property
267
def name(self):
268
"""
269
Return the name of the wrapped object.
270
271
This would be called by the document pipeline to determine
272
the ``name`` to be inserted into the event, as shown above.
273
"""
274
pass
275
276
@property
277
def subcommand_table(self):
278
"""These are the commands that may follow after the help command"""
279
return self._subcommand_table
280
281
@property
282
def related_items(self):
283
"""This is list of items that are related to the help command"""
284
return self._related_items
285
286
def __call__(self, args, parsed_globals):
287
if args:
288
subcommand_parser = ArgTableArgParser({}, self.subcommand_table)
289
parsed, remaining = subcommand_parser.parse_known_args(args)
290
if getattr(parsed, 'subcommand', None) is not None:
291
return self.subcommand_table[parsed.subcommand](
292
remaining, parsed_globals
293
)
294
295
# Create an event handler for a Provider Document
296
instance = self.EventHandlerClass(self)
297
# Now generate all of the events for a Provider document.
298
# We pass ourselves along so that we can, in turn, get passed
299
# to all event handlers.
300
docevents.generate_events(self.session, self)
301
self.renderer.render(self.doc.getvalue())
302
instance.unregister()
303
304
305
class ProviderHelpCommand(HelpCommand):
306
"""Implements top level help command.
307
308
This is what is called when ``aws help`` is run.
309
310
"""
311
312
EventHandlerClass = ProviderDocumentEventHandler
313
314
def __init__(
315
self, session, command_table, arg_table, description, synopsis, usage
316
):
317
HelpCommand.__init__(self, session, None, command_table, arg_table)
318
self.description = description
319
self.synopsis = synopsis
320
self.help_usage = usage
321
self._subcommand_table = None
322
self._topic_tag_db = None
323
self._related_items = ['aws help topics']
324
325
@property
326
def event_class(self):
327
return 'aws'
328
329
@property
330
def name(self):
331
return 'aws'
332
333
@property
334
def subcommand_table(self):
335
if self._subcommand_table is None:
336
if self._topic_tag_db is None:
337
self._topic_tag_db = TopicTagDB()
338
self._topic_tag_db.load_json_index()
339
self._subcommand_table = self._create_subcommand_table()
340
return self._subcommand_table
341
342
def _create_subcommand_table(self):
343
subcommand_table = {}
344
# Add the ``aws help topics`` command to the ``topic_table``
345
topic_lister_command = TopicListerCommand(self.session)
346
subcommand_table['topics'] = topic_lister_command
347
topic_names = self._topic_tag_db.get_all_topic_names()
348
349
# Add all of the possible topics to the ``topic_table``
350
for topic_name in topic_names:
351
topic_help_command = TopicHelpCommand(self.session, topic_name)
352
subcommand_table[topic_name] = topic_help_command
353
return subcommand_table
354
355
356
class ServiceHelpCommand(HelpCommand):
357
"""Implements service level help.
358
359
This is the object invoked whenever a service command
360
help is implemented, e.g. ``aws ec2 help``.
361
362
"""
363
364
EventHandlerClass = ServiceDocumentEventHandler
365
366
def __init__(
367
self, session, obj, command_table, arg_table, name, event_class
368
):
369
super().__init__(
370
session, obj, command_table, arg_table
371
)
372
self._name = name
373
self._event_class = event_class
374
375
@property
376
def event_class(self):
377
return self._event_class
378
379
@property
380
def name(self):
381
return self._name
382
383
384
class OperationHelpCommand(HelpCommand):
385
"""Implements operation level help.
386
387
This is the object invoked whenever help for a service is requested,
388
e.g. ``aws ec2 describe-instances help``.
389
390
"""
391
392
EventHandlerClass = OperationDocumentEventHandler
393
394
def __init__(self, session, operation_model, arg_table, name, event_class):
395
HelpCommand.__init__(self, session, operation_model, None, arg_table)
396
self.param_shorthand = ParamShorthandParser()
397
self._name = name
398
self._event_class = event_class
399
400
@property
401
def event_class(self):
402
return self._event_class
403
404
@property
405
def name(self):
406
return self._name
407
408
409
class TopicListerCommand(HelpCommand):
410
EventHandlerClass = TopicListerDocumentEventHandler
411
412
def __init__(self, session):
413
super().__init__(session, None, {}, {})
414
415
@property
416
def event_class(self):
417
return 'topics'
418
419
@property
420
def name(self):
421
return 'topics'
422
423
424
class TopicHelpCommand(HelpCommand):
425
EventHandlerClass = TopicDocumentEventHandler
426
427
def __init__(self, session, topic_name):
428
super().__init__(session, None, {}, {})
429
self._topic_name = topic_name
430
431
@property
432
def event_class(self):
433
return 'topics.' + self.name
434
435
@property
436
def name(self):
437
return self._topic_name
438
439