Class: ConfigParser

Inherits:
Object show all
Includes:
Utils
Defined in:
lib/config_parser.rb,
lib/config_parser/utils.rb,
lib/config_parser/option.rb,
lib/config_parser/switch.rb

Overview

ConfigParser is the Configurable equivalent of OptionParser and uses a similar, simplified (see below) syntax to declare options.

opts = {}
parser = ConfigParser.new do |psr|
  psr.on "-s", "--long LONG", "a standard option" do |value|
    opts[:long] = value
  end

  psr.on "--[no-]switch", "a switch" do |value|
    opts[:switch] = value
  end

  psr.on "--flag", "a flag" do
    # note: no value is parsed; the block 
    # only executes if the flag is found
    opts[:flag] = true
  end
end

parser.parse("a b --long arg --switch --flag c")   # => ['a', 'b', 'c']
opts             # => {:long => 'arg', :switch => true, :flag => true}

ConfigParser formalizes this pattern of setting values in a hash as they occur, and adds the ability to specify default values. The syntax is not quite as friendly as for ordinary options, but meshes well with Configurable classes:

psr = ConfigParser.new
psr.define(:key, 'default', :desc => 'a standard option')

psr.parse('a b --key option c')                 # => ['a', 'b', 'c']
psr.config                                      # => {:key => 'option'}

psr.parse('a b c')                              # => ['a', 'b', 'c']
psr.config                                      # => {:key => 'default'}

And now directly from a Configurable class, the equivalent of the original example:

class ConfigClass
  include Configurable

  config :long, 'default', :short => 's'  # a standard option
  config :switch, false, &c.switch        # a switch
  config :flag, false, &c.flag            # a flag
end

psr = ConfigParser.new
psr.add(ConfigClass.configurations)

psr.parse("a b --long arg --switch --flag c")   # => ['a', 'b', 'c']
psr.config    # => {:long => 'arg', :switch => true, :flag => true}

psr.parse("a b --long=arg --no-switch c")       # => ['a', 'b', 'c']
psr.config    # => {:long => 'arg', :switch => false, :flag => false}

psr.parse("a b -sarg c")                        # => ['a', 'b', 'c']
psr.config    # => {:long => 'arg', :switch => false, :flag => false}

As you might expect, config attributes are used by ConfigParser to correctly build a corresponding option. In configurations like :switch, the block implies the => :switch attribute and so the config is made into a switch-style option by ConfigParser.

Use the to_s method to convert a ConfigParser into command line documentation:

"\nconfigurations:\n#{psr.to_s}"
# => %q{
# configurations:
#     -s, --long LONG                  a standard option
#         --[no-]switch                a switch
#         --flag                       a flag
# }

Simplifications

ConfigParser simplifies the OptionParser syntax for ‘on’. ConfigParser does not support automatic conversion of values, gets rid of ‘optional’ arguments for options, and only supports a single description string. Hence:

psr = ConfigParser.new

# incorrect, raises error as this will look
# like multiple descriptions are specified
psr.on("--delay N", 
       Float,
       "Delay N seconds before executing")        # !> ArgumentError

# correct
psr.on("--delay N", "Delay N seconds before executing") do |value|
  value.to_f
end

# this ALWAYS requires the argument and raises
# an error because multiple descriptions are
# specified
psr.on("-i", "--inplace [EXTENSION]",
       "Edit ARGV files in place",
       "  (make backup if EXTENSION supplied)")   # !> ArgumentError

# correct
psr.on("-i", "--inplace EXTENSION", 
       "Edit ARGV files in place\n  (make backup if EXTENSION supplied)")

Defined Under Namespace

Modules: Utils Classes: Option, Switch

Constant Summary

Constants included from Utils

Utils::ALT_SHORT_OPTION, Utils::LONG_OPTION, Utils::OPTION_BREAK, Utils::SHORT_OPTION

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Utils

longify, prefix_long, setup_flag, setup_list, setup_option, setup_switch, shortify

Constructor Details

#initialize(config = {}) {|_self| ... } ⇒ ConfigParser

Initializes a new ConfigParser and passes it to the block, if given.

Yields:

  • (_self)

Yield Parameters:

  • _self (ConfigParser)

    the object that the method was called on



166
167
168
169
170
171
172
173
# File 'lib/config_parser.rb', line 166

def initialize(config={})
  @options = []
  @switches = {}
  @config = config
  @default_config = {}

  yield(self) if block_given?
end

Instance Attribute Details

#configObject

The hash receiving configurations produced by parse.



160
161
162
# File 'lib/config_parser.rb', line 160

def config
  @config
end

#default_configObject (readonly)

A hash of default configurations merged into config during parse.



163
164
165
# File 'lib/config_parser.rb', line 163

def default_config
  @default_config
end

#switchesObject (readonly)

A hash of (switch, Option) pairs mapping command line switches like ‘-s’ or ‘–long’ to the Option that handles them.



157
158
159
# File 'lib/config_parser.rb', line 157

def switches
  @switches
end

Class Method Details

.nest(hash, split_char = ":") ⇒ Object

Splits and nests compound keys of a hash.

ConfigParser.nest('key' => 1, 'compound:key' => 2)
# => {
# 'key' => 1,
# 'compound' => {'key' => 2}
# }

Nest does not do any consistency checking, so be aware that results will be ambiguous for overlapping compound keys.

ConfigParser.nest('key' => {}, 'key:overlap' => 'value')
# =? {'key' => {}}
# =? {'key' => {'overlap' => 'value'}}


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/config_parser.rb', line 131

def nest(hash, split_char=":")
  result = {}
  hash.each_pair do |compound_key, value|
    if compound_key.kind_of?(String)
      keys = compound_key.split(split_char)
  
      unless keys.length == 1
        nested_key = keys.pop
        nested_hash = keys.inject(result) {|target, key| target[key] ||= {}}
        nested_hash[nested_key] = value
        next
      end
    end

    result[compound_key] = value
  end
  
  result
end

Instance Method Details

#[](key) ⇒ Object

Returns the config value for key.



176
177
178
# File 'lib/config_parser.rb', line 176

def [](key)
  config[key]
end

#[]=(key, value) ⇒ Object

Sets the config value for key.



181
182
183
# File 'lib/config_parser.rb', line 181

def []=(key, value)
  config[key] = value
end

#add(delegates, nesting = nil) ⇒ Object

Adds a hash of delegates (for example the configurations for a Configurable class) to self. Delegates will be sorted by their :declaration_order attribute, then added like:

define(key, delegate.default, delegate.attributes)

Nesting

When you nest Configurable classes, a special syntax is necessary to specify nested configurations in a flat format compatible with the command line. As such, nested delegates, ie delegates with a DelegateHash as a default value, are recursively added with their key as a prefix. For instance:

delegate_hash = DelegateHash.new(:key => Delegate.new(:reader))
delegates = {}
delegates[:nest] = Delegate.new(:reader, :writer=, delegate_hash)

psr = ConfigParser.new
psr.add(delegates)
psr.parse('--nest:key value')

psr.config                 # => {'nest:key' => 'value'}
psr.nested_config          # => {'nest' => {'key' => 'value'}}

Side note: The fact that all the keys end up as strings underscores the importance of having indifferent access for delegates. If you set use_indifferent_access(false), be prepared to symbolize nested keys as necessary.



402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/config_parser.rb', line 402

def add(delegates, nesting=nil)
  delegates.each_pair do |key, delegate|
    key = nesting ? "#{nesting}:#{key}" : key
    default = delegate.default(false)
    
    if delegate.is_nest?
      unless delegate[:type] == :hidden
        add(default.delegates, key)
      end
    else
      define(key, default, delegate.attributes)
    end
  end
end

#define(key, default_value = nil, attributes = {}) ⇒ Object

Defines and registers a config-style option with self. Define does not take a block; the default value will be added to config, and any parsed value will override the default. Normally the key will be turned into the long switch; specify an alternate long, a short, description, etc using attributes.

psr = ConfigParser.new
psr.define(:one, 'default')
psr.define(:two, 'default', :long => '--long', :short => '-s')

psr.parse("--one one --long two")
psr.config             # => {:one => 'one', :two => 'two'}

Define support several types of configurations that define a special block to handle the values parsed from the command line. See the ‘setup_<type>’ methods in Utils. Any type with a corresponding setup method is valid:

psr = ConfigParser.new
psr.define(:flag, false, :type => :flag)
psr.define(:switch, false, :type => :switch)
psr.define(:list, [], :type => :list)

psr.parse("--flag --switch --list one --list two --list three")
psr.config             # => {:flag => true, :switch => true, :list => ['one', 'two', 'three']}

New, valid types may be added by implementing new setup_<type> methods following this pattern:

module SpecialType
  def setup_special(key, default_value, attributes)
    # modify attributes if necessary
    attributes[:long] = "--#{key}"
    attributes[:arg_name] = 'ARG_NAME'

    # return a block handling the input
    lambda {|input| config[key] = input.reverse }
  end
end

psr = ConfigParser.new.extend SpecialType
psr.define(:opt, false, :type => :special)

psr.parse("--opt value")
psr.config             # => {:opt => 'eulav'}

The :hidden type causes no configuration to be defined. Raises an error if key is already set by a different option.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/config_parser.rb', line 346

def define(key, default_value=nil, attributes={})
  # check for conflicts and register
  if default_config.has_key?(key)
    raise ArgumentError, "already set by a different option: #{key.inspect}"
  end
  default_config[key] = default_value
  
  # ensure setup does not modifiy input attributes
  attributes = attributes.dup
  
  block = case attributes[:type]
  when :switch then setup_switch(key, default_value, attributes)
  when :flag   then setup_flag(key, default_value, attributes)
  when :list, :list_select then setup_list(key, attributes)
  when :hidden then return nil
  else
    if respond_to?("setup_#{attributes[:type]}")
      send("setup_#{attributes[:type]}", key, default_value, attributes)
    else
      setup_option(key, attributes)
    end
  end
  
  on(attributes, &block)
end

#nested_configObject

Returns the nested form of config (see ConfigParser.nest). Primarily useful when nested configurations have been added with add.



187
188
189
# File 'lib/config_parser.rb', line 187

def nested_config
  ConfigParser.nest(config)
end

#on(*args, &block) ⇒ Object

Constructs an Option using args and registers it with self. Args may contain (in any order) a short switch, a long switch, and a description string. Either the short or long switch may signal that the option should take an argument by providing an argument name.

psr = ConfigParser.new

# this option takes an argument
psr.on('-s', '--long ARG_NAME', 'description') do |value|
  # ...
end

# so does this one
psr.on('-o ARG_NAME', 'description') do |value|
  # ...
end

# this option does not
psr.on('-f', '--flag') do
  # ...
end

A switch-style option can be specified by prefixing the long switch with ‘–[no-]’. Switch options will pass true to the block for the positive form and false for the negative form.

psr.on('--[no-]switch') do |value|
  # ...
end

Args may also contain a trailing hash defining all or part of the option:

psr.on('-k', :long => '--key', :desc => 'description')
  # ...
end


255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/config_parser.rb', line 255

def on(*args, &block)
  attributes = args.last.kind_of?(Hash) ? args.pop : {}
  args.each do |arg|
    # split switch arguments... descriptions
    # still won't match as a switch even
    # after a split
    switch, arg_name = arg.kind_of?(String) ? arg.split(' ', 2) : arg
    
    # determine the kind of argument specified
    key = case switch
    when SHORT_OPTION then :short
    when LONG_OPTION  then :long
    else :desc
    end
    
    # check for conflicts
    if attributes[key]
      raise ArgumentError, "conflicting #{key} options: [#{attributes[key].inspect}, #{arg.inspect}]"
    end
    
    # set the option attributes
    case key
    when :long, :short
      attributes[key] = switch
      attributes[:arg_name] = arg_name if arg_name
    else
      attributes[key] = arg
    end
  end
  
  # check if a switch-style option is specified
  klass = case
  when attributes[:long].to_s =~ /^--\[no-\](.*)$/ 
    attributes[:long] = "--#{$1}"
    Switch
  else 
    Option
  end
  
  # instantiate and register the new option
  register klass.new(attributes, &block) 
end

#optionsObject

Returns an array of the options registered with self.



192
193
194
195
196
# File 'lib/config_parser.rb', line 192

def options
  @options.select do |opt|
    opt.kind_of?(Option)
  end
end

#parse(argv = ARGV, options = {}) ⇒ Object

Parses options from argv in a non-destructive manner and returns an array of arguments remaining after options have been removed. If a string argv is provided, it will be splits into an array using Shellwords.

Options

clear_config

clears the currently parsed configs (true)

add_defaults

adds the default values to config (true)

ignore_unknown_options

causes unknown options to be ignored (false)



428
429
430
431
# File 'lib/config_parser.rb', line 428

def parse(argv=ARGV, options={})
  argv = argv.dup unless argv.kind_of?(String)
  parse!(argv, options)
end

#parse!(argv = ARGV, options = {}) ⇒ Object

Same as parse, but removes parsed args from argv.



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/config_parser.rb', line 434

def parse!(argv=ARGV, options={})
  options = {
    :clear_config => true,
    :add_defaults => true,
    :ignore_unknown_options => false
  }.merge(options)
  
  config.clear if options[:clear_config]
  argv = Shellwords.shellwords(argv) if argv.kind_of?(String)
  args = []
  
  while !argv.empty?
    arg = argv.shift

    # determine if the arg is an option
    unless arg.kind_of?(String) && arg[0] == ?-
      args << arg
      next
    end
    
    # add the remaining args and break
    # for the option break
    if arg == OPTION_BREAK
      args.concat(argv)
      break
    end

    # split the arg...
    # switch= $1
    # value = $4 || $3 (if arg matches SHORT_OPTION, value is $4 or $3 otherwise)
    arg =~ LONG_OPTION || arg =~ SHORT_OPTION || arg =~ ALT_SHORT_OPTION 

    # lookup the option
    unless option = @switches[$1]
      if options[:ignore_unknown_options]
        args << arg
        next
      end
      
      raise "unknown option: #{$1}"
    end

    option.parse($1, $4 || $3, argv)
  end
  
  default_config.each_pair do |key, default|
    config[key] = default unless config.has_key?(key)
  end if options[:add_defaults]
  
  argv.replace(args)
  argv
end

#register(opt) ⇒ Object

Registers the option with self by adding opt to options and mapping the opt switches. Raises an error for conflicting switches.



205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/config_parser.rb', line 205

def register(opt)
  @options << opt unless @options.include?(opt)

  opt.switches.each do |switch|
    case @switches[switch]
    when opt then next
    when nil then @switches[switch] = opt
    else raise ArgumentError, "switch is already mapped to a different option: #{switch}"
    end
  end

  opt
end

#separator(str) ⇒ Object

Adds a separator string to self, used in to_s.



199
200
201
# File 'lib/config_parser.rb', line 199

def separator(str)
  @options << str
end

#to_sObject

Converts the options and separators in self into a help string suitable for display on the command line.



489
490
491
492
493
# File 'lib/config_parser.rb', line 489

def to_s
  @options.collect do |option|
    option.to_s.rstrip
  end.join("\n") + "\n"
end