drydock 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
2
+ <html lang='en' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'>
3
+ <head>
4
+ <title>Drydock, A seaworthy DSL for command-line apps.</title>
5
+ <meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
6
+ </head>
7
+ <frameset border='1' bordercolor='gray' cols='20%, *' frameborder='1'>
8
+ <frameset rows='15%, 35%, 50%'>
9
+ <frame name='Files' src='fr_file_index.html' title='Files'></frame>
10
+ <frame name='Classes' src='fr_class_index.html'></frame>
11
+ <frame name='Methods' src='fr_method_index.html'></frame>
12
+ </frameset>
13
+ <frame name='docwin' src='files/README_rdoc.html'></frame>
14
+ </frameset>
15
+ </html>
@@ -0,0 +1,319 @@
1
+ html, body {
2
+ height: 100%; }
3
+
4
+ body {
5
+ font-family: Lucida Grande , Verdana, Arial, Helvetica, sans-serif;
6
+ font-size: 90%;
7
+ margin: 0;
8
+ padding: 0;
9
+ background: white;
10
+ color: black; }
11
+
12
+ #wrapper {
13
+ min-height: 100%;
14
+ height: auto !important;
15
+ height: 100%;
16
+ margin: 0 auto -43px; }
17
+
18
+ #footer-push {
19
+ height: 43px; }
20
+
21
+ div.header, #footer {
22
+ background: #eee; }
23
+
24
+ #footer {
25
+ border-top: 1px solid silver;
26
+ margin-top: 12px;
27
+ padding: 0 2em;
28
+ line-height: 30px;
29
+ text-align: center;
30
+ font-variant: small-caps;
31
+ font-size: 95%; }
32
+
33
+ .clearing:after {
34
+ content: ".";
35
+ visibility: hidden;
36
+ height: 0;
37
+ display: block;
38
+ clear: both; }
39
+ * html .clearing {
40
+ height: 1px; }
41
+ .clearing *:first-child + html {
42
+ overflow: hidden; }
43
+
44
+ h1, h2, h3, h4, h5, h6 {
45
+ margin: 0;
46
+ font-weight: normal; }
47
+
48
+ a {
49
+ color: #0b3e71; }
50
+ a:hover {
51
+ background: #336699;
52
+ text-decoration: none;
53
+ color: #eef; }
54
+
55
+ #diagram img {
56
+ border: 0; }
57
+
58
+ #description a, .method .description a, .header a {
59
+ color: #336699; }
60
+ #description a:hover, .method .description a:hover, .header a:hover {
61
+ color: #eee; }
62
+ #description h1 a, #description h2 a, #description h3 a, #description h4 a, #description h5 a, #description h6 a, .method .description h1 a, .method .description h2 a, .method .description h3 a, .method .description h4 a, .method .description h5 a, .method .description h6 a, .header h1 a, .header h2 a, .header h3 a, .header h4 a, .header h5 a, .header h6 a {
63
+ color: #0b3e71; }
64
+
65
+ ol {
66
+ margin: 0;
67
+ padding: 0;
68
+ list-style: none; }
69
+ ol li {
70
+ margin-left: 0;
71
+ white-space: nowrap; }
72
+ ol li.other {
73
+ display: none; }
74
+
75
+ ol.expanded li.other {
76
+ display: list-item; }
77
+
78
+ table {
79
+ margin-bottom: 1em;
80
+ font-size: 1em;
81
+ border-collapse: collapse; }
82
+ table td, table th {
83
+ padding: .4em .8em; }
84
+ table thead {
85
+ background-color: #e8e8e8; }
86
+ table thead th {
87
+ font-variant: small-caps;
88
+ color: #666666; }
89
+ table tr {
90
+ border-bottom: 1px solid silver; }
91
+
92
+ #index a.show, div.header a.show {
93
+ text-decoration: underline;
94
+ font-style: italic;
95
+ color: #666666; }
96
+ #index a.show:after, div.header a.show:after {
97
+ content: " ..."; }
98
+ #index a.show:hover, div.header a.show:hover {
99
+ color: black;
100
+ background: #ffe; }
101
+
102
+ #index {
103
+ font: 85%/1.2 Arial, Helvetica, sans-serif; }
104
+ #index a {
105
+ text-decoration: none; }
106
+ #index h1 {
107
+ padding: .2em .5em .1em;
108
+ background: #ccc;
109
+ font: small-caps 1.2em Georgia, serif;
110
+ color: #333;
111
+ border-bottom: 1px solid gray; }
112
+ #index form {
113
+ margin: 0;
114
+ padding: 0; }
115
+ #index form input {
116
+ margin: .4em;
117
+ margin-bottom: 0;
118
+ width: 90%; }
119
+ #index form #search.untouched {
120
+ color: #777777; }
121
+ #index ol {
122
+ padding: .4em .5em; }
123
+ #index ol li {
124
+ white-space: nowrap; }
125
+ #index #index-entries li a {
126
+ padding: 1px 2px; }
127
+ #index #index-entries.classes {
128
+ font-size: 1.1em; }
129
+ #index #index-entries.classes ol {
130
+ padding: 0; }
131
+ #index #index-entries.classes span.nodoc {
132
+ display: none; }
133
+ #index #index-entries.classes span.nodoc, #index #index-entries.classes a {
134
+ font-weight: bold; }
135
+ #index #index-entries.classes .parent {
136
+ font-weight: normal; }
137
+ #index #index-entries.methods li, #index #search-results.methods li {
138
+ margin-bottom: 0.2em; }
139
+ #index #index-entries.methods li a .method_name, #index #search-results.methods li a .method_name {
140
+ margin-right: 0.25em; }
141
+ #index #index-entries.methods li a .module_name, #index #search-results.methods li a .module_name {
142
+ color: #666666; }
143
+ #index #index-entries.methods li a:hover .module_name, #index #search-results.methods li a:hover .module_name {
144
+ color: #ddd; }
145
+
146
+ div.header {
147
+ font-size: 80%;
148
+ padding: .5em 2%;
149
+ font-family: Arial, Helvetica, sans-serif;
150
+ border-bottom: 1px solid silver; }
151
+ div.header .name {
152
+ font-size: 1.6em;
153
+ font-family: Georgia, serif; }
154
+ div.header .name .type {
155
+ color: #666666;
156
+ font-size: 80%;
157
+ font-variant: small-caps; }
158
+ div.header h1.name {
159
+ font-size: 2.2em; }
160
+ div.header .paths, div.header .last-update, div.header .parent {
161
+ color: #666666; }
162
+ div.header .last-update .datetime {
163
+ color: #484848; }
164
+ div.header .parent {
165
+ margin-top: .3em; }
166
+ div.header .parent strong {
167
+ font-weight: normal;
168
+ color: #484848; }
169
+
170
+ #content {
171
+ padding: 12px 2%; }
172
+ div.class #content {
173
+ position: relative;
174
+ width: 72%; }
175
+ #content pre, #content .method .synopsis {
176
+ font: 14px Monaco, DejaVu Sans Mono , Bitstream Vera Sans Mono , Courier New , monospace; }
177
+ #content pre {
178
+ color: black;
179
+ background: #eee;
180
+ border: 1px solid silver;
181
+ padding: .5em .8em;
182
+ overflow: auto; }
183
+ #content p code, #content p tt, #content li code, #content li tt, #content dl code, #content dl tt {
184
+ font: 14px Monaco, DejaVu Sans Mono , Bitstream Vera Sans Mono , Courier New , monospace;
185
+ background: #ffffe3;
186
+ padding: 2px 3px;
187
+ line-height: 1.4; }
188
+ #content h1 code, #content h1 tt, #content h2 code, #content h2 tt, #content h3 code, #content h3 tt, #content h4 code, #content h4 tt, #content h5 code, #content h5 tt, #content h6 code, #content h6 tt {
189
+ font-size: 1.1em; }
190
+ #content #text {
191
+ position: relative; }
192
+ #content #description p {
193
+ margin-top: .5em; }
194
+ #content #description h1, #content #description h2, #content #description h3, #content #description h4, #content #description h5, #content #description h6 {
195
+ font-family: Georgia, serif; }
196
+ #content #description h1 {
197
+ font-size: 2.2em;
198
+ margin-bottom: .2em;
199
+ border-bottom: 3px double #d8d8d8;
200
+ padding-bottom: .1em; }
201
+ #content #description h2 {
202
+ font-size: 1.8em;
203
+ color: #0e3062;
204
+ margin: .8em 0 .3em 0; }
205
+ #content #description h3 {
206
+ font-size: 1.6em;
207
+ margin: .8em 0 .3em 0;
208
+ color: #666666; }
209
+ #content #description h4 {
210
+ font-size: 1.4em;
211
+ margin: .8em 0 .3em 0; }
212
+ #content #description h5 {
213
+ font-size: 1.2em;
214
+ margin: .8em 0 .3em 0;
215
+ color: #0e3062; }
216
+ #content #description h6 {
217
+ font-size: 1.0em;
218
+ margin: .8em 0 .3em 0;
219
+ color: #666666; }
220
+ #content #description ul, #content #description ol, #content .method .description ul, #content .method .description ol {
221
+ margin: .8em 0;
222
+ padding-left: 1.5em; }
223
+ #content #description ol, #content .method .description ol {
224
+ list-style: decimal; }
225
+ #content #description ol li, #content .method .description ol li {
226
+ white-space: normal; }
227
+
228
+ #method-list {
229
+ position: absolute;
230
+ top: 0px;
231
+ right: -33%;
232
+ width: 28%;
233
+ background: #eee;
234
+ border: 1px solid silver;
235
+ padding: .4em 1%;
236
+ overflow: hidden; }
237
+ #method-list h2 {
238
+ font-size: 1.3em; }
239
+ #method-list h3 {
240
+ font-variant: small-caps;
241
+ text-transform: capitalize;
242
+ font-family: Georgia, serif;
243
+ color: #666;
244
+ font-size: 1.1em; }
245
+ #method-list ol {
246
+ padding: 0 0 .5em .5em; }
247
+
248
+ #context {
249
+ border-top: 1px dashed silver;
250
+ margin-top: 1em;
251
+ margin-bottom: 1em; }
252
+
253
+ #context h2, #section h2 {
254
+ font: small-caps 1.2em Georgia, serif;
255
+ color: #444;
256
+ margin: 1em 0 .2em 0; }
257
+
258
+ #methods .method {
259
+ border: 1px solid silver;
260
+ margin-top: .5em;
261
+ background: #eee; }
262
+ #methods .method .synopsis {
263
+ color: black;
264
+ background: silver;
265
+ padding: .2em 1em; }
266
+ #methods .method .synopsis .name {
267
+ font-weight: bold; }
268
+ #methods .method .synopsis a {
269
+ text-decoration: none; }
270
+ #methods .method .description {
271
+ padding: 0 1em; }
272
+ #methods .method .description pre {
273
+ background: #f8f8f8; }
274
+ #methods .method .source {
275
+ margin: .5em 0; }
276
+ #methods .method .source-toggle {
277
+ font-size: 85%;
278
+ margin-left: 1em; }
279
+ #methods .public-class {
280
+ background: #ffffe4; }
281
+ #methods .public-instance .synopsis {
282
+ color: #eee;
283
+ background: #336699; }
284
+
285
+ #content .method .source pre {
286
+ background: #262626;
287
+ color: #ffdead;
288
+ margin: 1em;
289
+ padding: 0.5em;
290
+ border: 1px dashed #999;
291
+ overflow: auto; }
292
+ #content .method .source pre .ruby-constant {
293
+ color: #7fffd4;
294
+ background: transparent; }
295
+ #content .method .source pre .ruby-keyword {
296
+ color: #00ffff;
297
+ background: transparent; }
298
+ #content .method .source pre .ruby-ivar {
299
+ color: #eedd82;
300
+ background: transparent; }
301
+ #content .method .source pre .ruby-operator {
302
+ color: #00ffee;
303
+ background: transparent; }
304
+ #content .method .source pre .ruby-identifier {
305
+ color: #ffdead;
306
+ background: transparent; }
307
+ #content .method .source pre .ruby-node {
308
+ color: #ffa07a;
309
+ background: transparent; }
310
+ #content .method .source pre .ruby-comment {
311
+ color: #b22222;
312
+ font-weight: bold;
313
+ background: transparent; }
314
+ #content .method .source pre .ruby-regexp {
315
+ color: #ffa07a;
316
+ background: transparent; }
317
+ #content .method .source pre .ruby-value {
318
+ color: #7fffd4;
319
+ background: transparent; }
@@ -0,0 +1,53 @@
1
+ @spec = Gem::Specification.new do |s|
2
+ s.name = %q{drydock}
3
+ s.version = "0.3.0"
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.date = %q{2008-08-17}
9
+ s.description = %q{A seaworthy DSL for writing command line apps inspired by Blake Mizerany's Frylock}
10
+ s.email = %q{delano@solutious.com}
11
+ s.files = %w(
12
+ CHANGES.txt
13
+ LICENSE.txt
14
+ README.rdoc
15
+ Rakefile
16
+ bin/example
17
+ drydock.gemspec
18
+ lib/drydock.rb
19
+ test/command_test.rb
20
+ doc
21
+ doc/classes
22
+ doc/classes/Drydock
23
+ doc/classes/Drydock/Command.html
24
+ doc/classes/Drydock/InvalidArgument.html
25
+ doc/classes/Drydock/MissingArgument.html
26
+ doc/classes/Drydock/NoCommandsDefined.html
27
+ doc/classes/Drydock/UnknownCommand.html
28
+ doc/classes/Drydock.html
29
+ doc/created.rid
30
+ doc/files
31
+ doc/files/bin
32
+ doc/files/bin/example.html
33
+ doc/files/CHANGES_txt.html
34
+ doc/files/lib
35
+ doc/files/lib/drydock_rb.html
36
+ doc/files/LICENSE_txt.html
37
+ doc/files/README_rdoc.html
38
+ doc/fr_class_index.html
39
+ doc/fr_file_index.html
40
+ doc/fr_method_index.html
41
+ doc/index.html
42
+ doc/rdoc-style.css
43
+ )
44
+ s.has_rdoc = true
45
+ s.homepage = %q{http://github.com/delano/drydock}
46
+ s.extra_rdoc_files = %w[README.rdoc LICENSE.txt CHANGES.txt]
47
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Drydock: a DSL for command-line apps", "--main", "README.rdoc"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.1.1}
50
+ s.summary = %q{A seaworthy DSL for writing command line apps}
51
+
52
+ s.rubyforge_project = "drydock"
53
+ end
@@ -0,0 +1,482 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ #
5
+ #
6
+ module Drydock
7
+ # The base class for all command objects. There is an instance of this class
8
+ # for every command defined. Global and command-specific options are added
9
+ # as attributes to this class dynamically.
10
+ #
11
+ # i.e. "example -v date -f yaml"
12
+ #
13
+ # global_option :v, :verbose, "I want mooooore!"
14
+ # option :f, :format, String, "Long date format"
15
+ # command :date do |obj|
16
+ # puts obj.verbose #=> true
17
+ # puts obj.format #=> "yaml"
18
+ # end
19
+ #
20
+ # You can inherit from this class to create your own: EatFood < Drydock::Command.
21
+ # And then specific your class in the command definition:
22
+ #
23
+ # command :eat => EatFood do |obj|; ...; end
24
+ #
25
+ class Command
26
+ attr_reader :cmd, :alias
27
+ # +cmd+ is the short name of this command.
28
+ # +b+ is the block associated to this command.
29
+ def initialize(cmd, &b)
30
+ @cmd = (cmd.kind_of?(Symbol)) ? cmd : cmd.to_sym
31
+ @b = b
32
+ end
33
+
34
+ # Execute the block.
35
+ #
36
+ # +cmd_str+ is the short name used to evoke this command. It will equal @cmd
37
+ # unless an alias was used used to evoke this command.
38
+ # +argv+ an array of unnamed arguments. If ignore :options was declared this
39
+ # will contain the arguments exactly as they were defined on the command-line.
40
+ # +stdin+ contains the output of stdin do; ...; end otherwise it's a STDIN IO handle.
41
+ # +global_options+ a hash of the global options specified on the command-line
42
+ # +options+ a hash of the command-specific options specific on the command-line.
43
+ def call(cmd_str=nil, argv=[], stdin=[], global_options={}, options={})
44
+ @alias = cmd_str.nil? ? @cmd : cmd_str
45
+ global_options.merge(options).each_pair do |n,v|
46
+ self.send("#{n}=", v)
47
+ end
48
+ block_args = [self, argv, stdin] # TODO: review order
49
+ @b.call(*block_args[0..(@b.arity-1)]) # send only as many args as defined
50
+ end
51
+
52
+ # The name of the command
53
+ def to_s
54
+ @cmd.to_s
55
+ end
56
+ end
57
+ end
58
+
59
+ module Drydock
60
+ class UnknownCommand < RuntimeError
61
+ attr_reader :name
62
+ def initialize(name)
63
+ @name = name || :unknown
64
+ end
65
+ def message
66
+ "Unknown command: #{@name}"
67
+ end
68
+ end
69
+ class NoCommandsDefined < RuntimeError
70
+ def message
71
+ "No commands defined"
72
+ end
73
+ end
74
+ class InvalidArgument < RuntimeError
75
+ attr_accessor :args
76
+ def initialize(args)
77
+ @args = args || []
78
+ end
79
+ def message
80
+ "Unknown option: #{@args.join(", ")}"
81
+ end
82
+ end
83
+ class MissingArgument < InvalidArgument
84
+ def message
85
+ "Option requires a value: #{@args.join(", ")}"
86
+ end
87
+ end
88
+ end
89
+
90
+ # Drydock is a DSL for command-line apps.
91
+ # See bin/example for usage examples.
92
+ module Drydock
93
+ extend self
94
+
95
+ VERSION = 0.3
96
+
97
+ private
98
+ # Stolen from Sinatra!
99
+ def delegate(*args)
100
+ args.each do |m|
101
+ eval(<<-end_eval, binding, "(__Drydock__)", __LINE__)
102
+ def #{m}(*args, &b)
103
+ Drydock.#{m}(*args, &b)
104
+ end
105
+ end_eval
106
+ end
107
+ end
108
+
109
+ delegate :before, :after, :alias_command, :commands
110
+ delegate :global_option, :global_usage, :usage, :command
111
+ delegate :debug, :option, :stdin, :default, :ignore, :command_alias
112
+
113
+ @@debug = false
114
+ @@has_run = false
115
+ @@run = true
116
+
117
+ public
118
+ # Enable or disable debug output.
119
+ #
120
+ # debug :on
121
+ # debug :off
122
+ #
123
+ # Calling without :on or :off will toggle the value.
124
+ #
125
+ def debug(toggle=false)
126
+ if toggle.is_a? Symbol
127
+ @@debug = true if toggle == :on
128
+ @@debug = false if toggle == :off
129
+ else
130
+ @@debug = (!@@debug)
131
+ end
132
+ end
133
+ # Returns true if debug output is enabled.
134
+ def debug?
135
+ @@debug
136
+ end
137
+
138
+ # Define a default command.
139
+ #
140
+ # default :task
141
+ #
142
+ def default(cmd)
143
+ @@default_command = canonize(cmd)
144
+ end
145
+
146
+ # Define a block for processing STDIN before the command is called.
147
+ # The command block receives the return value of this block in a named argument:
148
+ #
149
+ # command :task do |obj, argv, stdin|; ...; end
150
+ #
151
+ # If a stdin block isn't defined, +stdin+ above will be the STDIN IO handle.
152
+ def stdin(&b)
153
+ @@stdin_block = b
154
+ end
155
+
156
+ # Define a block to be called before the command.
157
+ # This is useful for opening database connections, etc...
158
+ def before(&b)
159
+ @@before_block = b
160
+ end
161
+
162
+ # Define a block to be called after the command.
163
+ # This is useful for stopping, closing, etc... the stuff in the before block.
164
+ def after(&b)
165
+ @@after_block = b
166
+ end
167
+
168
+ # Define the default global usage banner. This is displayed
169
+ # with "script -h".
170
+ def global_usage(msg)
171
+ @@global_options ||= OpenStruct.new
172
+ global_opts_parser.banner = "USAGE: #{msg}"
173
+ end
174
+
175
+ # Define a command-specific usage banner. This is displayed
176
+ # with "script command -h"
177
+ def usage(msg)
178
+ get_current_option_parser.banner = "USAGE: #{msg}"
179
+ end
180
+
181
+ # Grab the options parser for the current command or create it if it doesn't exist.
182
+ def get_current_option_parser
183
+ @@command_opts_parser ||= []
184
+ @@command_index ||= 0
185
+ (@@command_opts_parser[@@command_index] ||= OptionParser.new)
186
+ end
187
+
188
+ # Tell the Drydock parser to ignore something.
189
+ # Drydock will currently only listen to you if you tell it to "ignore :options",
190
+ # otherwise it will ignore you!
191
+ #
192
+ # +what+ the thing to ignore. When it equals :options Drydock will not parse
193
+ # the command-specific arguments. It will pass the Command object the list of
194
+ # arguments. This is useful when you want to parse the arguments in some a way
195
+ # that's too crazy, dangerous for Drydock to handle automatically.
196
+ def ignore(what=:nothing)
197
+ @@command_opts_parser[@@command_index] = :ignore if what == :options || what == :all
198
+ end
199
+
200
+ # Define a global option. See +option+ for more info.
201
+ def global_option(*args, &b)
202
+ args.unshift(global_opts_parser)
203
+ global_option_names << option_parser(args, &b)
204
+ end
205
+
206
+ # Define a command-specific option.
207
+ #
208
+ # +args+ is passed directly to OptionParser.on so it can contain anything
209
+ # that's valid to that method. Some examples:
210
+ # [:h, :help, "Displays this message"]
211
+ # [:m, :max, Integer, "Maximum threshold"]
212
+ # ['-l x,y,z', '--lang=x,y,z', Array, "Requested languages"]
213
+ # If a class is included, it will tell OptionParser to expect a value
214
+ # otherwise it assumes a boolean value.
215
+ #
216
+ # All calls to +option+ must come before the command they're associated
217
+ # to. Example:
218
+ #
219
+ # option :l, :longname, String, "Description" do; ...; end
220
+ # command :task do |obj|; ...; end
221
+ #
222
+ # When calling your script with a specific command-line option, the value
223
+ # is available via obj.longname inside the command block.
224
+ #
225
+ def option(*args, &b)
226
+ args.unshift(get_current_option_parser)
227
+ current_command_option_names << option_parser(args, &b)
228
+ end
229
+
230
+ # Define a command.
231
+ #
232
+ # command :task do
233
+ # ...
234
+ # end
235
+ #
236
+ # A custom command class can be specified using Hash syntax. The class
237
+ # must inherit from Drydock::Command (class CustomeClass < Drydock::Command)
238
+ #
239
+ # command :task => CustomCommand do
240
+ # ...
241
+ # end
242
+ #
243
+ def command(*cmds, &b)
244
+ @@command_index ||= 0
245
+ @@command_opts_parser ||= []
246
+ @@command_option_names ||= []
247
+ cmds.each do |cmd|
248
+ if cmd.is_a? Hash
249
+ c = cmd.values.first.new(cmd.keys.first, &b)
250
+ else
251
+ c = Drydock::Command.new(cmd, &b)
252
+ end
253
+ commands[c.cmd] = c
254
+ command_index_map[c.cmd] = @@command_index
255
+ @@command_index += 1
256
+ end
257
+
258
+ end
259
+
260
+ # Used to create an alias to a defined command.
261
+ # Here's an example:
262
+ #
263
+ # command :task do; ...; end
264
+ # alias_command :pointer, :task
265
+ #
266
+ # Either name can be used on the command-line:
267
+ #
268
+ # $ script task [options]
269
+ # $ script pointer [options]
270
+ #
271
+ # Inside of the command definition, you have access to the
272
+ # command name that was used via obj.alias.
273
+ def alias_command(aliaz, cmd)
274
+ return unless commands.has_key? cmd
275
+ @@commands[aliaz] = commands[cmd]
276
+ end
277
+ alias :command_alias :alias_command
278
+
279
+ # An array of the currently defined Drydock::Command objects
280
+ def commands
281
+ @@commands ||= {}
282
+ end
283
+
284
+ # Returns true if automatic execution is enabled.
285
+ def run?
286
+ @@run
287
+ end
288
+
289
+ # Disable automatic execution (enabled by default)
290
+ #
291
+ # Drydock.run = false
292
+ def run=(v)
293
+ @@run = (v == true) ? true : false
294
+ end
295
+
296
+ # Return true if a command has been executed.
297
+ def has_run?
298
+ @@has_run
299
+ end
300
+
301
+ # Execute the given command.
302
+ # By default, Drydock automatically executes itself and provides handlers for known errors.
303
+ # You can override this functionality by calling +Drydock.run!+ yourself. Drydock
304
+ # will only call +run!+ once.
305
+ def run!(argv=[], stdin=STDIN)
306
+ return if has_run?
307
+ @@has_run = true
308
+ raise NoCommandsDefined.new unless commands
309
+ @@global_options, cmd_name, @@command_options, argv = process_arguments(argv)
310
+
311
+ cmd_name ||= default_command
312
+
313
+ raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
314
+
315
+ stdin = (defined? @@stdin_block) ? @@stdin_block.call(stdin, []) : stdin
316
+ @@before_block.call if defined? @@before_block
317
+
318
+ call_command(cmd_name, argv, stdin)
319
+
320
+ @@after_block.call if defined? @@after_block
321
+
322
+ rescue OptionParser::InvalidOption => ex
323
+ raise Drydock::InvalidArgument.new(ex.args)
324
+ rescue OptionParser::MissingArgument => ex
325
+ raise Drydock::MissingArgument.new(ex.args)
326
+ end
327
+
328
+ private
329
+
330
+ # Executes the block associated to +cmd+
331
+ def call_command(cmd, argv=[], stdin=nil)
332
+ return unless command?(cmd)
333
+ get_command(cmd).call(cmd, argv, stdin, @@global_options || {}, @@command_options || {})
334
+ end
335
+
336
+ # Returns the Drydock::Command object with the name +cmd+
337
+ def get_command(cmd)
338
+ return unless command?(cmd)
339
+ @@commands[canonize(cmd)]
340
+ end
341
+
342
+ # Returns true if a command with the name +cmd+ has been defined.
343
+ def command?(cmd)
344
+ name = canonize(cmd)
345
+ (@@commands || {}).has_key? name
346
+ end
347
+
348
+ # Canonizes a string to the symbol format for command names
349
+ def canonize(cmd)
350
+ return unless cmd
351
+ return cmd if cmd.kind_of?(Symbol)
352
+ cmd.tr('-', '_').to_sym
353
+ end
354
+
355
+ # Processes calls to option and global_option. Symbols are converted into
356
+ # OptionParser style strings (:h and :help become '-h' and '--help').
357
+ def option_parser(args=[], &b)
358
+ return if args.empty?
359
+ opts_parser = args.shift
360
+
361
+ arg_name = ''
362
+ symbol_switches = []
363
+ args.each_with_index do |arg, index|
364
+ if arg.is_a? Symbol
365
+ arg_name = arg.to_s if arg.to_s.size > arg_name.size
366
+ args[index] = (arg.to_s.length == 1) ? "-#{arg.to_s}" : "--#{arg.to_s}"
367
+ symbol_switches << args[index]
368
+ elsif arg.kind_of?(Class)
369
+ symbol_switches.each do |arg|
370
+ arg << "=S"
371
+ end
372
+ end
373
+ end
374
+
375
+ if args.size == 1
376
+ opts_parser.on(args.shift)
377
+ else
378
+ opts_parser.on(*args) do |v|
379
+ block_args = [v, opts_parser]
380
+ result = (b.nil?) ? v : b.call(*block_args[0..(b.arity-1)])
381
+ end
382
+ end
383
+
384
+ arg_name
385
+ end
386
+
387
+
388
+ # Split the +argv+ array into global args and command args and
389
+ # find the command name.
390
+ # i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
391
+ # returns [global_options, cmd, command_options, argv]
392
+ def process_arguments(argv=[])
393
+ global_options = command_options = {}
394
+ cmd = nil
395
+
396
+ global_options = global_opts_parser.getopts(argv)
397
+
398
+ cmd_name = (argv.empty?) ? @@default_command : argv.shift
399
+ raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
400
+
401
+ cmd = get_command(cmd_name)
402
+
403
+ command_parser = @@command_opts_parser[get_command_index(cmd_name)]
404
+ command_options = {}
405
+
406
+ # We only need to parse the options out of the arguments when
407
+ # there are args available, there is a valid parser, and
408
+ # we weren't requested to ignore the options.
409
+ if !argv.empty? && command_parser && command_parser != :ignore
410
+ command_options = command_parser.getopts(argv)
411
+ end
412
+
413
+ # Add accessors to the Drydock::Command object
414
+ # for the global and command specific options
415
+ [global_option_names, (command_option_names[get_command_index(cmd_name)] || [])].flatten.each do |n|
416
+ unless cmd.respond_to?(n)
417
+ cmd.class.send(:define_method, n) do
418
+ instance_variable_get("@#{n}")
419
+ end
420
+ end
421
+ unless cmd.respond_to?("#{n}=")
422
+ cmd.class.send(:define_method, "#{n}=") do |val|
423
+ instance_variable_set("@#{n}", val)
424
+ end
425
+ end
426
+ end
427
+
428
+ [global_options, cmd_name, command_options, argv]
429
+ end
430
+
431
+ def global_option_names
432
+ @@global_option_names ||= []
433
+ end
434
+
435
+ # Grab the current list of command-specific option names. This is a list of the
436
+ # long names.
437
+ def current_command_option_names
438
+ @@command_option_names ||= []
439
+ @@command_index ||= 0
440
+ (@@command_option_names[@@command_index] ||= [])
441
+ end
442
+
443
+ def command_index_map
444
+ @@command_index_map ||= {}
445
+ end
446
+
447
+ def get_command_index(cmd)
448
+ command_index_map[canonize(cmd)] || -1
449
+ end
450
+
451
+ def command_option_names
452
+ @@command_option_names ||= []
453
+ end
454
+
455
+ def global_opts_parser
456
+ @@global_opts_parser ||= OptionParser.new
457
+ end
458
+
459
+ def default_command
460
+ @@default_command ||= nil
461
+ end
462
+
463
+ end
464
+
465
+ include Drydock
466
+
467
+ trap ("SIGINT") do
468
+ puts "#{$/}Exiting..."
469
+ exit 1
470
+ end
471
+
472
+
473
+ at_exit {
474
+ begin
475
+ Drydock.run!(ARGV, STDIN) if Drydock.run? && !Drydock.has_run?
476
+ rescue => ex
477
+ STDERR.puts "ERROR: #{ex.message}"
478
+ STDERR.puts ex.backtrace if Drydock.debug?
479
+ end
480
+ }
481
+
482
+