cmd 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/CHANGELOG +5 -0
- data/INSTALL +11 -0
- data/MIT-LICENSE +20 -0
- data/README +411 -0
- data/Rakefile +141 -0
- data/THANKS +12 -0
- data/TODO +124 -0
- data/example/calc.rb +86 -0
- data/example/phonebook.rb +69 -0
- data/lib/cmd.rb +557 -0
- data/setup.rb +1360 -0
- data/test/tc_cmd.rb +284 -0
- metadata +67 -0
data/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
|