Skip to content

Instantly share code, notes, and snippets.

@grignaak
Created June 16, 2011 17:35
Show Gist options
  • Save grignaak/1029763 to your computer and use it in GitHub Desktop.
Save grignaak/1029763 to your computer and use it in GitHub Desktop.
Args parser
class Args
def self.expect &proc
parser = self.new
parser.instance_eval &proc
return parser
end
attr_reader :positional
def [](name)
@args.fetch(name.to_s, NullMarshaler).value
end
def initialize
@positional = []
@args = {}
end
def parse(args)
until args.empty?
arg = args.shift
marshaler(arg).parse(args)
end
end
def self.make_marshaler(*names, &create_parser)
names.each do |name|
define_method name do |*specs|
normalize_specs(specs).each do |name, default|
@args[name.to_s] = create_parser.call(default)
end
end
end
end
private
def normalize_specs(specs_or_names)
specs_or_names.inject({}) do |specs, spec_or_name|
spec = Hash.try_convert(spec_or_name) || {spec_or_name => nil}
spec.merge specs
end
end
def marshaler(arg)
@args.fetch(arg[/-(\w+)/, 1]) do |key|
@positional << arg
NullMarshaler
end
end
module NullMarshaler
def self.parse(args) end
def self.value() end
end
class BooleanMarshaler
Args.make_marshaler :boolean do |default|
BooleanMarshaler.new(default)
end
attr_reader :value
def initialize(default)
@value = default || false
end
def parse(args)
@value = true
end
end
class ArgumentMarshaler
def self.make_marshaler(base, *names, &convert)
Args.make_marshaler(*names) do |default|
ArgumentMarshaler.new(default || base, &convert)
end
end
make_marshaler 0, :integer, :int, &:to_i
make_marshaler '', :string, &:to_s
make_marshaler [], :integer_list, :int_list do |args|
args.split(',').map &:to_i
end
make_marshaler [], :string_list, :list do |args|
args.split(',')
end
attr_reader :value
def initialize(default, &convert)
@value = default
@convert = convert
end
def parse(args)
raise "last argument is missing value" if args.empty?
@value = @convert.call args.shift
end
end
end
require 'args'
describe Args, "example usage" do
it "should work using `expect'" do
args = Args.expect do
boolean :p,:q
integer :j,:i => 1
string :x
end
assert_example args
end
it "should work using `new'" do
args = Args.new
args.boolean :p, :q
args.integer :j, :i => 1
args.string :x
assert_example args
end
def assert_example(args)
args.parse %w{1 2 -p -x foo -nice}
args['p'].should == true
args[:q].should == false
args[:x].should == "foo"
args.positional.should == %w{1 2 -nice}
end
end
describe Args, "when parsing" do
def self.args(*its, &proc)
it its do
runner = ArgsRunner.new Args.expect {}
runner.instance_eval &proc
end
end
args "should default boolean arguments to false" do
boolean :p
parse ""
arg(:p).should == false
end
args "should set specified boolean args as true" do
boolean :p
parse "-p"
arg(:p).should == true
end
args "should accept two booleans" do
boolean :p, :q
parse "-p"
arg(:p).should == true
arg(:q).should == false
end
args "should recognize both booleans" do
boolean :p, :q
parse "-p -q"
arg(:p).should == true
arg(:q).should == true
end
args "should parse string arguments" do
string :s, :t
parse "-s yes"
arg(:s).should == 'yes'
arg(:t).should == ''
end
args "should parse integer arguments" do
integer :i,:j
parse "-i 1"
arg(:i).should == 1
arg(:j).should == 0
end
args "should not barf on unexpected inputs" do
parse ""
arg(:i).should == nil
end
args "should keep the unrecognized parameters" do
boolean :p
parse "1 -p 2 3 -f 4"
positional.should == %w{1 2 3 -f 4}
end
args "should accept default values" do
integer :i => 5, :j => 6
string :f => 'bar'
parse "-f foo"
arg(:i).should == 5
arg(:j).should == 6
arg(:f).should == 'foo'
end
args "should accept integer lists" do
integer_list :is
parse "-is 1,2,3,4"
arg(:is).should == [1, 2, 3, 4]
end
args "should accept string lists" do
list :ss
parse "-ss foo,bar,baz"
arg(:ss).should == %w{foo bar baz}
end
class ArgsRunner
def initialize(parser)
@parser = parser
end
def method_missing(name, *arguments)
@parser.send name, *arguments
end
def parse(argument_string)
@parser.parse argument_string.split
end
def arg(name)
@parser[name]
end
end
end
@grignaak
Copy link
Author

There are a few things I wanted to point out.

First, the use of duck-typing and composition. There is no inheritance (including extends or includes) nor are there any ifs (excepting a single assertion which isn't necessary). Note a module acts as a NullObject.

Second, the declarative additions to Args. This is seen best in ArgumentMarshaler.

Third, the wrapping of it in the specs. I think this made the tests extremely clear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment