# = Como # # == Introduction # Como provides low manifest command line option parsing and # handling. Command line options are described in a compact table # format and option values are stored to conveniently named # properties. Como displays the command usage information based on # the option table (+ generic program info). # # == Simple example # Below is a small example program ("como_test") that demonstrates # typical usage. # # === Program listing # require "como" # include Como # # # Define command line arguments: # Spec.defineCheckHelp( "como_test", "Programmer", "2013", # [ # [ :silent, "help", "-h", "Display usage info." ], # [ :single, "file", "-f", "File argument." ], # [ :switch, "debug", "-d", "Enable debugging." ], # ] ) # # puts "File option: #{Opt['file'].value}" # puts "Debugging selected!" if Opt['debug'].given? # # "Spec.defineCheckHelp" method takes 4 arguments: # [progname] Name of the program (or command). # [author] Author of the program. # [year] Year (or any date) for the program. # [option table] Description of the command options in format with 4 # entries in each sub-array. # # Each option table entry is an Array of 4 values: type, name, # mnemonic, doc. Three different types are present in the example: # [:silent] Silent is left out from the "usage" printout (see # below). Also "help" is reserved as special option name to # designate command line usage help. # [:single] Single means that the option requires one argument (and only one). # [:switch] Switch is an optional flag (default value is false). # # Option name is used to reference the option value from Opt class. # The command line option values are stored to Opt class # automatically. For example the file option value is returned by # executing "Opt['file'].value". The option name also doubles as # long option, i.e. one could use "--file " on the command # line. # # Existence of optional options can be tested using the "given" # method. For example "Opt['debug'].given" would return "true" if # "-d" was given on the command line. # # === Example executions # Normal behavior would be achieved by executing: # shell> como_test -f example -d # # The program would execute with the following output: # File option: example # Debugging selected! # # Como includes certain "extra" behavior out-of-box. For example # given the command: # shell> como_test # # The following is displayed on the screen: # # como_test error: Option "-f" missing... # # # Usage: # como_test -f [-d] # # -f File argument. # -d Enable debugging. # # # Copyright (c) 2013 by Programmer # # Missing option error is displayed since "file" is a mandatory # option. The error display is followed by "usage" display. # # shell> como_test -h # # Would display the same "usage" screen except without the error # line. Documentation string is taken from the specification to # "usage" display. # # # == Option types # # The following types can be defined for the command line options: # [:switch] Single switch option (no arguments). # [:single] Mandatory single argument option. # [:multi] Mandatory multiple argument option. Option values in array. # [:opt_single] Optional single argument option. # [:opt_multi] Optional multiple argument option. Option values in array. # [:opt_any] Optional multiple argument option (also none accepted). # Option values in array. # [:default] Default option (no switch associated). Any name and # option String value can be supplied to the spec, since # only the document string is used. Default option is # referred with "nil". # [:exclusive] Option that does not coexist with other options. # [:silent] Option that does not coexist with other options and is not # displayed as an option in "usage" display. In effect a # sub-option of :exclusive. # # # == Specification method options # # The common method for specifying the options is to use # "Spec.defineCheckHelp". Method invocation includes definition # of the options, parsing the command line, checking for missing # mandatory options, and it will automatically display "usage" if # "help" option is given. # # Automatic "help" option processing can be avoided using # "Spec.defineCheck" instead. # # Both methods above accept additional parameters passed in a # Hash. The usable hash keys: # [:header] Header lines before standard usage printout. # [:footer] Footer lines after standard usage printout. # [:check] Check for missing arguments (default: true). # [:help_exit] Exit program if help displayed (default: true). # [:error_exit] Exit program if error in options (default: true). # # # == Using Opt class # # Opt class includes the parsed option values. All options can be # tested whether they are specified on the command line using # "Opt['name'].given" # # "Opt['name'].value" returns the provided option value. For # ":switch" type it is true/false value and for the other types a # String or an Array of Strings. # # If an option takes multiple arguments, the value for the option is # an Array. The values can be iterated simply by: # Opt['files'].value.each do |val| # puts val # end # # With ":opt_any" type, the user should first check if the option was given: # Opt['many_files_or_none'].given # Then check how many arguments where given: # Opt['many_files_or_none'].value.length # And finally decide what to do. # # If the user gives the "--" option, the arguments after that option # is returned as an Array with "Opt.external" # # # == Customization # # If the default behavior is not satisfactory, changes can be # implemented simply by overloading the existing functions. Some # knowledge of the internal workings of Como is required though. # # # == Additional checks # # Sometimes the options have to be used in combination to make sense # for the program. Como provides a facility to create relations # between options. Consider the following options spec: # Spec.defineCheckHelp( "como_fulltest", "Programmer", "2013", # [ # [ :silent, "help", "-h", "Display usage info." ], # [ :single, "file", "-f", "File argument." ], # [ :switch, "o1", "-o1", "o1" ], # [ :opt_single, "o2", "-o2", "o2" ], # [ :opt_single, "o3", "-o3", "o3" ], # [ :opt_multi, "o4", "-o4", "o4" ], # [ :opt_any, "o5", "-o5", "o5" ], # [ :switch, "debug", "-d", "Enable debugging." ], # ] ) # # Spec.checkRule do # all( 'file', # one( # all( 'o1', 'o2' ), # one( 'o3', 'o4', 'o5' ) # ) # ) # end # # This spec includes multiple optional options ("o?"). The # "Spec.checkRule" method accepts a block where option rule # check DSL (Domain Specific Language) is used. The rule specifies # that the "file" option has to be used in combination with some other # options. These are "all( 'o1', 'o2' )" or "one( 'o3', 'o4', 'o5' )", # i.e. either both "o1" and "o2", or one of ["o3","o4","o5]. The # checker will validate this rule and error if for example the command # line reads: # shell> como_fulltest --file myfile -o3 black -o5 # # The following rules can be used (in combination): # [all] All options in the list. # [one] One and only one from the list. # [any] At least one of the list is given. # [none] No options are required. # [incr] Incremental options in order i.e. have to have previous to # have later. # [follow] Incremental options in order i.e. have to have later if has # previous (combined options). module Como # IO stream options for Como classes. class ComoCommon # Default value for display output. @@io = STDOUT # Set @@io. def ComoCommon.setIo( io ) @@io = io end # Get @@io. def ComoCommon.getIo @@io end end # User interface for Como. class Spec < ComoCommon # Command line options source. @@argv = ARGV # Set of default options for prinout. @@options = { :header => nil, :footer => nil, :check => true, :help_exit => true, :error_exit => true, } # Set command line options source, i.e. @@argv (default: ARGV). def Spec.setArgv( newArgv ) @@argv = newArgv end # Display program usage (and optionally exit). def Spec.usage @@io.puts Spec.usageNormal exit( 1 ) if @@options[ :help_exit ] end # Usage info for Opt:s. def Spec.usageNormal str = "" if @@options[ :header ] str += @@options[ :header ] str += "\n" end str += " Usage: #{Opt.progname} #{Opt.cmdline.join(" ")} " Opt.doc.each do |i| str += ( " %-8s%s" % [ i[0], i[1..-1].join(" ") ] ) str += "\n" end str += " Copyright (c) #{Opt.year} by #{Opt.author} " if @@options[ :footer ] str += @@options[ :footer ] str += "\n" end str end # Set optional header for "usage". def Spec.setUsageHeader( str ) @@options[ :header ] = str end # Set optional footer for "usage". def Spec.setUsageFooter( str ) @@options[ :footer ] = str end # The primary entry point to Como. Defines the command # switches and parses the command line. Performs "usage" # display if "help" was selected. # @param prog [String] Program (i.e. command) name. # @param author [String] Author of the program. # @param year [String] Year (or dates) for program. # @param defs [Array] Option definitions. # @param option [Hash] Option definition's behavioral config (changes @@options defaults). def Spec.defineCheckHelp( prog, author, year, defs, option = {} ) Spec.defineCheck( prog, author, year, defs, option ) Spec.usage if Opt['help'].given end # Same as "defineCheckHelp" except without automatic "help" # option processing. def Spec.defineCheck( prog, author, year, defs, option = {} ) begin Spec.applyOptionDefaults( option ) @@options = option Opt.specify( prog, author, year, defs ) Spec.check rescue Opt::MissingArgument, Opt::InvalidOption => str @@io.puts Opt.error( str ) Spec.usage exit( 1 ) if @@options[ :error_exit ] end end # Check only. def Spec.check Opt.parse( @@argv, @@options[ :check ] ) Opt.checkMissing end # Overlay "option" on top of options defaults (@@options). def Spec.applyOptionDefaults( option ) option.replace( @@options.merge( option ) ) end # Check option combination rules. def Spec.checkRule( &rule ) begin raise( Opt::InvalidOption, "Option combination mismatch!" ) unless Opt.checkRule( &rule ) rescue Opt::MissingArgument, Opt::InvalidOption => str @@io.puts Opt.error( str ) # Display the possible combination: @@io.puts "\n Option combination rules:\n\n" Opt::RuleDisplay.new.evalAndDisplay( &rule ) Spec.usage end end # Additional option check. # @param opt [String] Option name. # @param error [String] Error string for false return values (from check). # @param check [Proc] Checker proc run for the option. Either return false or generate an exception when errors found. def Spec.checkAlso( opt, error, &check ) begin if Opt[opt].check( &check ) != true raise Opt::InvalidOption, error end rescue Opt::MissingArgument, Opt::InvalidOption => str @@io.puts Opt.error( str ) Spec.usage exit( 1 ) end end end # Opt includes all options spec information and parsed options # and their values. Option instance is accessed with # "Opt['name']". The option status (Opt instance) can be # queried with for example "given" and "value" methods. class Opt < ComoCommon class Error < StandardError; end class MissingArgument < Error; end class InvalidOption < Error; end # Option name. attr_accessor :name # Short option string. attr_accessor :opt # Long option string. attr_accessor :longOpt # Option type. attr_accessor :type # Option value. attr_accessor :value # Option documentation string. attr_accessor :doc # Is option specified? attr_accessor :given # Is option hidden (usage). attr_accessor :silent # Parsed option specs and option values. @@opts = [] # Program external arguments (e.g. subprogram args) @@external = nil # Create Opt object: # [name] Option name string. # [opt] Switch string. # [type] Option type. One of: # * :switch # * :single # * :multi # * :opt_single # * :opt_multi # * :opt_any # * :default # * :exclusive # * :silent # [doc] Option documentation. # [value] Default value. def initialize( name, opt, type, doc, value = nil ) @name = name @opt = opt @longOpt = "--#{name}" @type = type @value = value @doc = doc @silent = false # Whether option was set or not. @given = false @@opts.push self end # Options list iterator. def Opt.each @@opts.each do |o| yield o end end # Options iterator for given options. def Opt.each_given @@opts.each do |o| yield o if o.given end end # Number of given options. def Opt.givenCount cnt = 0 Opt.each_given do |i| cnt += 1 end cnt end # Return option value if given otherwise the default. # Example usage: fileName = Opt["file"].apply( "no_name.txt" ) def apply( default = nil ) if given value else default end end # Check for any non-given required arguments. def Opt.checkMissing # Check for any exclusive args first @@opts.each do |o| if o.type == :exclusive && o.given return end end @@opts.each do |o| if o.isRequired raise MissingArgument, "Option \"#{o.opt}\" missing..." if !o.given end end end # Reset "dynamic" class members. def Opt.reset @@opts = [] @@external = nil end # Select option object by name operator. def Opt.[](str) Opt.arg(str) end # Select option object by name. def Opt.arg( str ) if str == nil @@opts.each do |o| if o.type == :default return o end end nil else @@opts.each do |o| if str == o.name return o end end nil end end # Return program name. def Opt.progname @@progname end # Return program year. def Opt.year @@year end # Return author. def Opt.author @@author end # Return options that are specified as command external # (i.e. after '--'). def Opt.external @@external end # Return document strings for options. def Opt.doc doc = [] @@opts.each do |o| next if o.silent? doc.push( [o.opt ? o.opt : o.longOpt, o.doc] ) end doc end # Set of methods which represent option combination checking. # In effect this is a meta language (DSL) for option # combinations. # # Example: # RuleCheck.checkRule do # one( # incr( "gcov", "exclude", "refreshed" ), # "manifest", # "pairs", # "files" # ) # end class RuleCheck # Get given count. def getScore( *args ) score = 0 args.each do |i| if i.class == TrueClass || i.class == FalseClass score += 1 if i else score += 1 if Opt[i].given end end score end # Special condition when no options are given. def none Opt.givenCount == 0 end # Incremental options in order i.e. have to have previous # to have later. def incr( *args ) had = 0 add = true scoreAll = 0 i = 0 while args[i] score = getScore( args[i] ) had += 1 if ( score == 1 ) && add add = false if ( score == 0 ) scoreAll += score i += 1 end ( had == scoreAll && had > 0 ) end # Incremental options in order i.e. have to have all later # if had first. def follow( *args ) if getScore( args[0] ) getScore( *args ) == args.length else true end end # One of list given. def one( *args ) getScore( *args ) == 1 end # At least one is given. def any( *args ) getScore( *args ) > 0 end # All are given. def all( *args ) getScore( *args ) == args.length end end # Display utility for RuleCheck. Same usage model. # # Example expansion of options: # # |--# One of: # | |--# Adding in order: # | | |-- # | | |-- # | | |-- # | |-- # | |-- # | |-- # class RuleDisplay < ComoCommon def initialize # Prefix string for lines. Rules add/rm from it. @prefixStr = " " self end # Eval rules to get an nested array and then display it. def evalAndDisplay( &rule ) printRule( instance_eval( &rule ) ) end # Increase prefix string. def addPrefix( str ) @prefixStr += str end # Remove from prefix (either str or length ). def rmPrefix( item ) if item.class == String cnt = item.length else cnt = item end @prefixStr = @prefixStr[0..-(cnt+1)] end # Print prefix + str. def p( str ) @@io.puts( @prefixStr + str ) end # Recursively go through the nested array of rule items and # print out rules. def printRule( arr ) p( "|--# #{arr[0]}:" ) item = "| " addPrefix( item ) arr[1..-1].each do |i| if i.class == Array printRule( i ) else p( "|--<#{i}>" ) end end rmPrefix( item ) end # Special condition where no arguments are given. def none [ "NONE" ] end # Incremental options in order i.e. have to have previous # to have later. def incr( *args ) [ "Adding in order", *args ] end # Incremental options in order i.e. have to have all later # if had first. def follow( *args ) [ "If first then rest", *args ] end # One of list given. def one( *args ) [ "One of", *args ] end # At least one is given. def any( *args ) [ "One or more of", *args ] end # All are given. def all( *args ) [ "All of", *args ] end end # Check against option combination rules. def Opt.checkRule( &rule ) RuleCheck.new.instance_eval( &rule ) end # Create option spec. def Opt.full( name, opt, type, doc = "No doc." ) new( name, opt, type, doc ) end # Create switch option spec. def Opt.switch( name, opt, doc = "No doc." ) new( name, opt, :switch, doc, false ) end # Create exclusive option spec. def Opt.exclusive( name, opt, doc = "No doc.", silent = false ) new( name, opt, :exclusive, doc, false ) @@opts[-1].silent = silent end # Create default option spec, no switch. def Opt.default( doc = "No doc." ) new( nil, "", :default, doc, [] ) end # Specify and check options spec. def Opt.specify( progname, author, year, table ) # Type checks for valid user input. check = Proc.new do |cond,str| raise( ArgumentError, str ) unless cond end check.call( progname.class == String, "Program name is not a String" ) check.call( author.class == String, "Author name is not a String" ) check.call( year.class == String, "Year is not a String" ) check.call( table.class == Array, "Option table is not an Array" ) table.each do |i| check.call( i.class == Array, "Option table entry is not an Array" ) check.call( i.length == 4, "Option table entry length not 4" ) end @@progname = progname @@year = year @@author = author table.each do |e| case e[0] when :switch Opt.switch( e[1], e[2], e[3] ) when :exclusive Opt.exclusive( e[1], e[2], e[3] ) when :silent Opt.exclusive( e[1], e[2], e[3], true ) when :single, :multi, :opt_single, :opt_multi, :opt_any Opt.full( e[1], e[2], e[0], e[3] ) when :default Opt.default( e[3] ) end end end # Test whether str is an option. def Opt.isOpt( str ) str[0..0] == "-" end # Test whether str is an option list terminator. def Opt.isOptTerm( str ) str == "--" end # Format value string if escaped. def Opt.toValue( str ) if str[0..0] == "\\" str[1..-1] else str end end # Option requires argument? def hasArg case @type when :single, :multi, :opt_single, :opt_multi, :opt_any; true else false end end # Option requires many arguments? def hasMany case @type when :multi, :opt_multi, :opt_any; true else false end end # Is mandatory argument? def isRequired case @type when :single, :multi; true else false end end # Test if option is of switch type. def isSwitch case @type when :switch, :exclusive, :default; true else false end end # Test if option is silent. def silent? @silent end # Custom check for the option. User have to know some Como # internals. def check( &check ) instance_eval &check end # Find option object by option str. def Opt.findOpt( str ) if str == nil ( @@opts.select { |i| i.type == :default } )[0] elsif str[0..1] == "--" ( @@opts.select { |i| i.longOpt == str } )[0] else ( @@opts.select { |i| i.opt == str } )[0] end end # Como error printout. def Opt.error( str ) @@io.puts "#{@@progname} error: #{str}" end # Parse cmdline options from args. def Opt.parse( args, checkInvalids = true ) ind = 0 while args[ind] if Opt.isOptTerm( args[ind] ) # Rest of the args do not belong to this program. ind += 1 @@external = args[ind..-1] break elsif Opt.isOpt( args[ind] ) o = Opt.findOpt( args[ind] ) if !o if checkInvalids raise InvalidOption, \ "Unknown option (\"#{args[ind]}\")..." else o = Opt.findOpt( nil ) if !o raise InvalidOption, "No default option specified for \"#{args[ind]}\"..." else # Default option. o.value.push Opt.toValue( args[ind] ) ind += 1 end end elsif o && o.hasArg ind += 1 if ( !args[ind] || Opt.isOpt( args[ind] ) ) && o.type != :opt_any raise MissingArgument, \ "No argument given for \"#{o.opt}\"..." else if o.hasMany # Get all argument for multi-option. o.value = [] if !o.given while ( args[ind] && !Opt.isOpt( args[ind] ) ) o.value.push Opt.toValue( args[ind] ) ind += 1 end else # Get one argument for single-option. if o.given raise InvalidOption, \ "Too many arguments for option (\"#{o.name}\")..." else o.value = Opt.toValue( args[ind] ) end ind += 1 end end o.given = true else if !o raise InvalidOption, "No valid options specified..." else o.value = !o.value if !o.given o.given = true ind += 1 end end else # Default option. o = Opt.findOpt( nil ) if !o raise InvalidOption, "No default option specified for \"#{args[ind]}\"..." else while ( args[ind] && !Opt.isOpt( args[ind] ) ) o.value.push Opt.toValue( args[ind] ) ind += 1 end end end end end # Return cmdline usage strings for options in an Array. def Opt.cmdline opts = [] @@opts.each do |o| next if o.silent? prural = nil case o.type when :multi, :opt_multi; prural = "+" when :opt_any; prural = "*" else prural = "" end if !( o.isSwitch ) name = " <#{o.name}>#{prural}" else name = "" end if o.opt == nil opt = o.longOpt else opt = o.opt end if o.isRequired opts.push "#{opt}#{name}" else opts.push "[#{opt}#{name}]" end end opts end end end