configurable 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"