clamp 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YzQ1YTM1YTRmM2Q2ZDRiMDEzY2M1YzBmM2Y5YjM4ZDlhZTYzNWFmZA==
4
+ NGJhZjcxZjg4MjljZTZhODNlM2M0NzFhNGRmODFlMGQ1ZDdjYzFhNQ==
5
5
  data.tar.gz: !binary |-
6
- MDMzN2JhYTAxYjYwNmZhZjhlMzEwZDJmMTM0MTBhOWVhZjFkMjIxYQ==
6
+ NGVhZDMyYzU2ODk5MjY0NjY0YWZhYjRlODhkYzZhZGQ5MzlhZTUzMA==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NzA4ZmI2ZmNjMmE4YzZlZDY4ZjUwZTVlMDIyYTJjZmQ5NDkxNDZkMjI0NDhi
10
- M2E3NmUwZDE3NTUxNWViM2FmMDc5ZDc3NzRiNjQ2YjY1NjlkMzgwMjM0NTgy
11
- NWM0ODJmODdkYTY2MTc3ZDBiZDdhZTU5N2Q1OTRjYTcyYzNlYWM=
9
+ ZTJhZjg3NWJmYmNmNjE2ODZmMmRhOWFmNWUwNWVhNmFlMmMwZGFhMjZlZjU4
10
+ NmM5ODRmNjIxNjU3MzRhNjAwY2YyZDk1YzcyNTU1OGUwMGQ1M2YxZGYwMmZl
11
+ MWI3OTc0MWRlYjkxZjA2N2ZkNmEyNmE1OWNjMjRjN2QwNjczNDU=
12
12
  data.tar.gz: !binary |-
13
- ZDU4Yzg2ODViNjBlOWRmY2Y3OTZjNGEyYzZhMmFjZDZiYjc4MWI5ZjY5NTZl
14
- OWZjN2I4YTEwYjE0ZTZiMTFkMmRmYjk3OTE3ODMyNzhjYWY4MDc1NjZiYTE4
15
- OGNkZmYxODFiYjlhMzM5ZThiOTQ0M2U0NDllZDcyNWQyMjA0ZDk=
13
+ OTE5Y2Y0YWIyMzJmZmY5MGJjYzJiNTYyOGQ4NjQ4MjljNjRkM2M5YWE4NzU0
14
+ OTE0NzY0MzkzNzBiNGY4YmMwYWFmNzhkZmZmOWJkZjEwMzExYzVlMzFjZmI5
15
+ M2I2Y2MxM2Y5YzhiYmY4YjczMjVmY2Q5MzgxMmI0ZjQwNWQzN2I=
data/CHANGES.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Changelog
2
2
 
3
- ## 0.6.0 (2013-04-??)
3
+ ## 0.6.0 (2013-04-28)
4
4
 
5
5
  * Introduce "banner" to describe a command (replacing "self.description=").
6
+ * Introduce "Clamp do ... end" syntax sugar.
6
7
  * Allow parameters to be specified before a subcommand.
7
8
  * Add support for :multivalued options.
8
9
  * Multi valued options and parameters get an "#append_to_foo_list" method, rather than
9
10
  "#foo_list=".
10
-
11
+ * default_subcommand must be specified before any subcommands.
data/README.md CHANGED
@@ -17,7 +17,30 @@ Yeah, sorry. There are a bunch of existing command-line parsing libraries out t
17
17
  Quick Start
18
18
  -----------
19
19
 
20
- Clamp models a command as a Ruby class; a subclass of `Clamp::Command`. They look something like this:
20
+ A typical Clamp script looks like this:
21
+
22
+ require 'clamp'
23
+
24
+ Clamp do
25
+
26
+ option "--loud", :flag, "say it loud"
27
+ option ["-n", "--iterations"], "N", "say it N times", :default => 1 do |s|
28
+ Integer(s)
29
+ end
30
+
31
+ parameter "WORDS ...", "the thing to say", :attribute_name => :words
32
+
33
+ def execute
34
+ the_truth = words.join(" ")
35
+ the_truth.upcase! if loud?
36
+ iterations.times do
37
+ puts the_truth
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ Internally, Clamp models a command as a Ruby class (a subclass of `Clamp::Command`), and a command execution as an instance of that class. The example above is really just syntax-sugar for:
21
44
 
22
45
  require 'clamp'
23
46
 
@@ -42,9 +65,7 @@ Clamp models a command as a Ruby class; a subclass of `Clamp::Command`. They lo
42
65
 
43
66
  SpeakCommand.run
44
67
 
45
- Class-level methods like `option` and `parameter` declare attributes (in a similar way to `attr_accessor`), and arrange for them to be populated automatically based on command-line arguments. They are also used to generate `help` documentation.
46
-
47
- The call to `run` creates an instance of the command class, then invokes it with command-line arguments (from `ARGV`).
68
+ Class-level methods like `option` and `parameter` declare attributes, in a similar way to `attr_accessor`, and arrange for them to be populated automatically based on command-line arguments. They are also used to generate `help` documentation.
48
69
 
49
70
  There are more examples demonstrating various features of Clamp [on Github][examples].
50
71
 
@@ -63,7 +84,7 @@ For example:
63
84
 
64
85
  option "--flavour", "FLAVOUR", "ice-cream flavour"
65
86
 
66
- It works a little like `attr_accessor`, defining reader and writer methods on the command class. The attribute name is derived from the switch (in this case, "`flavour`"). When you pass options to your command, Clamp will populate the attributes, which are then available for use in your `#execute` method.
87
+ It works a little like `attr_accessor`, defining reader and writer methods on the command class. The attribute name is inferred from the switch (in this case, "`flavour`"). When you pass options to your command, Clamp will populate the attributes, which are then available for use in your `#execute` method.
67
88
 
68
89
  def execute
69
90
  puts "You chose #{flavour}. Excellent choice!"
@@ -102,6 +123,14 @@ Although 'required option' is a an oxymoron, Clamp lets you mark an option as re
102
123
 
103
124
  Note that it makes no sense to mark a `:flag` option, or one with a `:default`, as `:required`.
104
125
 
126
+ ### Multivalued options
127
+
128
+ Declaring an option "`:multivalued`" allows it to be specified multiple times on the command line.
129
+
130
+ option "--format", "FORMAT", "output format", :multivalued => true
131
+
132
+ The underlying attribute becomes an Array, and the suffix "`_list`" is appended to the default attribute name. In this case, an attribute called "`format_list`" would be generated (unless you override the default by specifying an `:attribute_name`).
133
+
105
134
  Declaring parameters
106
135
  --------------------
107
136
 
@@ -122,13 +151,14 @@ Wrapping a parameter name in square brackets indicates that it's optional, e.g.
122
151
 
123
152
  parameter "[TARGET_DIR]", "target directory"
124
153
 
125
- ### Greedy parameters
154
+ ### Multivalued (aka "greedy") parameters
126
155
 
127
156
  Three dots at the end of a parameter name makes it "greedy" - it will consume all remaining command-line arguments. For example:
128
157
 
129
- parameter "FILE ...", "input files"
158
+ parameter "FILE ...", "input files", :attribute_name => :files
130
159
 
131
- The suffix "`_list`" is appended to the default attribute name for greedy parameters; in this case, an attribute called "`file_list`" would be generated.
160
+
161
+ Like multivalued options, greedy parameters are backed by an Array attribute (named with a "`_list`" suffix, by default).
132
162
 
133
163
  Parsing and validation of options and parameters
134
164
  ------------------------------------------------
@@ -137,11 +167,12 @@ When you `#run` a command, it will first attempt to `#parse` command-line argume
137
167
 
138
168
  Clamp will verify that all required (ie. non-optional) parameters are present, and signal a error if they aren't.
139
169
 
140
- ### Validation block
170
+ ### Validation
141
171
 
142
172
  Both `option` and `parameter` accept an optional block. If present, the block will be
143
- called with the raw string option argument, and is expected to coerce it to
144
- the correct type, e.g.
173
+ called with the raw string argument, and is expected to validate it. The value returned by the block will be assigned to the underlying attribute, so it's also a good place to coerce the String to a different type, if appropriate.
174
+
175
+ For example:
145
176
 
146
177
  option "--port", "PORT", "port to listen on" do |s|
147
178
  Integer(s)
@@ -152,6 +183,17 @@ If the block raises an ArgumentError, Clamp will catch it, and report that the v
152
183
  !!!plain
153
184
  ERROR: option '--port': invalid value for Integer: "blah"
154
185
 
186
+ For multivalued options and parameters, the validation block will be called for each value specified.
187
+
188
+ More complex validation, e.g. those involving multiple options/parameters, should be performed within the `#execute` method. Use `#signal_usage_error` to tell the user what they did wrong, e.g.
189
+
190
+ def execute
191
+ if port < 1024 && user != 'root'
192
+ signal_usage_error "port restricted for non-root users"
193
+ end
194
+ # ... carry on ...
195
+ end
196
+
155
197
  ### Advanced option/parameter handling
156
198
 
157
199
  While Clamp provides an attribute-writer method for each declared option or parameter, you always have the option of overriding it to provide custom argument-handling logic, e.g.
@@ -199,7 +241,7 @@ Subcommand support helps you wrap a number of related commands into a single scr
199
241
 
200
242
  Unsuprisingly, subcommands are declared using the `subcommand` method. e.g.
201
243
 
202
- class MainCommand < Clamp::Command
244
+ Clamp do
203
245
 
204
246
  subcommand "init", "Initialize the repository" do
205
247
 
@@ -231,7 +273,7 @@ Clamp generates an anonymous subclass of the current class, to represent the sub
231
273
 
232
274
  You can set a default subcommand, at the class level, as follows:
233
275
 
234
- class MainCommand < Clamp::Command
276
+ Clamp do
235
277
 
236
278
  self.default_subcommand = "status"
237
279
 
@@ -13,11 +13,7 @@ module Clamp
13
13
 
14
14
  def define_reader_for(attribute)
15
15
  define_method(attribute.read_method) do
16
- if instance_variable_defined?(attribute.ivar_name)
17
- instance_variable_get(attribute.ivar_name)
18
- else
19
- send(attribute.default_method)
20
- end
16
+ attribute.of(self)._read
21
17
  end
22
18
  end
23
19
 
@@ -32,14 +28,7 @@ module Clamp
32
28
  if block
33
29
  value = instance_exec(value, &block)
34
30
  end
35
- if attribute.multivalued?
36
- unless instance_variable_defined?(attribute.ivar_name)
37
- instance_variable_set(attribute.ivar_name, [])
38
- end
39
- instance_variable_get(attribute.ivar_name) << value
40
- else
41
- instance_variable_set(attribute.ivar_name, value)
42
- end
31
+ attribute.of(self)._write(value)
43
32
  end
44
33
  end
45
34
 
@@ -1,3 +1,5 @@
1
+ require 'clamp/attribute/instance'
2
+
1
3
  module Clamp
2
4
  module Attribute
3
5
 
@@ -65,6 +67,10 @@ module Clamp
65
67
  end
66
68
  end
67
69
 
70
+ def of(command)
71
+ Attribute::Instance.new(self, command)
72
+ end
73
+
68
74
  private
69
75
 
70
76
  def default_description
@@ -0,0 +1,76 @@
1
+ module Clamp
2
+ module Attribute
3
+
4
+ # Represents an option/parameter of a Clamp::Command instance.
5
+ #
6
+ class Instance
7
+
8
+ def initialize(attribute, command)
9
+ @attribute = attribute
10
+ @command = command
11
+ end
12
+
13
+ attr_reader :attribute, :command
14
+
15
+ def defined?
16
+ command.instance_variable_defined?(attribute.ivar_name)
17
+ end
18
+
19
+ # get value directly
20
+ def get
21
+ command.instance_variable_get(attribute.ivar_name)
22
+ end
23
+
24
+ # set value directly
25
+ def set(value)
26
+ command.instance_variable_set(attribute.ivar_name, value)
27
+ end
28
+
29
+ def default
30
+ command.send(attribute.default_method)
31
+ end
32
+
33
+ # default implementation of read_method
34
+ def _read
35
+ if self.defined?
36
+ get
37
+ else
38
+ default
39
+ end
40
+ end
41
+
42
+ # default implementation of write_method
43
+ def _write(value)
44
+ if attribute.multivalued?
45
+ current_values = get || []
46
+ set(current_values + [value])
47
+ else
48
+ set(value)
49
+ end
50
+ end
51
+
52
+ def read
53
+ command.send(attribute.read_method)
54
+ end
55
+
56
+ def write(value)
57
+ command.send(attribute.write_method, value)
58
+ end
59
+
60
+ def default_from_environment
61
+ return if self.defined?
62
+ return if attribute.environment_variable.nil?
63
+ return unless ENV.has_key?(attribute.environment_variable)
64
+ # Set the parameter value if it's environment variable is present
65
+ value = ENV[attribute.environment_variable]
66
+ begin
67
+ write(value)
68
+ rescue ArgumentError => e
69
+ command.send(:signal_usage_error, "$#{attribute.environment_variable}: #{e.message}")
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
@@ -28,7 +28,7 @@ module Clamp
28
28
  @invocation_path = invocation_path
29
29
  @context = context
30
30
  parent_attribute_values.each do |attribute, value|
31
- instance_variable_set(attribute.ivar_name, value)
31
+ attribute.of(self).set(value)
32
32
  end
33
33
  end
34
34
 
@@ -49,9 +49,7 @@ module Clamp
49
49
  #
50
50
  def parse(arguments)
51
51
  @remaining_arguments = arguments.dup
52
- parse_environment_options
53
52
  parse_options
54
- parse_environment_parameters
55
53
  parse_parameters
56
54
  parse_subcommand
57
55
  handle_remaining_arguments
@@ -6,6 +6,7 @@ module Clamp
6
6
  protected
7
7
 
8
8
  def parse_options
9
+
9
10
  while remaining_arguments.first =~ /\A-/
10
11
 
11
12
  switch = remaining_arguments.shift
@@ -28,13 +29,18 @@ module Clamp
28
29
  value = option.extract_value(switch, remaining_arguments)
29
30
 
30
31
  begin
31
- send(option.write_method, value)
32
+ option.of(self).write(value)
32
33
  rescue ArgumentError => e
33
34
  signal_usage_error "option '#{switch}': #{e.message}"
34
35
  end
35
36
 
36
37
  end
37
38
 
39
+ # Fill in gap from environment
40
+ self.class.recognised_options.each do |option|
41
+ option.of(self).default_from_environment
42
+ end
43
+
38
44
  # Verify that all required options are present
39
45
  self.class.recognised_options.each do |option|
40
46
  # If this option is required and the value is nil, there's an error.
@@ -49,15 +55,6 @@ module Clamp
49
55
  end
50
56
  end
51
57
 
52
- def parse_environment_options
53
- self.class.recognised_options.each do |option|
54
- next if option.environment_variable.nil?
55
- next unless ENV.has_key?(option.environment_variable)
56
- value = ENV[option.environment_variable]
57
- send(option.write_method, value)
58
- end
59
- end
60
-
61
58
  private
62
59
 
63
60
  def find_option(switch)
@@ -10,23 +10,15 @@ module Clamp
10
10
  self.class.parameters.each do |parameter|
11
11
  begin
12
12
  parameter.consume(remaining_arguments).each do |value|
13
- send(parameter.write_method, value)
13
+ parameter.of(self).write(value)
14
14
  end
15
15
  rescue ArgumentError => e
16
16
  signal_usage_error "parameter '#{parameter.name}': #{e.message}"
17
17
  end
18
18
  end
19
19
 
20
- end
21
-
22
- def parse_environment_parameters
23
-
24
20
  self.class.parameters.each do |parameter|
25
- next if parameter.environment_variable.nil?
26
- next unless ENV.has_key?(parameter.environment_variable)
27
- # Set the parameter value if it's environment variable is present
28
- value = ENV[parameter.environment_variable]
29
- send(parameter.write_method, value)
21
+ parameter.of(self).default_from_environment
30
22
  end
31
23
 
32
24
  end
@@ -39,6 +39,10 @@ module Clamp
39
39
  parameters.take_while { |p| p != @subcommand_parameter }
40
40
  end
41
41
 
42
+ def inheritable_attributes
43
+ recognised_options + parameters_before_subcommand
44
+ end
45
+
42
46
  def default_subcommand=(name)
43
47
  if has_subcommands?
44
48
  raise Clamp::DeclarationError, "default_subcommand must be defined before subcommands"
@@ -16,18 +16,14 @@ module Clamp
16
16
  def instatiate_subcommand(name)
17
17
  subcommand_class = find_subcommand_class(name)
18
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)
19
+ self.class.inheritable_attributes.each do |attribute|
20
+ if attribute.of(self).defined?
21
+ parent_attribute_values[attribute] = attribute.of(self).get
22
22
  end
23
23
  end
24
24
  subcommand_class.new("#{invocation_path} #{name}", context, parent_attribute_values)
25
25
  end
26
26
 
27
- def inheritable_attributes
28
- self.class.recognised_options + self.class.parameters_before_subcommand
29
- end
30
-
31
27
  def find_subcommand_class(name)
32
28
  subcommand_def = self.class.find_subcommand(name) || signal_usage_error("No such sub-command '#{name}'")
33
29
  subcommand_def.subcommand_class
@@ -1,3 +1,3 @@
1
1
  module Clamp
2
- VERSION = "0.6.0".freeze
2
+ VERSION = "0.6.1".freeze
3
3
  end
@@ -283,6 +283,11 @@ describe Clamp::Command do
283
283
  before do
284
284
  command.class.option ["-f", "--flavour"], "FLAVOUR", "Flavour of the month"
285
285
  command.class.option ["-c", "--color"], "COLOR", "Preferred hue"
286
+ command.class.option ["--scoops"], "N", "Number of scoops",
287
+ :default => 1,
288
+ :environment_variable => "DEFAULT_SCOOPS" do |arg|
289
+ Integer(arg)
290
+ end
286
291
  command.class.option ["-n", "--[no-]nuts"], :flag, "Nuts (or not)\nMay include nuts"
287
292
  command.class.parameter "[ARG] ...", "extra arguments", :attribute_name => :arguments
288
293
  end
@@ -441,24 +446,23 @@ describe Clamp::Command do
441
446
 
442
447
  end
443
448
 
444
- describe "when option-writer raises an ArgumentError" do
449
+ describe "when a bad option value is specified on the command-line" do
445
450
 
446
- before do
447
- command.class.class_eval do
451
+ it "signals a UsageError" do
452
+ lambda do
453
+ command.parse(%w(--scoops reginald))
454
+ end.should raise_error(Clamp::UsageError, /^option '--scoops': invalid value for Integer/)
455
+ end
448
456
 
449
- def color=(c)
450
- unless c == "black"
451
- raise ArgumentError, "sorry, we're out of #{c}"
452
- end
453
- end
457
+ end
454
458
 
455
- end
456
- end
459
+ describe "when a bad option value is specified in the environment" do
457
460
 
458
- it "re-raises it as a UsageError" do
461
+ it "signals a UsageError" do
462
+ ENV["DEFAULT_SCOOPS"] = "marjorie"
459
463
  lambda do
460
- command.parse(%w(--color red))
461
- end.should raise_error(Clamp::UsageError, /^option '--color': sorry, we're out of red/)
464
+ command.parse([])
465
+ end.should raise_error(Clamp::UsageError, /^\$DEFAULT_SCOOPS: invalid value for Integer/)
462
466
  end
463
467
 
464
468
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-28 00:00:00.000000000 Z
11
+ date: 2013-05-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: ! "Clamp provides an object-model for command-line utilities. \nIt handles
14
14
  parsing of command-line options, and generation of usage help.\n"
@@ -35,6 +35,7 @@ files:
35
35
  - lib/clamp.rb
36
36
  - lib/clamp/attribute/declaration.rb
37
37
  - lib/clamp/attribute/definition.rb
38
+ - lib/clamp/attribute/instance.rb
38
39
  - lib/clamp/command.rb
39
40
  - lib/clamp/errors.rb
40
41
  - lib/clamp/help.rb