bone 0.2.1
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/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
|
+
|