cmd 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/CHANGELOG +5 -0
- data/INSTALL +11 -0
- data/MIT-LICENSE +20 -0
- data/README +411 -0
- data/Rakefile +141 -0
- data/THANKS +12 -0
- data/TODO +124 -0
- data/example/calc.rb +86 -0
- data/example/phonebook.rb +69 -0
- data/lib/cmd.rb +557 -0
- data/setup.rb +1360 -0
- data/test/tc_cmd.rb +284 -0
- metadata +67 -0
data/AUTHORS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Marcel Molina Jr. -- http://vernix.org/marcel/
|
data/CHANGELOG
ADDED
data/INSTALL
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2005 Marcel Molina Jr.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,411 @@
|
|
1
|
+
= Cmd
|
2
|
+
|
3
|
+
== What is Cmd?
|
4
|
+
|
5
|
+
cmd is a library for building line-oriented command interpreters in Ruby.
|
6
|
+
Simply inherit from cmd's Cmd class, and methods whose names start with +do_+
|
7
|
+
become interactive commands. cmd is inspired by the
|
8
|
+
{Python library}[http://docs.python.org/lib/module-cmd.html] of the same name, but
|
9
|
+
offers a distinctive Ruby feel and several additional features.
|
10
|
+
|
11
|
+
== An Example
|
12
|
+
|
13
|
+
Consider the following example of a small program to manage a lightweight phone
|
14
|
+
book.
|
15
|
+
|
16
|
+
We want to be able to add, find, list and delete phone book entries.
|
17
|
+
|
18
|
+
We are keeping it realy simple so the entries will be stored in a Hash with
|
19
|
+
names as keys and numbers as values. Let's assume that <tt>@numbers</tt> is
|
20
|
+
our hash. First we'll write a command to add an entry. Entries will be entered
|
21
|
+
like so:
|
22
|
+
|
23
|
+
PhoneBook> add Sam, 312-555-1212
|
24
|
+
|
25
|
+
So we define a +do_add+ method.
|
26
|
+
|
27
|
+
def do_add(args)
|
28
|
+
name, number = args.to_s.split(/, +/)
|
29
|
+
@numbers[name.strip] = number
|
30
|
+
end
|
31
|
+
|
32
|
+
We add another entry for good measure.
|
33
|
+
|
34
|
+
PhoneBook> add Amy, 227-328-2868
|
35
|
+
|
36
|
+
We make a +print_name_and_number+ method to format our entries.
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def print_name_and_number(*args)
|
41
|
+
puts "%-25s %s" % args
|
42
|
+
end
|
43
|
+
|
44
|
+
Writing a command to list all numbers is straightforward:
|
45
|
+
|
46
|
+
def do_list
|
47
|
+
@numbers.sort.each do |name, number|
|
48
|
+
print_name_and_number(name, number)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
We run our +list+ command:
|
53
|
+
|
54
|
+
PhoneBook> list
|
55
|
+
Amy 227-328-2868
|
56
|
+
Sam 312-555-1212
|
57
|
+
|
58
|
+
Then we write a find command to get the number for a given person.
|
59
|
+
|
60
|
+
def do_find(name)
|
61
|
+
name.to_s.strip!
|
62
|
+
if @numbers[name]
|
63
|
+
print_name_and_number(name, @numbers[name])
|
64
|
+
else
|
65
|
+
puts "#{name} isn't in the phone book"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
PhoneBook> find Sam
|
70
|
+
Sam 312-555-1212
|
71
|
+
PhoneBook> find Matz
|
72
|
+
Matz isn't in the phone book
|
73
|
+
|
74
|
+
Well we are cruising for burgers. But say we have a falling out with Amy (she
|
75
|
+
was taking up too much disk space anyway). No reason to keep her in the phone
|
76
|
+
book, so we'll define a +delete+ command.
|
77
|
+
|
78
|
+
def do_delete(name)
|
79
|
+
@numbers.delete(name) || write("No entry for '#{name}'")
|
80
|
+
end
|
81
|
+
|
82
|
+
PhoneBook> delete Amy
|
83
|
+
PhoneBook> list
|
84
|
+
Sam 312-555-1212
|
85
|
+
|
86
|
+
== Shortcuts
|
87
|
+
|
88
|
+
Commands like +add+ and +delete+ have clear names. They are self-documenting.
|
89
|
+
But it can get tedious to type them all out all the time.
|
90
|
+
|
91
|
+
You can add shortcuts for commands using Cmd::ClassMethods.shortcut.
|
92
|
+
|
93
|
+
shortcut '+', :add
|
94
|
+
|
95
|
+
The default +help+ command lists shortcuts for a given command. The +help+
|
96
|
+
command itself has a shortcut: +?+.
|
97
|
+
|
98
|
+
PhoneBook> ? add
|
99
|
+
add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
|
100
|
+
|
101
|
+
Additionally, any unambiguous abbreviation of a command will be translated
|
102
|
+
to the full command (so aliases that simply shorten the name of a given command
|
103
|
+
are unnecessary).
|
104
|
+
|
105
|
+
Since we only have one command that starts with +h+, the above could have been
|
106
|
+
written as:
|
107
|
+
|
108
|
+
PhoneBook> h add
|
109
|
+
add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
|
110
|
+
|
111
|
+
Furthermore, abbreviations are acceptable in any place a command name appears,
|
112
|
+
so you could write the above in an even more abbreviated way:
|
113
|
+
|
114
|
+
PhoneBook> h a
|
115
|
+
add -- Add an entry (ex: add Sam, 312-555-1212) (aliases: +)
|
116
|
+
|
117
|
+
== Documenting your commands
|
118
|
+
|
119
|
+
Our phone list now has its basic functionality. Let's add some documentation so
|
120
|
+
that someone other than you can figure out how to use it.
|
121
|
+
|
122
|
+
You document your commands using Cmd::ClassMethods.doc. We'll add docs for our
|
123
|
+
four commands so far:
|
124
|
+
|
125
|
+
doc :add, 'Add an entry (ex: add Sam, 312-555-1212)'
|
126
|
+
doc :find, 'Look up an entry (ex: find Sam)'
|
127
|
+
doc :list, 'List all entries'
|
128
|
+
doc :delete, 'Remove an entry'
|
129
|
+
|
130
|
+
== Getting Help
|
131
|
+
|
132
|
+
As illustrated above, there is a predefined +help+ command. Called without
|
133
|
+
arguments, it displays a help line for each command that has been documented
|
134
|
+
using the Cmd::ClassMethods.doc class method. (See <tt>Documenting your
|
135
|
+
commands</tt> for more on this.) By default, commands without documentation are
|
136
|
+
listed at the end of the +help+ output; this can be turned off by setting
|
137
|
+
YourCmdClass.hide_undocumented_commands = true. You can get help for a single
|
138
|
+
command by passing it as an argument to the +help+ command.
|
139
|
+
|
140
|
+
PhoneBook> help add
|
141
|
+
add -- Add an entry (ex: add Sam, 312-555-1212)
|
142
|
+
|
143
|
+
Typing +help+ affords you tab completion on all available commands with
|
144
|
+
documentation, so the above could be accomplished (assuming there are no other
|
145
|
+
documented commands that start with the letter +a+) by typing:
|
146
|
+
|
147
|
+
PhoneBook> help a<Tab>
|
148
|
+
|
149
|
+
The help command is aliased to +?+.
|
150
|
+
|
151
|
+
== Subcommands
|
152
|
+
|
153
|
+
Any method that is of the form +do_command_subcommand+ will be interpreted as a
|
154
|
+
subcommand of +command+. For example, if there was an +add+ command, a
|
155
|
+
+do_add_cellphone+ method would be invoked if 'add cellphone' was entered at
|
156
|
+
the prompt. If there was no +add+ command, +do_add_cellphone+ would not be
|
157
|
+
interpreted as a subcommand; you'd need to enter 'add_cellphone' to invoke it.
|
158
|
+
|
159
|
+
== Missing commands
|
160
|
+
|
161
|
+
Much like +method_missing+, there is a +command_missing+ method which is called
|
162
|
+
if an undefined command is entered in at the prompt. By default it simply
|
163
|
+
reports that the command does not exist; subclasses can override this behavior.
|
164
|
+
+command_missing+ is passed the entered command name as well as any arguments.
|
165
|
+
You must define your +command_missing+ this way.
|
166
|
+
|
167
|
+
Let's make phone book entry lookups more convenient by having +command_missing+
|
168
|
+
delegate to the +find+ command.
|
169
|
+
|
170
|
+
protected
|
171
|
+
|
172
|
+
def command_missing(command, args)
|
173
|
+
do_find(command)
|
174
|
+
end
|
175
|
+
|
176
|
+
Now we can do
|
177
|
+
|
178
|
+
PhoneBook> Sam
|
179
|
+
Sam 312-555-1212
|
180
|
+
|
181
|
+
== Lifecycle callbacks
|
182
|
+
|
183
|
+
Right now our phone book isn't really useful as the hash gets lost any time you
|
184
|
+
quit the program. Let's implement a simple storage scheme so that our phone
|
185
|
+
book entries will persist between invocations. A simple solution is just to
|
186
|
+
serialize the phone book hash to YAML in a file.
|
187
|
+
|
188
|
+
First we'll choose a place to store the file (apologies to people running
|
189
|
+
Windows).
|
190
|
+
|
191
|
+
PHONEBOOK_FILE = File.expand_path('~/.phonebook')
|
192
|
+
|
193
|
+
When the command loop is started, your subclass's +setup+ method is called.
|
194
|
+
Consider this your +initialize+. We can use this to grab the contents of our
|
195
|
+
phone book file.
|
196
|
+
|
197
|
+
protected
|
198
|
+
|
199
|
+
def setup
|
200
|
+
@numbers = get_store || {}
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_store
|
204
|
+
File.open(PHONEBOOK_FILE) {|store| YAML.load(store)} rescue nil
|
205
|
+
end
|
206
|
+
|
207
|
+
Now when we start up our phone book it will grab our entries or create a fresh
|
208
|
+
Hash in which to add entries. But we don't have any code to save our phone book
|
209
|
+
entries!
|
210
|
+
|
211
|
+
A Cmd session happens mostly inside a loop. This loop accepts commands until it
|
212
|
+
is told to stop. Like +setup+, there are several methods that are called
|
213
|
+
automatically during the lifetime of this loop. One such method is +postloop+,
|
214
|
+
which, as the name suggests, is called after the loop is done, or in other
|
215
|
+
words, once the Cmd session is completed. This turns out to be a good candidate
|
216
|
+
for the task of saving our phone book entries.
|
217
|
+
|
218
|
+
protected
|
219
|
+
|
220
|
+
def postloop
|
221
|
+
File.open(PHONEBOOK_FILE, 'w') {|store| store.write YAML.dump(@numbers)}
|
222
|
+
end
|
223
|
+
|
224
|
+
And that is that. Now when we exit the phone book our numbers will be saved to
|
225
|
+
our phone book file.
|
226
|
+
|
227
|
+
$ ruby phonebook.rb
|
228
|
+
PhoneBook> l
|
229
|
+
PhoneBook> a Sam, 312-555-1212
|
230
|
+
PhoneBook> Sam
|
231
|
+
Sam 312-555-1212
|
232
|
+
PhoneBook> exit
|
233
|
+
$ ruby phonebook.rb
|
234
|
+
PhoneBook> l
|
235
|
+
Sam 312-555-1212
|
236
|
+
|
237
|
+
There are five life-cycle callbacks. The complete list is below:
|
238
|
+
|
239
|
+
+setup+:: Called when your Cmd subclass is created, like +initialize+.
|
240
|
+
+preloop+:: Called before the command loop begins
|
241
|
+
+precmd+:: Called before each command
|
242
|
+
+postcmd+:: Called after each command; has access to the +current_command+
|
243
|
+
method, which returns the name of the current command
|
244
|
+
+postloop+:: Called after the command loop ends
|
245
|
+
|
246
|
+
Here we can have a look at a working copy of our
|
247
|
+
{PhoneBook}[http://svn.vernix.org/main/library/cmd/trunk/example/phonebook.rb].
|
248
|
+
|
249
|
+
== Customizing command completion
|
250
|
+
|
251
|
+
By default Cmd supports readline functionality if it is enabled on your system.
|
252
|
+
This affords you command line history as well as command completion. The
|
253
|
+
default completion procedure will complete command names for you when you hit
|
254
|
+
the Tab key.
|
255
|
+
|
256
|
+
PhoneBook> l<Tab>
|
257
|
+
PhoneBook> list
|
258
|
+
|
259
|
+
As is the case in your standard shell, hitting tab twice when there is nothing
|
260
|
+
to complete will list all commands.
|
261
|
+
|
262
|
+
PhoneBook> <Tab><Tab>
|
263
|
+
add delete exit find help list shell
|
264
|
+
|
265
|
+
Completion can be customized on a per-command basis by defining a method of
|
266
|
+
the form +complete_command+ (where +command+ is a command name) which
|
267
|
+
returns an array with zero or more strings. The following (contrived) example
|
268
|
+
illustrates the idea:
|
269
|
+
|
270
|
+
$ grep -A 3 complete_add
|
271
|
+
def complete_add
|
272
|
+
%w{ cellphone fax home office }
|
273
|
+
end
|
274
|
+
|
275
|
+
PhoneBook> add <Tab>
|
276
|
+
cellphone fax home office
|
277
|
+
PhoneBook> add o<Tab>
|
278
|
+
PhoneBook> add office
|
279
|
+
|
280
|
+
If a given command has subcommands, Cmd's built in completion method will
|
281
|
+
complete with those subcommands automatically, so the above example would be
|
282
|
+
redundant were there to be command methods such as +do_add_office+ and
|
283
|
+
+do_add_home+, etc.
|
284
|
+
|
285
|
+
FIXME These docs lie I'm afraid. The API is not that simple yet, though the
|
286
|
+
above is the intended API. Check out the part of the link:files/TODO.html
|
287
|
+
file that talks about improving how completion works.
|
288
|
+
|
289
|
+
== Setting your prompt
|
290
|
+
|
291
|
+
By default the prompt will look like 'YourSubclass> '. So in the example above,
|
292
|
+
where we have been writing all that code inside a PhoneBook class that
|
293
|
+
inherits from Cmd, the prompt reads 'PhoneBook> '. The
|
294
|
+
Cmd::ClassMethods.prompt_with macro style method can be used to set a custom
|
295
|
+
prompt. The simplest prompt would just be a static string:
|
296
|
+
|
297
|
+
prompt_with '> '
|
298
|
+
|
299
|
+
You can, alternatively, pass Cmd::ClassMethods.prompt_with a Proc or method reference.
|
300
|
+
|
301
|
+
=== Proc
|
302
|
+
|
303
|
+
# Contrived...
|
304
|
+
prompt_with { "#{Time.now}> " }
|
305
|
+
|
306
|
+
=== Method reference
|
307
|
+
|
308
|
+
prompt_with :set_prompt
|
309
|
+
|
310
|
+
protected
|
311
|
+
|
312
|
+
# This assumes current_directory is defined by your Cmd subclass
|
313
|
+
def set_prompt
|
314
|
+
"#{ENV['USER']:#{current_directory}$ "
|
315
|
+
end
|
316
|
+
|
317
|
+
Using a method reference affords you access to all the state of your Cmd
|
318
|
+
instance.
|
319
|
+
|
320
|
+
N.B. The result of whatever is passed to Cmd::ClassMethods.prompt_with has +to_s+ called on it.
|
321
|
+
|
322
|
+
== Trapping user interrupts
|
323
|
+
|
324
|
+
If a user attempts to exit the command loop (using, for example, Ctrl-C), the
|
325
|
+
Cmd.user_interrupt method is called. Subclasses may override this. By default
|
326
|
+
it simply exits.
|
327
|
+
|
328
|
+
== Customizing passed arguments
|
329
|
+
|
330
|
+
For commands that take arguments (determined by whether or not you define your
|
331
|
+
+do_+ method with arguments), a method Cmd#tokenize_args is called on the passed
|
332
|
+
arguments. The default implementation has no side effects.
|
333
|
+
|
334
|
+
N.B. This API will more than likely change to something far more useful and
|
335
|
+
Rubyesque. Please checkout the link:files/TODO.html for details.
|
336
|
+
|
337
|
+
== Handling exceptions
|
338
|
+
|
339
|
+
All exceptions raised within the command loop are caught. You can specify what
|
340
|
+
action should be taken if a specific exception is raised by using the
|
341
|
+
Cmd::ClassMethods.handle method.
|
342
|
+
|
343
|
+
handle StackOverflowError, :handle_stack_overflow
|
344
|
+
|
345
|
+
If you specify a symbol the referenced method will be called. If you supply a
|
346
|
+
string, such as
|
347
|
+
|
348
|
+
handle StackOverflowError, 'Stack underflowed'
|
349
|
+
|
350
|
+
the string will be displayed to the user.
|
351
|
+
|
352
|
+
All other exceptions are passed to a Cmd.handle_exception method which by
|
353
|
+
default simply reraises the exception. Subclasses may use this to customize how
|
354
|
+
exceptions are handled.
|
355
|
+
|
356
|
+
def handle_exception(exception)
|
357
|
+
write 'Error'
|
358
|
+
end
|
359
|
+
|
360
|
+
== Not running interactively
|
361
|
+
|
362
|
+
Though subclasses of Cmd are meant to be run interactively, you may find that
|
363
|
+
you'd like to have access to a given command without starting up a session with
|
364
|
+
the interactive interpreter. Cmd allows you to run commands from the command
|
365
|
+
line. If you supply a command (with optional arguments) when invoking the
|
366
|
+
program that runs your command loop, the supplied command will be invoked, and
|
367
|
+
execution will stop.
|
368
|
+
|
369
|
+
$ ruby phonebook.rb Sam
|
370
|
+
Sam 312-555-1212
|
371
|
+
|
372
|
+
== Empty command lines
|
373
|
+
|
374
|
+
If a user enters an empty line at the command prompt, the +empty_line+ method
|
375
|
+
is called. By default it does nothing.
|
376
|
+
|
377
|
+
== Stopping the loop
|
378
|
+
|
379
|
+
Calling the +stoploop+ method will stop the command loop once the current
|
380
|
+
command is complete.
|
381
|
+
|
382
|
+
== Hidding undocumented commands
|
383
|
+
|
384
|
+
By default undocumented commands (if any) are listed at the bottom of the
|
385
|
+
default help message. This behaviour can be disabled by setting
|
386
|
+
+hide_undocumented_commands+ to +true+.
|
387
|
+
|
388
|
+
MyCmdClass.hide_undocumented_commands = true
|
389
|
+
|
390
|
+
== Callback reference
|
391
|
+
|
392
|
+
+handle_exception+:: see Handling exceptions
|
393
|
+
+user_interrupt+:: see Trapping user interrupts
|
394
|
+
+tokenize_args+:: see Customizing passed in arguments
|
395
|
+
+setup+:: see Setting up your environment
|
396
|
+
+command_missing+:: see Missing commands
|
397
|
+
+empty_line+:: see Emtpy command lines
|
398
|
+
|
399
|
+
== Download
|
400
|
+
|
401
|
+
Subversion
|
402
|
+
|
403
|
+
* http://svn.vernix.org/main/library/cmd/trunk
|
404
|
+
|
405
|
+
Documentation can be found at
|
406
|
+
|
407
|
+
* http://code.vernix.org/cmd
|
408
|
+
|
409
|
+
== Install
|
410
|
+
|
411
|
+
See the link:files/INSTALL.html doc.
|