cmd 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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