lib/como.rb in como-0.0.2 vs lib/como.rb in como-0.1.0

- old
+ new

@@ -1,216 +1,426 @@ # = Como # # == Introduction +# # Como provides low manifest command line option parsing and -# handling. Command line options are described in a compact table +# deployment. The command line options are described in 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). +# 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. # -# == Simple example -# Below is a small example program ("como_test") that demonstrates +# +# +# == 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 +# ==== Program listing +# # require "como" # include Como # # # Define command line arguments: -# Spec.defineCheckHelp( "como_test", "Programmer", "2013", +# Spec.command( "como_simple", "Programmer", "2013", # [ -# [ :silent, "help", "-h", "Display usage info." ], -# [ :single, "file", "-f", "File argument." ], +# [ :single, "file", "-f", "File argument." ], # [ :switch, "debug", "-d", "Enable debugging." ], # ] ) # -# puts "File option: #{Opt['file'].value}" -# puts "Debugging selected!" if Opt['debug'].given +# puts " File option: #{Opt['file'].value}" +# puts " Debugging selected!" if Opt['debug'].given # -# "Spec.defineCheckHelp" method takes 4 arguments: +# +# 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 in format with 4 -# entries in each sub-array. +# [option table] Description of the command options. # -# 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). +# 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 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 <filename>" on the command -# line. +# 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 <filename>" 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. +# method. For example +# Opt['debug'].given +# would return "true" if "-d" was given on the command line. # -# === Example executions +# 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_test -f example -d +# shell> como_simple -f example -d # # The program would execute with the following output: -# File option: example -# Debugging selected! +# File option: example +# Debugging selected! # -# Como includes certain "extra" behavior out-of-box. For example -# given the command: -# shell> como_test +# 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_test error: Option "-f" missing... -# -# # Usage: -# como_test -f <file> [-d] +# como_simple -f <file> [-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. +# option. The error message is followed by "usage" display (Usage +# Help). Documentation string is taken from the option specification to +# "usage" display. # -# shell> como_test -h +# Given the command: +# shell> como_simple -h # -# Would display the same "usage" screen except without the error -# line. Documentation string is taken from the specification to -# "usage" display. +# would display the same "usage" screen except without the error +# line. # +# === Subccommand example # -# == Option types +# 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 +# +# subcmd( "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." ], +# ] ) +# +# checkRule 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 "subcmd" method call defines the main command +# ("Opt.main") which represents the program. It has two "subcmd" +# options. +# +# The rest of the "subcmd" methods define subcommands for the parent +# command. This example includes one subcommand level, but multiple +# levels are allowed. +# +# The "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 "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 <password>] [-u <username>] -f <file> +# +# -fo Force operation. +# -p User password. +# -u Username. +# -f File. +# +# +# Option Combinations: +# |--# One of: +# | |--<-fo> +# | |--# All of: +# | | |--<password> +# | | |--<username> +# +# Since the combination rule requires either "-fo" or both "password" +# and "username" in a pair. +# +# 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. Option values in array. +# [:multi] Mandatory multiple argument option (one or many). 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". +# [: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 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 +# 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 ] # -# == Specification method options +# "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 # -# 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. +# Option behavior can be controlled with several configuration options. # -# Automatic "help" option processing can be avoided using -# "Spec.defineCheck" instead. +# 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. # -# Both methods above accept additional parameters passed in a -# Hash. The usable hash keys: +# 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. -# [: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). +# [:subcheck] Automatically check that a subcommand is provided +# (default: true). +# [:check_missing] Check for missing arguments (default: true). +# [:tab] Tab stop column for option documentation (default: 12). +# [:help_exit] Exit program if help displayed (default: true). +# [:error_exit] Exit program if error in options (default: true). # # -# == Using Opt class +# +# == 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" +# 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 +# The "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. # -# If the user gives the "--" option, the arguments after that option -# is returned as an Array with "Opt.external" +# === 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 } # -# == Customization +# === Subcommand options # -# 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. +# The given subcommand for the parent command is return by +# "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 # -# == Additional checks +# If the user gives the "--" option (double-dash), the arguments after +# that option is returned as an Array with "Opt.external". # -# 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 # +# +# == 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 +# +# 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. module Como + # IO stream options for Como classes. class ComoCommon # Default value for display output. @@io = STDOUT @@ -228,173 +438,455 @@ # User interface for Como. class Spec < ComoCommon - # Command line options source. - @@argv = ARGV + # 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 ) + if config + Opt.configOverlay( config ) + end + spec = Spec.new( author, year ) + spec.instance_eval( &defs ) + Opt.main.check( ArgsParseState.new( @@argv ) ) + end - # 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 + # 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<Array>] Option definitions. + # @param config [Hash] Option definition's behavioral config + # (changes @@config defaults). + def Spec.command( prog, author, year, defs, config = {} ) + Spec.defineCheck( prog, author, year, defs, config ) + Spec.usage if Opt['help'].given end - # Display program usage (and optionally exit). - def Spec.usage - @@io.puts Spec.usageNormal - exit( 1 ) if @@options[ :help_exit ] + # Alias to Spec.command. + def Spec.defineCheckHelp( prog, author, year, defs, config = {} ) + Spec.command( prog, author, year, defs, config ) end - - # Usage info for Opt:s. - def Spec.usageNormal - str = "" + # Same as "defineCheckHelp" except without automatic "help" + # option processing. + def Spec.defineCheck( prog, author, year, defs, config = {} ) + spec = Spec.new( author, year ) + spec.subcmd( prog, defs, config ) + Opt.main.check( ArgsParseState.new( @@argv ) ) + end - if @@options[ :header ] - str += @@options[ :header ] - str += "\n" + + # 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<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 - str += " + # Overlay user config on top of default. + subcmd.applyConfig( config ) - Usage: - #{Opt.progname} #{Opt.cmdline.join(" ")} + if subcmd.config[ :autohelp ] + # Automatically add the help option. + defs.insert( 0, [ :silent, "help", "-h", "Display usage info." ] ) + end -" - Opt.doc.each do |i| - str += ( " %-8s%s" % [ i[0], i[1..-1].join(" ") ] ) - str += "\n" + subcmd.setSubopt( *Spec.specify( defs ) ) + subcmd + + end + + + # Specify and check options spec. + # + # @param table [Array<Array>] Option definition table. + def Spec.specify( table ) + + options = {} + subcmds = {} + + # Type checks for valid user input. + Spec.ArgCheck( table.class == Array, "Option table is not an Array" ) + + table.each_index do |idx| + + i = table[ idx ] + + Spec.ArgCheck( i.class == Array, "Option table entry is not an Array" ) + + if i[0] == :default && i.length == 2 + + # Add 2 dummy entries for :default type if needed. + table[ idx ] = [ i[0], nil, nil, i[1] ] + + elsif i[0] == :subcmd && i.length == 3 + + # Add 1 dummy entry for :subcmd type if needed. + table[ idx ] = [ i[0], i[1], nil, i[2] ] + end + + Spec.ArgCheck( table[ idx ].length == 4, "Option table entry length not 4" ) end - str += " - Copyright (c) #{Opt.year} by #{Opt.author} + + table.each do |e| -" + if e[0] == :subcmd - if @@options[ :footer ] - str += @@options[ :footer ] - str += "\n" + subcmds[ e[1] ] = Opt.subcmd( e[1], e[3] ) + + else + + option = nil + + case e[0] + + when :switch + option = Opt.switch( e[1], e[2], e[3] ) + + when :exclusive + option = Opt.exclusive( e[1], e[2], e[3] ) + + when :silent + option = Opt.exclusive( e[1], e[2], e[3], true ) + + when :single, :multi, :opt_single, :opt_multi, :opt_any + option = Opt.full( e[1], e[2], e[0], e[3] ) + + when :default + option = Opt.defaultOpt( e[3] ) + + else + raise "Unknown option type: \"#{e[0]}\"..." + end + + options[ option.name ] = option + + end + end + + [ options.values, subcmds.values ] + end - str + + # 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 ) - @@options[ :header ] = str + Opt.main.setUsageHeader( str ) end + # Set optional footer for "usage". def Spec.setUsageFooter( str ) - @@options[ :footer ] = str + Opt.main.setUsageFooter( 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 + # 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 - # 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 ] + + # 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 + # 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 - # Check only. - def Spec.check - Opt.parse( @@argv, @@options[ :check ] ) - Opt.checkMissing + + private + + def Spec.ArgCheck( cond, str ) + raise( ArgumentError, str ) unless cond end - # Overlay "option" on top of options defaults (@@options). - def Spec.applyOptionDefaults( option ) - option.replace( @@options.merge( option ) ) + 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 + attr_reader :data + def initialize( message = nil, data = nil ) + super( message ) + @data = data + end 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 ) + # Missing argument exception. + class MissingArgument < ErrorWithData; end - # Display the possible combination: - @@io.puts "\n Option combination rules:\n\n" - Opt::RuleDisplay.new.evalAndDisplay( &rule ) + # Invalid (non-existing) option exception. + class InvalidOption < ErrorWithData; end - Spec.usage + + + # ------------------------------------------------------------ + # 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, + :tab => 12, + :help_exit => true, + :error_exit => 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 - # 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 + # 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 - rescue Opt::MissingArgument, Opt::InvalidOption => str - @@io.puts - Opt.error( str ) - Spec.usage - exit( 1 ) end + + ret end - end + # Return program name. + def Opt.progname + @@main.name + 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. + # Return program year. + def Opt.year + @@main.year + end - class Opt < ComoCommon - class Error < StandardError; end - class MissingArgument < Error; end - class InvalidOption < Error; 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 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 ) + o = new( name, opt, :exclusive, doc, false ) + o.silent = silent + o + end + + # Create default option spec, no switch. + def Opt.defaultOpt( doc = "No doc." ) + new( "<default>", "<args>", :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 + + + # ------------------------------------------------------------ + # Opt properties: + + + # Subcommand parent (i.e. host). + attr_accessor :parent + + # Option name. attr_accessor :name # Short option string. attr_accessor :opt @@ -415,17 +907,23 @@ attr_writer :given # Is option hidden (usage). attr_accessor :silent - # Parsed option specs and option values. - @@opts = [] + # List of suboptions. + attr_reader :subopt - # Program external arguments (e.g. subprogram args) - @@external = nil + # List of subcommands. + attr_reader :subcmd + # Opt configuration. + attr_reader :config + # Opt rules. + attr_reader :rules + + # Create Opt object: # [name] Option name string. # [opt] Switch string. # [type] Option type. One of: # * :switch @@ -439,49 +937,384 @@ # * :silent # [doc] Option documentation. # [value] Default value. def initialize( name, opt, type, doc, value = nil ) + @parent = 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 + @subopt = nil + @subcmd = nil + @rules = nil + + @config = @@config.dup + + Opt.addOpt( self ) end + # Set subcommand suboptions. + # + # @param opts [Array<Opt>] + def setSubopt( opts, subs ) + opts.each do |i| + i.parent = self + end + + subs.each do |i| + i.parent = self + end + + @subopt = opts + @subcmd = subs + 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 + + begin + + # Parse and check for invalid arguments. + begin + top = top.parse( argsState, top.config[ :check_missing ] ) + end while( top ) + + # Check for any missing valid arguments. + checkMissing + + rescue Opt::MissingArgument, Opt::InvalidOption => err + + @@io.puts + + error( err.to_s ) + + # Display subcmd specific usage info. + err.data.usage + + exit( 1 ) if Opt.main.config[ :error_exit ] + + 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 for \"#{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 + + 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.value = !o.value if !o.given + 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 for \"#{args.get}\"...", + self ) + else + # Default option. + o.given = true + o.value.push args.toValue + args.next + end + + else + + 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 + + # Full cmd name. + cmd = ( getParents.map do |i| i.name end ).join( ' ' ) + + # Check for any exclusive args first. + @subopt.each do |o| + if o.type == :exclusive && 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 + error( 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 + error( 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 + + + # All subcommand options, options and subcommands. + def suball + @subopt + @subcmd + end + + # Options list iterator. - def Opt.each - @@opts.each do |o| + def each( &blk ) + suball.each do |o| yield o end end # Options iterator for given options. - def Opt.each_given - @@opts.each do |o| + def each_given( &blk ) + suball.each do |o| yield o if o.given end end # Number of given options. - def Opt.givenCount + def givenCount cnt = 0 - Opt.each_given do |i| + 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 value @@ -492,566 +1325,732 @@ # 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. - def given( &prog ) + # + # @param yieldOpt [Boolean] Pass Opt to block instead of its + # value. + def given( yieldOpt = false, &prog ) if block_given? if @given - yield( @value ) + if yieldOpt + yield( self ) + else + yield( @value ) + end else false end else @given end end - # Check for any non-given required arguments. - def Opt.checkMissing + # Alias for given. + alias given? given - # 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 + # Return the selected subcommand. + def givenSubcmd + ( @subcmd.select do |o| o.given end )[0] end - # Reset "dynamic" class members. - def Opt.reset - @@opts = [] - @@external = nil + # 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 - # Select option object by name operator. - def Opt.[](str) - Opt.arg(str) + # Return default options. + def default + argByName( nil ) end # Select option object by name. - def Opt.arg( str ) - if str == nil - @@opts.each do |o| + def argByName( str ) + if str == nil || str == :default + @subopt.each do |o| if o.type == :default return o end end nil else - @@opts.each do |o| + suball.each do |o| if str == o.name return o end end nil end end - # Return program name. - def Opt.progname - @@progname + # 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 - # Return program year. - def Opt.year - @@year + + # Option requires argument? + def hasArg + case @type + when :single, :multi, :opt_single, :opt_multi, :opt_any; true + else false + end end - # Return author. - def Opt.author - @@author + + # Option requires many arguments? + def hasMany + case @type + when :multi, :opt_multi, :opt_any; true + else false + end end - # Return options that are specified as command external - # (i.e. after '--'). - def Opt.external - @@external + # Is mandatory argument? + def isRequired + case @type + when :single, :multi; true + else false + end 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] ) + + # Test if option is silent. + def silent? + @silent + end + + + # Test if option is of switch type. + def isSwitch + case @type + when :switch, :exclusive, :default; true + else false end - doc end + # ------------------------------------------------------------ + # Opt "Usage Help" user interface: - # Set of methods which represent option combination checking. - # In effect this is a meta language (DSL) for option - # combinations. + # 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). # - # Example: - # RuleCheck.checkRule do - # one( - # incr( "gcov", "exclude", "refreshed" ), - # "manifest", - # "pairs", - # "files" - # ) - # end - class RuleCheck + # @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 - # 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 + @@io.puts usageNormal - # Special condition when no options are given. - def none - Opt.givenCount == 0 + if ruleHelp + @@io.puts "\n Option Combinations:" + @@io.puts RuleDisplay.print( &@rules ) 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 + exit( 1 ) if doExit + 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 + # Display program usage (and optionally exit). + def usageIfHelp + if self['help'].given + usage + elsif hasSubcmd && givenSubcmd + givenSubcmd.usageIfHelp end + end - # One of list given. - def one( *args ) - getScore( *args ) == 1 - end - # At least one is given. - def any( *args ) - getScore( *args ) > 0 + # 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 += "\n" + str += @config[ :header ] + str += "\n" end - # All are given. - def all( *args ) - getScore( *args ) == args.length + str += usageCommand + + if @config[ :footer ] + str += @config[ :footer ] + str += "\n" end + + str end + # Return cmdline usage strings for options in an Array. + def cmdline + opts = [] - # Display utility for RuleCheck. Same usage model. - # - # Example expansion of options: - # - # |--# One of: - # | |--# Adding in order: - # | | |--<gcov> - # | | |--<exclude> - # | | |--<refreshed> - # | |--<manifest> - # | |--<pairs> - # | |--<files> - # - class RuleDisplay < ComoCommon + @subopt.each do |o| - def initialize - # Prefix string for lines. Rules add/rm from it. - @prefixStr = " " - self - end + next if o.silent? - # Eval rules to get an nested array and then display it. - def evalAndDisplay( &rule ) - printRule( instance_eval( &rule ) ) - end + prural = nil + case o.type + when :multi, :opt_multi; prural = "+" + when :opt_any; prural = "*" + else prural = "" + 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 + if !( o.isSwitch ) + name = " <#{o.name}>#{prural}" else - cnt = item + name = "" end - @prefixStr = @prefixStr[0..-(cnt+1)] + + 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 + - # Print prefix + str. - def p( str ) - @@io.puts( @prefixStr + str ) + if hasSubcmd + opts.push "<<subcommand>>" end - # Recursively go through the nested array of rule items and - # print out rules. - def printRule( arr ) - p( "|--# #{arr[0]}:" ) - item = "| " - addPrefix( item ) + opts - arr[1..-1].each do |i| - if i.class == Array - printRule( i ) - else - p( "|--<#{i}>" ) - end - end - rmPrefix( item ) - end + 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 + # Return document strings for options. + def suboptDoc - # Incremental options in order i.e. have to have all later - # if had first. - def follow( *args ) - [ "If first then rest", *args ] - end + str = "" + # format = Proc.new do |s,d| ( " %-#{@config[ :tab ]}s%s\n" % [ s, d ] ) end - # One of list given. - def one( *args ) - [ "One of", *args ] + str += suboptDocFormat( "Options:", "" ) if hasSubcmd && hasVisibleOptions + + @subopt.each do |o| + next if o.silent? + str += suboptDocFormat( o.opt ? o.opt : o.longOpt, o.doc ) end - # At least one is given. - def any( *args ) - [ "One or more of", *args ] + str += "\n" + suboptDocFormat( "Subcommands:", "" ) if hasSubcmd + + @subcmd.each do |o| + str += suboptDocFormat( o.name, o.doc ) end - # All are given. - def all( *args ) - [ "All of", *args ] + 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. + def error( str ) + @@io.puts "#{Opt.progname} error: #{str}" end + - # Check against option combination rules. - def Opt.checkRule( &rule ) - RuleCheck.new.instance_eval( &rule ) + # ------------------------------------------------------------ + # 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 - # Create option spec. - def Opt.full( name, opt, type, doc = "No doc." ) - new( name, opt, type, doc ) + # Return list of parents. + def getParents + list = [] + cur = self + begin + list.push cur + cur = cur.parent + end while cur + + list.reverse end - # Create switch option spec. - def Opt.switch( name, opt, doc = "No doc." ) - new( name, opt, :switch, doc, false ) + + # Full command name. + def fullCommand + ( getParents.map do |i| i.name end ).join( ' ' ) 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 + + # Test if option is of subcmd type. + def isSubcmd + @type == :subcmd end - # Create default option spec, no switch. - def Opt.default( doc = "No doc." ) - new( nil, "<arg>", :default, doc, [] ) + + # Test if option is of subcmd type. + def hasVisibleOptions + # Count non-silent options. + ( @subopt.select do |i| !i.silent? end ).length > 0 end - # Specify and check options spec. - def Opt.specify( progname, author, year, table ) + # Test if option is of subcmd type. + def hasSubcmd + !@subcmd.empty? + end - # 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 + # Custom check for the option. User has to know some Como + # internals. + def evalCheck( &check ) + instance_eval &check + end - @@progname = progname - @@year = year - @@author = author + # 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 ) - table.each do |e| + # Format doc nicely to multiple lines if newline is + # followed by tab stop. - 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] ) + 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 + + + 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 = " + Usage: + #{fullCommand} #{cmdline.join(" ")} + +" + str += suboptDoc + + str += " + + Copyright (c) #{Opt.year} by #{Opt.author} + +" + end + + end + + + # Command argument parsing state. + class ArgsParseState + + # @param list [Array<String>] 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 Opt.isOpt( str ) + def isOpt( str = get ) str[0..0] == "-" end # Test whether str is an option list terminator. - def Opt.isOptTerm( str ) + def isOptTerm( str = get ) str == "--" end # Format value string if escaped. - def Opt.toValue( str ) + def toValue( str = get ) 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 + end - # Option requires many arguments? - def hasMany - case @type - when :multi, :opt_multi, :opt_any; true - else false - 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 + + def RuleCheck.check( opt, &rule ) + rc = RuleCheck.new( opt ) + rc.instance_eval( &rule ) end - # Is mandatory argument? - def isRequired - case @type - when :single, :multi; true - else false - end + def initialize( opt, &rule ) + @opt = opt end - # Test if option is of switch type. - def isSwitch - case @type - when :switch, :exclusive, :default; true - else false + + # 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 - # Test if option is silent. - def silent? - @silent + # Special condition when no options are given. + def none + @opt.givenCount == 0 end - # Custom check for the option. User have to know some Como - # internals. - def check( &check ) - instance_eval &check + # 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 - - # 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] + # 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 - ( @@opts.select { |i| i.opt == str } )[0] + true end end - - # Como error printout. - def Opt.error( str ) - @@io.puts "#{@@progname} error: #{str}" + + # One of list given. + def one( *args ) + getScore( *args ) == 1 end + # At least one is given. + def any( *args ) + getScore( *args ) > 0 + end - # Parse cmdline options from args. - def Opt.parse( args, checkInvalids = true ) - ind = 0 + # All are given. + def all( *args ) + getScore( *args ) == args.length + end - while args[ind] + # Logical inversion. + def inv( *args ) + getScore( *args ) == 0 + end - if Opt.isOptTerm( args[ind] ) + # Dont care. + def meh( *args ) + true + end + end - # 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] ) + # Display utility for RuleCheck. Usage model. + # + # RuleDisplay.new.evalAndDisplay( &rule ) + # + # Example expansion of options: + # + # |--# One of: + # | |--# Adding in order: + # | | |--<gcov> + # | | |--<exclude> + # | | |--<refreshed> + # | |--<manifest> + # | |--<pairs> + # | |--<files> + # + class RuleDisplay < ComoCommon - 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 + # Eval rules to get an nested array and then display it. + def RuleDisplay.print( prefixStr = " ", &rule ) + rd = RuleDisplay.new( prefixStr ) + rd.evalAndDisplay( &rule ) + end - elsif o && o.hasArg + def initialize( prefixStr ) + # Prefix string for lines. Rules add/rm from it. + @prefixStr = prefixStr + end - ind += 1 + def evalAndDisplay( &rule ) + printRule( instance_eval( &rule ) ) + end - if ( !args[ind] || Opt.isOpt( args[ind] ) ) && - o.type != :opt_any + # Increase prefix string. + def addPrefix( str ) + @prefixStr += str + end - raise MissingArgument, \ - "No argument given for \"#{o.opt}\"..." + # 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 - else + # Print prefix + str. + def p( str ) + @@io.puts( @prefixStr + str ) + end - 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 + # Recursively go through the nested array of rule items and + # print out rules. + def printRule( arr ) + p( "|--# #{arr[0]}:" ) + item = "| " + addPrefix( item ) - 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 - + arr[1..-1].each do |i| + if i.class == Array + printRule( i ) 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 + p( "|--<#{i}>" ) end end + rmPrefix( item ) end + # Special condition where no arguments are given. + def none + [ "NONE" ] + end - # Return cmdline usage strings for options in an Array. - def Opt.cmdline - opts = [] + # Incremental options in order i.e. have to have previous + # to have later. + def incr( *args ) + [ "Adding in order", *args ] + end - @@opts.each do |o| + # Incremental options in order i.e. have to have all later + # if had first. + def follow( *args ) + [ "If first then rest", *args ] + end - next if o.silent? + # One of list given. + def one( *args ) + [ "One of", *args ] + end - prural = nil - case o.type - when :multi, :opt_multi; prural = "+" - when :opt_any; prural = "*" - else prural = "" - end + # At least one is given. + def any( *args ) + [ "One or more of", *args ] + end - if !( o.isSwitch ) - name = " <#{o.name}>#{prural}" - else - name = "" - end + # All are given. + def all( *args ) + [ "All of", *args ] + end - if o.opt == nil - opt = o.longOpt - else - opt = o.opt - end + # Logical inversion. + def inv( *args ) + [ "Not", *args ] + end - if o.isRequired - opts.push "#{opt}#{name}" - else - opts.push "[#{opt}#{name}]" - end - end - - opts + # Dont care. + def meh( *args ) + [ "Ignoring", *args ] end end + end