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

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

Constructor Details

#initialize(config = {}, default_parse_options = {}) {|_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



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/config_parser.rb', line 198

def initialize(config={}, default_parse_options={})
  @registry = []
  @switches = {}
  @config = config
  @defaults = {}
  @default_parse_options = {
    :clear_config => true,
    :add_defaults => true,
    :ignore_unknown_options => false,
    :option_break => OPTION_BREAK,
    :keep_break => false
  }.merge(default_parse_options)

  yield(self) if block_given?
end

Instance Attribute Details

#configObject

The hash receiving configurations produced by parse.



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

def config
  @config
end

#default_parse_optionsObject (readonly)

A hash of default parsing options that adjust the behavior of parse (see parse).



195
196
197
# File 'lib/config_parser.rb', line 195

def default_parse_options
  @default_parse_options
end

#defaultsObject (readonly)

A hash of default configurations merged into config during parse. These defaults are defined as options are added to self (via define, add, etc) and do not need to be manually specified.



191
192
193
# File 'lib/config_parser.rb', line 191

def defaults
  @defaults
end

#registryObject (readonly)

Returns an array of the options registered with self, in the order in which they were added. Separators are also stored in the registry.



178
179
180
# File 'lib/config_parser.rb', line 178

def registry
  @registry
end

#switchesObject (readonly)

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



183
184
185
# File 'lib/config_parser.rb', line 183

def switches
  @switches
end

Class Method Details

.bind(config = {}) {|parser| ... } ⇒ Object

Generates a new parser bound to a specific config. All this really means is that each time the parser calls parse, configurations will be added to the config without clearing the config, or adding default values. This can be useful in signaling situations where a config needs to be updated multiple times.

psr = ConfigParser.bind
psr.define('a', 'default')
psr.define('b', 'default')

psr.parse %w{--a value}
psr.config                       # => {"a" => "value"}

psr.parse %w{--b value}
psr.config                       # => {"a" => "value", "b" => "value"}

Yields:

  • (parser)


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

def bind(config={})
  parser = new(config, :clear_config => false, :add_defaults => false)
  yield(parser) if block_given?
  parser
end

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



215
216
217
# File 'lib/config_parser.rb', line 215

def [](key)
  config[key]
end

#[]=(key, value) ⇒ Object

Sets the config value for key.



220
221
222
# File 'lib/config_parser.rb', line 220

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. Configs are 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 are recursively added with their key as a prefix. For instance:

class NestClass
  include Configurable
  nest :nest do
    config :key, 'value'
  end
end

psr = ConfigParser.new
psr.add(NestClass.configurations)
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.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/config_parser.rb', line 420

def add(delegates, nesting=nil)
  delegates.each_pair do |key, delegate|
    key = nesting ? "#{nesting}:#{key}" : key
    
    case delegate[:type]
    when :hidden
      next
    when :nest
      add(delegate.nest_class.configurations, key)
    else
      define(key, delegate.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.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/config_parser.rb', line 363

def define(key, default_value=nil, attributes={})
  # check for conflicts and register
  if defaults.has_key?(key)
    raise ArgumentError, "already set by a different option: #{key.inspect}"
  end
  defaults[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.



226
227
228
# File 'lib/config_parser.rb', line 226

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


306
307
308
# File 'lib/config_parser.rb', line 306

def on(*args, &block)
  register new_option(args, &block)
end

#on!(*args, &block) ⇒ Object

Same as on, but overrides options with overlapping switches.



311
312
313
# File 'lib/config_parser.rb', line 311

def on!(*args, &block)
  register new_option(args, &block), true
end

#optionsObject

Returns an array of the options registered with self.



231
232
233
234
235
# File 'lib/config_parser.rb', line 231

def options
  @registry.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)



446
447
448
449
# File 'lib/config_parser.rb', line 446

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.



452
453
454
455
456
457
458
459
460
461
# File 'lib/config_parser.rb', line 452

def parse!(argv=ARGV, options={})
  argv = Shellwords.shellwords(argv) if argv.kind_of?(String)
  
  args = []
  remainder = scan(argv, options) {|arg| args << arg}
  args.concat(remainder)
  argv.replace(args)
  
  argv
end

#register(opt, override = false) ⇒ Object

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

If override is specified, options with conflicting switches are removed and no error is raised. Note that this may remove multiple options.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/config_parser.rb', line 247

def register(opt, override=false)
  if override
    existing = opt.switches.collect do |switch|
      @switches.delete(switch)
    end
    @registry -= existing
  end
  
  unless @registry.include?(opt)
    @registry << opt
  end
  
  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

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



463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/config_parser.rb', line 463

def scan(argv=ARGV, options={})
  options = default_parse_options.merge(options)
  config.clear if options[:clear_config]
  
  option_break = options[:option_break]
  while !argv.empty?
    arg = argv.shift

    # determine if the arg is an option
    unless arg.kind_of?(String) && arg[0] == ?-
      yield(arg)
      next
    end
    
    # add the remaining args and break
    # for the option break
    if option_break === arg
      argv.unshift(arg) if options[:keep_break]
      break
    end

    # split the arg...
    # switch= $1
    # value = $2
    arg =~ LONG_OPTION || arg =~ SHORT_OPTION || arg =~ ALT_SHORT_OPTION 

    # lookup the option
    unless option = @switches[$1]
      raise "unknown option: #{$1 || arg}"
    end

    option.parse($1, $2, argv)
  end
  
  defaults.each_pair do |key, default|
    config[key] = default unless config.has_key?(key)
  end if options[:add_defaults]
  
  argv
end

#separator(str) ⇒ Object

Adds a separator string to self, used in to_s.



238
239
240
# File 'lib/config_parser.rb', line 238

def separator(str)
  @registry << str
end

#to_sObject

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



512
513
514
515
516
# File 'lib/config_parser.rb', line 512

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

#warn_ignored_args(args) ⇒ Object



504
505
506
507
508
# File 'lib/config_parser.rb', line 504

def warn_ignored_args(args)
  if args && !args.empty?
    warn "ignoring args: #{args.inspect}"
  end
end