# Como # # = Introduction # # Como provides low manifest command line option parsing and # deployment. The command line options are described in compact table # format and option values are stored to conveniently named # properties. Como builds command usage information based on the # option table (+ generic program info) and displays it automatically # if necessary. Como supports also subcommands and checking for option # combinations using a simple DSL. # # # # = Usage Examples # # Two simple examples are presented in this section. First one # includes a straight forward command definition and the second is a # bit more complicated with subcommand feature in use. # # == Simple example # # Below is a small example program ("como_simple") that demonstrates # typical usage. # # === Program listing # # require "como" # include Como # # # Define command line arguments: # Spec.command( "como_simple", "Programmer", "2013", # [ # [ :single, "file", "-f", "File argument." ], # [ :switch, "debug", "-d", "Enable debugging." ], # ] ) # # puts " File option: #{Opt['file'].value}" # puts " Debugging selected!" if Opt['debug'].given # # # First Como is required and Como module is included. # # {Spec.command} 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. # # Each option table entry (row/sub-array) includes 4 fields and # specifies one option: # [ type, name, mnemonic, doc ] # # Two different types are present in the example: # [: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 that user has # given. The command line option values are stored automatically. For # example the file option value is returned by: # Opt['file'].value # The option name also doubles as long option format, i.e. one could # use "--file " on the command line. # # Existence of optional options can be tested using the {Opt#given} # method. For example # Opt['debug'].given # would return "true" if "-d" was given on the command line. # # Mnemonic is the short form option specification e.g. "-f". If short # form is replaced with "nil", the long option format is only # available. # # Doc includes documentation for the option. It is displayed when # "help" ("-h") option is given. Help option is added to the command # automatically as default behavior. # # === Simple example executions # # Normal behavior would be achieved by executing: # shell> como_simple -f example -d # # The program would execute with the following output: # File option: example # Debugging selected! # # Same output would be achieved with: # shell> como_simple --file example --debug # # Since option name doubles as long option. # # Como includes certain "extra" behavior out-of-box. Required # arguments are checked for existence and error is displayed if # arguments are not given. # # For example given the command: # shell> como_simple # # The following is displayed on the screen: # # como_simple error: Option "-f" missing for "como_simple"... # # como_simple -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 message is followed by "usage" display (Usage # Help). Documentation string is taken from the option specification to # "usage" display. # # Given the command: # shell> como_simple -h # # would display the same "usage" screen except without the error # line. # # == Subcommand example # # Subcmd example includes a program which has subcommands. Subcommands # can have their own command line switches and options. # # === Program listing # # require "como" # include Como # # Spec.program( "Programmer", "2013" ) do # # command( "como_subcmd", [ # [ :subcmd, "add", nil, "Add file." ], # [ :subcmd, "rm", nil, "Remove file." ], # ], ) # # subcmd( "add", [ # [ :switch, "force", "-fo", "Force operation." ], # [ :opt_single, "password", "-p", "User password." ], # [ :opt_single, "username", "-u", "Username." ], # [ :single, "file", "-f", "File." ], # ] ) # # check do # one( # '-fo', # all( 'password', 'username' ) # ) # end # # subcmd( "rm", [ # [ :single, "file", "-f", "File." ], # ] ) # # end # # subcmd = Opt.main.givenSubcmd # # case subcmd.name # when 'add'; puts " Adding file \"#{subcmd['file'].value}\"..." # when 'rm'; puts " Removing file \"#{subcmd['file'].value}\"..." # end # # {Spec.program} method defines a program (command) with # subcommands. The author and date are provided as parameters, and the # program and subcommand options are defined in block. # # The first {Spec#command} (or {Spec#subcmd}) method call defines the main command # ({Opt.main}) which represents the program. It has two "subcmd" # options ("add" and "rm"). # # The rest of the "subcmd" methods define subcommands for the parent # command. This example includes one subcommand level, but multiple # levels are allowed. # # The "check" (or "checkRule") method defines option combination # ({RuleCheck}) for the previous subcommand definition. In this case the # definition allows "add" to have either the "-fo" option defined or # "password" and "username" in combination. # # Main (root) commands can be referenced through # Opt.main # or alternatively # Opt['como_subcmd'] # # The subcommands can be referenced through {Opt.main} (etc.) # Opt.main['add'] # Opt['como_subcmd']['add'] # # or directly from {Opt} if subcommand names do not collide: # Opt['add'] # # The given subcommand can be accessed with {Opt#givenSubcmd} method from # each parent command. # # === Subcommand example executions # # Normal behavior would be achieved by executing: # shell> como_subcmd add -fo -f example # # The program would execute with the following output: # Adding file "example"... # # The option combinations for "add" subcommand are automatically # checked. Thus executing: # shell> como_subcmd add -f example # # Would result to: # como_subcmd error: Option combination mismatch! # # Subcommand "add" usage: # como_subcmd add [-fo] [-p ] [-u ] -f # # -fo Force operation. # -p User password. # -u Username. # -f File. # # # Option Combinations: # |--# One of: # | |--<-fo> # | |--# All of: # | | |-- # | | |-- # # since the combination rule requires either "-fo", or both "password" # and "username". # # Help is automatically provided on each command level, thus these are # both valid. # shell> como_subcmd -h # and # shell> como_subcmd rm -h # # # # = Option specification # # == Overview # # Option specification includes the minimum set of information # required for command line parsing. It is used to: # * Parse the command line. # * Check for wrong options and report. # * Check for mandatory arguments and report. # * Set the options given/non-given state. # * Set the options value. Array/String for all except true/false for # switches. # * Generate Usage Help printout. # # == Option types # # The following types can be defined for the command line options: # [:subcmd] Subcmd option. Subcmd specific options are provided # separately. # [:switch] Single switch option (no arguments). # [:single] Mandatory single argument option. # [:multi] Mandatory multiple argument option (one or many). Option # values in array. # [:opt_single] Optional single argument option. Value is nil when # option is not given. # [:opt_multi] Optional multiple argument option (one or many). Option # values in array. # [:opt_any] Optional multiple argument option (also none # accepted). Option values in array. # [:default] Default option (no switch associated). Name and option # String values can be left out, since only the document # string is used. Default option is referred with # ":default" or "nil". # [:exclusive] Option that does not co-exist with other # options. :exclusive can have arguments as with # :opt_any, however :exclusive is documented like # :switch. # [:silent] Option that does not coexist with other options and is not # displayed as an option in Usage Help display. In effect a # sub-option of :exclusive. # # Options use typically all the 4 option fields: # [ type, name, mnemonic, doc ] # # "type" field is mandatory for all options. # # "name" field is also mandatory for all options. "mnemonic" can be # left out, but then option accepts only long option format. # # ":default" uses only "doc" and ":subcmd" doesn't use the "mnemonic" # field. # # ":multi", ":opt_multi", and ":opt_any" option arguments are # terminated only when an option specifier is found. This can be a # problem if ":default" option follows. The recommended solution is to # use a ":silent" option that can be used to terminate the argument # list. For example: # [ :silent, "terminator", "-", "The terminator." ], # # # == Option specification method configuration # # Option behavior can be controlled with several configuration options. # # The configuration options are provided in a Hash. These are the # passed as the last regular parameter for both {Spec.command} and # {Spec.program} methods. Setting the configuration at {Spec.program} # will propagate the config options to all the subcommands as # well. Configuration can be given to each subcommand separately to # override the inherited config values. Subcommand settings are not # inherited, but apply only in the subcommand. # # The usable configuration Hash keys: # [:autohelp] Add help option automatically (default: true). Custom # help option can be provided and it can be made also # visible to user. # [:rulehelp] Include RuleCheck help to Usage Help (default: false). # [:header] Header lines before standard usage printout. # [:footer] Footer lines after standard usage printout. # [:subcheck] Automatically check that a subcommand is provided # (default: true). # [:check_missing] Check for missing arguments (default: true). # [:check_invalid] Error for unknown options (default: true). # [:tab] Tab stop column for option documentation (default: 12). # [:help_exit] Exit program if help displayed (default: true). # [:copyright] Display copyright/author in usage printout (default: true). # # # # = Option referencing # # == Existence and values # # Opt class includes the parsed option values. All options can be # tested whether they are specified on the command line using: # Opt['name'].given # # The {Opt#given} method takes optionally a block argument. When block # argument is used, the block is supplied with option value and the # block is executed if the option has been set (See: {Opt#given}). # # Provided value is returned by: # Opt['name'].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 # # Short syntax for value referencing is performed with unary operator # "~". Thus # ~Opt['files'] # is equal to # Opt['files'].value # # 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. # # == Options including parameters # # Sometimes it is convenient for the program to use an option to # include multiple parameter settings. These settings can be parsed # and mapped to a Hash. Como performs automatic conversion to numeric # values if possible. For example with option: # --set rounds=10 length=5 # Como can be used extract the parameter values with the "params" method: # Opt['set'].params # And a Hash is returned: # { 'rounds' => 10, 'length' => 5 } # # == Subcommand options # # The given subcommand for the parent command is return by # {Opt#givenSubcmd}. Commonly the program creator should just check # directly which subcommand has been selected and check for any # subcommand options set. For example: # if Opt['como_subcmd']['add'].given # ... # # == Program external options # # If the user gives the "--" option (double-dash), the arguments after # that option are returned as an Array with {Opt.external}. # # # # = Option combination checks # # Como provides a facility to create relations between options using # RuleCheck DSL. This is needed since sometimes options have to be # used in combination to make sense for the program. Also options # might be mutually exclusive. # # 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. # [inv] Logical negation for existence. # [incr] Incremental options in order i.e. have to have previous to # have later. # [follow] Incremental options in order i.e. have to have all later if # had first. # [meh] Dont care, always succeeds. # # Examples can be found above. # # # = Customization # # A Como user specific customization file can be referenced through # the "COMO" environment variable. If environment variable "COMO" is # defined, the referenced file is read in as Ruby file as a last phase # when Como is loaded from the program (require). Proposed naming # convention for the customization is: # # $HOME/.como # # User can define a pre and a post action hook in the file. # # The pre-hook can be used for example to change the Como config # defaults. It is run before the body of {Spec.command} or # {Spec.program} is executed. It is passed all the parameters that has # been passed to {Spec.command} or {Spec.program}, only collected into # a Hash. The Hash keys are method parameter names as symbols. # # Example: # # Define pre parser hook for Como. # Como.preHook do |args| # # # Get default config for options. # config = Como::Opt.configGet # # # Disable 'copyright' lines from usage. # config[ :copyright ] = false # # # Test if "Spec.command" is the entry method (it has arg named "prog"). # if args[ :prog ] # # Place a custom header for all programs. # config[ :header ] = "\nProgram \"#{args[ :prog ]}\" is ...\n\n" # end # end # # There is no predefined use cases for post-hook. Post-hook is passed # the {Opt.main} as parameter. # # {Spec.program} and {Spec.command} both process and check options in # one pass. Como user can separate the definition and checking # phase. Definition phase is performed by executing # {Spec.defineProgram} or {Spec.defineCommand}. After definition phase # the user can for example programmatically add new subcommands or # options, in addition to existing options. When the subcommands and # options are complete, {Spec.execute} should be called to perform # options checking. # # If the provided customization facilities are not satisfactory, # changes can be implemented simply by overloading the existing # functions. Some knowledge of the internal workings of Como is # required though. # # Como version is returned with: # Como.version module Como # IO stream options for Como classes. class ComoCommon # Default value for display output. @@io = STDOUT # Hooks. @@hook = {} # Set @@io. def ComoCommon.setIo( io ) @@io = io end # Get @@io. def ComoCommon.getIo @@io end # Set hook content. # # @param name [String] Hook name. # @yield code Hook code. def ComoCommon.setHook( name, &code ) @@hook[ name ] = code end # Run hook. # # @param name [String] Hook name. # @yield code Hook arguments. def ComoCommon.runHook( name, args ) @@hook[ name ].yield( args ) if @@hook[ name ] end end # Set "preHook" routine. def Como.preHook( &code ) ComoCommon.setHook( :preHook, &code ) end # Set "postHook" routine. def Como.postHook( &code ) ComoCommon.setHook( :postHook, &code ) end # User interface for Como. class Spec < ComoCommon # Create specification for program with subcmds. # # @param author [String] Program author. # @param year [String] Year (or dates) for program. # @yield [] Subcmd definitions. def Spec.program( author, year, config = nil, &defs ) Spec.defineProgram( author, year, config, &defs ) Spec.execute 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 config [Hash] Option definition's behavioral config # (changes @@config defaults). def Spec.command( prog, author, year, defs = [], config = {} ) Spec.defineCommand( prog, author, year, defs, config ) Spec.execute end # Define options specification for program. User should # perform {Spec.execute} separately. # # @param author [String] Program author. # @param year [String] Year (or dates) for program. # @yield [] Subcmd definitions. def Spec.defineProgram( author, year, config = nil, &defs ) preHookArgs = { :author => author, :year => year, :config => config, :defs => defs, } ComoCommon.runHook( :preHook, preHookArgs ) if config Opt.configOverlay( config ) end spec = Spec.new( author, year ) spec.instance_eval( &defs ) end # Define options specification for command. User should # perform {Spec.execute} separately. # # @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 config [Hash] Option definition's behavioral config # (changes @@config defaults). def Spec.defineCommand( prog, author, year, defs, config = {} ) preHookArgs = { :prog => prog, :author => author, :year => year, :defs => defs, :config => config, } ComoCommon.runHook( :preHook, preHookArgs ) spec = Spec.new( author, year ) spec.subcmd( prog, defs, config ) end # Alias for {Spec.command}. # # NOTE: This method is deprecated and will be removed in # future releases. def Spec.defineCheckHelp( prog, author, year, defs, config = {} ) Spec.command( prog, author, year, defs, config ) end # Perform command line options checking. def Spec.execute Opt.main.check( ArgsParseState.new( @@argv ) ) ComoCommon.runHook( :postHook, Opt.main ) end # Create Spec object that can handle subcmd definitions. # # @param author [String] Program author. # @param year [String] Year (or dates) for program. def initialize( author, year ) @author = author @year = year Spec.ArgCheck( author.class == String, "Author name is not a String" ) Spec.ArgCheck( year.class == String, "Year is not a String" ) end # Define subcommand options. # # @param cmd [String] Subcmd name. # @param defs [Array] Option definition table. # @param config [] Configuration options. def subcmd( cmd, defs = [], config = {} ) unless Opt.main main = MainOpt.new( @author, @year, cmd, nil, :subcmd, nil ) Opt.setMain( main ) subcmd = main else subcmd = Opt.findOpt( cmd ) Opt.setSubcmd( subcmd ) Spec.ArgCheck( false, "Subcommand \"#{cmd}\" not defined." ) unless subcmd end # Overlay user config on top of default. subcmd.applyConfig( config ) if subcmd.config[ :autohelp ] # Automatically add the help option. defs.insert( 0, [ :silent, "help", "-h", "Display usage info." ] ) end Spec.specify( subcmd, defs ) end # Alias to subcmd to highlight the main command. alias command subcmd # Check/fix options specs and create option objects for the # whole table. # # @param subcmd [Opt] Subcommand target. # @param table [Array] Option definition table for subcommand. def Spec.specify( subcmd, table ) options = {} subcmds = {} # Type checks for valid user input. Spec.ArgCheck( table.class == Array, "Option table is not an Array" ) table.each do |e| os = Spec.specifyOptOrSub( e ) case os.type when :subcmd; subcmds[ os.name ] = os else options[ os.name ] = os end end subcmd.setOptionSubcmd( options.values, subcmds.values ) subcmd end # Check/fix options specs and create option objects for the # whole table. # # @param opt_or_sub [Array] Option definition table. def Spec.specifyOptOrSub( opt_or_sub ) # Fix the table entries if needed. Spec.ArgCheck( opt_or_sub.class == Array, "Option table entry is not an Array" ) if opt_or_sub[0] == :default && opt_or_sub.length == 2 # Add 2 dummy entries for :default type if needed. opt_or_sub = [ opt_or_sub[0], nil, nil, opt_or_sub[1] ] elsif opt_or_sub[0] == :subcmd && opt_or_sub.length == 3 # Add 1 dummy entry for :subcmd type if needed. opt_or_sub = [ opt_or_sub[0], opt_or_sub[1], nil, opt_or_sub[2] ] end Spec.ArgCheck( opt_or_sub.length == 4, "Option table entry length not 4" ) if opt_or_sub[0] == :subcmd Opt.subcmd( opt_or_sub[1], opt_or_sub[3] ) else case opt_or_sub[0] when :switch, :exclusive, :silent, :single, :multi, :opt_single, :opt_multi, :opt_any Opt.full( opt_or_sub[1], opt_or_sub[2], opt_or_sub[0], opt_or_sub[3] ) when :default Opt.defaultOpt( opt_or_sub[3] ) else raise "Unknown option type: \"#{opt_or_sub[0]}\"..." end end end # Command line options source. @@argv = ARGV # 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 Opt.main.usage end # Set optional header for "usage". def Spec.setUsageHeader( str ) Opt.main.setUsageHeader( str ) end # Set optional footer for "usage". def Spec.setUsageFooter( str ) Opt.main.setUsageFooter( str ) end # Check option combination rules. # # @param opt [String] Opt name to which rules are set. If not # given, Opt.current is used. # @param rule [Proc] Rules to check. def Spec.checkRule( opt = nil, &rule ) if opt opt = Opt[ opt ] else opt = Opt.current end opt.setRuleCheck( &rule ) opt.checkRule end # Alias for Spec.checkRule. def Spec.check( opt = nil, &rule ) Spec.checkRule( opt = nil, &rule ) end # Check option combination rules. # # @param opt [String] Opt name to which rules are set. If not # given, Opt.current is used. # @param rule [Proc] Rules to check. def checkRule( opt = nil, &rule ) if opt opt = Opt[ opt ] else opt = Opt.current end opt.setRuleCheck( &rule ) end # Alias for checkRule alias check checkRule # 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 ) Opt.main.checkAlso( opt, error, &check ) end private # Argument checking assertion. def Spec.ArgCheck( cond, str ) raise( ArgumentError, str ) unless cond 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 # Create exception with capability to pass arbitrary data. class ErrorWithData < StandardError # Exception data. attr_reader :data # Create error exception. def initialize( message = nil, data = nil ) super( message ) @data = data end end # Missing argument exception. class MissingArgument < ErrorWithData; end # Invalid (non-existing) option exception. class InvalidOption < ErrorWithData; end # ------------------------------------------------------------ # Option specification: # Program i.e. highest level subcommand. @@main = nil # List of parsed option specs and option values. @@opts = [] # Current subcommand recorded. @@subcmd = nil # Set of default configs for printout. @@config = { :autohelp => true, :rulehelp => false, :header => nil, :footer => nil, :subcheck => true, :check_missing => true, :check_invalid => true, :tab => 12, :help_exit => true, :copyright => true, } # Set main option. def Opt.setMain( main ) @@main = main Opt.setSubcmd( main ) end # Get main option. def Opt.main @@main end # Add option to options list. def Opt.addOpt( opt ) @@opts.push opt end # Set current subcmd. def Opt.setSubcmd( opt ) @@subcmd = opt end # Current subcmd processed. def Opt.current @@subcmd end # Find option by name. def Opt.findOpt( name ) idx = @@opts.index do |i| i.name == name end if idx @@opts[ idx ] else nil end end # Reset "dynamic" class members. def Opt.reset @@opts = [] end # Select option object by name. Main is searched first and # then the flattened list of all options. def Opt.[](str) # Search Main first. ret = Opt.main.argByName( str ) unless ret ret = Opt.findOpt( str ) unless ret raise RuntimeError, "Option \"#{str}\" does not exist..." end end ret end # Return program name. def Opt.progname @@main.name end # Return program year. def Opt.year @@main.year end # Return author. def Opt.author @@main.author end # Return arguments (options) that are specified as command # external (i.e. after '--'). def Opt.external Opt.main.external end # Return arguments (options) that have no switch. def Opt.default Opt.main.default end # Create option spec. def Opt.full( name, opt, type, doc = "No doc." ) new( name, opt, type, doc ) end # Create sub-command option spec. def Opt.subcmd( name, doc = "No doc." ) new( name, nil, :subcmd, doc, false ) end # Create default option spec, no switch. def Opt.defaultOpt( doc = "No doc." ) new( "", "", :default, doc, [] ) end # Options iterator for all options. def Opt.each( &blk ) Opt.main.each &blk end # Options iterator for given options. def Opt.each_given( &blk ) Opt.main.each_given( &blk ) end # Overlay Opt default configuration options. def Opt.configOverlay( config ) @@config.merge!( config ) end # Return default config for Como options. def Opt.configGet @@config end # Set default config for Como options. User can manipulate the # defaults with "preHook". def Opt.configSet( config ) @@config = config end # Issue non-fatal user error. See {#error}. def Opt.error( str, nl = false ) Opt.main.error( str, nl ) end # Issue fatal user error. See {#fatal}. def Opt.fatal( str ) Opt.main.fatal( str ) end # Issue user warning. See {#warn}. def Opt.warn( str, nl = false ) Opt.main.warn( str, nl ) end # ------------------------------------------------------------ # Opt properties: # Subcommand parent (i.e. host). attr_accessor :parent # Option name. attr_accessor :name # Short option string. attr_accessor :shortOpt # 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_writer :given # List of suboptions. attr_reader :subopt # List of subcommands. attr_reader :subcmd # Opt configuration. attr_reader :config # Opt rules. attr_reader :rules # Create Opt object. # @param name [String] Option name. # @param opt [String] Switch. # @param type [Symbol] Option type. One of: # * :switch # * :single # * :multi # * :opt_single # * :opt_multi # * :opt_any # * :default # * :exclusive # * :silent # @param doc [String] Option documentation. # @param value [Object] Default value. def initialize( name, opt, type, doc, value = nil ) @parent = nil @name = name @shortOpt = opt @longOpt = "--#{name}" @type = type @value = value if hasMany && value == nil @value = [] end @doc = doc # Whether option was set or not. @given = false @subopt = [] @subcmd = [] @rules = nil @config = @@config.dup Opt.addOpt( self ) end # Set command (subcommand) suboptions and subcmds. # # @param opts [Array] def setOptionSubcmd( opts, subs ) opts.each do |i| addOption( i ) end subs.each do |i| addSubcmd( i ) end end # Add subcommand option. # # @param opt [Option] Option to add. def addOption( opt ) opt.parent = self @subopt.push opt end # Add subcommand subcmd. # # @param cmd [Option] Subcmd to add. def addSubcmd( cmd ) cmd.parent = self @subcmd.push cmd end # Merge config to base config. # # @param config [Hash] Configuration Hash to merge. def applyConfig( config ) @config.merge!( config ) end # Set rule checks for the option. # # @param rule [Proc] Rule to check after command line parsing. def setRuleCheck( &rule ) @rules = rule end # ------------------------------------------------------------ # Command line parsing and checking: # Check provided args. def check( argsState ) # Start at top. top = self parse = Proc.new do # Parse and check for invalid arguments. begin top = top.parse( argsState, top.config[ :check_invalid ] ) end while( top ) # Check for any missing valid arguments. checkMissing end error = Proc.new do |err| errornl( err.to_s ) # Display subcmd specific usage info. err.data.usage exit( 1 ) end begin parse.call rescue Opt::MissingArgument, Opt::InvalidOption => err error.call( err ) end # Revert back to top after hierarchy travelsal. usageIfHelp # Check rules. cur = self while cur cur.checkRule cur = cur.givenSubcmd end self end # Parse cmdline options from args. def parse( args, checkInvalids = true ) while args.get #puts "Opt.parse (#{@name}): #{args.get}" if args.isOptTerm # Rest of the args do not belong to this program. args.next Opt.main.external = args.rest break elsif args.isOpt o = findOpt( args.get ) if !o if checkInvalids raise \ InvalidOption.new( "Unknown option \"#{args.get}\"...", self ) else o = findOpt( nil ) if !o raise \ InvalidOption.new( "No default option specified to allow \"#{args.get}\"...", self ) else # Default option. o.value.push args.toValue args.next end end elsif o && o.hasArg args.next if ( !args.get || args.isOpt ) && o.type != :opt_any && o.type != :exclusive raise MissingArgument.new( "No argument given for \"#{o.opt}\"...", self ) else if o.hasMany # Get all argument for multi-option. o.value = [] if !o.given while args.get && !args.isOpt o.value.push args.toValue args.next end else # Get one argument for single-option. if o.given raise \ InvalidOption.new( "Too many arguments for option (\"#{o.name}\")...", self ) else o.value = args.toValue end args.next end end o.given = true else if !o raise InvalidOption.new( "No valid options specified...", self ) else o.given = true args.next end end else # Subcmd or default. Check for Subcmd first. # Search for Subcmd. o = findOpt( args.get ) if !o # Search for default option. o = findOpt( nil ) if !o raise \ InvalidOption.new( "No default option specified to allow \"#{args.get}\"...", self ) else # Default option. o.given = true o.value.push args.toValue args.next end else # Subcmd. o.given = true args.next return o end end end nil end # Check for any non-given required arguments recursively # through hierarchy of subcommands. MissingArgument Exception # is generated if argument is missing. def checkMissing return unless config[ :check_missing ] # Full cmd name. cmd = ( getParents.map do |i| i.name end ).join( ' ' ) # Check for any exclusive args first. @subopt.each do |o| if o.isExclusive && o.given return end end # Check for required arguments for this level before # subcmds. @subopt.each do |o| if o.isRequired unless o.given raise MissingArgument.new( "Option \"#{o.opt}\" missing for \"#{cmd}\"...", self ) end end end if hasSubcmd if @config[ :subcheck ] # Compulsory Subcommand checking enabled. subcmdMissing = true else subcmdMissing = false end else subcmdMissing = false end # Check for any subcmd args. sub = givenSubcmd if sub subcmdMissing = false # Use recursion to examine the next level. return sub.checkMissing end # If no subcmds are given, issue error. raise MissingArgument.new( "Subcommand required for \"#{cmd}\"...", self ) if subcmdMissing end # Check option combination rules. def checkRule return unless @rules begin raise Opt::InvalidOption.new( "Option combination mismatch!", self ) unless RuleCheck.check( self, &@rules ) rescue Opt::MissingArgument, Opt::InvalidOption => err @@io.puts errornl( err.to_s ) usage( nil, true ) 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 checkAlso( opt, error, &check ) begin if self[opt].evalCheck( &check ) != true raise Opt::InvalidOption.new( error, self ) end rescue Opt::MissingArgument, Opt::InvalidOption => err @@io.puts errornl( err.to_s ) err.data.usage exit( 1 ) end end # ------------------------------------------------------------ # Opt query user interface: # Select option object by name operator. def []( str ) ret = argByName( str ) unless ret raise RuntimeError, "Subopt \"#{str}\" does not exist for \"#{@name}\"!" end ret end # Option's opt id. Short if exists otherwise long. def opt @shortOpt ? @shortOpt : @longOpt end # All subcommand options, options and subcommands. def suball @subopt + @subcmd end # Options list iterator. def each( &blk ) suball.each do |o| yield o end end # Options iterator for given options. def each_given( &blk ) suball.each do |o| yield o if o.given end end # Number of given options. def givenCount cnt = 0 each_given do |i| cnt += 1 end cnt end # Short syntax for value reference. Example: "~Opt['file']". def ~() @value end # Return option value if given otherwise the default. # Example usage: fileName = Opt["file"].apply( "no_name.txt" ) def apply( default = nil ) if given if @type == :switch true else value end else default end end # Returns true if option is given, and block is not # present. When block is present, the block is executed (with # value as parameter) if option has been given. # # @param optArg [Boolean] Pass Opt to block instead of its # value. def given( optArg = false, &prog ) if block_given? if @given if optArg yield( self ) else yield( @value ) end else false end else @given end end # Alias for given. alias given? given # Return the selected subcommand. def givenSubcmd ( @subcmd.select do |o| o.given end )[0] end # Returns Hash of option value parameters. Example command # line content: # -p rounds=10 length=5 # Option value content in this case would be (Array of param # settings): # [ "rounds=10", "length=5" ] # @return [Hash] Parameter settings included in the option. def params map = {} @value.each do |i| name, value = i.split('=') value = str_to_num( value ) map[ name ] = value end map end # Return default options. def default argByName( nil ) end # Select option object by name. def argByName( str ) if str == nil || str == :default @subopt.each do |o| if o.type == :default return o end end nil else suball.each do |o| if str == o.name return o end end nil end end # Select option object by name/opt/longOpt. def argById( str ) if str == nil || str == :default @subopt.each do |o| if o.type == :default return o end end nil else suball.each do |o| if str == o.name || str == o.opt || str == o.longOpt return o end end nil end end # Option requires argument? def hasArg case @type when :single, :multi, :opt_single, :opt_multi, :opt_any, :exclusive; true else false end end # Option requires many arguments? def hasMany case @type when :multi, :opt_multi, :opt_any, :exclusive, :default; true else false end end # Is mandatory argument? def isRequired case @type when :single, :multi; true else false end end # Test if option is silent. def silent? @type == :silent end # Test if option is exclusive. In addition :exclusive also # :silent is exclusive. def isExclusive case @type when :exclusive, :silent; 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 # ------------------------------------------------------------ # Opt "Usage Help" user interface: # Set optional header for "usage". def setUsageHeader( str ) @config[ :header ] = str end # Set optional footer for "usage". def setUsageFooter( str ) @config[ :footer ] = str end # Display program usage (and optionally exit). # # @param doExit [Boolean] Exit program after help # display. Default to help_exit config # if nil. # @param ruleHelp [Boolean] Include rule help to help # display. Default to rulehelp config # if nil. def usage( doExit = nil, ruleHelp = nil ) doExit = @config[ :help_exit ] if doExit == nil ruleHelp = @config[ :rulehelp ] if ruleHelp == nil @@io.puts usageNormal if ruleHelp @@io.puts "\n Option Combinations:" @@io.puts RuleDisplay.print( &@rules ) end exit( 1 ) if doExit end # Display program usage (and optionally exit). def usageIfHelp if self.argByName( 'help' ) && self['help'].given usage elsif hasSubcmd && givenSubcmd givenSubcmd.usageIfHelp end end # Usage printout for command. def usageCommand str = "" str += "\ Subcommand \"#{@name}\" usage: #{fullCommand} #{cmdline.join(" ")} " str += suboptDoc str += "\n" end # Usage info for Opt:s. def usageNormal str = "" if @config[ :header ] str += @config[ :header ] else str += "\n" end str += usageCommand if @config[ :footer ] str += @config[ :footer ] else str += "\n" end str end # Return cmdline usage strings for options in an Array. def cmdline opts = [] @subopt.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.shortOpt == nil opt = o.longOpt else opt = o.shortOpt end if o.isRequired opts.push "#{opt}#{name}" else opts.push "[#{opt}#{name}]" end end if hasSubcmd opts.push "<>" end opts end # Return document strings for options. def suboptDoc str = "" # format = Proc.new do |s,d| ( " %-#{@config[ :tab ]}s%s\n" % [ s, d ] ) end str += "\n" str += " Options:\n" if hasSubcmd && hasVisibleOptions @subopt.each do |o| next if o.silent? str += suboptDocFormat( o.opt, o.doc ) end str += "\n" + suboptDocFormat( "Subcommands:", "" ) if hasSubcmd @subcmd.each do |o| str += suboptDocFormat( o.name, o.doc ) end str end # Find option object by option str. def findOpt( str ) if str == nil suball.detect { |i| i.type == :default } elsif str[0..1] == "--" suball.detect { |i| i.longOpt == str } elsif str[0..0] != "-" suball.detect { |i| i.name == str } else suball.detect { |i| i.opt == str } end end # Como error printout. # # @param str [String] Error message. # @param nl [Boolean] Prepend msg with newline. def error( str, nl = false ) nl = nl ? "\n" : "" STDERR.puts( "#{nl}#{Opt.progname} error: #{str}" ) end # Como error printout with pre-newline. def errornl( str ) error( str, true ) end # Como error printout with immediate exit. def fatal( str ) error( str ) exit( false ) end # Como warning printout. # # @param str [String] Warning message. # @param nl [Boolean] Prepend msg with newline. def warn( str, nl = false ) nl = nl ? "\n" : "" STDERR.puts( "#{nl}#{Opt.progname} warning: #{str}" ) end # Custom check for the option. User has to know some Como # internals. def evalCheck( &check ) instance_eval &check end # ------------------------------------------------------------ # Internal methods: private # Convert string to number if possible. The order of checks: # integer, float, and no-conversion. def str_to_num( str ) begin return Integer( str ); rescue ; end begin return Float( str ); rescue ; end str end # Return list of parents. def getParents list = [] cur = self begin list.push cur cur = cur.parent end while cur list.reverse end # Full command name. def fullCommand ( getParents.map do |i| i.name end ).join( ' ' ) end # Test if option is of subcmd type. def isSubcmd @type == :subcmd end # Test if option is of subcmd type. def hasVisibleOptions # Count non-silent options. ( @subopt.select do |i| !i.silent? end ).length > 0 end # Test if option is of subcmd type. def hasSubcmd !@subcmd.empty? end # Format option documentation. If newline is followed by tab, # rest of the documentation is aligned with the description of # previous line. def suboptDocFormat( switch, doc ) # Format doc nicely to multiple lines if newline is # followed by tab stop. parts = doc.split( "\n" ) lines = [ ( " %-#{@config[ :tab ]}s%s\n" % [ switch, parts[0] ] ) ] if parts[1] parts[1..-1].each do |p| if p[0] == "\t" lines.push ( " #{' '*@config[ :tab ]}%s\n" % [ p[1..-1] ] ) else lines.push p end end end lines.join end end # Specialized Opt class for program (i.e. highest level # subcommand). class MainOpt < Opt # Program external arguments: attr_accessor :external # Program author and year (date). attr_reader :author, :year # Create program main option. def initialize( author, year, name, opt, type, doc, value = nil ) @author = author @year = year @external = nil super( name, opt, type, doc, value = nil ) end # Full command name. def fullCommand Opt.progname end # Usage printout for command. def usageCommand str = "\ #{fullCommand} #{cmdline.join(" ")} " str += suboptDoc if @config[ :copyright ] str += " Copyright (c) #{Opt.year} by #{Opt.author} " end str end end # Command argument parsing state. class ArgsParseState # Create parse state. # # @param list [Array] List of Command Line Arguments # (default: ARGV). def initialize( list ) set( list ) @idx = 0 end # Set list of arguments. def set( list ) @args = list end # Step to next argument. def next @idx += 1 end # Step to previous argument. def prev @idx -= 1 end # Get current argument. def get( idx = @idx ) @args[ idx ] end # Get last argument. def last( idx = @idx ) idx == ( @args.length-1 ) end # Get rest of the arguments. def rest( idx = @idx ) @args[ idx..-1 ] end # Parser at argument list end? def done? @idx >= @list.length end # Test whether str is an option. def isOpt( str = get ) str[0..0] == "-" end # Test whether str is an option list terminator. def isOptTerm( str = get ) str == "--" end # Format value string if escaped. def toValue( str = get ) if str[0..0] == "\\" str[1..-1] else str end end end # Set of methods which represent option combination checking. # In effect this is a meta language (DSL) for option # combinations. # # Example: # RuleCheck.check( opt ) do # one( # incr( "gcov", "exclude", "refreshed" ), # "manifest", # "pairs", # "files" # ) # end class RuleCheck # Perform rule checking options. # # @param opt [Object] Options to check. # @yield rule Checking conditions. def RuleCheck.check( opt, &rule ) rc = RuleCheck.new( opt ) rc.instance_eval( &rule ) end # Build Rule checker. def initialize( opt, &rule ) @opt = opt end # 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.argById(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 ) # Opts given consecutive. consecutiveCnt = 0 # Consecutive flag. consecutive = true # All given opts. givenCnt = 0 i = 0 while args[i] score = getScore( args[i] ) # Still consecutive. consecutiveCnt += 1 if ( score == 1 ) && consecutive # No more consecutives. consecutive = false if ( score == 0 ) # Count all given. givenCnt += score i += 1 end ( consecutiveCnt == givenCnt ) && ( givenCnt > 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 false 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 # Logical inversion. def inv( *args ) getScore( *args ) == 0 end # Dont care. def meh( *args ) true end end # Display utility for RuleCheck. Usage model. # # RuleDisplay.new.evalAndDisplay( &rule ) # # Example expansion of options: # # |--# One of: # | |--# Adding in order: # | | |-- # | | |-- # | | |-- # | |-- # | |-- # | |-- # class RuleDisplay < ComoCommon # Eval rules to get an nested array and then display it. def RuleDisplay.print( prefixStr = " ", &rule ) rd = RuleDisplay.new( prefixStr ) rd.evalAndDisplay( &rule ) end # Create rule displayer. def initialize( prefixStr ) # Prefix string for lines. Rules add/rm from it. @prefixStr = prefixStr end # Display method. 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 # Logical inversion. def inv( *args ) [ "Not", *args ] end # Dont care. def meh( *args ) [ "Ignoring", *args ] end end end # Include version definitions. require_relative 'version' # Load user customizations (if any). load ENV['COMO'] if ENV['COMO']