drydock 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +16 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +70 -0
- data/Rakefile +72 -0
- data/bin/example +152 -0
- data/doc/classes/Drydock.html +535 -0
- data/doc/classes/Drydock/Command.html +180 -0
- data/doc/classes/Drydock/InvalidArgument.html +118 -0
- data/doc/classes/Drydock/MissingArgument.html +88 -0
- data/doc/classes/Drydock/NoCommandsDefined.html +88 -0
- data/doc/classes/Drydock/UnknownCommand.html +118 -0
- data/doc/created.rid +1 -0
- data/doc/files/CHANGES_txt.html +100 -0
- data/doc/files/LICENSE_txt.html +87 -0
- data/doc/files/README_rdoc.html +119 -0
- data/doc/files/bin/example.html +104 -0
- data/doc/files/lib/drydock_rb.html +75 -0
- data/doc/fr_class_index.html +19 -0
- data/doc/fr_file_index.html +24 -0
- data/doc/fr_method_index.html +4459 -0
- data/doc/index.html +15 -0
- data/doc/rdoc-style.css +319 -0
- data/drydock.gemspec +53 -0
- data/lib/drydock.rb +482 -0
- data/test/command_test.rb +40 -0
- metadata +90 -0
data/doc/index.html
ADDED
@@ -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>
|
data/doc/rdoc-style.css
ADDED
@@ -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; }
|
data/drydock.gemspec
ADDED
@@ -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
|
data/lib/drydock.rb
ADDED
@@ -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
|
+
|