bone 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +23 -0
- data/LICENSE.txt +19 -0
- data/README.md +62 -0
- data/Rakefile +55 -0
- data/Rudyfile +227 -0
- data/bin/bone +89 -0
- data/bone.gemspec +48 -0
- data/lib/bone.rb +150 -0
- data/lib/bone/cli.rb +69 -0
- data/try/bone.rb +9 -0
- data/vendor/drydock-0.6.8/CHANGES.txt +159 -0
- data/vendor/drydock-0.6.8/LICENSE.txt +22 -0
- data/vendor/drydock-0.6.8/README.rdoc +92 -0
- data/vendor/drydock-0.6.8/Rakefile +74 -0
- data/vendor/drydock-0.6.8/bin/example +210 -0
- data/vendor/drydock-0.6.8/drydock.gemspec +38 -0
- data/vendor/drydock-0.6.8/lib/drydock.rb +961 -0
- data/vendor/drydock-0.6.8/lib/drydock/console.rb +313 -0
- data/vendor/drydock-0.6.8/lib/drydock/mixins.rb +4 -0
- data/vendor/drydock-0.6.8/lib/drydock/mixins/object.rb +23 -0
- data/vendor/drydock-0.6.8/lib/drydock/mixins/string.rb +66 -0
- data/vendor/drydock-0.6.8/lib/drydock/screen.rb +33 -0
- metadata +91 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
= Drydock - v0.6
|
2
|
+
|
3
|
+
<b>Build seaworthy command-line apps like a Captain with a powerful Ruby DSL.</b>
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
Drydock is a seaworthy DSL for building really powerful command line applications. The core class is contained in a single .rb file so it's easy to copy directly into your project. See below for examples.
|
8
|
+
|
9
|
+
== Install
|
10
|
+
|
11
|
+
One of:
|
12
|
+
|
13
|
+
* gem install drydock
|
14
|
+
* copy lib/drydock.rb into your lib directory.
|
15
|
+
|
16
|
+
Or for GitHub fans:
|
17
|
+
|
18
|
+
* git clone git://github.com/delano/drydock.git
|
19
|
+
* gem install delano-drydock
|
20
|
+
|
21
|
+
== Examples
|
22
|
+
|
23
|
+
See bin/example for more.
|
24
|
+
|
25
|
+
require 'drydock'
|
26
|
+
extend Drydock
|
27
|
+
|
28
|
+
default :welcome
|
29
|
+
|
30
|
+
before do
|
31
|
+
# You can execute a block before the requests command is executed. Instance
|
32
|
+
# variables defined here will be available to all commands.
|
33
|
+
end
|
34
|
+
|
35
|
+
about "A friendly welcome to the Drydock"
|
36
|
+
command :welcome do
|
37
|
+
puts "Welcome to Drydock."
|
38
|
+
puts "For available commands:"
|
39
|
+
puts "#{$0} show-commands"
|
40
|
+
end
|
41
|
+
|
42
|
+
usage "USAGE: #{$0} laugh [-f]"
|
43
|
+
about "The captain commands his crew to laugh"
|
44
|
+
option :f, :faster, "A boolean value. Go even faster!"
|
45
|
+
command :laugh do |obj|
|
46
|
+
# +obj+ is an instance of Drydock::Command. The options you define are available
|
47
|
+
# via obj.option.name
|
48
|
+
|
49
|
+
answer = !obj.option.faster ? "Sort of" : "Yes! I'm literally laughing as fast as possible."
|
50
|
+
|
51
|
+
puts "Captain Stubing: Are you laughing?"
|
52
|
+
puts "Dr. Bricker: " << answer
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
class JohnWestSmokedOysters < Drydock::Command
|
57
|
+
# You can write your own command classes by inheriting from Drydock::Command
|
58
|
+
# and referencing it in the command definition.
|
59
|
+
def ahoy!; p "matey"; end
|
60
|
+
end
|
61
|
+
|
62
|
+
about "Do something with John West's Smoked Oysters"
|
63
|
+
command :oysters => JohnWestSmokedOysters do |obj|
|
64
|
+
p obj # => #<JohnWestSmokedOysters:0x42179c ... >
|
65
|
+
end
|
66
|
+
|
67
|
+
about "My way of saying hello!"
|
68
|
+
command :ahoy! => JohnWestSmokedOysters
|
69
|
+
# If you don't provide a block, Drydock will call JohnWestSmokedOysters#ahoy!
|
70
|
+
|
71
|
+
Drydock.run!
|
72
|
+
|
73
|
+
|
74
|
+
== More Information
|
75
|
+
|
76
|
+
* GitHub[http://github.com/delano/drydock]
|
77
|
+
* RDocs[http://drydock.rubyforge.org/]
|
78
|
+
* Inspiration[http://www.youtube.com/watch?v=m_wFEB4Oxlo]
|
79
|
+
|
80
|
+
== Thanks
|
81
|
+
|
82
|
+
* Solutious Inc for putting up with my endless references to the sea! (http://solutious.com)
|
83
|
+
* Blake Mizerany for the inspiration via bmizerany-frylock[http://github.com/bmizerany/frylock]
|
84
|
+
|
85
|
+
== Credits
|
86
|
+
|
87
|
+
* Delano Mandelbaum (delano@solutious.com)
|
88
|
+
* Bernie Kopell (bernie@solutious.com)
|
89
|
+
|
90
|
+
== License
|
91
|
+
|
92
|
+
See LICENSE.txt
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'hanna/rdoctask'
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
# SPECS ===============================================================
|
11
|
+
|
12
|
+
desc 'Run specs with unit test style output'
|
13
|
+
task :test do |t|
|
14
|
+
sh "ruby test/*_test.rb"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Run bin/example and tryouts'
|
18
|
+
task :tryouts do |t|
|
19
|
+
sh "ruby bin/example"
|
20
|
+
end
|
21
|
+
|
22
|
+
# PACKAGE =============================================================
|
23
|
+
|
24
|
+
name = "drydock"
|
25
|
+
load "#{name}.gemspec"
|
26
|
+
|
27
|
+
version = @spec.version
|
28
|
+
|
29
|
+
Rake::GemPackageTask.new(@spec) do |p|
|
30
|
+
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
31
|
+
end
|
32
|
+
|
33
|
+
task :release => [ :rdoc, :package ]
|
34
|
+
|
35
|
+
task :install => [ :rdoc, :package ] do
|
36
|
+
sh %{sudo gem install pkg/#{name}-#{version}.gem}
|
37
|
+
end
|
38
|
+
|
39
|
+
task :uninstall => [ :clean ] do
|
40
|
+
sh %{sudo gem uninstall #{name}}
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Rubyforge Release / Publish Tasks ==================================
|
45
|
+
|
46
|
+
desc 'Publish website to rubyforge'
|
47
|
+
task 'publish:rdoc' => 'doc/index.html' do
|
48
|
+
sh "scp -rp doc/* rubyforge.org:/var/www/gforge-projects/#{name}/"
|
49
|
+
end
|
50
|
+
|
51
|
+
task 'publish:gem' => [:package] do |t|
|
52
|
+
sh <<-end
|
53
|
+
rubyforge add_release -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.gem &&
|
54
|
+
rubyforge add_file -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.tgz
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
Rake::RDocTask.new do |t|
|
60
|
+
t.rdoc_dir = 'doc'
|
61
|
+
t.title = @spec.summary
|
62
|
+
t.options << '--line-numbers' << '-A cattr_accessor=object'
|
63
|
+
t.options << '--charset' << 'utf-8'
|
64
|
+
t.rdoc_files.include('LICENSE.txt')
|
65
|
+
t.rdoc_files.include('README.rdoc')
|
66
|
+
t.rdoc_files.include('CHANGES.txt')
|
67
|
+
t.rdoc_files.include('bin/*')
|
68
|
+
t.rdoc_files.include('lib/*.rb')
|
69
|
+
end
|
70
|
+
|
71
|
+
CLEAN.include [ 'pkg', '*.gem', '.config', 'doc' ]
|
72
|
+
|
73
|
+
|
74
|
+
|
@@ -0,0 +1,210 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Seafaring Drydock Examples
|
4
|
+
#
|
5
|
+
# This is a functioning script so you can copy it, run it,
|
6
|
+
# and just generally be a longshoreman about things. This is
|
7
|
+
# a drydock after all.
|
8
|
+
#
|
9
|
+
# If you're reading this via the Rdocs you won't see the code. See:
|
10
|
+
#
|
11
|
+
# http://github.com/delano/drydock/blob/master/bin/example
|
12
|
+
#
|
13
|
+
# For an example of a complex command-line application using
|
14
|
+
# Drydock, see:
|
15
|
+
#
|
16
|
+
# http://github.com/solutious/rudy/blob/master/bin/rudy
|
17
|
+
#
|
18
|
+
|
19
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), '..')), 'lib'
|
20
|
+
|
21
|
+
require 'drydock'
|
22
|
+
|
23
|
+
module Example
|
24
|
+
extend Drydock # Tell Drydock you want its methods!
|
25
|
+
|
26
|
+
default :welcome # The welcome command will be run if no command is given
|
27
|
+
capture :stderr # Drydock will capture STDERR and keep it in the hold.
|
28
|
+
# You can use this to suppress errors.
|
29
|
+
|
30
|
+
about "A friendly welcome to the Drydock"
|
31
|
+
command :welcome do
|
32
|
+
puts "Welcome to Drydock.", $/
|
33
|
+
puts "For available commands: #{$0} show-commands"
|
34
|
+
end
|
35
|
+
|
36
|
+
usage "USAGE: #{$0} laugh [-f]"
|
37
|
+
about "The captain commands his crew to laugh"
|
38
|
+
option :f, :faster, "A boolean value. Go even faster!"
|
39
|
+
command :laugh do |obj|
|
40
|
+
# +obj+ is an instance of Drydock::Command. The options you define are available
|
41
|
+
# via obj.option.name
|
42
|
+
|
43
|
+
answer = !obj.option.faster ? "Sort of" : "Yes! I'm literally laughing as fast as possible."
|
44
|
+
|
45
|
+
puts "Captain Stubing: Are you laughing?"
|
46
|
+
puts "Dr. Bricker: " << answer
|
47
|
+
end
|
48
|
+
|
49
|
+
global_usage "USAGE: #{File.basename($0)} [global options] command [command options]"
|
50
|
+
global :s, :seconds, "Display values in seconds"
|
51
|
+
global :v, :verbose, "Verbosity level (i.e. -vvv is greater than -v)" do |v|
|
52
|
+
# Use instance variables to maintain values between option blocks.
|
53
|
+
# This will increment for every -v found (i.e. -vvv)
|
54
|
+
@val ||= 0
|
55
|
+
@val += 1
|
56
|
+
end
|
57
|
+
|
58
|
+
before do |obj|
|
59
|
+
# You can execute a block before the requests command is executed. Instance
|
60
|
+
# variables defined here will be available to all commands.
|
61
|
+
# +obj+ is a reference to the command object, just like in command blocks.
|
62
|
+
end
|
63
|
+
|
64
|
+
after do |obj|
|
65
|
+
# And this will be called after the command.
|
66
|
+
end
|
67
|
+
|
68
|
+
usage "#{$0} [-s] [-vv] date"
|
69
|
+
about "Display the current date"
|
70
|
+
command :date do |obj|
|
71
|
+
require 'time'
|
72
|
+
now = Time.now
|
73
|
+
puts "(Not verbose enough. Try adding a -v.)" if (obj.global.verbose || 0) == 1
|
74
|
+
puts "More verbosely, the date is now: " if (obj.global.verbose || 0) >= 2
|
75
|
+
puts (obj.global.seconds) ? now.to_i : now.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
ignore :options
|
80
|
+
about "This command ignores options"
|
81
|
+
command :rogue do |obj|
|
82
|
+
# You can use ignore :options to tell Drydock to not process the
|
83
|
+
# command-specific options.
|
84
|
+
# Unnamed arguments are available from obj.argv
|
85
|
+
if obj.argv.empty?
|
86
|
+
puts "Had you supplied some arguments, I would have ignored them."
|
87
|
+
else
|
88
|
+
puts "Hi! You supplied some arguments but I ignored them."
|
89
|
+
puts "They're all still here in this array: %s" % obj.argv.join(', ')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class JohnWestSmokedOysters < Drydock::Command
|
94
|
+
# You can write your own command classes by inheriting from Drydock::Command
|
95
|
+
# and referencing it in the command definition.
|
96
|
+
def ahoy!; p "matey"; end
|
97
|
+
end
|
98
|
+
|
99
|
+
about "Do something with John West's Smoked Oysters"
|
100
|
+
command :oysters => JohnWestSmokedOysters do |obj|
|
101
|
+
p obj # => #<JohnWestSmokedOysters:0x42179c ... >
|
102
|
+
end
|
103
|
+
|
104
|
+
about "My way of saying hello!"
|
105
|
+
command [:ahoy!, :hello!] => JohnWestSmokedOysters
|
106
|
+
# If you don't provide a block, Drydock will call JohnWestSmokedOysters#ahoy!
|
107
|
+
|
108
|
+
|
109
|
+
require 'yaml'
|
110
|
+
|
111
|
+
usage 'ruby bin/example uri -c -d " " -t 15 http://solutious.com/'
|
112
|
+
usage 'echo "http://solutious.com/" | ruby bin/example uri -c -d " " -t 15'
|
113
|
+
about "Check for broken URIs"
|
114
|
+
option :c, :check, "Check response codes for each URI"
|
115
|
+
option :d, :delim, String, "Output delimiter"
|
116
|
+
option :t, :timeout, Float, "Timeout value for HTTP request" do |v|
|
117
|
+
# You can provide an block to process the option value.
|
118
|
+
# This block must return the final value.
|
119
|
+
v = 10 if (v > 10)
|
120
|
+
v
|
121
|
+
end
|
122
|
+
argv :uris
|
123
|
+
|
124
|
+
command :uri do |obj|
|
125
|
+
# This command processes the output of the stdin block (below this definition).
|
126
|
+
# The output of that block is available as obj.stdin. If there is no stdin block
|
127
|
+
# obj.stdin will be STDIN's IO object.
|
128
|
+
|
129
|
+
require 'net/http'
|
130
|
+
require 'uri'
|
131
|
+
require 'timeout'
|
132
|
+
|
133
|
+
uris = []
|
134
|
+
uris += obj.stdin if obj.stdin
|
135
|
+
uris += obj.argv.uris if obj.argv.uris
|
136
|
+
|
137
|
+
delim = obj.option.delim || ','
|
138
|
+
timeout = obj.option.timeout || 5
|
139
|
+
code = :notchecked # The default code when :check is false
|
140
|
+
|
141
|
+
if uris.empty?
|
142
|
+
puts "Frylock: You didn't provide any URIs. "
|
143
|
+
puts "Master Shake: Ya, see #{$0} #{obj.alias} -h"
|
144
|
+
exit 0
|
145
|
+
end
|
146
|
+
|
147
|
+
uris.each_with_index do |uri, index|
|
148
|
+
code = response_code(uri, timeout) if (obj.option.check)
|
149
|
+
puts [index+1, uri, code].join(delim)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
about "Prints the alias used to access the command"
|
155
|
+
# We can define command aliases by providing a list of command
|
156
|
+
# names. The first name is still consider to be the main name.
|
157
|
+
command :printalias, :reveal do |obj|
|
158
|
+
puts "This is printalias!"
|
159
|
+
if (obj.alias == obj.cmd)
|
160
|
+
puts "You did not use an alias"
|
161
|
+
else
|
162
|
+
puts "You used the alias " << obj.alias
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
stdin do |stdin, output|
|
167
|
+
# Pre-process STDIN for all commands. This example returns an array of lines.
|
168
|
+
# The command processuris uses this array.
|
169
|
+
|
170
|
+
# We only want piped data. If this is not included
|
171
|
+
# execution will wait for input from the user.
|
172
|
+
unless stdin.tty?
|
173
|
+
|
174
|
+
while !stdin.eof? do
|
175
|
+
line = stdin.readline
|
176
|
+
line.chomp!
|
177
|
+
(output ||= []) << line
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
output
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# And one final feature for the intrepid swabbies like myself.
|
186
|
+
# Drydock can handle unknown commands by catching them with a
|
187
|
+
# trawler. It's like the captain of all aliases. Just specify
|
188
|
+
# the command name to direct all unknown commands to. Simple!
|
189
|
+
trawler :printalias
|
190
|
+
|
191
|
+
|
192
|
+
# Return the HTTP response code for the given URI. Used by
|
193
|
+
# uri command.
|
194
|
+
#
|
195
|
+
# +uri+ A valid HTTP URI
|
196
|
+
# +duration+ The timeout threshold (in seconds) for the request.
|
197
|
+
def response_code(uri_str, duration=5) #:nodoc:
|
198
|
+
response = :unavailable
|
199
|
+
begin
|
200
|
+
uri = (uri_str.kind_of? URI::HTTP) ? uri_str : URI.parse(uri_str)
|
201
|
+
timeout(duration) do
|
202
|
+
response = Net::HTTP.get_response(uri).code
|
203
|
+
end
|
204
|
+
rescue Exception => ex
|
205
|
+
end
|
206
|
+
response
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
Drydock.run!
|
@@ -0,0 +1,38 @@
|
|
1
|
+
@spec = Gem::Specification.new do |s|
|
2
|
+
s.name = %q{drydock}
|
3
|
+
s.version = "0.6.8"
|
4
|
+
s.specification_version = 1 if s.respond_to? :specification_version=
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
|
7
|
+
s.authors = ["Delano Mandelbaum"]
|
8
|
+
s.description = %q{Build seaworthy command-line apps like a Captain with a powerful Ruby DSL.}
|
9
|
+
s.summary = s.description
|
10
|
+
s.email = %q{delano@solutious.com}
|
11
|
+
|
12
|
+
# = MANIFEST =
|
13
|
+
# git ls-files
|
14
|
+
s.files = %w(
|
15
|
+
CHANGES.txt
|
16
|
+
LICENSE.txt
|
17
|
+
README.rdoc
|
18
|
+
Rakefile
|
19
|
+
bin/example
|
20
|
+
drydock.gemspec
|
21
|
+
lib/drydock.rb
|
22
|
+
lib/drydock/console.rb
|
23
|
+
lib/drydock/mixins.rb
|
24
|
+
lib/drydock/mixins/object.rb
|
25
|
+
lib/drydock/mixins/string.rb
|
26
|
+
lib/drydock/screen.rb
|
27
|
+
)
|
28
|
+
|
29
|
+
# s.add_dependency ''
|
30
|
+
|
31
|
+
s.has_rdoc = true
|
32
|
+
s.homepage = %q{http://github.com/delano/drydock}
|
33
|
+
s.extra_rdoc_files = %w[README.rdoc LICENSE.txt CHANGES.txt]
|
34
|
+
s.rdoc_options = ["--line-numbers", "--title", "Drydock: #{s.description}", "--main", "README.rdoc"]
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.1.1}
|
37
|
+
s.rubyforge_project = "drydock"
|
38
|
+
end
|
@@ -0,0 +1,961 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module Drydock
|
7
|
+
require 'drydock/mixins'
|
8
|
+
|
9
|
+
autoload :Screen, 'drydock/screen'
|
10
|
+
autoload :Console, 'drydock/console'
|
11
|
+
|
12
|
+
class FancyArray < Array #:nodoc:
|
13
|
+
attr_reader :fields
|
14
|
+
def add_field(n)
|
15
|
+
@fields ||= []
|
16
|
+
field_name = n
|
17
|
+
eval <<-RUBY, binding, '(Drydock::FancyArray)', 1
|
18
|
+
def #{n}
|
19
|
+
if self.size > @fields.size && '#{n}'.to_sym == @fields.last
|
20
|
+
self[#{@fields.size}..-1]
|
21
|
+
else
|
22
|
+
self[#{@fields.size}]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def #{n}=(val)
|
26
|
+
if self.size > @fields.size && '#{n}'.to_sym == @fields.last
|
27
|
+
self[#{@fields.size}..-1] = val
|
28
|
+
else
|
29
|
+
self[#{@fields.size}] = val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
RUBY
|
33
|
+
@fields << n
|
34
|
+
n
|
35
|
+
end
|
36
|
+
def fields=(*args)
|
37
|
+
args.flatten.each do |field|
|
38
|
+
add_field(field)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ArgError < RuntimeError
|
44
|
+
attr_reader :arg, :cmd, :msg
|
45
|
+
def initialize(*args)
|
46
|
+
@msg = args.shift if args.size == 1
|
47
|
+
@arg, @cmd, @msg = *args
|
48
|
+
@cmd ||= 'COMMAND'
|
49
|
+
@msg = nil if @msg && @msg.empty?
|
50
|
+
end
|
51
|
+
def message; @msg || "Error: No #{@arg} provided"; end
|
52
|
+
def usage; "See: #{$0} #{@cmd} -h"; end
|
53
|
+
end
|
54
|
+
class OptError < ArgError
|
55
|
+
def message; @msg || "Error: No #{@arg} provided"; end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The base class for all command objects. There is an instance of this class
|
59
|
+
# for every command defined. Global and command-specific options are added
|
60
|
+
# as attributes to this class dynamically.
|
61
|
+
#
|
62
|
+
# i.e. "example -v select --location kumamoto"
|
63
|
+
#
|
64
|
+
# global :v, :verbose, "I want mooooore!"
|
65
|
+
# option :l, :location, String, "Source location"
|
66
|
+
# command :select do |obj|
|
67
|
+
# puts obj.global.verbose #=> true
|
68
|
+
# puts obj.option.location #=> "kumamoto"
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# You can sub-class it to create your own:
|
72
|
+
#
|
73
|
+
# class Malpeque < Drydock::Command
|
74
|
+
# # ... sea to it
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# And then specify your class in the command definition:
|
78
|
+
#
|
79
|
+
# command :eat => Malpeque do |obj|
|
80
|
+
# # ... do stuff with your obj
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
class Command
|
84
|
+
VERSION = "0.6.8"
|
85
|
+
# The canonical name of the command (the one used in the command definition). If you
|
86
|
+
# inherit from this class and add a method named +cmd+, you can leave omit the block
|
87
|
+
# in the command definition. That method will be called instead. See bin/examples.
|
88
|
+
attr_reader :cmd
|
89
|
+
# The name used to evoke this command (it's either the canonical name or the alias used).
|
90
|
+
attr_reader :alias
|
91
|
+
# The block that will be executed when this command is evoked. If the block is nil
|
92
|
+
# it will check if there is a method named +cmd+. If so, that will be executed.
|
93
|
+
attr_reader :b
|
94
|
+
# An OpenStruct object containing the command options specified at run-time.
|
95
|
+
attr_reader :option
|
96
|
+
# An OpenStruct object containing the global options specified at run-time.
|
97
|
+
attr_reader :global
|
98
|
+
# A friendly description of the command.
|
99
|
+
attr_accessor :desc
|
100
|
+
# An array of action names specified in the command definition
|
101
|
+
attr_accessor :actions
|
102
|
+
# An instance of Drydock::FancyArray. Acts like an array of unnamed arguments
|
103
|
+
# but also allows field names if supplied.
|
104
|
+
attr_accessor :argv
|
105
|
+
# Either an IO handle to STDIN or the output of the Drydock#stdin handler.
|
106
|
+
attr_reader :stdin
|
107
|
+
# The basename of the executable or script: File.basename($0)
|
108
|
+
attr_reader :executable
|
109
|
+
|
110
|
+
# The default constructor sets the short name of the command
|
111
|
+
# and stores a reference to the block (if supplied).
|
112
|
+
# You don't need to override this method to add functionality
|
113
|
+
# to your custom Command classes. Define an +init+ method instead.
|
114
|
+
# It will be called just before the block is executed.
|
115
|
+
# +cmd+ is the short name of this command.
|
116
|
+
# +b+ is the block associated to this command.
|
117
|
+
def initialize(cmd, &b)
|
118
|
+
@cmd = (cmd.kind_of?(Symbol)) ? cmd : cmd.to_sym
|
119
|
+
@b = b
|
120
|
+
@actions = []
|
121
|
+
@argv = Drydock::FancyArray.new # an array with field names
|
122
|
+
@stdin = STDIN
|
123
|
+
@option = OpenStruct.new
|
124
|
+
@global = OpenStruct.new
|
125
|
+
@executable = File.basename($0)
|
126
|
+
@global.verbose = 0
|
127
|
+
@global.quiet = false
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the command name (not the alias)
|
131
|
+
def name
|
132
|
+
@cmd
|
133
|
+
end
|
134
|
+
|
135
|
+
# Prepare this command object to be called.
|
136
|
+
#
|
137
|
+
# Calls self.init after setting attributes (if the method exists). You can
|
138
|
+
# implement an init method in your subclasses of Drydock::Command to handle
|
139
|
+
# your own initialization stuff.
|
140
|
+
#
|
141
|
+
# <li>+cmd_str+ is the short name used to evoke this command. It will equal @cmd
|
142
|
+
# unless an alias was used used to evoke this command.</li>
|
143
|
+
# <li>+argv+ an array of unnamed arguments. If ignore :options was declared this</li>
|
144
|
+
# will contain the arguments exactly as they were defined on the command-line.</li>
|
145
|
+
# <li>+stdin+ contains the output of stdin do; ...; end otherwise it's a STDIN IO handle.</li>
|
146
|
+
# <li>+global_options+ a hash of the global options specified on the command-line</li>
|
147
|
+
# <li>+options+ a hash of the command-specific options specific on the command-line.</li>
|
148
|
+
def prepare(cmd_str=nil, argv=[], stdin=[], global_options={}, options={})
|
149
|
+
@alias = cmd_str.nil? ? @cmd : cmd_str
|
150
|
+
|
151
|
+
global_options.each_pair do |n,v|
|
152
|
+
self.global.send("#{n}=", v) # Populate the object's globals
|
153
|
+
end
|
154
|
+
|
155
|
+
options.each_pair do |n,v|
|
156
|
+
self.option.send("#{n}=", v) # ... and also the command options
|
157
|
+
end
|
158
|
+
|
159
|
+
@argv << argv # TODO: Using += returns an Array instead of FancyArray
|
160
|
+
@argv.flatten! # NOTE: << creates @argv[[]]
|
161
|
+
@stdin = stdin
|
162
|
+
|
163
|
+
self.init if self.respond_to? :init # Must be called first!
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
# Calls the command in the following order:
|
168
|
+
#
|
169
|
+
# * print_header
|
170
|
+
# * validation (if methodname_valid? exists)
|
171
|
+
# * command block (@b)
|
172
|
+
# * print_footer
|
173
|
+
#
|
174
|
+
def call
|
175
|
+
self.print_header if self.respond_to? :print_header
|
176
|
+
|
177
|
+
# Execute the command block if it exists
|
178
|
+
if @b
|
179
|
+
run_validation
|
180
|
+
@b.call(self)
|
181
|
+
|
182
|
+
# Otherwise check to see if an action was specified
|
183
|
+
elsif !(chosen = find_action(self.option)).empty?
|
184
|
+
raise "Only one action at a time please! I can't #{chosen.join(' AND ')}." if chosen.size > 1
|
185
|
+
criteria = [[@cmd, chosen.first], [chosen.first, @cmd]]
|
186
|
+
meth = name = nil
|
187
|
+
# Try command_action, then action_command
|
188
|
+
criteria.each do |tuple|
|
189
|
+
name = tuple.join('_')
|
190
|
+
meth = name if self.respond_to?(name)
|
191
|
+
end
|
192
|
+
|
193
|
+
raise "#{self.class} needs a #{name} method!" unless meth
|
194
|
+
|
195
|
+
run_validation(meth)
|
196
|
+
self.send(meth)
|
197
|
+
|
198
|
+
# No block and no action. We'll try for the method name in the Drydock::Command class.
|
199
|
+
elsif self.respond_to? @cmd.to_sym
|
200
|
+
run_validation(@cmd)
|
201
|
+
self.send(@cmd)
|
202
|
+
|
203
|
+
# Well, then I have no idea what you want me to do!
|
204
|
+
else
|
205
|
+
raise "The command #{@alias} has no block and #{self.class} has no #{@cmd} method!"
|
206
|
+
end
|
207
|
+
|
208
|
+
self.print_footer if respond_to? :print_footer
|
209
|
+
end
|
210
|
+
|
211
|
+
# <li>+meth+ The method name used to determine the name of the validation method.
|
212
|
+
# If not supplied, the validation method is "valid?" otherwise it's "meth_valid?"</li>
|
213
|
+
# If the command class doesn't have the given validation method, we'll just continue
|
214
|
+
# on our way.
|
215
|
+
#
|
216
|
+
# Recognized validation methods are:
|
217
|
+
#
|
218
|
+
# def valid? # if we're executing a command block
|
219
|
+
# def command_valid? # if we're executing an object method
|
220
|
+
# def command_action_valid? # if the main meth is command_action
|
221
|
+
# def action_command_valid? # if the main meth is action_command
|
222
|
+
#
|
223
|
+
# This method raises a generic exception when the validation method returns false.
|
224
|
+
# However, <strong>it's more appropriate for the validation methods to raise
|
225
|
+
# detailed exceptions</strong>.
|
226
|
+
#
|
227
|
+
def run_validation(meth=nil)
|
228
|
+
vmeth = meth ? [meth, 'valid?'].join('_') : 'valid?'
|
229
|
+
is_valid = self.respond_to?(vmeth) ? self.send(vmeth) : true
|
230
|
+
raise "Your request is not valid. See #{$0} #{@cmd} -h" unless is_valid
|
231
|
+
end
|
232
|
+
private :run_validation
|
233
|
+
|
234
|
+
# Compares the list of known actions to the list of boolean switches supplied
|
235
|
+
# on the command line (if any).
|
236
|
+
# <li>+options+ is a hash of the named command line arguments (created by
|
237
|
+
# OptionParser#getopts)</li>
|
238
|
+
# Returns an array of action names (empty if no action was supplied)
|
239
|
+
def find_action(options)
|
240
|
+
options = options.marshal_dump if options.is_a?(OpenStruct)
|
241
|
+
boolkeys = options.keys.select { |n| options[n] == true } || []
|
242
|
+
boolkeys = boolkeys.collect { |n| n.to_s } # @agents contains Strings.
|
243
|
+
# Returns the elements in @actions that are also found in boolkeys
|
244
|
+
(@actions || []) & boolkeys
|
245
|
+
end
|
246
|
+
private :find_action
|
247
|
+
|
248
|
+
# Print the list of available commands to STDOUT. This is used as the
|
249
|
+
# "default" command unless another default commands is supplied. You
|
250
|
+
# can also write your own Drydock::Command#show_commands to override
|
251
|
+
# this default behaviour.
|
252
|
+
#
|
253
|
+
# The output was worked on here:
|
254
|
+
# http://etherpad.com/SXjqQGRr8M
|
255
|
+
#
|
256
|
+
def show_commands
|
257
|
+
project = " for #{Drydock.project}" if Drydock.project?
|
258
|
+
cmds = {}
|
259
|
+
Drydock.commands.keys.each do |cmd|
|
260
|
+
next if cmd == :show_commands
|
261
|
+
pretty = Drydock.decanonize(cmd)
|
262
|
+
# Out to sea
|
263
|
+
cmds[Drydock.commands[cmd].cmd] ||= {}
|
264
|
+
unless cmd === Drydock.commands[cmd].cmd
|
265
|
+
(cmds[Drydock.commands[cmd].cmd][:aliases] ||= []) << pretty
|
266
|
+
next
|
267
|
+
end
|
268
|
+
cmds[cmd][:desc] = Drydock.commands[cmd].desc
|
269
|
+
cmds[cmd][:desc] = nil if cmds[cmd][:desc] && cmds[cmd][:desc].empty?
|
270
|
+
cmds[cmd][:pretty] = pretty
|
271
|
+
end
|
272
|
+
|
273
|
+
cmd_names_sorted = cmds.keys.sort{ |a,b| a.to_s <=> b.to_s }
|
274
|
+
|
275
|
+
if @global.quiet
|
276
|
+
puts "Commands: "
|
277
|
+
line = []
|
278
|
+
cmd_names_sorted.each_with_index do |cmd,i|
|
279
|
+
line << cmd
|
280
|
+
if (line.size % 4 == 0) || i == (cmd_names_sorted.size - 1)
|
281
|
+
puts " %s" % line.join(', ')
|
282
|
+
line.clear
|
283
|
+
end
|
284
|
+
end
|
285
|
+
return
|
286
|
+
end
|
287
|
+
|
288
|
+
puts "%5s: %s" % ["Usage", "#{@executable} [global options] COMMAND [command options]"]
|
289
|
+
puts "%5s: %s" % ["Try", "#{@executable} -h"]
|
290
|
+
puts "%5s %s" % ["", "#{@executable} COMMAND -h"]
|
291
|
+
puts
|
292
|
+
|
293
|
+
puts "Commands: "
|
294
|
+
if @global.verbose > 0
|
295
|
+
puts # empty line
|
296
|
+
cmd_names_sorted.each do |cmd|
|
297
|
+
puts "$ %s" % [@executable] if Drydock.default?(cmd)
|
298
|
+
puts "$ %s %s" % [@executable, cmds[cmd][:pretty]]
|
299
|
+
puts "%10s: %s" % ["About", cmds[cmd][:desc]] if cmds[cmd][:desc]
|
300
|
+
if cmds[cmd][:aliases]
|
301
|
+
cmds[cmd][:aliases].sort!{ |a,b| a.size <=> b.size }
|
302
|
+
puts "%10s: %s" % ["Aliases", cmds[cmd][:aliases].join(', ')]
|
303
|
+
end
|
304
|
+
puts
|
305
|
+
end
|
306
|
+
|
307
|
+
else
|
308
|
+
cmd_names_sorted.each do |cmd|
|
309
|
+
aliases = cmds[cmd][:aliases] || []
|
310
|
+
aliases.sort!{ |a,b| a.size <=> b.size }
|
311
|
+
aliases = aliases.empty? ? '' : "(aliases: #{aliases.join(', ')})"
|
312
|
+
pattern = Drydock.default?(cmd) ? "* %-16s %s" : " %-16s %s"
|
313
|
+
puts pattern % [cmds[cmd][:pretty], aliases]
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# The name of the command
|
319
|
+
def to_s
|
320
|
+
@cmd.to_s
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
module Drydock
|
326
|
+
class UnknownCommand < RuntimeError
|
327
|
+
attr_reader :name
|
328
|
+
def initialize(name)
|
329
|
+
@name = name || :unknown
|
330
|
+
end
|
331
|
+
def message
|
332
|
+
"Unknown command: #{@name}"
|
333
|
+
end
|
334
|
+
end
|
335
|
+
class NoCommandsDefined < RuntimeError
|
336
|
+
def message
|
337
|
+
"No commands defined"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
class InvalidArgument < RuntimeError
|
341
|
+
attr_accessor :args
|
342
|
+
def initialize(args)
|
343
|
+
@args = args || []
|
344
|
+
end
|
345
|
+
def message
|
346
|
+
"Unknown option: #{@args.join(", ")}"
|
347
|
+
end
|
348
|
+
end
|
349
|
+
class MissingArgument < InvalidArgument
|
350
|
+
def message
|
351
|
+
"Option requires a value: #{@args.join(", ")}"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Drydock is a DSL for command-line apps.
|
357
|
+
# See bin/example for usage examples.
|
358
|
+
module Drydock
|
359
|
+
extend self
|
360
|
+
|
361
|
+
VERSION = 0.6
|
362
|
+
|
363
|
+
@@project = nil
|
364
|
+
|
365
|
+
@@debug = false
|
366
|
+
@@has_run = false
|
367
|
+
@@run = true
|
368
|
+
|
369
|
+
@@global_opts_parser = OptionParser.new
|
370
|
+
@@global_option_names = []
|
371
|
+
|
372
|
+
@@command_opts_parser = []
|
373
|
+
@@command_option_names = []
|
374
|
+
@@command_actions = []
|
375
|
+
|
376
|
+
@@default_command = nil
|
377
|
+
@@default_command_with_args = false
|
378
|
+
|
379
|
+
@@commands = {}
|
380
|
+
@@command_descriptions = []
|
381
|
+
@@command_index = 0
|
382
|
+
@@command_index_map = {}
|
383
|
+
@@command_argv_names = [] # an array of names for values of argv
|
384
|
+
|
385
|
+
@@capture = nil # contains one of :stdout, :stderr
|
386
|
+
@@captured = nil
|
387
|
+
|
388
|
+
@@trawler = nil
|
389
|
+
|
390
|
+
public
|
391
|
+
# Enable or disable debug output.
|
392
|
+
#
|
393
|
+
# debug :on
|
394
|
+
# debug :off
|
395
|
+
#
|
396
|
+
# Calling without :on or :off will toggle the value.
|
397
|
+
#
|
398
|
+
def debug(toggle=false)
|
399
|
+
if toggle.is_a? Symbol
|
400
|
+
@@debug = true if toggle == :on
|
401
|
+
@@debug = false if toggle == :off
|
402
|
+
else
|
403
|
+
@@debug = (!@@debug)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Returns true if debug output is enabled.
|
408
|
+
def debug?
|
409
|
+
@@debug
|
410
|
+
end
|
411
|
+
|
412
|
+
# Provide names for CLI arguments, in the order they appear.
|
413
|
+
#
|
414
|
+
# $ yourscript sample malpeque zinqy
|
415
|
+
# argv :name, :flavour
|
416
|
+
# command :sample do |obj|
|
417
|
+
# obj.argv.name # => malpeque
|
418
|
+
# obj.argv.flavour # => zinqy
|
419
|
+
# end
|
420
|
+
#
|
421
|
+
def argv(*args)
|
422
|
+
@@command_argv_names[@@command_index] ||= []
|
423
|
+
@@command_argv_names[@@command_index] += args.flatten
|
424
|
+
end
|
425
|
+
|
426
|
+
# The project name. This is currently only used when printing
|
427
|
+
# list of commands (see: Drydock::Command#show_commands). It may be
|
428
|
+
# used elsewhere in the future.
|
429
|
+
def project(txt=nil)
|
430
|
+
|
431
|
+
return @@project unless txt
|
432
|
+
|
433
|
+
#begin
|
434
|
+
# require txt.downcase
|
435
|
+
#rescue LoadError => ex
|
436
|
+
# Drydock.run = false # Prevent execution at_exit
|
437
|
+
# abort "Problem during require: #{ex.message}"
|
438
|
+
#end
|
439
|
+
@@project = txt
|
440
|
+
end
|
441
|
+
|
442
|
+
# Has the project been set?
|
443
|
+
def project?
|
444
|
+
(defined?(@@project) && !@@project.nil?)
|
445
|
+
end
|
446
|
+
|
447
|
+
# Define a default command. You can specify a command name that has
|
448
|
+
# been or will be defined in your script:
|
449
|
+
#
|
450
|
+
# default :task
|
451
|
+
#
|
452
|
+
# Or you can supply a block which will be used as the default command:
|
453
|
+
#
|
454
|
+
# default do |obj| # This command will be named "default"
|
455
|
+
# # ...
|
456
|
+
# end
|
457
|
+
#
|
458
|
+
# default :hullinspector do # This one will be named "hullinspector"
|
459
|
+
# # ...
|
460
|
+
# end
|
461
|
+
#
|
462
|
+
# If +with_args+ is specified, the default command will receive all unknown
|
463
|
+
# values as arguments. This is necessary to define explicitly because drydock
|
464
|
+
# parses arguments expecting a command name. If the default command accepts
|
465
|
+
# arguments and with_args is not specified, drydock will raise an unknown
|
466
|
+
# command exception for the first argument.
|
467
|
+
#
|
468
|
+
def default(cmd=nil, with_args=false, &b)
|
469
|
+
raise "Calling default requires a command name or a block" unless cmd || b
|
470
|
+
# Creates the command and returns the name or just stores given name
|
471
|
+
@@default_command = (b) ? command(cmd || :default, &b).cmd : canonize(cmd)
|
472
|
+
# IDEA: refactor out the argument parser to support different types of CLI
|
473
|
+
@@default_command_with_args = with_args ? true : false
|
474
|
+
@@default_command
|
475
|
+
end
|
476
|
+
|
477
|
+
# Is +cmd+ the default command?
|
478
|
+
def default?(cmd)
|
479
|
+
return false if @@default_command.nil?
|
480
|
+
(@@default_command == canonize(cmd))
|
481
|
+
end
|
482
|
+
|
483
|
+
#
|
484
|
+
def default_with_args?; @@default_command_with_args; end
|
485
|
+
|
486
|
+
|
487
|
+
# Define a block for processing STDIN before the command is called.
|
488
|
+
# The command block receives the return value of this block as obj.stdin:
|
489
|
+
#
|
490
|
+
# command :task do |obj|;
|
491
|
+
# obj.stdin # => ...
|
492
|
+
# end
|
493
|
+
#
|
494
|
+
# If a stdin block isn't defined, +stdin+ above will be the STDIN IO handle.
|
495
|
+
def stdin(&b)
|
496
|
+
@@stdin_block = b
|
497
|
+
end
|
498
|
+
|
499
|
+
# Define a block to be called before the command.
|
500
|
+
# This is useful for opening database connections, etc...
|
501
|
+
def before(&b)
|
502
|
+
@@before_block = b
|
503
|
+
end
|
504
|
+
|
505
|
+
# Define a block to be called after the command.
|
506
|
+
# This is useful for stopping, closing, etc... the stuff in the before block.
|
507
|
+
def after(&b)
|
508
|
+
@@after_block = b
|
509
|
+
end
|
510
|
+
|
511
|
+
# Define the default global usage banner. This is displayed
|
512
|
+
# with "script -h".
|
513
|
+
def global_usage(msg)
|
514
|
+
@@global_opts_parser.banner = "USAGE: #{msg}"
|
515
|
+
end
|
516
|
+
|
517
|
+
# Define a command-specific usage banner. This is displayed
|
518
|
+
# with "script command -h"
|
519
|
+
def usage(msg)
|
520
|
+
# The default value given by OptionParser starts with "Usage". That's how
|
521
|
+
# we know we can clear it.
|
522
|
+
get_current_option_parser.banner = "" if get_current_option_parser.banner =~ /^Usage:/
|
523
|
+
get_current_option_parser.banner << "USAGE: #{msg}" << $/
|
524
|
+
end
|
525
|
+
|
526
|
+
# Tell the Drydock parser to ignore something.
|
527
|
+
# Drydock will currently only listen to you if you tell it to "ignore :options",
|
528
|
+
# otherwise it will ignore you!
|
529
|
+
#
|
530
|
+
# +what+ the thing to ignore. When it equals :options Drydock will not parse
|
531
|
+
# the command-specific arguments. It will pass the arguments directly to the
|
532
|
+
# Command object. This is useful when you want to parse the arguments in some a way
|
533
|
+
# that's too crazy, dangerous for Drydock to handle automatically.
|
534
|
+
def ignore(what=:nothing)
|
535
|
+
@@command_opts_parser[@@command_index] = :ignore if what == :options || what == :all
|
536
|
+
end
|
537
|
+
|
538
|
+
# Define a global option. See +option+ for more info.
|
539
|
+
def global_option(*args, &b)
|
540
|
+
args.unshift(@@global_opts_parser)
|
541
|
+
@@global_option_names << option_parser(args, &b)
|
542
|
+
end
|
543
|
+
alias :global :global_option
|
544
|
+
|
545
|
+
# Define a command-specific option.
|
546
|
+
#
|
547
|
+
# +args+ is passed directly to OptionParser.on so it can contain anything
|
548
|
+
# that's valid to that method. If a class is included, it will tell
|
549
|
+
# OptionParser to expect a value otherwise it assumes a boolean value.
|
550
|
+
# Some examples:
|
551
|
+
#
|
552
|
+
# option :h, :help, "Displays this message"
|
553
|
+
# option '-l x,y,z', '--lang=x,y,z', Array, "Requested languages"
|
554
|
+
#
|
555
|
+
# You can also supply a block to fiddle with the values. The final
|
556
|
+
# value becomes the option's value:
|
557
|
+
#
|
558
|
+
# option :m, :max, Integer, "Maximum threshold" do |v|
|
559
|
+
# v = 100 if v > 100
|
560
|
+
# v
|
561
|
+
# end
|
562
|
+
#
|
563
|
+
# All calls to +option+ must come before the command they're associated
|
564
|
+
# to. Example:
|
565
|
+
#
|
566
|
+
# option :t, :tasty, "A boolean switch"
|
567
|
+
# option :reason, String, "Requires a parameter"
|
568
|
+
# command :task do |obj|;
|
569
|
+
# obj.options.tasty # => true
|
570
|
+
# obj.options.reason # => I made the sandwich!
|
571
|
+
# end
|
572
|
+
#
|
573
|
+
# When calling your script with a specific command-line option, the value
|
574
|
+
# is available via obj.longname inside the command block.
|
575
|
+
#
|
576
|
+
def option(*args, &b)
|
577
|
+
args.unshift(get_current_option_parser)
|
578
|
+
current_command_option_names << option_parser(args, &b)
|
579
|
+
end
|
580
|
+
|
581
|
+
# Define a command-specific action.
|
582
|
+
#
|
583
|
+
# This is functionally very similar to option, but with an exciting and buoyant twist:
|
584
|
+
# Drydock keeps track of actions for each command (in addition to treating it like an option).
|
585
|
+
# When an action is specified on the command line Drydock looks for command_action or
|
586
|
+
# action_command methods in the command class.
|
587
|
+
#
|
588
|
+
# action :E, :eat, "Eat something"
|
589
|
+
# command :oysters => Fresh::Oysters
|
590
|
+
#
|
591
|
+
# # Drydock will look for Fresh::Oysters#eat_oysters and Fresh::Oysters#oysters_eat.
|
592
|
+
#
|
593
|
+
def action(*args, &b)
|
594
|
+
ret = option(*args, &b) # returns an array of all the current option names
|
595
|
+
current_command_action << ret.last # the most recent is last
|
596
|
+
end
|
597
|
+
|
598
|
+
# Define a command.
|
599
|
+
#
|
600
|
+
# command :task do
|
601
|
+
# ...
|
602
|
+
# end
|
603
|
+
#
|
604
|
+
# A custom command class can be specified using Hash syntax. The class
|
605
|
+
# must inherit from Drydock::Command (class CustomeClass < Drydock::Command)
|
606
|
+
#
|
607
|
+
# command :task => CustomCommand do
|
608
|
+
# ...
|
609
|
+
# end
|
610
|
+
#
|
611
|
+
def command(*cmds, &b)
|
612
|
+
cmd = cmds.shift # Should we accept aliases here?
|
613
|
+
|
614
|
+
if cmd.is_a? Hash
|
615
|
+
klass = cmd.values.first
|
616
|
+
names = cmd.keys.first
|
617
|
+
if names.is_a? Array
|
618
|
+
cmd, cmds = names.shift, [names].flatten.compact
|
619
|
+
else
|
620
|
+
cmd = names
|
621
|
+
end
|
622
|
+
raise "#{klass} is not a subclass of Drydock::Command" unless klass.ancestors.member?(Drydock::Command)
|
623
|
+
c = klass.new(cmd, &b) # A custom class was specified
|
624
|
+
else
|
625
|
+
c = Drydock::Command.new(cmd, &b)
|
626
|
+
end
|
627
|
+
|
628
|
+
@@command_descriptions[@@command_index] ||= ""
|
629
|
+
@@command_actions[@@command_index] ||= []
|
630
|
+
@@command_argv_names[@@command_index] ||= []
|
631
|
+
|
632
|
+
c.desc = @@command_descriptions[@@command_index]
|
633
|
+
c.actions = @@command_actions[@@command_index]
|
634
|
+
c.argv.fields = @@command_argv_names[@@command_index]
|
635
|
+
|
636
|
+
# Default Usage Banner.
|
637
|
+
# Without this, there's no help displayed for the command.
|
638
|
+
option_parser = get_option_parser(@@command_index)
|
639
|
+
if option_parser.is_a?(OptionParser) && option_parser.banner !~ /^USAGE/
|
640
|
+
usage "#{c.executable} #{c.cmd}"
|
641
|
+
end
|
642
|
+
|
643
|
+
@@commands[c.cmd] = c
|
644
|
+
@@command_index_map[c.cmd] = @@command_index
|
645
|
+
@@command_index += 1 # This will point to the next command
|
646
|
+
|
647
|
+
# Created aliases to the command using any additional command names
|
648
|
+
# i.e. command :something, :sumpin => Something
|
649
|
+
cmds.each { |aliaz| command_alias(cmd, aliaz); } unless cmds.empty?
|
650
|
+
|
651
|
+
c # Return the Command object
|
652
|
+
end
|
653
|
+
|
654
|
+
# Used to create an alias to a defined command.
|
655
|
+
# Here's an example:
|
656
|
+
#
|
657
|
+
# command :task do; ...; end
|
658
|
+
# alias_command :pointer, :task
|
659
|
+
#
|
660
|
+
# Either name can be used on the command-line:
|
661
|
+
#
|
662
|
+
# $ yourscript task [options]
|
663
|
+
# $ yourscript pointer [options]
|
664
|
+
#
|
665
|
+
# Inside of the command definition, you have access to the
|
666
|
+
# command name that was used via obj.alias.
|
667
|
+
def alias_command(aliaz, cmd)
|
668
|
+
return unless commands.has_key? cmd
|
669
|
+
commands[canonize(aliaz)] = commands[cmd]
|
670
|
+
end
|
671
|
+
|
672
|
+
# Identical to +alias_command+ with reversed arguments.
|
673
|
+
# For whatever reason I forget the order so Drydock supports both.
|
674
|
+
# Tip: the argument order matches the method name.
|
675
|
+
def command_alias(cmd, aliaz)
|
676
|
+
return unless commands.has_key? cmd
|
677
|
+
commands[canonize(aliaz)] = commands[cmd]
|
678
|
+
end
|
679
|
+
|
680
|
+
# A hash of the currently defined Drydock::Command objects
|
681
|
+
def commands
|
682
|
+
@@commands
|
683
|
+
end
|
684
|
+
|
685
|
+
# An array of the currently defined commands names
|
686
|
+
def command_names
|
687
|
+
@@commands.keys.collect { |cmd| decanonize(cmd); }
|
688
|
+
end
|
689
|
+
|
690
|
+
# The trawler catches any and all unknown commands that pass through
|
691
|
+
# Drydock. It's like the captain of aliases.
|
692
|
+
# +cmd+ is the name of the command to direct unknowns to.
|
693
|
+
#
|
694
|
+
# trawler :command_name
|
695
|
+
#
|
696
|
+
def trawler(cmd)
|
697
|
+
@@trawler = cmd
|
698
|
+
end
|
699
|
+
|
700
|
+
# Has the trawler been set?
|
701
|
+
def trawler?
|
702
|
+
!@@trawler.nil? && !@@trawler.to_s.empty?
|
703
|
+
end
|
704
|
+
|
705
|
+
# Provide a description for a command
|
706
|
+
def about(txt)
|
707
|
+
@@command_descriptions += [txt]
|
708
|
+
return if get_current_option_parser.is_a?(Symbol)
|
709
|
+
get_current_option_parser.on "ABOUT: #{txt}"
|
710
|
+
end
|
711
|
+
# Deprecated. Use about.
|
712
|
+
def desc(txt)
|
713
|
+
STDERR.puts "'desc' is deprecated. Please use 'about' instead."
|
714
|
+
about(txt)
|
715
|
+
end
|
716
|
+
|
717
|
+
# Returns true if automatic execution is enabled.
|
718
|
+
def run?
|
719
|
+
@@run && has_run? == false
|
720
|
+
end
|
721
|
+
|
722
|
+
# Disable automatic execution (enabled by default)
|
723
|
+
#
|
724
|
+
# Drydock.run = false
|
725
|
+
def run=(v)
|
726
|
+
@@run = (v.is_a?(TrueClass)) ? true : false
|
727
|
+
end
|
728
|
+
|
729
|
+
# Return true if a command has been executed.
|
730
|
+
def has_run?
|
731
|
+
@@has_run
|
732
|
+
end
|
733
|
+
|
734
|
+
# Execute the given command.
|
735
|
+
# By default, Drydock automatically executes itself and provides handlers for known errors.
|
736
|
+
# You can override this functionality by calling +Drydock.run!+ yourself. Drydock
|
737
|
+
# will only call +run!+ once.
|
738
|
+
def run!(argv=[], stdin=STDIN)
|
739
|
+
return if has_run?
|
740
|
+
@@has_run = true
|
741
|
+
raise NoCommandsDefined.new if commands.empty?
|
742
|
+
|
743
|
+
global_options, cmd_name, command_options, argv = process_arguments(argv)
|
744
|
+
stdin = (defined? @@stdin_block) ? @@stdin_block.call(stdin, []) : stdin
|
745
|
+
|
746
|
+
command_obj = get_command(cmd_name)
|
747
|
+
command_obj.prepare(cmd_name, argv, stdin, global_options, command_options)
|
748
|
+
|
749
|
+
# Execute before block
|
750
|
+
@@before_block.call(command_obj) if defined? @@before_block
|
751
|
+
|
752
|
+
# Execute the requested command. We'll capture STDERR or STDOUT if desired.
|
753
|
+
@@captured = capture? ? capture_io(@@capture) { command_obj.call } : command_obj.call
|
754
|
+
|
755
|
+
# Execute after block
|
756
|
+
@@after_block.call(command_obj) if defined? @@after_block
|
757
|
+
|
758
|
+
rescue OptionParser::InvalidOption => ex
|
759
|
+
raise Drydock::InvalidArgument.new(ex.args)
|
760
|
+
rescue OptionParser::MissingArgument => ex
|
761
|
+
raise Drydock::MissingArgument.new(ex.args)
|
762
|
+
end
|
763
|
+
|
764
|
+
def capture(io)
|
765
|
+
@@capture = io
|
766
|
+
end
|
767
|
+
|
768
|
+
def captured
|
769
|
+
@@captured
|
770
|
+
end
|
771
|
+
|
772
|
+
def capture?
|
773
|
+
!@@capture.nil?
|
774
|
+
end
|
775
|
+
|
776
|
+
# Returns true if a command with the name +cmd+ has been defined.
|
777
|
+
def command?(cmd)
|
778
|
+
name = canonize(cmd)
|
779
|
+
@@commands.has_key? name
|
780
|
+
end
|
781
|
+
|
782
|
+
# Canonizes a string (+cmd+) to the symbol for command names
|
783
|
+
# '-' is replaced with '_'
|
784
|
+
def canonize(cmd)
|
785
|
+
return unless cmd
|
786
|
+
return cmd if cmd.kind_of?(Symbol)
|
787
|
+
cmd.to_s.tr('-', '_').to_sym
|
788
|
+
end
|
789
|
+
|
790
|
+
# Returns a string version of +cmd+, decanonized.
|
791
|
+
# Lowercase, '_' is replaced with '-'
|
792
|
+
def decanonize(cmd)
|
793
|
+
return unless cmd
|
794
|
+
cmd.to_s.tr('_', '-')
|
795
|
+
end
|
796
|
+
|
797
|
+
# Capture STDOUT or STDERR to prevent it from being printed.
|
798
|
+
#
|
799
|
+
# capture(:stdout) do
|
800
|
+
# ...
|
801
|
+
# end
|
802
|
+
#
|
803
|
+
def capture_io(stream, &block)
|
804
|
+
raise "We can only capture STDOUT or STDERR" unless stream == :stdout || stream == :stderr
|
805
|
+
begin
|
806
|
+
eval "$#{stream} = StringIO.new"
|
807
|
+
block.call
|
808
|
+
eval("$#{stream}").rewind # Otherwise we'll get nil
|
809
|
+
result = eval("$#{stream}").read
|
810
|
+
ensure
|
811
|
+
eval "$#{stream} = #{stream.to_s.upcase}" # Put it back!
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
private
|
816
|
+
|
817
|
+
# Returns the Drydock::Command object with the name +cmd+
|
818
|
+
def get_command(cmd)
|
819
|
+
return unless command?(cmd)
|
820
|
+
@@commands[canonize(cmd)]
|
821
|
+
end
|
822
|
+
|
823
|
+
# Processes calls to option and global_option. Symbols are converted into
|
824
|
+
# OptionParser style strings (:h and :help become '-h' and '--help').
|
825
|
+
def option_parser(args=[], &b)
|
826
|
+
return if args.empty?
|
827
|
+
opts_parser = args.shift
|
828
|
+
|
829
|
+
arg_name = ''
|
830
|
+
symbol_switches = []
|
831
|
+
args.each_with_index do |arg, index|
|
832
|
+
if arg.is_a? Symbol
|
833
|
+
arg_name = arg.to_s if arg.to_s.size > arg_name.size
|
834
|
+
args[index] = (arg.to_s.length == 1) ? "-#{arg.to_s}" : "--#{arg.to_s}"
|
835
|
+
symbol_switches << args[index]
|
836
|
+
elsif arg.kind_of?(Class)
|
837
|
+
symbol_switches.each do |arg|
|
838
|
+
arg << "=S"
|
839
|
+
end
|
840
|
+
end
|
841
|
+
end
|
842
|
+
|
843
|
+
if args.size == 1
|
844
|
+
opts_parser.on(args.shift)
|
845
|
+
else
|
846
|
+
opts_parser.on(*args) do |v|
|
847
|
+
block_args = [v, opts_parser]
|
848
|
+
result = (b.nil?) ? v : b.call(*block_args[0..(b.arity-1)])
|
849
|
+
end
|
850
|
+
end
|
851
|
+
|
852
|
+
arg_name
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
# Split the +argv+ array into global args and command args and
|
857
|
+
# find the command name.
|
858
|
+
# i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
|
859
|
+
# returns [global_options, cmd, command_options, argv]
|
860
|
+
def process_arguments(argv=[])
|
861
|
+
global_options = command_options = {}
|
862
|
+
cmd = nil
|
863
|
+
|
864
|
+
argv_copy = argv.clone # See: @@default_command_with_args below
|
865
|
+
|
866
|
+
global_options = @@global_opts_parser.getopts(argv)
|
867
|
+
cmd_name = (argv.empty?) ? @@default_command : argv.shift
|
868
|
+
|
869
|
+
unless command?(cmd_name)
|
870
|
+
# If requested, send all unknown arguments to the default command
|
871
|
+
if @@default_command_with_args
|
872
|
+
cmd_name = @@default_command
|
873
|
+
argv = argv_copy
|
874
|
+
else
|
875
|
+
raise UnknownCommand.new(cmd_name) unless trawler?
|
876
|
+
raise UnknownCommand.new(@@trawler) unless command?(@@trawler)
|
877
|
+
command_alias(@@trawler, cmd_name)
|
878
|
+
end
|
879
|
+
end
|
880
|
+
|
881
|
+
cmd = get_command(cmd_name)
|
882
|
+
|
883
|
+
command_parser = @@command_opts_parser[get_command_index(cmd.cmd)]
|
884
|
+
command_options = {}
|
885
|
+
|
886
|
+
# We only need to parse the options out of the arguments when
|
887
|
+
# there are args available, there is a valid parser, and
|
888
|
+
# we weren't requested to ignore the options.
|
889
|
+
if !argv.empty? && command_parser && command_parser != :ignore
|
890
|
+
command_options = command_parser.getopts(argv)
|
891
|
+
end
|
892
|
+
|
893
|
+
[global_options, cmd_name, command_options, argv]
|
894
|
+
end
|
895
|
+
|
896
|
+
|
897
|
+
# Grab the current list of command-specific option names. This is a list of the
|
898
|
+
# long names.
|
899
|
+
def current_command_option_names
|
900
|
+
(@@command_option_names[@@command_index] ||= [])
|
901
|
+
end
|
902
|
+
|
903
|
+
def current_command_action
|
904
|
+
(@@command_actions[@@command_index] ||= [])
|
905
|
+
end
|
906
|
+
|
907
|
+
def get_command_index(cmd)
|
908
|
+
@@command_index_map[canonize(cmd)] || -1
|
909
|
+
end
|
910
|
+
|
911
|
+
# Grab the options parser for the current command or create it if it doesn't exist.
|
912
|
+
# Returns an instance of OptionParser.
|
913
|
+
def get_current_option_parser
|
914
|
+
(@@command_opts_parser[@@command_index] ||= OptionParser.new)
|
915
|
+
end
|
916
|
+
|
917
|
+
# Grabs the options parser for the given command.
|
918
|
+
# +arg+ can be an index or command name.
|
919
|
+
# Returns an instance of OptionParser.
|
920
|
+
def get_option_parser(arg)
|
921
|
+
index = arg.is_a?(String) ? get_command_index(arg) : arg
|
922
|
+
(@@command_opts_parser[index] ||= OptionParser.new)
|
923
|
+
end
|
924
|
+
|
925
|
+
#
|
926
|
+
# These are the "reel" defaults
|
927
|
+
#
|
928
|
+
@@global_opts_parser.banner = " Try: #{$0} show-commands"
|
929
|
+
@@global_opts_parser.on "Usage: #{$0} [global options] COMMAND [command options] #{$/}"
|
930
|
+
@@command_descriptions = ["Display available commands with descriptions"]
|
931
|
+
@@default_command = Drydock.command(:show_commands).cmd
|
932
|
+
|
933
|
+
end
|
934
|
+
|
935
|
+
__END__
|
936
|
+
|
937
|
+
at_exit {
|
938
|
+
begin
|
939
|
+
if $@
|
940
|
+
puts $@ if Drydock.debug?
|
941
|
+
exit 1
|
942
|
+
end
|
943
|
+
Drydock.run!(ARGV, STDIN) if Drydock.run? && !Drydock.has_run?
|
944
|
+
rescue Drydock::ArgError, Drydock::OptError=> ex
|
945
|
+
STDERR.puts ex.message
|
946
|
+
STDERR.puts ex.usage
|
947
|
+
rescue Drydock::UnknownCommand => ex
|
948
|
+
STDERR.puts ex.message
|
949
|
+
STDERR.puts ex.backtrace if Drydock.debug?
|
950
|
+
rescue => ex
|
951
|
+
STDERR.puts "ERROR (#{ex.class.to_s}): #{ex.message}"
|
952
|
+
STDERR.puts ex.backtrace if Drydock.debug?
|
953
|
+
rescue Interrupt
|
954
|
+
puts "#{$/}Exiting... "
|
955
|
+
exit 1
|
956
|
+
rescue SystemExit
|
957
|
+
# Don't balk
|
958
|
+
end
|
959
|
+
}
|
960
|
+
|
961
|
+
|