clamp 0.5.1 → 0.6.0

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.
@@ -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