cmd 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (14) hide show
  1. data/AUTHORS +1 -0
  2. data/CHANGELOG +5 -0
  3. data/INSTALL +11 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README +411 -0
  6. data/Rakefile +141 -0
  7. data/THANKS +12 -0
  8. data/TODO +124 -0
  9. data/example/calc.rb +86 -0
  10. data/example/phonebook.rb +69 -0
  11. data/lib/cmd.rb +557 -0
  12. data/setup.rb +1360 -0
  13. data/test/tc_cmd.rb +284 -0
  14. metadata +67 -0
@@ -0,0 +1,141 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'rake/contrib/rubyforgepublisher'
6
+ require 'rake/contrib/sshpublisher'
7
+
8
+ require 'date'
9
+ require 'rbconfig'
10
+
11
+ PKG_NAME = 'cmd'
12
+ PKG_VERSION = '0.7.0'
13
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+ PKG_DESTINATION = "../#{PKG_NAME}"
15
+ PKG_AUTHOR = 'Marcel Molina Jr.'
16
+ PKG_AUTHOR_EMAIL = 'marcel@vernix.org'
17
+ PKG_HOMEPAGE = 'http://cmd.rubyforge.org'
18
+ PKG_REMOTE_PATH = "www/code/#{PKG_NAME}"
19
+ PKG_REMOTE_HOST = 'vernix.org'
20
+ PKG_REMOTE_USER = 'marcel'
21
+ PKG_ARCHIVES_DIR = 'download'
22
+ PKG_DOC_DIR = 'rdoc'
23
+
24
+ BASE_DIRS = %w( lib example test )
25
+
26
+ desc "Default Task"
27
+ task :default => [ :test ]
28
+
29
+ desc "Run unit tests"
30
+ task :test do
31
+ # Rake's TestTask seems to mess with my IO streams so I'm doing this the lame
32
+ # way.
33
+ system 'ruby test/tc_*.rb'
34
+ end
35
+
36
+ # Generate documentation ------------------------------------------------------
37
+
38
+ RDOC_FILES = [
39
+ 'AUTHORS',
40
+ 'CHANGELOG',
41
+ 'INSTALL',
42
+ 'README',
43
+ 'THANKS',
44
+ 'TODO',
45
+ 'lib/cmd.rb'
46
+ ]
47
+
48
+ desc "Generate documentation"
49
+ Rake::RDocTask.new do |rd|
50
+ rd.main = 'README'
51
+ rd.title = PKG_NAME
52
+ rd.rdoc_dir = PKG_DOC_DIR
53
+ rd.rdoc_files.include(RDOC_FILES)
54
+ rd.options << '--inline-source'
55
+ rd.options << '--line-numbers'
56
+ end
57
+
58
+
59
+ # Generate GEM ----------------------------------------------------------------
60
+
61
+ PKG_FILES = FileList[
62
+ '[a-zA-Z]*',
63
+ 'lib/**',
64
+ 'test/**',
65
+ 'example/**'
66
+ ]
67
+
68
+ spec = Gem::Specification.new do |s|
69
+ s.name = PKG_NAME
70
+ s.version = PKG_VERSION
71
+ s.summary = "A generic class to build line-oriented command interpreters."
72
+ s.description = s.summary
73
+
74
+ s.files = PKG_FILES.to_a.delete_if {|f| f.include?('.svn')}
75
+ s.require_path = 'lib'
76
+
77
+ s.has_rdoc = true
78
+ s.extra_rdoc_files = RDOC_FILES
79
+ s.rdoc_options << '--main' << 'README' <<
80
+ '--title' << PKG_NAME <<
81
+ '--line-numbers' <<
82
+ '--inline-source'
83
+
84
+ s.test_files = Dir.glob('test/tc_*.rb')
85
+
86
+ s.author = PKG_AUTHOR
87
+ s.email = PKG_AUTHOR_EMAIL
88
+ s.homepage = PKG_HOMEPAGE
89
+ s.rubyforge_project = PKG_NAME
90
+ end
91
+
92
+ Rake::GemPackageTask.new(spec) do |pkg|
93
+ pkg.need_zip = true
94
+ pkg.need_tar_gz = true
95
+ pkg.need_tar_bz2 = true
96
+ pkg.package_dir = PKG_ARCHIVES_DIR
97
+ end
98
+
99
+ # Support Tasks ---------------------------------------------------------------
100
+
101
+ def egrep(pattern)
102
+ Dir['**/*.rb'].each do |fn|
103
+ count = 0
104
+ open(fn) do |f|
105
+ while line = f.gets
106
+ count += 1
107
+ if line =~ pattern
108
+ puts "#{fn}:#{count}:#{line}"
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ desc "Look for TODO and FIXME tags in the code"
116
+ task :todo do
117
+ egrep /#.*(FIXME|TODO|XXX)/
118
+ end
119
+
120
+ # Push Release ----------------------------------------------------------------
121
+
122
+ desc "Push current archives to release server"
123
+ task :push_package => [ :package ] do
124
+ Rake::SshDirPublisher.new(
125
+ "#{PKG_REMOTE_USER}@#{PKG_REMOTE_HOST}",
126
+ "#{PKG_REMOTE_PATH}/#{PKG_ARCHIVES_DIR}",
127
+ PKG_ARCHIVES_DIR
128
+ ).upload
129
+ end
130
+
131
+ desc "Push current rdoc to release server"
132
+ task :push_rdoc => [ :rdoc ] do
133
+ Rake::SshDirPublisher.new(
134
+ "#{PKG_REMOTE_USER}@#{PKG_REMOTE_HOST}",
135
+ "#{PKG_REMOTE_PATH}/#{PKG_DOC_DIR}",
136
+ PKG_DOC_DIR
137
+ ).upload
138
+ end
139
+
140
+ desc "Push current version up to release server"
141
+ task :push_release => [ :push_rdoc, :push_package ]
data/THANKS ADDED
@@ -0,0 +1,12 @@
1
+ Sam Stephenson (http://conio.net/) was gracious enough to take a close look at
2
+ cmd.rb's API early on. If you find yourself pleased or impressed with a feature
3
+ of cmd.rb it's most likely something that was his idea. He also supplied me
4
+ with an excellent example of cmd.rb's usage with his calc.rb that you can find
5
+ in the example directory.
6
+
7
+ Jamis Buck (http://jamis.jamisbuck.org/) who enriched Cmd's domain language
8
+ with his suggestion for a 'handle' macro.
9
+
10
+ Mikael Brockman (http://www.phubuh.org/)
11
+
12
+ Scott Barron (http://scott.elitists.net/)
data/TODO ADDED
@@ -0,0 +1,124 @@
1
+ = Cmd Todo list
2
+
3
+ Send suggestions for this list to marcel@vernix.org.
4
+
5
+ == Todo list
6
+
7
+ * Writing a complete_(command name) is enough to have the completion results be
8
+ displayed, but not enough to actually complete. In order to complete as well
9
+ there must be additional logic such as what complete_grep does. So right now
10
+ to do a completion method for a command you really have to do something like:
11
+
12
+ def complete_find(line)
13
+ completion_grep(@numbers.keys.sort, line)
14
+ end
15
+
16
+ Where the first argument is the collection to complete against and line is
17
+ what is passed in. This API should be simplified and it should have a better
18
+ name than completion_grep. Also the subclass has to remember that the
19
+ complete method has to take an argument. It would be better to not have them
20
+ have to do that. Perhaps introduce (another macro) class method that just
21
+ takes a collection, or a method reference that returns a collection that then
22
+ is operated on internally.
23
+
24
+ complete :find, :with => :phonebook_names
25
+
26
+ # ...
27
+
28
+ def phonebook_names
29
+ @nubers.keys.sort
30
+ end
31
+
32
+ or
33
+
34
+ complete :some_command, :some_other_command, :with => { # Some Proc }
35
+
36
+ * Add a Documentation class (or some such) which collects list of subcommands
37
+ and shortcuts so that the default help command can be more helpful and
38
+ complete.
39
+
40
+ * Make it so that doc allows one to document arguments for commands that take
41
+ arguments so that rather than just:
42
+
43
+ add -- Add a number into the phonebook.
44
+
45
+ You'd get something more like
46
+
47
+ add name number [phone type] -- Add a number into the phonebook.
48
+
49
+ * Have doc work like the 'desc' method for rake where it preceeds the task to
50
+ which it describes rather than specifying the task excplicitly as args.
51
+
52
+ * Get rid of do_ method naming convention and define a 'command' method to
53
+ replace the naming convention.
54
+
55
+ def do_subtract
56
+ # ...
57
+ end
58
+
59
+ would become
60
+
61
+ command subtract do
62
+ # ...
63
+ end
64
+
65
+ How to deal with method arguments? Perhaps doing:
66
+
67
+ command subtract do |arg|
68
+ # ...
69
+ end
70
+
71
+ Sam suggests command being the death of the doc macro:
72
+
73
+ command :add, 'Add an entry' do |name, number|
74
+ @numbers[name.strip] = number
75
+ end
76
+
77
+ I think that's pretty nice.
78
+
79
+ * Take another shot at having more objects (e.g. Command, Subcommand,
80
+ Documentation, etc)
81
+
82
+ * Provide a means of documenting subcommands
83
+
84
+ * When passing arguments to do_ methods do a better job of just checking if the
85
+ method takes arguments and then passing them all in with *args. Do all the
86
+ arity checks and then pass it as many args as the do_ method takes. Raise
87
+ some client catchable exception if nothing can be done with the passed args
88
+ to satisfy the method signature of the do_ method. Basically make the do_
89
+ command methods as much like ruby methods as possible so that the arguments
90
+ are handed to the command so that it can access them directly rather than
91
+ having to fish them out.
92
+
93
+ So get rid of tokenize_args...it's a busted idea. Instead have
94
+
95
+ e.g.
96
+
97
+ def do_add(name, number)
98
+ # ...
99
+ end
100
+
101
+ If the method that takes care of passing a command the appropriate number of
102
+ arguments can't do its job based on the input given then the default could be
103
+ something like announcing that there was an argument error (perhaps
104
+ formalized using handle) and then the help for that command should be
105
+ displayed.
106
+
107
+ * Implement rudimentary interaction with the underlying shell using the
108
+ standard | pipe notation and > redirection notation so that someone could do:
109
+
110
+ prompt> command | sort
111
+
112
+ or
113
+
114
+ prompt> command > commands-output.txt
115
+
116
+ and maybe
117
+
118
+ prompt> command | sort > sorted-command-output.txt
119
+
120
+ Though I don't really want to write anything too fancy or complicated. I
121
+ think the most basic functionality of pipes and redirects would be useful
122
+ though.
123
+
124
+ * Perhaps allow subclasses to override the tab as the completion key.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'cmd'
5
+ rescue LoadError
6
+ require File.dirname(__FILE__) + '/../lib/cmd'
7
+ end
8
+ require 'mathn'
9
+
10
+ class StackUnderflowError < StandardError; end
11
+
12
+ class Calculator < Cmd
13
+ VALUE = /^-?\d+(\/\d+)?$/
14
+
15
+ prompt_with :prompt_command
16
+
17
+ shortcut '.', :pop
18
+ shortcut 'x', :swap
19
+
20
+ shortcut '+', :add
21
+ shortcut '*', :multiply
22
+ shortcut '-', :subtract
23
+ shortcut '/', :divide
24
+
25
+ doc :clear, "Clears the contents of the stack"
26
+ doc :dup, "Pushes the value of the stack's top item"
27
+ doc :pop, "Removes the top item from the stack and displays its value"
28
+ doc :push, "Pushes the values passed onto the stack"
29
+ doc :swap, "Swaps the order of the stack's top 2 items"
30
+
31
+ doc :add, "Pops 2 items, adds them, and pushes the result"
32
+ doc :multiply, "Pops 2 items, multiplies them, and pushes the result"
33
+ doc :subtract, "Pops 2 items, subtracts the topmost, and pushes the result"
34
+ doc :divide, "Pops 2 items, divides by the topmost, and pushes the result"
35
+
36
+ handle StackUnderflowError, 'Stack underflow'
37
+ handle ZeroDivisionError, 'Division by zero'
38
+
39
+ def do_clear; setup end
40
+ def do_dup; push peek end
41
+ def do_pop; print_value pop end
42
+ def do_push(values) push *values end
43
+ def do_swap; swap end
44
+
45
+ def do_add; push pop + pop end
46
+ def do_multiply; push pop * pop end
47
+ def do_subtract; swap; push pop - pop end
48
+ def do_divide; swap; push pop / pop end
49
+
50
+ protected
51
+
52
+ def setup; @stack = [] end
53
+ def peek; @stack.last or underflow end
54
+ def pop; @stack.pop or underflow end
55
+ def push(*values) @stack += values end
56
+ def swap; top = pop; push top, pop end
57
+ def underflow; raise StackUnderflowError end
58
+
59
+ def print_value(value)
60
+ puts "=> #{value.inspect}"
61
+ end
62
+
63
+ def contents
64
+ return "(empty)" if @stack.empty?
65
+ @stack.inspect
66
+ end
67
+
68
+ def command_missing(command, values)
69
+ return super unless command =~ VALUE
70
+ do_push values.unshift(eval(command))
71
+ end
72
+
73
+ def prompt_command
74
+ "#{self.class.name}#{contents}> "
75
+ end
76
+
77
+ def tokenize_args(args)
78
+ return args unless current_command =~ VALUE or current_command == "push"
79
+ args.to_s.split(/ +/).inject([]) do |a, v|
80
+ raise ArgumentError, "bad integer value #{v}" unless v =~ VALUE
81
+ a << eval(v)
82
+ end
83
+ end
84
+ end
85
+
86
+ Calculator.run
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'cmd'
5
+ rescue LoadError
6
+ require File.dirname(__FILE__) + '/../lib/cmd'
7
+ end
8
+ require 'yaml'
9
+
10
+ class PhoneBook < Cmd
11
+ PHONEBOOK_FILE = File.expand_path('~/.phonebook')
12
+
13
+ doc :add, 'Add an entry (ex: add Sam, 312-555-1212)'
14
+ def do_add(args)
15
+ name, number = args.to_s.split(/, +/)
16
+ @numbers[name.strip] = number
17
+ end
18
+ shortcut '+', :add
19
+
20
+ doc :find, 'Look up an entry (ex: find Sam)'
21
+ def do_find(name)
22
+ name.to_s.strip!
23
+ if @numbers[name]
24
+ print_name_and_number(name, @numbers[name])
25
+ else
26
+ puts "#{name} isn't in the phone book"
27
+ end
28
+ end
29
+
30
+ doc :list, 'List all entries'
31
+ def do_list
32
+ @numbers.sort.each do |name, number|
33
+ print_name_and_number(name, number)
34
+ end
35
+ end
36
+
37
+ doc :delete, 'Remove an entry'
38
+ def do_delete(name)
39
+ @numbers.delete(name) || write("No entry for '#{name}'")
40
+ end
41
+
42
+ protected
43
+
44
+ def setup
45
+ @numbers = get_store || {}
46
+ end
47
+
48
+ def complete_find(line)
49
+ completion_grep(@numbers.keys.sort, line)
50
+ end
51
+
52
+ def print_name_and_number(*args)
53
+ puts "%-25s %s" % args
54
+ end
55
+
56
+ def postloop
57
+ File.open(PHONEBOOK_FILE, 'w') {|store| store.write YAML.dump(@numbers)}
58
+ end
59
+
60
+ def get_store
61
+ File.open(PHONEBOOK_FILE) {|store| YAML.load(store)} rescue nil
62
+ end
63
+
64
+ def command_missing(command, args)
65
+ do_find(command)
66
+ end
67
+ end
68
+
69
+ PhoneBook.run
@@ -0,0 +1,557 @@
1
+ READLINE_SUPPORTED = begin require 'readline' or true rescue LoadError end
2
+ require 'abbrev'
3
+
4
+ # A simple framework for writing line-oriented command interpreters, based
5
+ # heavily on Python's {cmd.py}[http://docs.python.org/lib/module-cmd.html].
6
+ #
7
+ # These are often useful for test harnesses, administrative tools, and
8
+ # prototypes that will later be wrapped in a more sophisticated interface.
9
+ #
10
+ # A Cmd instance or subclass instance is a line-oriented interpreter
11
+ # framework. There is no good reason to instantiate Cmd itself; rather,
12
+ # it's useful as a superclass of an interpreter class you define yourself
13
+ # in order to inherit Cmd's methods and encapsulate action methods.
14
+ class Cmd
15
+
16
+ module ClassMethods
17
+ @@docs = {}
18
+ @@shortcuts = {}
19
+ @@handlers = {}
20
+ @@prompt = '> '
21
+ @@shortcut_table = {}
22
+
23
+ # Set documentation for a command
24
+ #
25
+ # doc :help, 'Display this help.'
26
+ # def do_help
27
+ # # etc
28
+ # end
29
+ def doc(command, docstring = nil)
30
+ docstring = docstring ? docstring : yield
31
+ @@docs[command.to_s] = docstring
32
+ end
33
+
34
+ def docs
35
+ @@docs
36
+ end
37
+ module_function :docs
38
+
39
+ # Set what to do in the event that the given exception is raised.
40
+ #
41
+ # handle StackOverflowError, :handle_stack_overflow
42
+ #
43
+ def handle(exception, handler)
44
+ @@handlers[exception.to_s] = handler
45
+ end
46
+ module_function :handle
47
+
48
+ # Sets what the prompt is. Accepts a String, a block or a Symbol.
49
+ #
50
+ # == Block
51
+ #
52
+ # prompt_with { Time.now }
53
+ #
54
+ # == Symbol
55
+ #
56
+ # prompt_with :set_prompt
57
+ #
58
+ # == String
59
+ #
60
+ # prompt_with "#{self.class.name}> "
61
+ #
62
+ def prompt_with(*p, &block)
63
+ @@prompt = block_given? ? block : p.first
64
+ end
65
+
66
+ # Returns the evaluation of expression passed to prompt_with. Result has
67
+ # +to_s+ called on it as Readline expects a String for its prompt.
68
+ # XXX This could probably be more robust
69
+ def prompt
70
+ case @@prompt
71
+ when Symbol: self.send @@prompt
72
+ when Proc: @@prompt.call
73
+ else @@prompt
74
+ end.to_s
75
+ end
76
+ module_function :prompt
77
+
78
+ # Create a command short cut
79
+ #
80
+ # shortcut '?', 'help'
81
+ # def do_help
82
+ # # etc
83
+ # end
84
+ def shortcut(short, command)
85
+ (@@shortcuts[command.to_s] ||= []).push short
86
+ @@shortcut_table[short] = command.to_s
87
+ end
88
+
89
+ def shortcut_table
90
+ @@shortcut_table
91
+ end
92
+ module_function :shortcut_table
93
+
94
+ def shortcuts
95
+ @@shortcuts
96
+ end
97
+ module_function :shortcuts
98
+
99
+ def custom_exception_handlers
100
+ @@handlers
101
+ end
102
+ module_function :custom_exception_handlers
103
+
104
+ # Defines a method which returns all defined methods which start with the
105
+ # passed in prefix followed by an underscore. Used to define methods to
106
+ # collect things such as all defined 'complete' and 'do' methods.
107
+ def define_collect_method(prefix)
108
+ method = 'collect_' + prefix
109
+ unless self.respond_to?(method)
110
+ define_method(method) do
111
+ self.methods.grep(/^#{prefix}_/).map {|meth| meth[prefix.size + 1..-1]}
112
+ end
113
+ end
114
+ end
115
+ end
116
+ extend ClassMethods
117
+ include ClassMethods
118
+
119
+ @hide_undocumented_commands = nil
120
+ class << self
121
+ # Flag that sets whether undocumented commands are listed in the help
122
+ attr_accessor :hide_undocumented_commands
123
+
124
+ def run(intro = nil)
125
+ new.cmdloop(intro)
126
+ end
127
+ end
128
+
129
+ # STDIN stream used
130
+ attr_writer :stdin
131
+
132
+ # STDOUT stream used
133
+ attr_writer :stdout
134
+
135
+ # The current command
136
+ attr_writer :current_command
137
+
138
+ prompt_with :default_prompt
139
+
140
+ def initialize
141
+ @stdin, @stdout = STDIN, STDOUT
142
+ @stop = false
143
+ setup
144
+ end
145
+
146
+ # Starts up the command loop
147
+ def cmdloop(intro = nil)
148
+ preloop
149
+ write intro if intro
150
+ begin
151
+ set_completion_proc(:complete)
152
+ begin
153
+ execute_command
154
+ # Catch ^C
155
+ rescue Interrupt
156
+ user_interrupt
157
+ # I don't know why ZeroDivisionError isn't caught below...
158
+ rescue ZeroDivisionError
159
+ handle_all_remaining_exceptions(ZeroDivisionError)
160
+ rescue => exception
161
+ handle_all_remaining_exceptions(exception)
162
+ end
163
+ end until @stop
164
+ postloop
165
+ end
166
+ alias :run :cmdloop
167
+
168
+ shortcut '?', 'help'
169
+ doc :help, 'This help message.'
170
+ def do_help(command = nil)
171
+ if command
172
+ command = translate_shortcut(command)
173
+ docs.include?(command) ? print_help(command) : no_help(command)
174
+ else
175
+ documented_commands.each {|cmd| print_help cmd}
176
+ print_undocumented_commands if undocumented_commands?
177
+ end
178
+ end
179
+
180
+ # Called when the +command+ has no associated documentation, this could
181
+ # potentially mean that the command is non existant
182
+ def no_help(command)
183
+ write "No help for command '#{command}'"
184
+ end
185
+
186
+ doc :exit, 'Terminate the program.'
187
+ def do_exit; stoploop end
188
+
189
+ # Turns off readline even if it is supported
190
+ def turn_off_readline
191
+ @readline_supported = false
192
+ self
193
+ end
194
+
195
+ protected
196
+
197
+ def execute_command
198
+ unless ARGV.empty?
199
+ stoploop
200
+ execute_line(ARGV * ' ')
201
+ else
202
+ execute_line(display_prompt(prompt, true))
203
+ end
204
+ end
205
+
206
+ def handle_all_remaining_exceptions(exception)
207
+ if exception_is_handled?(exception)
208
+ run_custom_exception_handling(exception)
209
+ else
210
+ handle_exception(exception)
211
+ end
212
+ end
213
+
214
+ def execute_line(command)
215
+ postcmd(run_command(precmd(command)))
216
+ end
217
+
218
+ def stoploop
219
+ @stop = true
220
+ end
221
+
222
+ # Indicates whether readline support is enabled
223
+ def readline_supported?
224
+ @readline_supported = READLINE_SUPPORTED if @readline_supported.nil?
225
+ @readline_supported
226
+ end
227
+
228
+ # Determines if the given exception has a custome handler.
229
+ def exception_is_handled?(exception)
230
+ custom_exception_handler(exception)
231
+ end
232
+
233
+ # Runs the customized exception handler for the given exception.
234
+ def run_custom_exception_handling(exception)
235
+ case handler = custom_exception_handler(exception)
236
+ when String: write handler
237
+ when Symbol: self.send(custom_exception_handler(exception))
238
+ end
239
+ end
240
+
241
+ # Returns the customized handler for the exception
242
+ def custom_exception_handler(exception)
243
+ custom_exception_handlers[exception.to_s]
244
+ end
245
+
246
+
247
+ # Called at object creation. This can be treated like 'initialize' for sub
248
+ # classes.
249
+ def setup
250
+ end
251
+
252
+ # Exceptions in the cmdloop are caught and passed to +handle_exception+.
253
+ # Custom exception classes must inherit from StandardError to be
254
+ # passed to +handle_exception+.
255
+ def handle_exception(exception)
256
+ raise exception
257
+ end
258
+
259
+ # Displays the prompt.
260
+ def display_prompt(prompt, with_history = true)
261
+ line = if readline_supported?
262
+ Readline::readline(prompt, with_history)
263
+ else
264
+ print prompt
265
+ @stdin.gets
266
+ end
267
+ line.respond_to?(:strip) ? line.strip : line
268
+ end
269
+
270
+ # The current command.
271
+ def current_command
272
+ translate_shortcut @current_command
273
+ end
274
+
275
+ # Called when the user hits ctrl-C or ctrl-D. Terminates execution by default.
276
+ def user_interrupt
277
+ write 'Terminating' # XXX get rid of this
278
+ stoploop
279
+ end
280
+
281
+ # XXX Not implementd yet. Called when a do_ method that takes arguments doesn't get any
282
+ def arguments_missing
283
+ write 'Invalid arguments'
284
+ do_help(current_command) if docs.include?(current_command)
285
+ end
286
+
287
+ # A bit of a hack I'm afraid. Since subclasses will be potentially
288
+ # overriding user_interrupt we want to ensure that it returns true so that
289
+ # it can be called with 'and return'
290
+ def interrupt
291
+ user_interrupt or true
292
+ end
293
+
294
+ # Displays the help for the passed in command.
295
+ def print_help(cmd)
296
+ offset = docs.keys.longest_string_length
297
+ write "#{cmd.ljust(offset)} -- #{docs[cmd]}" +
298
+ (has_shortcuts?(cmd) ? " #{display_shortcuts(cmd)}" : '')
299
+ end
300
+
301
+ def display_shortcuts(cmd)
302
+ "(aliases: #{shortcuts[cmd].join(', ')})"
303
+ end
304
+
305
+ # The method name that corresponds to the passed in command.
306
+ def command(cmd)
307
+ "do_#{cmd}".intern
308
+ end
309
+
310
+ # The method name that corresponds to the complete command for the pass in
311
+ # command.
312
+ def complete_method(cmd)
313
+ "complete_#{cmd}".intern
314
+ end
315
+
316
+ # Call back executed at the start of the cmdloop.
317
+ def preloop
318
+ end
319
+
320
+ # Call back executed at the end of the cmdloop.
321
+ def postloop
322
+ end
323
+
324
+ # Receives line submitted at prompt and passes it along to the command
325
+ # being called.
326
+ def precmd(line)
327
+ line
328
+ end
329
+
330
+ # Receives the returned value of the called command.
331
+ def postcmd(line)
332
+ line
333
+ end
334
+
335
+ # Called when an empty line is entered in response to the prompt.
336
+ def empty_line
337
+ end
338
+
339
+ define_collect_method('do')
340
+ define_collect_method('complete')
341
+
342
+ # The default completor. Looks up all do_* methods.
343
+ def complete(command)
344
+ commands = completion_grep(command_list, command)
345
+ if commands.size == 1
346
+ cmd = commands.first
347
+ set_completion_proc(complete_method(cmd)) if collect_complete.include?(cmd)
348
+ end
349
+ commands
350
+ end
351
+
352
+ # Lists of commands (i.e. do_* methods minus the 'do_' part).
353
+ def command_list
354
+ collect_do - subcommand_list
355
+ end
356
+
357
+ # Definitive list of shortcuts and abbreviations of a command.
358
+ def command_lookup_table
359
+ return @command_lookup_table if @command_lookup_table
360
+ @command_lookup_table = command_abbreviations.merge(shortcut_table)
361
+ end
362
+
363
+ # Returns lookup table of unambiguous identifiers for commands.
364
+ def command_abbreviations
365
+ return @command_abbreviations if @command_abbreviations
366
+ @command_abbreviations = Abbrev::abbrev(command_list)
367
+ end
368
+
369
+ # List of all subcommands.
370
+ def subcommand_list
371
+ with_underscore, without_underscore = collect_do.partition {|command| command.include?('_')}
372
+ with_underscore.find_all {|do_method| without_underscore.include?(do_method[/^[^_]+/])}
373
+ end
374
+
375
+ # Lists all subcommands of a given command.
376
+ def subcommands(command)
377
+ completion_grep(subcommand_list, translate_shortcut(command) + '_')
378
+ end
379
+
380
+ # Indicates whether a given command has any subcommands.
381
+ def has_subcommands?(command)
382
+ !subcommands(command).empty?
383
+ end
384
+
385
+ # List of commands which are documented.
386
+ def documented_commands
387
+ docs.keys.sort
388
+ end
389
+
390
+ # Indicates whether undocummented commands will be listed by the help
391
+ # command (they are listed by default).
392
+ def undocumented_commands_hidden?
393
+ self.class.hide_undocumented_commands
394
+ end
395
+
396
+ def print_undocumented_commands
397
+ return if undocumented_commands_hidden?
398
+ # TODO perhaps do some fancy stuff so that if the number of undocumented
399
+ # commands is greater than 80 cols or some such passed in number it
400
+ # presents them in a columnar fashion much the way readline does by default
401
+ write ' '
402
+ write 'Undocumented commands'
403
+ write '====================='
404
+ write undocumented_commands.join(' ' * 4)
405
+ end
406
+
407
+ # Returns list of undocumented commands.
408
+ def undocumented_commands
409
+ command_list - documented_commands
410
+ end
411
+
412
+ # Indicates if any commands are undocumeted.
413
+ def undocumented_commands?
414
+ !undocumented_commands.empty?
415
+ end
416
+
417
+ # Completor for the help command.
418
+ def complete_help(command)
419
+ completion_grep(documented_commands, command)
420
+ end
421
+
422
+ def completion_grep(collection, pattern)
423
+ collection.grep(/^#{Regexp.escape(pattern)}/)
424
+ end
425
+
426
+ # Writes out a message with newline.
427
+ def write(*strings)
428
+ # We want newlines at the end of every line, so don't join with "\n"
429
+ strings.each do |string|
430
+ @stdout.write string
431
+ @stdout.write "\n"
432
+ end
433
+ end
434
+ alias :puts :write
435
+
436
+ # Writes out a message without newlines appended.
437
+ def print(*strings)
438
+ strings.each {|string| @stdout.write string}
439
+ end
440
+
441
+ shortcut '!', 'shell'
442
+ doc :shell, 'Executes a shell.'
443
+ # Executes a shell, perhaps should only be defined by subclasses.
444
+ def do_shell(line)
445
+ shell = ENV['SHELL']
446
+ line ? write(%x(#{line}).strip) : system(shell)
447
+ end
448
+
449
+ # Takes care of collecting the current command and its arguments if any and
450
+ # dispatching the appropriate command.
451
+ def run_command(line)
452
+ cmd, args = parse_line(line)
453
+ sanitize_readline_history(line) if line
454
+ unless cmd then empty_line; return end
455
+
456
+ cmd = translate_shortcut(cmd)
457
+ self.current_command = cmd
458
+ set_completion_proc(complete_method(cmd)) if collect_complete.include?(complete_method(cmd))
459
+ cmd_method = command(cmd)
460
+ if self.respond_to?(cmd_method)
461
+ # Perhaps just catch exceptions here (related to arity) and call a
462
+ # method that reports a generic error like 'invalid arguments'
463
+ self.method(cmd_method).arity.zero? ? self.send(cmd_method) : self.send(cmd_method, tokenize_args(args))
464
+ else
465
+ command_missing(current_command, tokenize_args(args))
466
+ end
467
+ end
468
+
469
+ # Receives the line as it was passed from the prompt (barring modification
470
+ # in precmd) and splits it into a command section and an args section. The
471
+ # args are by default set to nil if they are boolean false or empty then
472
+ # joined with spaces. The tokenize method can be used to further alter the
473
+ # args.
474
+ def parse_line(line)
475
+ # line will be nil if ctr-D was pressed
476
+ user_interrupt and return if line.nil?
477
+
478
+ cmd, *args = line.split
479
+ args = args.to_s.empty? ? nil : args * ' '
480
+ if args and has_subcommands?(cmd)
481
+ if cmd = find_subcommand_in_args(subcommands(cmd), line.split)
482
+ # XXX Completion proc should be passed array of subcommands somewhere
483
+ args = line.split.join('_').match(/^#{cmd}/).post_match.gsub('_', ' ').strip
484
+ args = nil if args.empty?
485
+ end
486
+ end
487
+ [cmd, args]
488
+ end
489
+
490
+ # Extracts a subcommand if there is one from the command line submitted. I guess this is a hack.
491
+ def find_subcommand_in_args(subcommands, args)
492
+ (subcommands & (1..args.size).to_a.map {|num_elems| args.first(num_elems).join('_')}).max
493
+ end
494
+
495
+ # Looks up command shortcuts (e.g. '?' is a shortcut for 'help'). Short
496
+ # cuts can be added by using the shortcut class method.
497
+ def translate_shortcut(cmd)
498
+ command_lookup_table[cmd] || cmd
499
+ end
500
+
501
+ # Indicates if the passed in command has any registerd shortcuts.
502
+ def has_shortcuts?(cmd)
503
+ command_shortcuts(cmd)
504
+ end
505
+
506
+ # Returns the set of registered shortcuts for a command, or nil if none.
507
+ def command_shortcuts(cmd)
508
+ shortcuts[cmd]
509
+ end
510
+
511
+ # Called on command arguments as they are passed into the command.
512
+ def tokenize_args(args)
513
+ args
514
+ end
515
+
516
+ # Cleans up the readline history buffer by performing tasks such as
517
+ # removing empty lines and piggy-backed duplicates. Only executed if
518
+ # running with readline support.
519
+ def sanitize_readline_history(line)
520
+ return unless readline_supported?
521
+ # Strip out empty lines
522
+ Readline::HISTORY.pop if line.match(/^\s*$/)
523
+ # Remove duplicates
524
+ Readline::HISTORY.pop if Readline::HISTORY[-2] == line rescue IndexError
525
+ end
526
+
527
+ # Readline completion uses a procedure that takes the current readline
528
+ # buffer and returns an array of possible matches against the current
529
+ # buffer. This method sets the current procedure to use. Commands can
530
+ # specify customized completion procs by defining a method following the
531
+ # naming convetion complet_{command_name}.
532
+ def set_completion_proc(cmd)
533
+ return unless readline_supported?
534
+ Readline.completion_proc = self.method(cmd)
535
+ end
536
+
537
+ # Called when the line entered at the prompt does not map to any of the
538
+ # defined commands. By default it reports that there is no such command.
539
+ def command_missing(command, args)
540
+ write "No such command '#{command}'"
541
+ end
542
+
543
+ def default_prompt
544
+ "#{self.class.name}> "
545
+ end
546
+
547
+ end
548
+
549
+ module Enumerable #:nodoc:
550
+ def longest_string_length
551
+ inject(0) {|longest, item| longest >= item.size ? longest : item.size}
552
+ end
553
+ end
554
+
555
+ if __FILE__ == $0
556
+ Cmd.run
557
+ end