Created
June 16, 2011 17:35
-
-
Save grignaak/1029763 to your computer and use it in GitHub Desktop.
Args parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There are a few things I wanted to point out.
First, the use of duck-typing and composition. There is no inheritance (including
extend
s orinclude
s) 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.