clamp 0.0.7 → 0.0.9
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/README.markdown +9 -9
- data/examples/rename +2 -2
- data/examples/speak +1 -1
- data/lib/clamp/attribute_declaration.rb +38 -0
- data/lib/clamp/command.rb +15 -75
- data/lib/clamp/command/declaration.rb +15 -0
- data/lib/clamp/errors.rb +26 -0
- data/lib/clamp/{help_support.rb → help.rb} +6 -22
- data/lib/clamp/option.rb +14 -6
- data/lib/clamp/option/declaration.rb +56 -0
- data/lib/clamp/option/parsing.rb +36 -0
- data/lib/clamp/parameter.rb +63 -0
- data/lib/clamp/parameter/declaration.rb +24 -0
- data/lib/clamp/parameter/parsing.rb +30 -0
- data/lib/clamp/subcommand/declaration.rb +35 -0
- data/lib/clamp/subcommand/execution.rb +32 -0
- data/lib/clamp/version.rb +1 -1
- data/spec/clamp/command_group_spec.rb +5 -0
- data/spec/clamp/command_spec.rb +170 -92
- data/spec/clamp/option_spec.rb +3 -3
- data/spec/clamp/parameter_spec.rb +126 -0
- metadata +17 -7
- data/lib/clamp/option_support.rb +0 -75
- data/lib/clamp/subcommand_support.rb +0 -35
@@ -0,0 +1,63 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Parameter
|
4
|
+
|
5
|
+
def initialize(name, description, options = {})
|
6
|
+
@name = name
|
7
|
+
@description = description
|
8
|
+
infer_attribute_name_and_multiplicity
|
9
|
+
if options.has_key?(:attribute_name)
|
10
|
+
@attribute_name = options[:attribute_name].to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :name, :description, :attribute_name
|
15
|
+
|
16
|
+
def help
|
17
|
+
[name, description]
|
18
|
+
end
|
19
|
+
|
20
|
+
def consume(arguments)
|
21
|
+
if required? && arguments.empty?
|
22
|
+
raise ArgumentError, "no value provided"
|
23
|
+
end
|
24
|
+
if multivalued?
|
25
|
+
arguments.shift(arguments.length)
|
26
|
+
else
|
27
|
+
arguments.shift
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def infer_attribute_name_and_multiplicity
|
34
|
+
case @name
|
35
|
+
when /^\[(\S+)\]$/
|
36
|
+
@attribute_name = $1
|
37
|
+
when /^\[(\S+)\] ...$/
|
38
|
+
@attribute_name = "#{$1}_list"
|
39
|
+
@multivalued = true
|
40
|
+
when /^(\S+) ...$/
|
41
|
+
@attribute_name = "#{$1}_list"
|
42
|
+
@multivalued = true
|
43
|
+
@required = true
|
44
|
+
when /^\S+$/
|
45
|
+
@attribute_name = @name
|
46
|
+
@required = true
|
47
|
+
else
|
48
|
+
raise "invalid parameter name: '#{name}'"
|
49
|
+
end
|
50
|
+
@attribute_name = @attribute_name.downcase.tr('-', '_')
|
51
|
+
end
|
52
|
+
|
53
|
+
def multivalued?
|
54
|
+
@multivalued
|
55
|
+
end
|
56
|
+
|
57
|
+
def required?
|
58
|
+
@required
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'clamp/attribute_declaration'
|
2
|
+
require 'clamp/parameter'
|
3
|
+
|
4
|
+
module Clamp
|
5
|
+
class Parameter
|
6
|
+
|
7
|
+
module Declaration
|
8
|
+
|
9
|
+
include AttributeDeclaration
|
10
|
+
|
11
|
+
def parameters
|
12
|
+
@parameters ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def parameter(name, description, options = {}, &block)
|
16
|
+
parameter = Parameter.new(name, description, options)
|
17
|
+
parameters << parameter
|
18
|
+
define_accessors_for(parameter, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Clamp
|
2
|
+
class Parameter
|
3
|
+
|
4
|
+
module Parsing
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def parse_parameters
|
9
|
+
|
10
|
+
return false if self.class.parameters.empty?
|
11
|
+
|
12
|
+
self.class.parameters.each do |parameter|
|
13
|
+
begin
|
14
|
+
value = parameter.consume(arguments)
|
15
|
+
send("#{parameter.attribute_name}=", value)
|
16
|
+
rescue ArgumentError => e
|
17
|
+
signal_usage_error "parameter '#{parameter.name}': #{e.message}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
unless arguments.empty?
|
22
|
+
signal_usage_error "too many arguments"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Clamp
|
2
|
+
|
3
|
+
class Subcommand < Struct.new(:name, :description, :subcommand_class)
|
4
|
+
|
5
|
+
def help
|
6
|
+
[name, description]
|
7
|
+
end
|
8
|
+
|
9
|
+
module Declaration
|
10
|
+
|
11
|
+
def recognised_subcommands
|
12
|
+
@recognised_subcommands ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def subcommand(name, description, subcommand_class = self, &block)
|
16
|
+
if block
|
17
|
+
# generate a anonymous sub-class
|
18
|
+
subcommand_class = Class.new(subcommand_class, &block)
|
19
|
+
end
|
20
|
+
recognised_subcommands << Subcommand.new(name, description, subcommand_class)
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_subcommands?
|
24
|
+
!recognised_subcommands.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_subcommand(name)
|
28
|
+
recognised_subcommands.find { |sc| sc.name == name }
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Clamp
|
2
|
+
class Subcommand
|
3
|
+
|
4
|
+
module Execution
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def execute_subcommand
|
9
|
+
signal_usage_error "no subcommand specified" if arguments.empty?
|
10
|
+
subcommand_name = arguments.shift
|
11
|
+
subcommand_class = find_subcommand_class(subcommand_name)
|
12
|
+
subcommand = subcommand_class.new("#{name} #{subcommand_name}", context)
|
13
|
+
subcommand.parent_command = self
|
14
|
+
subcommand.run(arguments)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def find_subcommand(name)
|
20
|
+
self.class.find_subcommand(name) ||
|
21
|
+
signal_usage_error("No such sub-command '#{name}'")
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_subcommand_class(name)
|
25
|
+
subcommand = find_subcommand(name)
|
26
|
+
subcommand.subcommand_class if subcommand
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/clamp/version.rb
CHANGED
@@ -83,6 +83,11 @@ describe Clamp::Command do
|
|
83
83
|
stdout.should =~ /walking north/
|
84
84
|
end
|
85
85
|
|
86
|
+
it "accepts parents options (specified before the subcommand)" do
|
87
|
+
@command.run(["--direction", "north", "walk"])
|
88
|
+
stdout.should =~ /walking north/
|
89
|
+
end
|
90
|
+
|
86
91
|
it "has access to command context" do
|
87
92
|
@command.run(["walk"])
|
88
93
|
stdout.should =~ /wandering south by default/
|
data/spec/clamp/command_spec.rb
CHANGED
@@ -27,25 +27,6 @@ describe Clamp::Command do
|
|
27
27
|
|
28
28
|
end
|
29
29
|
|
30
|
-
describe "#parse" do
|
31
|
-
|
32
|
-
it "sets arguments" do
|
33
|
-
@command.parse(%w(a b c))
|
34
|
-
@command.arguments.should == %w(a b c)
|
35
|
-
end
|
36
|
-
|
37
|
-
describe "with an unrecognised option" do
|
38
|
-
|
39
|
-
it "raises a UsageError" do
|
40
|
-
lambda do
|
41
|
-
@command.parse(%w(--foo bar))
|
42
|
-
end.should raise_error(Clamp::UsageError)
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
30
|
describe "#run" do
|
50
31
|
|
51
32
|
before do
|
@@ -72,6 +53,19 @@ describe Clamp::Command do
|
|
72
53
|
@command.flavour.should == "chocolate"
|
73
54
|
end
|
74
55
|
|
56
|
+
describe "with type :flag" do
|
57
|
+
|
58
|
+
before do
|
59
|
+
@command.class.option "--verbose", :flag, "Be heartier"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "declares a predicate-style reader" do
|
63
|
+
@command.should respond_to(:verbose?)
|
64
|
+
@command.should_not respond_to(:verbose)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
75
69
|
describe "with explicit :attribute_name" do
|
76
70
|
|
77
71
|
before do
|
@@ -102,26 +96,56 @@ describe Clamp::Command do
|
|
102
96
|
|
103
97
|
end
|
104
98
|
|
105
|
-
|
99
|
+
describe "with a block" do
|
106
100
|
|
101
|
+
before do
|
102
|
+
@command.class.option "--port", "PORT", "Port to listen on" do |port|
|
103
|
+
Integer(port)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it "uses the block to validate and convert the option argument" do
|
108
|
+
lambda do
|
109
|
+
@command.port = "blah"
|
110
|
+
end.should raise_error(ArgumentError)
|
111
|
+
@command.port = "1234"
|
112
|
+
@command.port.should == 1234
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
107
119
|
describe "with options declared" do
|
108
120
|
|
109
121
|
before do
|
110
122
|
@command.class.option "--flavour", "FLAVOUR", "Flavour of the month"
|
111
123
|
@command.class.option "--color", "COLOR", "Preferred hue"
|
124
|
+
@command.class.option "--[no-]nuts", :flag, "Nuts (or not)"
|
112
125
|
end
|
113
126
|
|
114
127
|
describe "#parse" do
|
115
128
|
|
129
|
+
describe "with an unrecognised option" do
|
130
|
+
|
131
|
+
it "raises a UsageError" do
|
132
|
+
lambda do
|
133
|
+
@command.parse(%w(--foo bar))
|
134
|
+
end.should raise_error(Clamp::UsageError)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
116
139
|
describe "with options" do
|
117
140
|
|
118
141
|
before do
|
119
|
-
@command.parse(%w(--flavour strawberry --color blue a b c))
|
142
|
+
@command.parse(%w(--flavour strawberry --nuts --color blue a b c))
|
120
143
|
end
|
121
144
|
|
122
|
-
it "
|
145
|
+
it "maps the option values onto the command object" do
|
123
146
|
@command.flavour.should == "strawberry"
|
124
147
|
@command.color.should == "blue"
|
148
|
+
@command.nuts?.should == true
|
125
149
|
end
|
126
150
|
|
127
151
|
it "retains unconsumed arguments" do
|
@@ -148,6 +172,53 @@ describe Clamp::Command do
|
|
148
172
|
|
149
173
|
end
|
150
174
|
|
175
|
+
describe "with --flag" do
|
176
|
+
|
177
|
+
before do
|
178
|
+
@command.parse(%w(--nuts))
|
179
|
+
end
|
180
|
+
|
181
|
+
it "sets the flag" do
|
182
|
+
@command.nuts?.should be_true
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "with --no-flag" do
|
188
|
+
|
189
|
+
before do
|
190
|
+
@command.nuts = true
|
191
|
+
@command.parse(%w(--no-nuts))
|
192
|
+
end
|
193
|
+
|
194
|
+
it "clears the flag" do
|
195
|
+
@command.nuts?.should be_false
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "when option-writer raises an ArgumentError" do
|
201
|
+
|
202
|
+
before do
|
203
|
+
@command.class.class_eval do
|
204
|
+
|
205
|
+
def color=(c)
|
206
|
+
unless c == "black"
|
207
|
+
raise ArgumentError, "sorry, we're out of #{c}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
it "re-raises it as a UsageError" do
|
215
|
+
lambda do
|
216
|
+
@command.parse(%w(--color red))
|
217
|
+
end.should raise_error(Clamp::UsageError, /^option '--color': sorry, we're out of red/)
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
|
151
222
|
end
|
152
223
|
|
153
224
|
describe "#help" do
|
@@ -165,117 +236,124 @@ describe Clamp::Command do
|
|
165
236
|
|
166
237
|
end
|
167
238
|
|
168
|
-
describe "
|
239
|
+
describe ".parameter" do
|
169
240
|
|
170
|
-
|
171
|
-
@command.class.
|
241
|
+
it "declares option argument accessors" do
|
242
|
+
@command.class.parameter "FLAVOUR", "flavour of the month"
|
243
|
+
@command.flavour.should == nil
|
244
|
+
@command.flavour = "chocolate"
|
245
|
+
@command.flavour.should == "chocolate"
|
172
246
|
end
|
173
247
|
|
174
|
-
|
175
|
-
@command.should respond_to(:verbose?)
|
176
|
-
@command.should_not respond_to(:verbose)
|
177
|
-
end
|
248
|
+
describe "with explicit :attribute_name" do
|
178
249
|
|
179
|
-
|
250
|
+
before do
|
251
|
+
@command.class.parameter "FOO", "a foo", :attribute_name => :bar
|
252
|
+
end
|
180
253
|
|
181
|
-
|
254
|
+
it "uses the specified attribute_name name to name accessors" do
|
255
|
+
@command.bar = "chocolate"
|
256
|
+
@command.bar.should == "chocolate"
|
257
|
+
end
|
182
258
|
|
183
|
-
|
184
|
-
@command.parse(%w(--verbose foo))
|
185
|
-
end
|
259
|
+
end
|
186
260
|
|
187
|
-
|
188
|
-
@command.should be_verbose
|
189
|
-
end
|
261
|
+
describe "with a block" do
|
190
262
|
|
191
|
-
|
192
|
-
|
263
|
+
before do
|
264
|
+
@command.class.parameter "PORT", "port to listen on" do |port|
|
265
|
+
Integer(port)
|
193
266
|
end
|
267
|
+
end
|
194
268
|
|
269
|
+
it "uses the block to validate and convert the argument" do
|
270
|
+
lambda do
|
271
|
+
@command.port = "blah"
|
272
|
+
end.should raise_error(ArgumentError)
|
273
|
+
@command.port = "1234"
|
274
|
+
@command.port.should == 1234
|
195
275
|
end
|
196
276
|
|
197
277
|
end
|
198
278
|
|
199
279
|
end
|
200
280
|
|
201
|
-
describe "with
|
281
|
+
describe "with no parameters declared" do
|
202
282
|
|
283
|
+
describe "#parse" do
|
284
|
+
|
285
|
+
it "retains arguments for handling by #execute" do
|
286
|
+
@command.parse(["crash", "bang", "wallop"])
|
287
|
+
@command.arguments.should == ["crash", "bang", "wallop"]
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "with parameters declared" do
|
295
|
+
|
203
296
|
before do
|
204
|
-
@command.class.
|
297
|
+
@command.class.parameter "X", "x"
|
298
|
+
@command.class.parameter "Y", "y"
|
299
|
+
@command.class.parameter "[Z]", "z"
|
205
300
|
end
|
206
301
|
|
207
302
|
describe "#parse" do
|
208
|
-
|
209
|
-
describe "with
|
210
|
-
|
303
|
+
|
304
|
+
describe "with arguments for all parameters" do
|
305
|
+
|
211
306
|
before do
|
212
|
-
@command.parse(
|
307
|
+
@command.parse(["crash", "bang", "wallop"])
|
213
308
|
end
|
214
309
|
|
215
|
-
it "
|
216
|
-
@command.
|
310
|
+
it "maps arguments onto the command object" do
|
311
|
+
@command.x.should == "crash"
|
312
|
+
@command.y.should == "bang"
|
313
|
+
@command.z.should == "wallop"
|
217
314
|
end
|
218
315
|
|
219
|
-
|
220
|
-
|
221
|
-
describe "with --no-flag" do
|
222
|
-
|
223
|
-
before do
|
224
|
-
@command.sync = true
|
225
|
-
@command.parse(%w(--no-sync))
|
226
|
-
end
|
227
|
-
|
228
|
-
it "clears the flag" do
|
229
|
-
@command.sync?.should be_false
|
316
|
+
it "consumes all the arguments" do
|
317
|
+
@command.arguments.should == []
|
230
318
|
end
|
231
|
-
|
319
|
+
|
232
320
|
end
|
233
321
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
Integer(port)
|
322
|
+
describe "with insufficient arguments" do
|
323
|
+
|
324
|
+
it "raises a UsageError" do
|
325
|
+
lambda do
|
326
|
+
@command.parse(["crash"])
|
327
|
+
end.should raise_error(Clamp::UsageError, "parameter 'Y': no value provided")
|
328
|
+
end
|
329
|
+
|
243
330
|
end
|
244
|
-
end
|
245
|
-
|
246
|
-
it "uses the block to validate and convert the option argument" do
|
247
|
-
lambda do
|
248
|
-
@command.port = "blah"
|
249
|
-
end.should raise_error(ArgumentError)
|
250
|
-
@command.port = "1234"
|
251
|
-
@command.port.should == 1234
|
252
|
-
end
|
253
|
-
|
254
|
-
describe "#parse" do
|
255
331
|
|
256
|
-
describe "with
|
332
|
+
describe "with optional argument omitted" do
|
257
333
|
|
258
|
-
it "
|
259
|
-
@command.parse(
|
260
|
-
@command.
|
334
|
+
it "defaults the optional argument to nil" do
|
335
|
+
@command.parse(["crash", "bang"])
|
336
|
+
@command.x.should == "crash"
|
337
|
+
@command.y.should == "bang"
|
338
|
+
@command.z.should == nil
|
261
339
|
end
|
262
|
-
|
340
|
+
|
263
341
|
end
|
264
342
|
|
265
|
-
describe "with
|
266
|
-
|
343
|
+
describe "with too many arguments" do
|
344
|
+
|
267
345
|
it "raises a UsageError" do
|
268
346
|
lambda do
|
269
|
-
@command.parse(
|
270
|
-
end.should raise_error(Clamp::UsageError,
|
347
|
+
@command.parse(["crash", "bang", "wallop", "kapow"])
|
348
|
+
end.should raise_error(Clamp::UsageError, "too many arguments")
|
271
349
|
end
|
272
|
-
|
350
|
+
|
273
351
|
end
|
274
|
-
|
352
|
+
|
275
353
|
end
|
276
|
-
|
354
|
+
|
277
355
|
end
|
278
|
-
|
356
|
+
|
279
357
|
describe "with explicit usage" do
|
280
358
|
|
281
359
|
given_command("blah") do
|