configurable 0.1.0 → 0.3.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.
@@ -0,0 +1,38 @@
1
+ require 'rdoc/generators/html_generator'
2
+
3
+ # Defines a specialized generator so it can be called for using a --fmt option.
4
+ class CDocHTMLGenerator < Generators::HTMLGenerator # :nodoc:
5
+ end
6
+
7
+ module Generators # :nodoc:
8
+ const_set(:RubyToken, RDoc::RubyToken)
9
+
10
+ class HtmlClass < ContextUser # :nodoc:
11
+ alias cdoc_original_value_hash value_hash
12
+
13
+ def value_hash
14
+ # split attributes into configurations and regular attributes
15
+ configurations, attributes = @context.attributes.partition do |attribute|
16
+ attribute.kind_of?(CDoc::ConfigAttr)
17
+ end
18
+
19
+ # set the context attributes to JUST the regular
20
+ # attributes and process as usual.
21
+ @context.attributes.clear.concat attributes
22
+ values = cdoc_original_value_hash
23
+
24
+ # set the context attributes to the configurations
25
+ # and echo the regular processing to produce a list
26
+ # of configurations
27
+ @context.attributes.clear.concat configurations
28
+ @context.sections.each_with_index do |section, i|
29
+ secdata = values["sections"][i]
30
+
31
+ al = build_attribute_list(section)
32
+ secdata["configurations"] = al unless al.empty?
33
+ end
34
+
35
+ values
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ require 'rdoc/generators/template/html/html'
2
+
3
+ #
4
+ # Add a template for documenting configurations. Do so by inserting in the
5
+ # template into the content regions used to template html.
6
+ # (see 'rdoc/generators/html_generator' line 864)
7
+ #
8
+ [
9
+ RDoc::Page::BODY,
10
+ RDoc::Page::FILE_PAGE,
11
+ RDoc::Page::METHOD_LIST].each do |content|
12
+
13
+ # this substitution method duplicates the attribute template for configurations
14
+ # (see rdoc\generators\template\html line 523)
15
+ #
16
+ #IF:attributes
17
+ # <div id="attribute-list">
18
+ # <h3 class="section-bar">Attributes</h3>
19
+ #
20
+ # <div class="name-list">
21
+ # <table>
22
+ #START:attributes
23
+ # <tr class="top-aligned-row context-row">
24
+ # <td class="context-item-name">%name%</td>
25
+ #IF:rw
26
+ # <td class="context-item-value">&nbsp;[%rw%]&nbsp;</td>
27
+ #ENDIF:rw
28
+ #IFNOT:rw
29
+ # <td class="context-item-value">&nbsp;&nbsp;</td>
30
+ #ENDIF:rw
31
+ # <td class="context-item-desc">%a_desc%</td>
32
+ # </tr>
33
+ #END:attributes
34
+ # </table>
35
+ # </div>
36
+ # </div>
37
+ #ENDIF:attributes
38
+ #
39
+ content.gsub!(/IF:attributes.*?ENDIF:attributes/m) do |match|
40
+ match + "\n\n" + match.gsub(/attributes/, 'configurations').gsub(/Attributes/, 'Configurations')
41
+ end
42
+ end
data/lib/config_parser.rb CHANGED
@@ -3,6 +3,114 @@ require 'config_parser/switch'
3
3
 
4
4
  autoload(:Shellwords, 'shellwords')
5
5
 
6
+ # ConfigParser is the Configurable equivalent of
7
+ # {OptionParser}[http://www.ruby-doc.org/core/classes/OptionParser.html]
8
+ # and uses a similar, simplified (see below) syntax to declare options.
9
+ #
10
+ # opts = {}
11
+ # parser = ConfigParser.new do |psr|
12
+ # psr.on "-s", "--long LONG", "a standard option" do |value|
13
+ # opts[:long] = value
14
+ # end
15
+ #
16
+ # psr.on "--[no-]switch", "a switch" do |value|
17
+ # opts[:switch] = value
18
+ # end
19
+ #
20
+ # psr.on "--flag", "a flag" do
21
+ # # note: no value is parsed; the block
22
+ # # only executes if the flag is found
23
+ # opts[:flag] = true
24
+ # end
25
+ # end
26
+ #
27
+ # parser.parse("a b --long arg --switch --flag c") # => ['a', 'b', 'c']
28
+ # opts # => {:long => 'arg', :switch => true, :flag => true}
29
+ #
30
+ # ConfigParser formalizes this pattern of setting values in a hash as they
31
+ # occur, and adds the ability to specify default values. The syntax is
32
+ # not quite as friendly as for ordinary options, but meshes well with
33
+ # Configurable classes:
34
+ #
35
+ # psr = ConfigParser.new
36
+ # psr.define(:key, 'default', :desc => 'a standard option')
37
+ #
38
+ # psr.parse('a b --key option c') # => ['a', 'b', 'c']
39
+ # psr.config # => {:key => 'option'}
40
+ #
41
+ # psr.parse('a b c') # => ['a', 'b', 'c']
42
+ # psr.config # => {:key => 'default'}
43
+ #
44
+ # And now directly from a Configurable class, the equivalent of the
45
+ # original example:
46
+ #
47
+ # class ConfigClass
48
+ # include Configurable
49
+ #
50
+ # config :long, 'default', :short => 's' # a standard option
51
+ # config :switch, false, &c.switch # a switch
52
+ # config :flag, false, &c.flag # a flag
53
+ # end
54
+ #
55
+ # psr = ConfigParser.new
56
+ # psr.add(ConfigClass.configurations)
57
+ #
58
+ # psr.parse("a b --long arg --switch --flag c") # => ['a', 'b', 'c']
59
+ # psr.config # => {:long => 'arg', :switch => true, :flag => true}
60
+ #
61
+ # psr.parse("a b --long=arg --no-switch c") # => ['a', 'b', 'c']
62
+ # psr.config # => {:long => 'arg', :switch => false, :flag => false}
63
+ #
64
+ # psr.parse("a b -sarg c") # => ['a', 'b', 'c']
65
+ # psr.config # => {:long => 'arg', :switch => false, :flag => false}
66
+ #
67
+ # As you might expect, config attributes are used by ConfigParser to
68
+ # correctly build a corresponding option. In configurations like :switch,
69
+ # the block implies the {:type => :switch} attribute and so the
70
+ # config is made into a switch-style option by ConfigParser.
71
+ #
72
+ # Use the to_s method to convert a ConfigParser into command line
73
+ # documentation:
74
+ #
75
+ # "\nconfigurations:\n#{psr.to_s}"
76
+ # # => %q{
77
+ # # configurations:
78
+ # # -s, --long LONG a standard option
79
+ # # --[no-]switch a switch
80
+ # # --flag a flag
81
+ # # }
82
+ #
83
+ # ==== Simplifications
84
+ #
85
+ # ConfigParser simplifies the OptionParser syntax for 'on'. ConfigParser does
86
+ # not support automatic conversion of values, gets rid of 'optional' arguments
87
+ # for options, and only supports a single description string. Hence:
88
+ #
89
+ # psr = ConfigParser.new
90
+ #
91
+ # # incorrect, raises error as this will look
92
+ # # like multiple descriptions are specified
93
+ # psr.on("--delay N",
94
+ # Float,
95
+ # "Delay N seconds before executing") # !> ArgumentError
96
+ #
97
+ # # correct
98
+ # psr.on("--delay N", "Delay N seconds before executing") do |value|
99
+ # value.to_f
100
+ # end
101
+ #
102
+ # # this ALWAYS requires the argument and raises
103
+ # # an error because multiple descriptions are
104
+ # # specified
105
+ # psr.on("-i", "--inplace [EXTENSION]",
106
+ # "Edit ARGV files in place",
107
+ # " (make backup if EXTENSION supplied)") # !> ArgumentError
108
+ #
109
+ # # correct
110
+ # psr.on("-i", "--inplace EXTENSION",
111
+ # "Edit ARGV files in place\n (make backup if EXTENSION supplied)")
112
+ #
113
+ #
6
114
  class ConfigParser
7
115
  class << self
8
116
  # Splits and nests compound keys of a hash.
@@ -43,14 +151,18 @@ class ConfigParser
43
151
 
44
152
  include Utils
45
153
 
46
- # A hash of (switch, Option) pairs mapping switches to
47
- # options.
154
+ # A hash of (switch, Option) pairs mapping command line
155
+ # switches like '-s' or '--long' to the Option that
156
+ # handles them.
48
157
  attr_reader :switches
49
158
 
159
+ # The most recent hash of configurations produced by parse.
50
160
  attr_reader :config
51
161
 
162
+ # A hash of default configurations merged into parsed configs.
52
163
  attr_reader :default_config
53
164
 
165
+ # Initializes a new ConfigParser and passes it to the block, if given.
54
166
  def initialize
55
167
  @options = []
56
168
  @switches = {}
@@ -59,21 +171,27 @@ class ConfigParser
59
171
 
60
172
  yield(self) if block_given?
61
173
  end
174
+
175
+ # Returns the nested form of config (see ConfigParser.nest). Primarily
176
+ # useful when nested configurations have been added with add.
177
+ def nested_config
178
+ ConfigParser.nest(config)
179
+ end
62
180
 
63
- # Returns an array of options registered with self.
181
+ # Returns an array of the options registered with self.
64
182
  def options
65
183
  @options.select do |opt|
66
184
  opt.kind_of?(Option)
67
185
  end
68
186
  end
69
187
 
70
- # Adds a separator string to self.
188
+ # Adds a separator string to self, used in to_s.
71
189
  def separator(str)
72
190
  @options << str
73
191
  end
74
192
 
75
193
  # Registers the option with self by adding opt to options and mapping
76
- # the opt switches. Raises an error for conflicting keys and switches.
194
+ # the opt switches. Raises an error for conflicting switches.
77
195
  def register(opt)
78
196
  @options << opt unless @options.include?(opt)
79
197
 
@@ -87,36 +205,50 @@ class ConfigParser
87
205
 
88
206
  opt
89
207
  end
90
-
91
- # Defines and registers a config with self.
92
- def define(key, default_value=nil, options={})
93
- # check for conflicts and register
94
- if default_config.has_key?(key)
95
- raise ArgumentError, "already set by a different option: #{key.inspect}"
96
- end
97
- default_config[key] = default_value
98
-
99
- block = case options[:type]
100
- when :switch then setup_switch(key, default_value, options)
101
- when :flag then setup_flag(key, default_value, options)
102
- when :list then setup_list(key, options)
103
- when nil then setup_option(key, options)
104
- when respond_to?("setup_#{options[:type]}")
105
- send("setup_#{options[:type]}", key, default_value, options)
106
- else
107
- raise ArgumentError, "unsupported type: #{options[:type]}"
108
- end
109
-
110
- on(options, &block)
111
- end
112
208
 
209
+ # Constructs an Option using args and registers it with self. Args may
210
+ # contain (in any order) a short switch, a long switch, and a description
211
+ # string. Either the short or long switch may signal that the option
212
+ # should take an argument by providing an argument name.
213
+ #
214
+ # psr = ConfigParser.new
215
+ #
216
+ # # this option takes an argument
217
+ # psr.on('-s', '--long ARG_NAME', 'description') do |value|
218
+ # # ...
219
+ # end
220
+ #
221
+ # # so does this one
222
+ # psr.on('-o ARG_NAME', 'description') do |value|
223
+ # # ...
224
+ # end
225
+ #
226
+ # # this option does not
227
+ # psr.on('-f', '--flag') do
228
+ # # ...
229
+ # end
230
+ #
231
+ # A switch-style option can be specified by prefixing the long switch with
232
+ # '--[no-]'. Switch options will pass true to the block for the positive
233
+ # form and false for the negative form.
234
+ #
235
+ # psr.on('--[no-]switch') do |value|
236
+ # # ...
237
+ # end
238
+ #
239
+ # Args may also contain a trailing hash defining all or part of the option:
240
+ #
241
+ # psr.on('-k', :long => '--key', :desc => 'description')
242
+ # # ...
243
+ # end
244
+ #
113
245
  def on(*args, &block)
114
- options = args.last.kind_of?(Hash) ? args.pop : {}
246
+ attributes = args.last.kind_of?(Hash) ? args.pop : {}
115
247
  args.each do |arg|
116
248
  # split switch arguments... descriptions
117
249
  # still won't match as a switch even
118
250
  # after a split
119
- switch, arg_name = arg.split(' ', 2)
251
+ switch, arg_name = arg.kind_of?(String) ? arg.split(' ', 2) : arg
120
252
 
121
253
  # determine the kind of argument specified
122
254
  key = case switch
@@ -126,37 +258,158 @@ class ConfigParser
126
258
  end
127
259
 
128
260
  # check for conflicts
129
- if options[key]
130
- raise ArgumentError, "conflicting #{key} options: [#{options[key]}, #{arg}]"
261
+ if attributes[key]
262
+ raise ArgumentError, "conflicting #{key} options: [#{attributes[key].inspect}, #{arg.inspect}]"
131
263
  end
132
264
 
133
- # set the option
265
+ # set the option attributes
134
266
  case key
135
267
  when :long, :short
136
- options[key] = switch
137
- options[:arg_name] = arg_name.strip if arg_name
268
+ attributes[key] = switch
269
+ attributes[:arg_name] = arg_name if arg_name
138
270
  else
139
- options[key] = arg.strip
271
+ attributes[key] = arg
140
272
  end
141
273
  end
142
274
 
143
- # check if the option is specifying a Switch
275
+ # check if a switch-style option is specified
144
276
  klass = case
145
- when options[:long].to_s =~ /^--\[no-\](.*)$/
146
- options[:long] = "--#{$1}"
277
+ when attributes[:long].to_s =~ /^--\[no-\](.*)$/
278
+ attributes[:long] = "--#{$1}"
147
279
  Switch
148
280
  else
149
281
  Option
150
282
  end
151
283
 
152
284
  # instantiate and register the new option
153
- register klass.new(options, &block)
285
+ register klass.new(attributes, &block)
286
+ end
287
+
288
+ # Defines and registers a config-style option with self. Define does not
289
+ # take a block; the default value will be added to config, and any parsed
290
+ # value will override the default. Normally the key will be turned into
291
+ # the long switch; specify an alternate long, a short, description, etc
292
+ # using attributes.
293
+ #
294
+ # psr = ConfigParser.new
295
+ # psr.define(:one, 'default')
296
+ # psr.define(:two, 'default', :long => '--long', :short => '-s')
297
+ #
298
+ # psr.parse("--one one --long two")
299
+ # psr.config # => {:one => 'one', :two => 'two'}
300
+ #
301
+ # Define support several types of configurations that define a special
302
+ # block to handle the values parsed from the command line. See the
303
+ # 'setup_<type>' methods in Utils. Any type with a corresponding setup
304
+ # method is valid:
305
+ #
306
+ # psr = ConfigParser.new
307
+ # psr.define(:flag, false, :type => :flag)
308
+ # psr.define(:switch, false, :type => :switch)
309
+ # psr.define(:list, [], :type => :list)
310
+ #
311
+ # psr.parse("--flag --switch --list one --list two --list three")
312
+ # psr.config # => {:flag => true, :switch => true, :list => ['one', 'two', 'three']}
313
+ #
314
+ # New, valid types may be added by implementing new setup_<type> methods
315
+ # following this pattern:
316
+ #
317
+ # module SpecialType
318
+ # def setup_special(key, default_value, attributes)
319
+ # # modify attributes if necessary
320
+ # attributes[:long] = "--#{key}"
321
+ # attributes[:arg_name] = 'ARG_NAME'
322
+ #
323
+ # # return a block handling the input
324
+ # lambda {|input| config[key] = input.reverse }
325
+ # end
326
+ # end
327
+ #
328
+ # psr = ConfigParser.new.extend SpecialType
329
+ # psr.define(:opt, false, :type => :special)
330
+ #
331
+ # psr.parse("--opt value")
332
+ # psr.config # => {:opt => 'eulav'}
333
+ #
334
+ # Define raises an error if key is already set by a different option.
335
+ def define(key, default_value=nil, attributes={})
336
+ # check for conflicts and register
337
+ if default_config.has_key?(key)
338
+ raise ArgumentError, "already set by a different option: #{key.inspect}"
339
+ end
340
+ default_config[key] = default_value
341
+
342
+ # ensure setup does not modifiy input attributes
343
+ attributes = attributes.dup
344
+
345
+ block = case attributes[:type]
346
+ when :switch then setup_switch(key, default_value, attributes)
347
+ when :flag then setup_flag(key, default_value, attributes)
348
+ when :list then setup_list(key, attributes)
349
+ when nil then setup_option(key, attributes)
350
+ else
351
+ if respond_to?("setup_#{attributes[:type]}")
352
+ send("setup_#{attributes[:type]}", key, default_value, attributes)
353
+ else
354
+ raise ArgumentError, "unsupported type: #{attributes[:type]}"
355
+ end
356
+ end
357
+
358
+ on(attributes, &block)
154
359
  end
155
360
 
156
- # Parse is non-destructive to argv. If a string argv is provided, parse
157
- # splits it into an array using Shellwords.
361
+ # Adds a hash of delegates (for example the configurations for a Configurable
362
+ # class) to self. Delegates will be sorted by their :declaration_order
363
+ # attribute, then added like:
364
+ #
365
+ # define(key, delegate.default, delegate.attributes)
366
+ #
367
+ # ==== Nesting
368
+ #
369
+ # When you nest Configurable classes, a special syntax is necessary to
370
+ # specify nested configurations in a flat format compatible with the
371
+ # command line. As such, nested delegates, ie delegates with a
372
+ # DelegateHash as a default value, are recursively added with their
373
+ # key as a prefix. For instance:
374
+ #
375
+ # delegate_hash = DelegateHash.new(:key => Delegate.new(:reader))
376
+ # delegates = {}
377
+ # delegates[:nest] = Delegate.new(:reader, :writer=, delegate_hash)
158
378
  #
159
- def parse(argv=ARGV, config={})
379
+ # psr = ConfigParser.new
380
+ # psr.add(delegates)
381
+ # psr.parse('--nest:key value')
382
+ #
383
+ # psr.config # => {'nest:key' => 'value'}
384
+ # psr.nested_config # => {'nest' => {'key' => 'value'}}
385
+ #
386
+ # Side note: The fact that all the keys end up as strings underscores
387
+ # the importance of having indifferent access for delegates. If you
388
+ # set use_indifferent_access(false), be prepared to symbolize nested
389
+ # keys as necessary.
390
+ #
391
+ def add(delegates, nesting=nil)
392
+ delegates.each_pair do |key, delegate|
393
+ key = nesting ? "#{nesting}:#{key}" : key
394
+ default = delegate.default(false)
395
+
396
+ if delegate.is_nest?
397
+ add(default.delegates, key)
398
+ else
399
+ define(key, default, delegate.attributes)
400
+ end
401
+ end
402
+ end
403
+
404
+ # Parses options from argv in a non-destructive manner and returns an
405
+ # array of arguments remaining after options have been removed. If a
406
+ # string argv is provided, it will be splits into an array using
407
+ # Shellwords.
408
+ #
409
+ # A config may be provided to receive configurations; it will be set
410
+ # as self.config.
411
+ #
412
+ def parse(argv=ARGV, config={}, merge_default=true)
160
413
  @config = config
161
414
  argv = argv.kind_of?(String) ? Shellwords.shellwords(argv) : argv.dup
162
415
  args = []
@@ -192,23 +445,20 @@ class ConfigParser
192
445
 
193
446
  default_config.each_pair do |key, default|
194
447
  config[key] = default unless config.has_key?(key)
195
- end
448
+ end if merge_default
196
449
 
197
450
  args
198
451
  end
199
452
 
200
453
  # Same as parse, but removes parsed args from argv.
201
- def parse!(argv=ARGV, config={})
202
- argv.replace(parse(argv, config))
454
+ def parse!(argv=ARGV, config={}, merge_default=true)
455
+ result = parse(argv, config, merge_default)
456
+ argv.kind_of?(Array) ? argv.replace(result) : result
203
457
  end
204
-
458
+
459
+ # Converts the options and separators in self into a help string suitable for
460
+ # display on the command line.
205
461
  def to_s
206
- comments = @options.collect do |option|
207
- next unless option.respond_to?(:desc)
208
- option.desc.kind_of?(Lazydoc::Comment) ? option.desc : nil
209
- end.compact
210
- Lazydoc.resolve_comments(comments)
211
-
212
462
  @options.collect do |option|
213
463
  option.to_s.rstrip
214
464
  end.join("\n") + "\n"