clamp 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,26 +1,8 @@
1
1
  module Clamp
2
- class Option
2
+ module Option
3
3
 
4
4
  module Parsing
5
5
 
6
- # For :flag options with environment variables attached, this is a list
7
- # of possible values that are accepted as 'true'
8
- #
9
- # Example:
10
- #
11
- # option "--foo", :flag, "Use foo", :environment_variable => "FOO"
12
- #
13
- # All of these will set 'foo' to true:
14
- #
15
- # FOO=1 ./myprogram
16
- # FOO=true ./myprogram
17
- # FOO=yes ./myprogram
18
- # FOO=on ./myprogram
19
- # FOO=enable ./myprogram
20
- #
21
- # See {Clamp::Command.option} for more information.
22
- TRUTHY_ENVIRONMENT_VALUES = %w(1 yes enable on true)
23
-
24
6
  protected
25
7
 
26
8
  def parse_options
@@ -46,7 +28,7 @@ module Clamp
46
28
  value = option.extract_value(switch, remaining_arguments)
47
29
 
48
30
  begin
49
- send("#{option.attribute_name}=", value)
31
+ send(option.write_method, value)
50
32
  rescue ArgumentError => e
51
33
  signal_usage_error "option '#{switch}': #{e.message}"
52
34
  end
@@ -72,12 +54,7 @@ module Clamp
72
54
  next if option.environment_variable.nil?
73
55
  next unless ENV.has_key?(option.environment_variable)
74
56
  value = ENV[option.environment_variable]
75
- if option.flag?
76
- # Set true if the environment value is truthy.
77
- send("#{option.attribute_name}=", TRUTHY_ENVIRONMENT_VALUES.include?(value))
78
- else
79
- send("#{option.attribute_name}=", value)
80
- end
57
+ send(option.write_method, value)
81
58
  end
82
59
  end
83
60
 
@@ -1,12 +1,12 @@
1
- require 'clamp/attribute_declaration'
2
- require 'clamp/parameter'
1
+ require 'clamp/attribute/declaration'
2
+ require 'clamp/parameter/definition'
3
3
 
4
4
  module Clamp
5
- class Parameter
5
+ module Parameter
6
6
 
7
7
  module Declaration
8
8
 
9
- include Clamp::AttributeDeclaration
9
+ include Clamp::Attribute::Declaration
10
10
 
11
11
  def parameters
12
12
  @parameters ||= []
@@ -17,9 +17,10 @@ module Clamp
17
17
  end
18
18
 
19
19
  def parameter(name, description, options = {}, &block)
20
- parameter = Parameter.new(name, description, options)
21
- parameters << parameter
22
- define_accessors_for(parameter, &block)
20
+ Parameter::Definition.new(name, description, options).tap do |parameter|
21
+ parameters << parameter
22
+ define_accessors_for(parameter, &block)
23
+ end
23
24
  end
24
25
 
25
26
  end
@@ -0,0 +1,48 @@
1
+ require 'clamp/attribute/definition'
2
+
3
+ module Clamp
4
+ module Parameter
5
+
6
+ class Definition < Attribute::Definition
7
+
8
+ def initialize(name, description, options = {})
9
+ @name = name
10
+ @description = description
11
+ super(options)
12
+ @multivalued = (@name =~ ELLIPSIS_SUFFIX)
13
+ @required = options.fetch(:required) do
14
+ (@name !~ OPTIONAL)
15
+ end
16
+ end
17
+
18
+ attr_reader :name
19
+
20
+ def help_lhs
21
+ name
22
+ end
23
+
24
+ def consume(arguments)
25
+ raise ArgumentError, "no value provided" if required? && arguments.empty?
26
+ arguments.shift(multivalued? ? arguments.length : 1)
27
+ end
28
+
29
+ private
30
+
31
+ ELLIPSIS_SUFFIX = / \.\.\.$/
32
+ OPTIONAL = /^\[(.*)\]/
33
+
34
+ VALID_ATTRIBUTE_NAME = /^[a-z0-9_]+$/
35
+
36
+ def infer_attribute_name
37
+ inferred_name = name.downcase.tr('-', '_').sub(ELLIPSIS_SUFFIX, '').sub(OPTIONAL) { $1 }
38
+ unless inferred_name =~ VALID_ATTRIBUTE_NAME
39
+ raise "cannot infer attribute_name from #{name.inspect}"
40
+ end
41
+ inferred_name += "_list" if multivalued?
42
+ inferred_name
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  module Clamp
2
- class Parameter
2
+ module Parameter
3
3
 
4
4
  module Parsing
5
5
 
@@ -9,8 +9,9 @@ module Clamp
9
9
 
10
10
  self.class.parameters.each do |parameter|
11
11
  begin
12
- value = parameter.consume(remaining_arguments)
13
- send("#{parameter.attribute_name}=", value) unless value.nil?
12
+ parameter.consume(remaining_arguments).each do |value|
13
+ send(parameter.write_method, value)
14
+ end
14
15
  rescue ArgumentError => e
15
16
  signal_usage_error "parameter '#{parameter.name}': #{e.message}"
16
17
  end
@@ -25,7 +26,7 @@ module Clamp
25
26
  next unless ENV.has_key?(parameter.environment_variable)
26
27
  # Set the parameter value if it's environment variable is present
27
28
  value = ENV[parameter.environment_variable]
28
- send("#{parameter.attribute_name}=", value)
29
+ send(parameter.write_method, value)
29
30
  end
30
31
 
31
32
  end
@@ -1,7 +1,8 @@
1
- require 'clamp/subcommand'
1
+ require 'clamp/errors'
2
+ require 'clamp/subcommand/definition'
2
3
 
3
4
  module Clamp
4
- class Subcommand
5
+ module Subcommand
5
6
 
6
7
  module Declaration
7
8
 
@@ -10,11 +11,20 @@ module Clamp
10
11
  end
11
12
 
12
13
  def subcommand(name, description, subcommand_class = self, &block)
14
+ unless has_subcommands?
15
+ @subcommand_parameter = if @default_subcommand
16
+ parameter "[SUBCOMMAND]", "subcommand", :attribute_name => :subcommand_name, :default => @default_subcommand
17
+ else
18
+ parameter "SUBCOMMAND", "subcommand", :attribute_name => :subcommand_name, :required => false
19
+ end
20
+ remove_method :default_subcommand_name
21
+ parameter "[ARG] ...", "subcommand arguments", :attribute_name => :subcommand_arguments
22
+ end
13
23
  if block
14
24
  # generate a anonymous sub-class
15
25
  subcommand_class = Class.new(subcommand_class, &block)
16
26
  end
17
- recognised_subcommands << Subcommand.new(name, description, subcommand_class)
27
+ recognised_subcommands << Subcommand::Definition.new(name, description, subcommand_class)
18
28
  end
19
29
 
20
30
  def has_subcommands?
@@ -25,16 +35,25 @@ module Clamp
25
35
  recognised_subcommands.find { |sc| sc.is_called?(name) }
26
36
  end
27
37
 
28
- attr_writer :default_subcommand
38
+ def parameters_before_subcommand
39
+ parameters.take_while { |p| p != @subcommand_parameter }
40
+ end
41
+
42
+ def default_subcommand=(name)
43
+ if has_subcommands?
44
+ raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands"
45
+ end
46
+ @default_subcommand = name
47
+ end
29
48
 
30
49
  def default_subcommand(*args, &block)
31
50
  if args.empty?
32
- @default_subcommand ||= nil
51
+ @default_subcommand
33
52
  else
34
53
  $stderr.puts "WARNING: Clamp default_subcommand syntax has changed; check the README."
35
54
  $stderr.puts " (from #{caller.first})"
36
- subcommand(*args, &block)
37
55
  self.default_subcommand = args.first
56
+ subcommand(*args, &block)
38
57
  end
39
58
  end
40
59
 
@@ -0,0 +1,25 @@
1
+ module Clamp
2
+ module Subcommand
3
+
4
+ class Definition < Struct.new(:name, :description, :subcommand_class)
5
+
6
+ def initialize(names, description, subcommand_class)
7
+ @names = Array(names)
8
+ @description = description
9
+ @subcommand_class = subcommand_class
10
+ end
11
+
12
+ attr_reader :names, :description, :subcommand_class
13
+
14
+ def is_called?(name)
15
+ names.member?(name)
16
+ end
17
+
18
+ def help
19
+ [names.join(", "), description]
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  module Clamp
2
- class Subcommand
2
+ module Subcommand
3
3
 
4
4
  module Execution
5
5
 
@@ -7,11 +7,30 @@ module Clamp
7
7
 
8
8
  def execute
9
9
  # delegate to subcommand
10
- @subcommand.run(remaining_arguments)
10
+ subcommand = instatiate_subcommand(subcommand_name)
11
+ subcommand.run(subcommand_arguments)
11
12
  end
12
13
 
13
- def handle_remaining_arguments
14
- # no-op, because subcommand will handle them
14
+ private
15
+
16
+ def instatiate_subcommand(name)
17
+ subcommand_class = find_subcommand_class(name)
18
+ parent_attribute_values = {}
19
+ inheritable_attributes.each do |option|
20
+ if instance_variable_defined?(option.ivar_name)
21
+ parent_attribute_values[option] = instance_variable_get(option.ivar_name)
22
+ end
23
+ end
24
+ subcommand_class.new("#{invocation_path} #{name}", context, parent_attribute_values)
25
+ end
26
+
27
+ def inheritable_attributes
28
+ self.class.recognised_options + self.class.parameters_before_subcommand
29
+ end
30
+
31
+ def find_subcommand_class(name)
32
+ subcommand_def = self.class.find_subcommand(name) || signal_usage_error("No such sub-command '#{name}'")
33
+ subcommand_def.subcommand_class
15
34
  end
16
35
 
17
36
  end
@@ -1,7 +1,7 @@
1
1
  require 'clamp/subcommand/execution'
2
2
 
3
3
  module Clamp
4
- class Subcommand
4
+ module Subcommand
5
5
 
6
6
  module Parsing
7
7
 
@@ -9,31 +9,13 @@ module Clamp
9
9
 
10
10
  def parse_subcommand
11
11
  return false unless self.class.has_subcommands?
12
- subcommand_name = parse_subcommand_name
13
- @subcommand = instatiate_subcommand(subcommand_name)
14
12
  self.extend(Subcommand::Execution)
15
13
  end
16
14
 
17
15
  private
18
16
 
19
- def parse_subcommand_name
20
- remaining_arguments.shift || self.class.default_subcommand || request_help
21
- end
22
-
23
- def find_subcommand(name)
24
- self.class.find_subcommand(name) ||
25
- signal_usage_error("No such sub-command '#{name}'")
26
- end
27
-
28
- def instatiate_subcommand(name)
29
- subcommand_class = find_subcommand(name).subcommand_class
30
- subcommand = subcommand_class.new("#{invocation_path} #{name}", context)
31
- self.class.recognised_options.each do |option|
32
- if instance_variable_defined?(option.ivar_name)
33
- subcommand.instance_variable_set(option.ivar_name, instance_variable_get(option.ivar_name))
34
- end
35
- end
36
- subcommand
17
+ def default_subcommand_name
18
+ self.class.default_subcommand || request_help
37
19
  end
38
20
 
39
21
  end
@@ -0,0 +1,9 @@
1
+ module Clamp
2
+
3
+ TRUTHY_VALUES = %w(1 yes enable on true)
4
+
5
+ def self.truthy?(arg)
6
+ TRUTHY_VALUES.include?(arg.to_s.downcase)
7
+ end
8
+
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Clamp
2
- VERSION = "0.5.1".freeze
2
+ VERSION = "0.6.0".freeze
3
3
  end
@@ -50,7 +50,7 @@ describe Clamp::Command do
50
50
  describe "#help" do
51
51
 
52
52
  it "shows subcommand parameters in usage" do
53
- command.help.should include("flipflop [OPTIONS] SUBCOMMAND [ARGS] ...")
53
+ command.help.should include("flipflop [OPTIONS] SUBCOMMAND [ARG] ...")
54
54
  end
55
55
 
56
56
  it "lists subcommands" do
@@ -132,6 +132,8 @@ describe Clamp::Command do
132
132
 
133
133
  given_command "admin" do
134
134
 
135
+ self.default_subcommand = "status"
136
+
135
137
  subcommand "status", "Show status" do
136
138
 
137
139
  def execute
@@ -140,8 +142,6 @@ describe Clamp::Command do
140
142
 
141
143
  end
142
144
 
143
- self.default_subcommand = "status"
144
-
145
145
  end
146
146
 
147
147
  context "executed with no subcommand" do
@@ -180,6 +180,53 @@ describe Clamp::Command do
180
180
 
181
181
  end
182
182
 
183
+ describe "declaring a default subcommand after subcommands" do
184
+
185
+ it "is not supported" do
186
+
187
+ lambda do
188
+ Class.new(Clamp::Command) do
189
+
190
+ subcommand "status", "Show status" do
191
+
192
+ def execute
193
+ puts "All good!"
194
+ end
195
+
196
+ end
197
+
198
+ self.default_subcommand = "status"
199
+
200
+ end
201
+ end.should raise_error(/default_subcommand must be defined before subcommands/)
202
+
203
+ end
204
+
205
+ end
206
+
207
+ describe "with subcommands, declared after a parameter" do
208
+
209
+ given_command "with" do
210
+
211
+ parameter "THING", "the thing"
212
+
213
+ subcommand "spit", "spit it" do
214
+ def execute
215
+ puts "spat the #{thing}"
216
+ end
217
+ end
218
+
219
+ end
220
+
221
+ it "allows the parameter to be specified first" do
222
+
223
+ command.run(["dummy", "spit"])
224
+ stdout.strip.should == "spat the dummy"
225
+
226
+ end
227
+
228
+ end
229
+
183
230
  describe "each subcommand" do
184
231
 
185
232
  let(:command_class) do
@@ -111,7 +111,24 @@ describe Clamp::Command do
111
111
 
112
112
  end
113
113
 
114
- describe "with :environment_variable value" do
114
+ describe "with :multivalued" do
115
+
116
+ before do
117
+ command.class.option "--flavour", "FLAVOUR", "flavour(s)", :multivalued => true, :attribute_name => :flavours
118
+ end
119
+
120
+ it "defaults to empty array" do
121
+ command.flavours.should == []
122
+ end
123
+
124
+ it "supports multiple values" do
125
+ command.parse(%w(--flavour chocolate --flavour vanilla))
126
+ command.flavours.should == %w(chocolate vanilla)
127
+ end
128
+
129
+ end
130
+
131
+ describe "with :environment_variable" do
115
132
 
116
133
  before do
117
134
  command.class.option "--port", "PORT", "port to listen on", :default => 4321, :environment_variable => "PORT" do |value|
@@ -119,22 +136,39 @@ describe Clamp::Command do
119
136
  end
120
137
  end
121
138
 
122
- it "should use the default if neither flag nor env var are present" do
123
- ENV.delete("PORT")
124
- command.parse([])
125
- command.port.should == 4321
126
- end
139
+ context "when no environment variable is present" do
140
+
141
+ before do
142
+ ENV.delete("PORT")
143
+ end
144
+
145
+ it "uses the default" do
146
+ command.parse([])
147
+ command.port.should == 4321
148
+ end
127
149
 
128
- it "should use the env value if present (instead of default)" do
129
- ENV["PORT"] = rand(10000).to_s
130
- command.parse([])
131
- command.port.should == ENV["PORT"].to_i
132
150
  end
133
151
 
134
- it "should use the the flag value if present (instead of env)" do
135
- ENV["PORT"] = "12345"
136
- command.parse(%w(--port 1500))
137
- command.port.should == 1500
152
+ context "when environment variable is present" do
153
+
154
+ before do
155
+ ENV["PORT"] = "12345"
156
+ end
157
+
158
+ it "uses the environment variable" do
159
+ command.parse([])
160
+ command.port.should == 12345
161
+ end
162
+
163
+ context "and a value is specified on the command-line" do
164
+
165
+ it "uses command-line value" do
166
+ command.parse(%w(--port 1500))
167
+ command.port.should == 1500
168
+ end
169
+
170
+ end
171
+
138
172
  end
139
173
 
140
174
  describe "#help" do
@@ -147,88 +181,83 @@ describe Clamp::Command do
147
181
 
148
182
  end
149
183
 
150
- describe "with :environment_variable value on a :flag option" do
184
+ describe "with :environment_variable and type :flag" do
151
185
 
152
186
  before do
153
187
  command.class.option "--[no-]enable", :flag, "enable?", :default => false, :environment_variable => "ENABLE"
154
188
  end
155
189
 
156
- it "should use the default if neither flag nor env var are present" do
157
- command.parse([])
158
- command.enable?.should == false
159
- end
190
+ context "when no environment variable is present" do
160
191
 
161
- Clamp::Option::Parsing::TRUTHY_ENVIRONMENT_VALUES.each do |value|
162
- it "should use environment value '#{value}' to mean true" do
163
- ENV["ENABLE"] = value
164
- command.parse([])
165
- command.enable?.should == true
192
+ before do
193
+ ENV.delete("ENABLE")
166
194
  end
167
- end
168
-
169
- # Make sure tests fail if ever the TRUTHY_ENVIRONMENT_VALUES loses a
170
- # value. This is just a safety check to make sure maintainers update
171
- # any relevant docs and aware that they could be breaking compatibility.
172
- it "should accept only these values as 'truthy' environment values: 1, yes, enable, on, true" do
173
- Clamp::Option::Parsing::TRUTHY_ENVIRONMENT_VALUES.should == %w(1 yes enable on true)
174
- end
175
195
 
176
- it "should use an env value other than truthy ones to mean false" do
177
- [nil, "0", "no", "whatever"].each do |val|
178
- ENV["ENABLE"] = val
196
+ it "uses the default" do
179
197
  command.parse([])
180
198
  command.enable?.should == false
181
199
  end
182
- end
183
200
 
184
- it "should use the the flag value if present (instead of env)" do
185
- ENV["ENABLE"] = "1"
186
- command.parse(%w(--no-enable))
187
- command.enable?.should == false
188
201
  end
189
202
 
190
- end
203
+ %w(1 yes enable on true).each do |truthy_value|
191
204
 
192
- describe "with :required value" do
205
+ context "when environment variable is #{truthy_value.inspect}" do
193
206
 
194
- before do
195
- command.class.option "--port", "PORT", "port to listen on", :required => true
196
- end
207
+ it "sets the flag" do
208
+ ENV["ENABLE"] = truthy_value
209
+ command.parse([])
210
+ command.enable?.should == true
211
+ end
212
+
213
+ end
197
214
 
198
- it "should fail if a required option is not provided" do
199
- expect { command.parse([]) }.to raise_error(Clamp::UsageError)
200
215
  end
201
216
 
202
- it "should succeed if a required option is provided" do
203
- command.parse(["--port", "12345"])
217
+ %w(0 no disable off false).each do |falsey_value|
218
+
219
+ context "when environment variable is #{falsey_value.inspect}" do
220
+
221
+ it "clears the flag" do
222
+ ENV["ENABLE"] = falsey_value
223
+ command.parse([])
224
+ command.enable?.should == false
225
+ end
226
+
227
+ end
228
+
204
229
  end
205
230
 
206
231
  end
207
232
 
208
- describe "with :required value with :env" do
233
+ describe "with :required" do
209
234
 
210
235
  before do
211
- command.class.option "--port", "PORT", "port to listen on", :required => true, :environment_variable => "PORT"
236
+ command.class.option "--port", "PORT", "port to listen on", :required => true
212
237
  end
213
238
 
214
- it "should fail if a required option is not provided" do
215
- ENV.delete("PORT")
216
- expect { command.parse([]) }.to raise_error(Clamp::UsageError)
217
- end
239
+ context "when no value is provided" do
240
+
241
+ it "raises a UsageError" do
242
+ expect do
243
+ command.parse([])
244
+ end.to raise_error(Clamp::UsageError)
245
+ end
218
246
 
219
- it "should succeed if a required option is provided via arguments" do
220
- ENV.delete("PORT")
221
- command.parse(["--port", "12345"])
222
247
  end
223
248
 
224
- it "should succeed if a required option is provided via env" do
225
- ENV["PORT"] = "12345"
226
- command.parse([])
249
+ context "when a value is provided" do
250
+
251
+ it "does not raise an error" do
252
+ expect do
253
+ command.parse(["--port", "12345"])
254
+ end.not_to raise_error
255
+ end
256
+
227
257
  end
228
258
 
229
259
  end
230
260
 
231
-
232
261
  describe "with a block" do
233
262
 
234
263
  before do
@@ -764,11 +793,11 @@ describe Clamp::Command do
764
793
 
765
794
  end
766
795
 
767
- describe "with a description" do
796
+ describe "with a banner" do
768
797
 
769
798
  given_command("punt") do
770
799
 
771
- self.description = <<-EOF
800
+ banner <<-EOF
772
801
  Punt is an example command. It doesn't do much, really.
773
802
 
774
803
  The prefix at the beginning of this description should be normalised
@@ -779,7 +808,7 @@ describe Clamp::Command do
779
808
 
780
809
  describe "#help" do
781
810
 
782
- it "includes the description" do
811
+ it "includes the banner" do
783
812
  command.help.should =~ /^ Punt is an example command/
784
813
  command.help.should =~ /^ The prefix/
785
814
  end