# # http://optiflag.rubyforge.org/ # # Author:: Daniel O. Eklund # Copyright:: Copyright (c) 2006 Daniel O. Eklund. All rights reserved. # License:: Ruby license. module OptiFlag VERSION = "0.6" end #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------- Clause-Level Flag-Modifiers via the EachFlag class ----------------------- #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ module OptiFlag module Flagset @dash_symbol = "-" attr_reader :dash_symbol module_function :dash_symbol def self.flag_symbol(val) @dash_symbol = val end def self.increment_order_counter() @counter ||= 0 @counter = @counter + 1 return @counter -1 end class EachFlag attr_reader :name, :flag, :ordered_added, :position_validator,:validation_error, :position_enumerated_values_validator,:the_pretranslate,:the_posttranslate, :the_posttranslate_all,:the_pretranslate_all,:enclosing_module, :the_description,:the_long_dash_symbol,:the_dash_symbol, :the_arity,:the_long_form,:the_alternate_forms,:the_is_required attr_writer :the_form_that_is_actually_used attr_accessor :value,:for_help,:for_extended_help,:proxied_bound_method def initialize(name,flag,enclosing_module) # these next two lines are a highly complicated hack needed to make # the use of two module definitions in one file, one with a flag_symbol # and one without.. See tc_change_symbols.rb for the two tests that used to # cause the problem... also see the changes as part of the definition of # OptiFlag.Flagset().... -- D.O.E 5/30/06 # Search for 'def OptiFlag.Flagset(hash)' in this file singleton_class_of_enclosing_module = class << enclosing_module; self; end; x = singleton_class_of_enclosing_module.included_modules.select do |x| (x.to_s =~ /OptiF/) or (x.to_s =~ /# 0 end end # top-level flag-declarer def extended_help_flag(*args) first,*rest = args optional_flag [first] do self.for_extended_help = true description "Extended Help" no_args alternate_forms *rest if rest.length > 0 end end # top-level flag-declarer def character_flag(switch,group="default",&the_block) throw "Character switches can only be 1 character long" if switch.to_s.length > 1 flag(switch.to_sym,&the_block) @group ||= {} the_flag_we_just_added = @all_flags[switch.to_sym] key = [group.to_sym,the_flag_we_just_added.the_dash_symbol] # puts "#{ key.join(',') }" @group[key] ||= [] @group[key] << the_flag_we_just_added # re-assert ourselves flag [switch.to_s, switch] do optional arity 0 compresses end end # top-level flag-declarer def flag_properties(symb,&the_block) raise "Block needed for flag_properties" if not block_given? @all_flags ||= {} obj = @all_flags[symb.to_sym] return if obj==nil obj.instance_eval &the_block if block_given? end alias :properties :flag_properties def argument_order(*args) puts "You want the arguments in this order: #{ args }" end end end # defining the callable client-interface module OptiFlag module Flagset module NewInterface attr_accessor :errors,:flag_value,:specification_errors,:help_requested_on,:warnings attr_writer :help_requested,:extended_help_requested def errors? self.errors != nil end def warnings? self.warnings != nil end def help_requested? @help_requested != nil end def extended_help_requested? @extended_help_requested !=nil end end class Errors attr_accessor :missing_flags,:other_errors,:validation_errors def initialize @missing_flags,@other_errors,@validation_errors = [],[],[] end def any_errors? @missing_flags.length >0 or @other_errors.length >0 or @validation_errors.length > 0 end def divulge_problems(output=$stdout) output.puts "Errors found:" if @missing_flags.length >0 output.puts "Missing Flags:" @missing_flags.each do |x| output.puts " #{ x }" end end if @other_errors.length >0 output.puts "Other Errors:" @other_errors.each do |x| output.puts " #{ x }" end end if @validation_errors.length >0 output.puts "Validation Errors:" @validation_errors.each do |x| output.puts " #{ x }" end end end end def create_new_value_class() klass = Hash.new klass.instance_eval do def init_with_these(all_objs) @all_flags = all_objs end end klass.init_with_these(@all_flags) @all_flags.each_value do |y| # only allow alphabetic symbols to create methods if (y.name.to_s =~ /^[a-zA-Z]+/) klass.instance_eval %{ def #{y.name}() @all_flags[:#{ y.name }].value if @all_flags[:#{ y.name }] end def #{y.name}_details() @all_flags[:#{ y.name }] if @all_flags[:#{ y.name }] end} end all_names = [y.name] all_names << y.the_alternate_forms if y.the_alternate_forms.length > 0 all_names.flatten! all_names = all_names.select{|x| x.to_s =~ /^[a-zA-Z]+/} all_names.each do |x| klass.instance_eval %{ def #{x}?() ret = @all_flags[:#{ y.name }].value return false if ret == nil return @all_flags[:#{ y.name }].value if @all_flags[:#{ y.name }] end} end end return klass end end # end of Flagset module end # end of OptiFlag module #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------- The actual Parsing and Error Finding Code ----------------------- #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------------ module OptiFlag module Flagset def parse(args,clone=true) safe_args = args.clone if clone safe_args = args if ! clone # the following 10 lines were changed so # that a module could reparse a command-line # and not have a global-state change for # everyone... I had mulled just mandating that # a SomeArgModule::parse(ARGV) statement # could only occur once, but since everything else # is getting ugly, might as well allow this # --D.O.E 5/30/2006 new_self = self.clone new_flags = {} @all_flags.each_pair do |key,val| val = val.clone new_flags[key] = val end new_self.module_eval do @all_flags = new_flags safe_args = search_for_missing_character_switches(safe_args) safe_args = create_api(safe_args) safe_args = search_for_missing_flags(safe_args) populate_values(safe_args) now_populate_hash(@all_flags,safe_args) end return safe_args end private def now_populate_hash(all_flags,safe_args) all_flags.each_pair do |k,v| safe_args.flag_value[k.to_sym] = v.value v.the_alternate_forms.each do |x| safe_args.flag_value[x.to_sym] = v.value end end end def find_help_flags(safe_args) arg_copy = safe_args.clone #first we get rid of all non-help flags... non_help_flags = @all_flags.values.select{|value| !value.for_help } non_help_flags.each do |val| flag_finder_and_stripper(val.as_the_form_that_is_actually_used,val.the_arity,arg_copy) end # ...because the help flag is overloaded... it can have an arity of 0 or 1 help_flag = @all_flags.values.select{|value| value.for_help } if help_flag.length > 0 flag = help_flag[0].as_the_form_that_is_actually_used found,discard = flag_finder_and_stripper(flag,1,arg_copy) if found.length > 0 safe_args.help_requested = true if found.length == 2 safe_args.help_requested_on = found[1] end end end arg_copy = safe_args.clone ext_help_flag = @all_flags.values.select{|value| value.for_extended_help } if ext_help_flag.length > 0 flag = ext_help_flag[0].as_the_form_that_is_actually_used found,discard = flag_finder_and_stripper(flag,0,arg_copy) if found.length > 0 safe_args.extended_help_requested = true end end end def flag_finder_and_stripper(flag,arity,args) idx = args.index(flag) if idx == nil return [], args end the_range = idx..(idx + arity) flag_and_values = args[the_range] fragment = args[the_range].clone args[the_range] = nil return fragment, args end def populate_values(safe_args) find_the_flag_that_is_actually_used(safe_args) arg_copy = safe_args.clone @all_flags.values.each do |flag_obj| the_string_flag = flag_obj.as_the_form_that_is_actually_used flag_and_values, arg_copy = flag_finder_and_stripper(the_string_flag,flag_obj.the_arity,arg_copy) if flag_and_values.length >2 discard,*theRest = flag_and_values flag_obj.value = theRest end flag_obj.value = flag_and_values[1] if flag_and_values.length ==2 flag_obj.value = true if flag_and_values.length == 1 and flag_obj.the_arity == 0 if flag_and_values.length == 1 and flag_obj.the_arity >0 problem = "Argument(s) missing for flag #{ flag_obj.as_the_form_that_is_actually_used }" safe_args.errors ||= Errors.new safe_args.errors.validation_errors << problem end end if arg_copy.length > 0 # is there anything left over safe_args.warnings ||= [] safe_args.warnings << "There are extra arguments left over: [#{ arg_copy.join(', ') }]. " end validate_values(safe_args) find_help_flags(safe_args) return safe_args end def validate_values(safe_args) run_pre_translate(safe_args) #TODO: the two following helper methods # are essentially the same... maybe, we can pull the # logic into the EachFlag object itself... # something to think about (D.O.E. 5/22/2006) validate_values_by_regexp(safe_args) validate_values_by_enumerated_values(safe_args) run_post_translate(safe_args) end def validate_values_by_enumerated_values(safe_args) flags_requiring_validation = @all_flags.values.select do |x| x.position_enumerated_values_validator.length > 0 && x.value end flags_requiring_validation.each do |flag_obj| value = flag_obj.value value = [value] if value.class != Array flag_obj.position_enumerated_values_validator.each_with_index do |enum_vals,idx| something_matches = enum_vals.select{|x| x.to_s == value[idx] } if something_matches.length == 0 problem = "For the flag: '#{ flag_obj.as_string_basic }' the value you gave was '#{ value[idx] }'." problem << "\n But the value must be one of the following: [#{ enum_vals.join(', ') }]" flag_obj.validation_error << problem safe_args.errors ||= Errors.new safe_args.errors.validation_errors << problem end end end end def validate_values_by_regexp(safe_args) flags_requiring_validation = @all_flags.values.select do |x| x.position_validator.length > 0 && x.value end flags_requiring_validation.each do |flag_obj| value = flag_obj.value value = [value] if value.class != Array flag_obj.position_validator.each_with_index do |(desc,regex),idx| if ! value[idx].match regex problem = "For the flag: '#{ flag_obj.as_string_basic }' the value you gave was '#{ value[idx] }'." problem << "\n #{ desc }" flag_obj.validation_error << problem safe_args.errors ||= Errors.new safe_args.errors.validation_errors << problem end end end end def run_pre_translate(safe_args) flags_requiring_pre_translating = @all_flags.values.select do |x| x.the_pretranslate && x.value end standard_translating(flags_requiring_pre_translating,:the_pretranslate) end def run_post_translate(safe_args) flags_requiring_post_translating = @all_flags.values.select do |x| x.the_posttranslate && x.value end standard_translating(flags_requiring_post_translating,:the_posttranslate) end def standard_translating(arr,pre_or_post) arr.each do |flag| flag.send(pre_or_post).each_with_index do |translate,idx| the_value = flag.value the_value = [the_value] if the_value.class != Array if translate.arity > 1 retVal = translate.call *the_value retVal ||= [] retVal = [retVal] if retVal.class !=Array if retVal.length != translate.arity raise "Error: the translate block you used had #{ translate.arity } arguments, but your block returned #{ retVal.length } values. They must be equal." end flag.value = retVal elsif translate.arity == 1 retVal = translate.call(the_value[idx]) flag.value[idx] = retVal end end end end def find_the_flag_that_is_actually_used(safe_args) there_might_be_errors = safe_args.errors || Errors.new args_copy = safe_args.clone @all_flags.values.each do |x| shortform,longform = x.as_string_basic,x.as_string_extended all_forms = [shortform,longform] + x.as_alternate_forms form_found_mask = all_forms.collect do |form| is_form_found, args_copy = flag_finder_and_stripper(form,x.the_arity,args_copy) [ (is_form_found.length > 0), is_form_found ] end any_found = form_found_mask.select{|(found,parms)| found} if any_found.length > 1 there_might_be_errors.other_errors << "More than one flag form of -- is present. This is ambiguous. Choose one only." end if any_found.length == 1 x.the_form_that_is_actually_used = any_found[0][1][0] end end if there_might_be_errors.any_errors? safe_args.errors = there_might_be_errors end return safe_args end def search_for_missing_character_switches(safe_args) these_args = safe_args.clone return safe_args if @group == nil chars_found_for_this_group = {} all_chars = "" @group.each_pair do |k,val| name_of_flag = val.collect{|x| x.name} all_chars_alphabetical = name_of_flag.join('').unpack('c*').sort.pack('c*') args_namespaced = these_args.select{|x| x.match("^#{ k[1] }") } seems_to_match = [] args_namespaced.each do |flag| flag_value = flag.match("^#{ k[1]}").post_match potential = all_chars_alphabetical.tr(flag_value,"") if potential.length == all_chars_alphabetical.length - flag_value.length seems_to_match << flag_value all_chars_alphabetical = all_chars_alphabetical.tr(flag_value,"") these_args = these_args - [flag] end end seems_to_match = seems_to_match.flatten.join('') chars_found_for_this_group[k] = seems_to_match all_chars << seems_to_match end all_chars.split(//).each do |x| opt_flag = @all_flags[x.to_sym] opt_flag.value = true end return safe_args end def search_for_missing_flags(safe_args) there_might_be_errors = Errors.new required_flags = @all_flags.values.sort{|x,y| x.ordered_added <=> y.ordered_added }.select{|x| x.the_is_required } args_copy = safe_args.clone required_flags.each do |x| shortform,longform = x.as_string_basic,x.as_string_extended all_forms = [shortform,longform] + x.as_alternate_forms form_found_mask = all_forms.collect do |form| is_form_found, args_copy = flag_finder_and_stripper(form,x.the_arity,args_copy) [ (is_form_found.length > 0), is_form_found ] end is_first_found,is_second_found = form_found_mask any_found = form_found_mask.select{|(found,parms)| found} if any_found.length == 0 there_might_be_errors.missing_flags << x.as_string_basic end if is_second_found[0] && is_first_found[0] there_might_be_errors.other_errors << "Both forms #{ x.as_string_basic } and #{x.as_string_extended } are present. This is ambiguous. Choose one only." end if any_found.length == 1 x.the_form_that_is_actually_used = any_found[0][1][0] end end if there_might_be_errors.any_errors? safe_args.errors = there_might_be_errors end return safe_args end def create_api(safe_args) safe_args.extend NewInterface safe_args.flag_value = create_new_value_class() return safe_args end end end module OptiFlag def OptiFlag.Flagset(hash) # changed this from just returning Flagset... # Reason Being: a user can specify two modules in one file # one with this method, and one just using Flagset... # if you don't clone at this point, you are left with # a global change... BUT to get the cloning working # I had to do some singleton_class trickeration as part of # the initialize method for EachFlag... I am not # 100% sure I understand what I just did. -- D.O.E 5/30/06 mod = Flagset.clone hash.each_pair do |symb,val| mod.send(symb.to_sym,val) end return mod end end module OptiFlag module Flagset def handle_errors_and_help(level=:not_strict) return if !@all_flags parse(ARGV,false) if ARGV.help_requested? if !ARGV.help_requested_on show_help elsif the_on = ARGV.help_requested_on show_individual_extended_help(the_on.to_sym) end exit end if ARGV.extended_help_requested? show_extended_help exit end if ARGV.errors? ARGV.errors.divulge_problems show_help exit end if ARGV.warnings? and level == :with_no_warnings puts "In strict warning handling mode. Warnings will cause process to exit." ARGV.warnings.each do |x| puts " #{ x }" end puts "Please fix these warnings and try again." exit end ARGV end # end of method handle_errors_and_help end # end of Flagset end # end of OptiFlag # Specification error possibilities (at FlagsetDefinition) # 1) a value_matches and a value_in_set on the same argument # 2) ambiguous flag names (e.g. two flags both with name -help) # 3) short symbol flag and long symbol flags match each other # Specification error possibilities (at DefinitionSwitcher) # 1) # 2) # Warning conditions # 1) Left-over arguments #--------------------------------------------------------------------------------------------------------------- #--------------------------------------------------------------------------------------------------------------- #--------------------------------------------------------------------------------------------------------------- #---------------------------------- help and error rendering stuff -------------------------------------------- #--------------------------------------------------------------------------------------------------------------- #--------------------------------------------------------------------------------------------------------------- #--------------------------------------------------------------------------------------------------------------- module OptiFlag module Flagset module Help class Bundle attr_accessor :help,:extended_help,:banner; end module StandardHelpBundle STANDARD_HELP_BANNER = proc do |render_on| render_on.printf(" %-10s %-15s %5s\n","Flag","Name","Is Required?") end STANDARD_HELP = proc do |render_on,short,long,dash_short,dash_long,required_or_not,description| render_on.printf(" #{ dash_short }%-10s %-15s %5s\n", short,short,required_or_not) end STANDARD_EXTENDED_HELP = proc do |render_on,short,long,dash_short,dash_long,required_or_not,description,arity,alternate_forms,name| render_on.puts "--------------------------------" desc = <<-EOF Name: #{ name } Simple Flag: #{ dash_short }#{ short } Required?: #{ required_or_not } # of Arguments: #{ (arity > 0) ? arity : 'None' } EOF desc << <<-EOF if description Description: #{ description } EOF desc << <<-EOF if long Long Form Flag: #{ dash_long }#{ long } EOF desc << <<-EOF if alternate_forms.length > 0 Alternate Flags: #{ dash_short }[#{ alternate_forms.join(', ') }] EOF render_on.puts desc end end end end end module OptiFlag module Flagset attr_accessor :registered_help,:help_banner,:registered_extended_help; def register_bundle(bundle) self.registered_extended_help = bundle.extended_help self.registered_help = bundle.help self.help_banner = bundle.banner end def registered_help; Help::StandardHelpBundle::STANDARD_HELP; end def registered_extended_help; Help::StandardHelpBundle::STANDARD_EXTENDED_HELP; end def help_banner; Help::StandardHelpBundle::STANDARD_HELP_BANNER; end def render_help(stdo=$stdout) help = self.registered_help help_banner = self.help_banner help_banner.call stdo @all_flags.each_pair do |key,val| help.call stdo,val.flag,val.the_long_form,val.the_dash_symbol,val.the_long_dash_symbol,val.the_is_required,val.the_description end end def show_help(start_message="",stdo=$stdout) stdo.puts start_message render_help(stdo) @all_keywords ||= [] @all_keywords.each do |x| stdo.puts "Flagless Argument: #{ x }" end end def show_extended_help(start_message="",stdo=$stdout) show_help(start_message,stdo) @all_flags.keys.each do |name| show_individual_extended_help(name,stdo) end end def show_individual_extended_help(name,stdo=$stdout) x = @all_flags[name] return if !x extended_help = self.registered_extended_help extended_help.call stdo, x.flag, x.the_long_form,x.the_dash_symbol,x.the_long_dash_symbol, x.the_is_required,x.the_description,x.the_arity,x.the_alternate_forms,x.name end end end