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.
- 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/Rakefile
ADDED
@@ -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.
|
data/example/calc.rb
ADDED
@@ -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
|
data/lib/cmd.rb
ADDED
@@ -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
|