module Main class Parameter class Error < StandardError include Softspoken attribute 'wrapped' end class Arity < Error; end class NotGiven < Arity; 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 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 attribute 'error_handler_before' attribute 'error_handler_instead' attribute 'error_handler_after' 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? adding_handlers do check_arity apply_casting check_validation end true end def check_arity (raise Arity, "#{ typename })" if values.size.zero? and argument_required?) unless arity == -1 if arity >= 0 min = arity sign = '' else min = arity.abs - 1 sign = '-' end arity = min if values.size < arity if argument_required? or argument_none? raise Arity, "#{ typename }) #{ values.size }/#{ sign }#{ arity }" if(values.size < arity) elsif argument_optional? raise Arity, "#{ typename }) #{ values.size }/#{ sign }#{ arity }" if(values.size < arity and values.size > 0) end end end def apply_casting if cast? op = cast.respond_to?('call') ? cast : Cast[cast] values.map! do |val| Parameter.wrap_errors{ op.call val } end end end def check_validation if validate? values.each do |value| validate[value] or raise InValid, "invalid: #{ typename }=#{ value.inspect }" end end end def add_handlers e esc = class << e self end this = self %w[ before instead after ].each do |which| getter = "error_handler_#{ which }" query = "error_handler_#{ which }?" if send(query) handler = send getter esc.module_eval do define_method(getter) do |main| main.instance_eval_block self, &handler end end end end end def adding_handlers begin yield rescue Exception => e add_handlers e raise 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 ||= options 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 ex.extend Softspoken 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| if p.arity >= 0 p.arity.times do break if argv.empty? value = argv.shift p.add_value value end else arity = p.arity.abs - 1 arity.times do break if argv.empty? value = argv.shift p.add_value value end argv.size.times do value = argv.shift p.add_value value end 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| if(p.defaults? and (not p.given?)) p.defaults.each do |default| p.values << default # so as NOT to set 'given?' end end end end def validate! each do |p| p.adding_handlers do next if p.arity == -1 raise NotGiven, "#{ p.typename } not given" if(p.required? and (not p.given?)) end end end [:parameter, :option, :argument, :keyword, :environment].each do |m| define_method("#{ m }s"){ select{|p| p.type == m or m == :parameter} } define_method("has_#{ m }?") do |name, *names| catch :has do names = Cast.list_of_string name, *names send("#{ m }s").each do |param| common = Cast.list_of_string(param.names) & names throw :has, true unless common.empty? end false end 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 value = -1 if value.to_s == '*' p.arity = Integer value end def arity? p.arity? end def error which = :instead, &block p.send "error_handler_#{ which }=", block 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