clamp 0.6.0 → 0.6.1

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