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