module Main class Parameter class Error < StandardError; attribute 'wrapped'; end class NotGiven < Error; end class Arity < Error; end class InValid < Error; end class NoneSuch < Error; end class AmbigousOption < Error; end class NeedlessArgument < Error; end class MissingArgument < Error; end class InvalidOption < Error; end class << self def new *a, &b raise end def wrapped_error w e = Error.new "(#{ w.message } (#{ w.class }))" e.wrapped = w e.set_backtrace(w.backtrace || []) e end def wrap_errors begin yield rescue => e raise wrapped_error(e) end end Types = [] def inherited other Types << other end def sym @sym ||= name.split(%r/::/).last.downcase.to_sym end def class_for type sym = type.to_s.downcase.to_sym c = Types.detect{|t| t.sym == sym} raise ArgumentError, type.inspect unless c c end def create type, *a, &b c = class_for type obj = c.allocate obj.type = c.sym obj.instance_eval{ initialize *a, &b } obj end end attribute 'type' attribute 'names' attribute 'abbreviations' attribute 'argument' attribute 'given' attribute 'cast' attribute 'validate' attribute 'description' attribute 'synopsis' attribute('values'){ [] } attribute('defaults'){ [] } attribute 'arity' => 1 attribute 'required' => false def initialize name, *names, &block @names = Cast.list_of_string name, *names @names.map! do |name| if name =~ %r/^-+/ name.gsub! %r/^-+/, '' end if name =~ %r/=.*$/ argument( name =~ %r/=\s*\[.*$/ ? :optional : :required ) name.gsub! %r/=.*$/, '' end name end @names = @names.sort.reverse @names[1..-1].each do |name| raise ArgumentError, "only one long name allowed (#{ @names.inspect })" if name.size > 1 end DSL.evaluate(self, &block) if block end def name names.first end def default defaults.first end def typename prefix = '--' if type.to_s =~ %r/option/ "#{ type }(#{ prefix }#{ name })" end def add_value value given true values << value end def value values.first end def argument_none? argument.nil? end def argument_required? argument and argument.to_s.downcase.to_sym == :required end def argument_optional? argument and argument.to_s.downcase.to_sym == :optional end def optional? not required? end def optional= bool self.required !bool end def setup! return false unless given? validate_arity! cast! validate! true end def validate_arity! raise Arity, "#{ typename })" if values.size.zero? and argument_required? if values.size < arity if argument_required? raise Arity, "#{ typename }) #{ values.size }/#{ arity }" if(values.size < arity) elsif argument_optional? raise Arity, "#{ typename }) #{ values.size }/#{ arity }" if(values.size < arity and values.size > 0) end end end def validate! if validate? values.each do |value| validate[value] or raise InValid, "#{ typename }=#{ value.inspect }" end end end def cast! if cast? op = cast.respond_to?('call') ? cast : Cast[cast] values.map! do |val| Parameter.wrap_errors{ op.call val } end end end class Argument < Parameter attribute 'required' => true attribute 'synopsis' do label = name op = required ? "->" : "~>" value = defaults.size > 0 ? "#{ name }=#{ defaults.join ',' }" : name value = "#{ cast }(#{ value })" if cast "#{ label } [ #{ arity } #{ op } #{ value } ]" end end class Option < Parameter attribute 'required' => false attribute 'synopsis' do long, *short = names value = cast || name rhs = argument ? (argument == :required ? "=#{ name }" : "=[#{ name }]") : nil label = ["--#{ long }#{ rhs }", short.map{|s| "-#{ s }"}].flatten.join(", ") unless argument_none? op = required ? "->" : "~>" value = defaults.size > 0 ? "#{ name }=#{ defaults.join ',' }" : name value = "#{ cast }(#{ value })" if cast "#{ label } [ #{ arity } #{ op } #{ value } ]" else label end end end class Keyword < Parameter attribute 'required' => false attribute 'argument' => :required attribute 'synopsis' do label = "#{ name }=#{ name }" op = required ? "->" : "~>" value = defaults.size > 0 ? "#{ name }=#{ defaults.join ',' }" : name value = "#{ cast }(#{ value })" if cast "#{ label } [ #{ arity } #{ op } #{ value } ]" end end class Environment < Parameter attribute 'argument' => :required attribute 'synopsis' do label = "env[#{ name }]=#{ name }" op = required ? "->" : "~>" value = defaults.size > 0 ? "#{ name }=#{ defaults.join ',' }" : name value = "#{ cast }(#{ value })" if cast "#{ label } [ #{ arity } #{ op } #{ value } ]" end end class List < ::Array def parse argv, env parse_options argv return 'help' if detect{|p| p.name.to_s == 'help' and p.given?} parse_keywords argv parse_arguments argv parse_environment env defaults! validate! self end def parse_options argv, params = nil params ||= select{|p| p.type == :option} spec, h, s = [], {}, {} params.each do |p| head, *tail = p.names long = "--#{ head }" shorts = tail.map{|t| "-#{ t }"} type = if p.argument_required? then GetoptLong::REQUIRED_ARGUMENT elsif p.argument_optional? then GetoptLong::OPTIONAL_ARGUMENT else GetoptLong::NO_ARGUMENT end a = [ long, shorts, type ].flatten spec << a h[long] = p s[long] = a end begin GetoptLong.new(argv, *spec).each do |long, value| value = case s[long].last when GetoptLong::NO_ARGUMENT value.empty? ? true : value when GetoptLong::OPTIONAL_ARGUMENT value.empty? ? true : value when GetoptLong::REQUIRED_ARGUMENT value end p = h[long] p.add_value value end rescue GetoptLong::AmbigousOption, GetoptLong::NeedlessArgument, GetoptLong::MissingArgument, GetoptLong::InvalidOption => e c = Parameter.const_get e.class.name.split(/::/).last ex = c.new e.message ex.set_backtrace e.message raise ex end params.each do |p| p.setup! end end def parse_arguments argv, params=nil params ||= select{|p| p.type == :argument} params.each do |p| p.arity.times do break if argv.empty? value = argv.shift p.add_value value end end params.each do |p| p.setup! end end def parse_keywords argv, params=nil params ||= select{|p| p.type == :keyword} replacements = {} params.each do |p| names = p.names name = names.first kre = %r/^\s*(#{ names.join '|' })\s*=/ opt = "--#{ name }" i = 0 argv.each_with_index do |a, idx| b = argv[idx + 1] m, key, *ignored = kre.match("#{ a }#{ b }").to_a if m replacements[i] ||= a.gsub %r/^\s*#{ key }/, opt end i += 1 end end replacements.each do |i, r| argv[i] = r end parse_options argv, params end def parse_environment env, params=nil params ||= select{|p| p.type == :environment} params.each do |p| names = p.names name = names.first value = env[name] next unless value p.add_value value end params.each do |p| p.setup! end end def defaults! each do |p| #p.add_value p.default if(p.default? and (not p.given?)) if(p.defaults? and (not p.given?)) p.defaults.each do |default| #p.add_value default p.values << default # so as NOT to set given? end end end end def validate! each do |p| raise NotGiven, p.typename if(p.required? and (not p.given?)) end end end class DSL def self.evaluate param, &block new(param).evaluate(&block) end attr 'p' alias_method 'evaluate', 'instance_eval' def initialize param @p = param end def type sym p.type = sym end def type? p.type? end def synopsis arg p.synopsis arg end def argument arg p.argument arg end def argument_required bool = true if bool p.argument :required else p.argument false end end def argument_required? p.argument_required? end def argument_optional bool = true if bool p.argument :optional else p.argument false end end def argument_optional? p.argument_optional? end def required bool = true p.required = bool end def required? p.required? end def optional bool = true if bool p.required !bool else p.required bool end end def optional? p.optional? end def cast sym=nil, &b p.cast = sym || b end def cast? p.cast? end def validate sym=nil, &b p.validate = sym || b end def validate? p.validate? end def description s p.description = s.to_s end def description? p.description? end alias_method 'desc', 'description' def default value, *values p.defaults.push value p.defaults.push *values p.defaults end def defaults? p.defaults? end def defaults value, *values p.defaults.push value p.defaults.push *values p.defaults end def defaults? p.defaults? end def arity value p.arity = Integer value end def arity? p.arity? end end class Table < ::Array def initialize super() self.fields = [] extend BoundsCheck end module BoundsCheck def [] *a, &b p = super ensure raise NoneSuch, a.join(',') unless p end end end end end