Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Lib/cmd.py
12 views
1
"""A generic class to build line-oriented command interpreters.
2
3
Interpreters constructed with this class obey the following conventions:
4
5
1. End of file on input is processed as the command 'EOF'.
6
2. A command is parsed out of each line by collecting the prefix composed
7
of characters in the identchars member.
8
3. A command `foo' is dispatched to a method 'do_foo()'; the do_ method
9
is passed a single argument consisting of the remainder of the line.
10
4. Typing an empty line repeats the last command. (Actually, it calls the
11
method `emptyline', which may be overridden in a subclass.)
12
5. There is a predefined `help' method. Given an argument `topic', it
13
calls the command `help_topic'. With no arguments, it lists all topics
14
with defined help_ functions, broken into up to three topics; documented
15
commands, miscellaneous help topics, and undocumented commands.
16
6. The command '?' is a synonym for `help'. The command '!' is a synonym
17
for `shell', if a do_shell method exists.
18
7. If completion is enabled, completing commands will be done automatically,
19
and completing of commands args is done by calling complete_foo() with
20
arguments text, line, begidx, endidx. text is string we are matching
21
against, all returned matches must begin with it. line is the current
22
input line (lstripped), begidx and endidx are the beginning and end
23
indexes of the text being matched, which could be used to provide
24
different completion depending upon which position the argument is in.
25
26
The `default' method may be overridden to intercept commands for which there
27
is no do_ method.
28
29
The `completedefault' method may be overridden to intercept completions for
30
commands that have no complete_ method.
31
32
The data member `self.ruler' sets the character used to draw separator lines
33
in the help messages. If empty, no ruler line is drawn. It defaults to "=".
34
35
If the value of `self.intro' is nonempty when the cmdloop method is called,
36
it is printed out on interpreter startup. This value may be overridden
37
via an optional argument to the cmdloop() method.
38
39
The data members `self.doc_header', `self.misc_header', and
40
`self.undoc_header' set the headers used for the help function's
41
listings of documented functions, miscellaneous topics, and undocumented
42
functions respectively.
43
"""
44
45
import string, sys
46
47
__all__ = ["Cmd"]
48
49
PROMPT = '(Cmd) '
50
IDENTCHARS = string.ascii_letters + string.digits + '_'
51
52
class Cmd:
53
"""A simple framework for writing line-oriented command interpreters.
54
55
These are often useful for test harnesses, administrative tools, and
56
prototypes that will later be wrapped in a more sophisticated interface.
57
58
A Cmd instance or subclass instance is a line-oriented interpreter
59
framework. There is no good reason to instantiate Cmd itself; rather,
60
it's useful as a superclass of an interpreter class you define yourself
61
in order to inherit Cmd's methods and encapsulate action methods.
62
63
"""
64
prompt = PROMPT
65
identchars = IDENTCHARS
66
ruler = '='
67
lastcmd = ''
68
intro = None
69
doc_leader = ""
70
doc_header = "Documented commands (type help <topic>):"
71
misc_header = "Miscellaneous help topics:"
72
undoc_header = "Undocumented commands:"
73
nohelp = "*** No help on %s"
74
use_rawinput = 1
75
76
def __init__(self, completekey='tab', stdin=None, stdout=None):
77
"""Instantiate a line-oriented interpreter framework.
78
79
The optional argument 'completekey' is the readline name of a
80
completion key; it defaults to the Tab key. If completekey is
81
not None and the readline module is available, command completion
82
is done automatically. The optional arguments stdin and stdout
83
specify alternate input and output file objects; if not specified,
84
sys.stdin and sys.stdout are used.
85
86
"""
87
if stdin is not None:
88
self.stdin = stdin
89
else:
90
self.stdin = sys.stdin
91
if stdout is not None:
92
self.stdout = stdout
93
else:
94
self.stdout = sys.stdout
95
self.cmdqueue = []
96
self.completekey = completekey
97
98
def cmdloop(self, intro=None):
99
"""Repeatedly issue a prompt, accept input, parse an initial prefix
100
off the received input, and dispatch to action methods, passing them
101
the remainder of the line as argument.
102
103
"""
104
105
self.preloop()
106
if self.use_rawinput and self.completekey:
107
try:
108
import readline
109
self.old_completer = readline.get_completer()
110
readline.set_completer(self.complete)
111
readline.parse_and_bind(self.completekey+": complete")
112
except ImportError:
113
pass
114
try:
115
if intro is not None:
116
self.intro = intro
117
if self.intro:
118
self.stdout.write(str(self.intro)+"\n")
119
stop = None
120
while not stop:
121
if self.cmdqueue:
122
line = self.cmdqueue.pop(0)
123
else:
124
if self.use_rawinput:
125
try:
126
line = input(self.prompt)
127
except EOFError:
128
line = 'EOF'
129
else:
130
self.stdout.write(self.prompt)
131
self.stdout.flush()
132
line = self.stdin.readline()
133
if not len(line):
134
line = 'EOF'
135
else:
136
line = line.rstrip('\r\n')
137
line = self.precmd(line)
138
stop = self.onecmd(line)
139
stop = self.postcmd(stop, line)
140
self.postloop()
141
finally:
142
if self.use_rawinput and self.completekey:
143
try:
144
import readline
145
readline.set_completer(self.old_completer)
146
except ImportError:
147
pass
148
149
150
def precmd(self, line):
151
"""Hook method executed just before the command line is
152
interpreted, but after the input prompt is generated and issued.
153
154
"""
155
return line
156
157
def postcmd(self, stop, line):
158
"""Hook method executed just after a command dispatch is finished."""
159
return stop
160
161
def preloop(self):
162
"""Hook method executed once when the cmdloop() method is called."""
163
pass
164
165
def postloop(self):
166
"""Hook method executed once when the cmdloop() method is about to
167
return.
168
169
"""
170
pass
171
172
def parseline(self, line):
173
"""Parse the line into a command name and a string containing
174
the arguments. Returns a tuple containing (command, args, line).
175
'command' and 'args' may be None if the line couldn't be parsed.
176
"""
177
line = line.strip()
178
if not line:
179
return None, None, line
180
elif line[0] == '?':
181
line = 'help ' + line[1:]
182
elif line[0] == '!':
183
if hasattr(self, 'do_shell'):
184
line = 'shell ' + line[1:]
185
else:
186
return None, None, line
187
i, n = 0, len(line)
188
while i < n and line[i] in self.identchars: i = i+1
189
cmd, arg = line[:i], line[i:].strip()
190
return cmd, arg, line
191
192
def onecmd(self, line):
193
"""Interpret the argument as though it had been typed in response
194
to the prompt.
195
196
This may be overridden, but should not normally need to be;
197
see the precmd() and postcmd() methods for useful execution hooks.
198
The return value is a flag indicating whether interpretation of
199
commands by the interpreter should stop.
200
201
"""
202
cmd, arg, line = self.parseline(line)
203
if not line:
204
return self.emptyline()
205
if cmd is None:
206
return self.default(line)
207
self.lastcmd = line
208
if line == 'EOF' :
209
self.lastcmd = ''
210
if cmd == '':
211
return self.default(line)
212
else:
213
try:
214
func = getattr(self, 'do_' + cmd)
215
except AttributeError:
216
return self.default(line)
217
return func(arg)
218
219
def emptyline(self):
220
"""Called when an empty line is entered in response to the prompt.
221
222
If this method is not overridden, it repeats the last nonempty
223
command entered.
224
225
"""
226
if self.lastcmd:
227
return self.onecmd(self.lastcmd)
228
229
def default(self, line):
230
"""Called on an input line when the command prefix is not recognized.
231
232
If this method is not overridden, it prints an error message and
233
returns.
234
235
"""
236
self.stdout.write('*** Unknown syntax: %s\n'%line)
237
238
def completedefault(self, *ignored):
239
"""Method called to complete an input line when no command-specific
240
complete_*() method is available.
241
242
By default, it returns an empty list.
243
244
"""
245
return []
246
247
def completenames(self, text, *ignored):
248
dotext = 'do_'+text
249
return [a[3:] for a in self.get_names() if a.startswith(dotext)]
250
251
def complete(self, text, state):
252
"""Return the next possible completion for 'text'.
253
254
If a command has not been entered, then complete against command list.
255
Otherwise try to call complete_<command> to get list of completions.
256
"""
257
if state == 0:
258
import readline
259
origline = readline.get_line_buffer()
260
line = origline.lstrip()
261
stripped = len(origline) - len(line)
262
begidx = readline.get_begidx() - stripped
263
endidx = readline.get_endidx() - stripped
264
if begidx>0:
265
cmd, args, foo = self.parseline(line)
266
if cmd == '':
267
compfunc = self.completedefault
268
else:
269
try:
270
compfunc = getattr(self, 'complete_' + cmd)
271
except AttributeError:
272
compfunc = self.completedefault
273
else:
274
compfunc = self.completenames
275
self.completion_matches = compfunc(text, line, begidx, endidx)
276
try:
277
return self.completion_matches[state]
278
except IndexError:
279
return None
280
281
def get_names(self):
282
# This method used to pull in base class attributes
283
# at a time dir() didn't do it yet.
284
return dir(self.__class__)
285
286
def complete_help(self, *args):
287
commands = set(self.completenames(*args))
288
topics = set(a[5:] for a in self.get_names()
289
if a.startswith('help_' + args[0]))
290
return list(commands | topics)
291
292
def do_help(self, arg):
293
'List available commands with "help" or detailed help with "help cmd".'
294
if arg:
295
# XXX check arg syntax
296
try:
297
func = getattr(self, 'help_' + arg)
298
except AttributeError:
299
try:
300
doc=getattr(self, 'do_' + arg).__doc__
301
if doc:
302
self.stdout.write("%s\n"%str(doc))
303
return
304
except AttributeError:
305
pass
306
self.stdout.write("%s\n"%str(self.nohelp % (arg,)))
307
return
308
func()
309
else:
310
names = self.get_names()
311
cmds_doc = []
312
cmds_undoc = []
313
topics = set()
314
for name in names:
315
if name[:5] == 'help_':
316
topics.add(name[5:])
317
names.sort()
318
# There can be duplicates if routines overridden
319
prevname = ''
320
for name in names:
321
if name[:3] == 'do_':
322
if name == prevname:
323
continue
324
prevname = name
325
cmd=name[3:]
326
if cmd in topics:
327
cmds_doc.append(cmd)
328
topics.remove(cmd)
329
elif getattr(self, name).__doc__:
330
cmds_doc.append(cmd)
331
else:
332
cmds_undoc.append(cmd)
333
self.stdout.write("%s\n"%str(self.doc_leader))
334
self.print_topics(self.doc_header, cmds_doc, 15,80)
335
self.print_topics(self.misc_header, sorted(topics),15,80)
336
self.print_topics(self.undoc_header, cmds_undoc, 15,80)
337
338
def print_topics(self, header, cmds, cmdlen, maxcol):
339
if cmds:
340
self.stdout.write("%s\n"%str(header))
341
if self.ruler:
342
self.stdout.write("%s\n"%str(self.ruler * len(header)))
343
self.columnize(cmds, maxcol-1)
344
self.stdout.write("\n")
345
346
def columnize(self, list, displaywidth=80):
347
"""Display a list of strings as a compact set of columns.
348
349
Each column is only as wide as necessary.
350
Columns are separated by two spaces (one was not legible enough).
351
"""
352
if not list:
353
self.stdout.write("<empty>\n")
354
return
355
356
nonstrings = [i for i in range(len(list))
357
if not isinstance(list[i], str)]
358
if nonstrings:
359
raise TypeError("list[i] not a string for i in %s"
360
% ", ".join(map(str, nonstrings)))
361
size = len(list)
362
if size == 1:
363
self.stdout.write('%s\n'%str(list[0]))
364
return
365
# Try every row count from 1 upwards
366
for nrows in range(1, len(list)):
367
ncols = (size+nrows-1) // nrows
368
colwidths = []
369
totwidth = -2
370
for col in range(ncols):
371
colwidth = 0
372
for row in range(nrows):
373
i = row + nrows*col
374
if i >= size:
375
break
376
x = list[i]
377
colwidth = max(colwidth, len(x))
378
colwidths.append(colwidth)
379
totwidth += colwidth + 2
380
if totwidth > displaywidth:
381
break
382
if totwidth <= displaywidth:
383
break
384
else:
385
nrows = len(list)
386
ncols = 1
387
colwidths = [0]
388
for row in range(nrows):
389
texts = []
390
for col in range(ncols):
391
i = row + nrows*col
392
if i >= size:
393
x = ""
394
else:
395
x = list[i]
396
texts.append(x)
397
while texts and not texts[-1]:
398
del texts[-1]
399
for col in range(len(texts)):
400
texts[col] = texts[col].ljust(colwidths[col])
401
self.stdout.write("%s\n"%str(" ".join(texts)))
402
403