# vim:et:ft=sh:sts=2:sw=2 # # Copyright 2008-2017 Kate Ward. All Rights Reserved. # Released under the Apache License 2.0 license. # http://www.apache.org/licenses/LICENSE-2.0 # # shFlags -- Advanced command-line flag library for Unix shell scripts. # https://github.com/kward/shflags # # Author: kate.ward@forestent.com (Kate Ward) # # This module implements something like the gflags library available # from https://github.com/gflags/gflags. # # FLAG TYPES: This is a list of the DEFINE_*'s that you can do. All flags take # a name, default value, help-string, and optional 'short' name (one-letter # name). Some flags have other arguments, which are described with the flag. # # DEFINE_string: takes any input, and interprets it as a string. # # DEFINE_boolean: does not take any arguments. Say --myflag to set # FLAGS_myflag to true, or --nomyflag to set FLAGS_myflag to false. For short # flags, passing the flag on the command-line negates the default value, i.e. # if the default is true, passing the flag sets the value to false. # # DEFINE_float: takes an input and interprets it as a floating point number. As # shell does not support floats per-se, the input is merely validated as # being a valid floating point value. # # DEFINE_integer: takes an input and interprets it as an integer. # # SPECIAL FLAGS: There are a few flags that have special meaning: # --help (or -?) prints a list of all the flags in a human-readable fashion # --flagfile=foo read flags from foo. (not implemented yet) # -- as in getopt(), terminates flag-processing # # EXAMPLE USAGE: # # -- begin hello.sh -- # #! /bin/sh # . ./shflags # DEFINE_string name 'world' "somebody's name" n # FLAGS "$@" || exit $? # eval set -- "${FLAGS_ARGV}" # echo "Hello, ${FLAGS_name}." # -- end hello.sh -- # # $ ./hello.sh -n Kate # Hello, Kate. # # CUSTOMIZABLE BEHAVIOR: # # A script can override the default 'getopt' command by providing the path to # an alternate implementation by defining the FLAGS_GETOPT_CMD variable. # # NOTES: # # * Not all systems include a getopt version that supports long flags. On these # systems, only short flags are recognized. #============================================================================== # shFlags # # Shared attributes: # flags_error: last error message # flags_output: last function output (rarely valid) # flags_return: last return value # # __flags_longNames: list of long names for all flags # __flags_shortNames: list of short names for all flags # __flags_boolNames: list of boolean flag names # # __flags_opts: options parsed by getopt # # Per-flag attributes: # FLAGS_: contains value of flag named 'flag_name' # __flags__default: the default flag value # __flags__help: the flag help string # __flags__short: the flag short name # __flags__type: the flag type # # Notes: # - lists of strings are space separated, and a null value is the '~' char. # ### ShellCheck (http://www.shellcheck.net/) # $() are not fully portable (POSIX != portable). # shellcheck disable=SC2006 # [ p -a q ] are well defined enough (vs [ p ] && [ q ]). # shellcheck disable=SC2166 # Return if FLAGS already loaded. test -n $(FLAGS_VERSION:-) && return 0 setglobal FLAGS_VERSION = ''1.2.3pre'' # Return values that scripts can use. setglobal FLAGS_TRUE = '0' setglobal FLAGS_FALSE = '1' setglobal FLAGS_ERROR = '2' # Logging levels. setglobal FLAGS_LEVEL_DEBUG = '0' setglobal FLAGS_LEVEL_INFO = '1' setglobal FLAGS_LEVEL_WARN = '2' setglobal FLAGS_LEVEL_ERROR = '3' setglobal FLAGS_LEVEL_FATAL = '4' setglobal __FLAGS_LEVEL_DEFAULT = $(FLAGS_LEVEL_WARN) # Determine some reasonable command defaults. setglobal __FLAGS_EXPR_CMD = ''expr --'' setglobal __FLAGS_UNAME_S = $[uname -s] if test $(__FLAGS_UNAME_S) = 'BSD' { setglobal __FLAGS_EXPR_CMD = ''gexpr --'' } else { setglobal _flags_output_ = $[$(__FLAGS_EXPR_CMD) !2 > !1] if test $Status -eq $(FLAGS_TRUE) -a $(_flags_output_) = '--' { # We are likely running inside BusyBox. setglobal __FLAGS_EXPR_CMD = ''expr'' } unset _flags_output_ } # Commands a user can override if desired. setglobal FLAGS_EXPR_CMD = $(FLAGS_EXPR_CMD:-${__FLAGS_EXPR_CMD}) setglobal FLAGS_GETOPT_CMD = $(FLAGS_GETOPT_CMD:-getopt) # Specific shell checks. if test -n $(ZSH_VERSION:-) { setopt |grep "^shwordsplit$" >/dev/null if test $Status -ne $(FLAGS_TRUE) { _flags_fatal 'zsh shwordsplit option is required for proper zsh operation' } if test -z $(FLAGS_PARENT:-) { _flags_fatal "zsh does not pass \$0 through properly. please declare' \ \"FLAGS_PARENT=\$0\" before calling shFlags" } } # Can we use built-ins? shell { echo $(FLAGS_TRUE#0); } >/dev/null 2>&1 if test $Status -eq $(FLAGS_TRUE) { setglobal __FLAGS_USE_BUILTIN = $(FLAGS_TRUE) } else { setglobal __FLAGS_USE_BUILTIN = $(FLAGS_FALSE) } # # Constants. # # Reserved flag names. setglobal __FLAGS_RESERVED_LIST = '' ARGC ARGV ERROR FALSE GETOPT_CMD HELP PARENT TRUE '' setglobal __FLAGS_RESERVED_LIST = ""$(__FLAGS_RESERVED_LIST) VERSION "" # Determined getopt version (standard or enhanced). setglobal __FLAGS_GETOPT_VERS_STD = '0' setglobal __FLAGS_GETOPT_VERS_ENH = '1' # shellcheck disable=SC2120 proc _flags_getopt_vers { setglobal _flags_getopt_cmd_ = $(1:-${FLAGS_GETOPT_CMD}) match $[$(_flags_getopt_cmd_) -lfoo '' --foo !2 > !1] { with ' -- --foo' echo $(__FLAGS_GETOPT_VERS_STD) with ' --foo --' echo $(__FLAGS_GETOPT_VERS_ENH) # Unrecognized output. Assuming standard getopt version. with * echo $(__FLAGS_GETOPT_VERS_STD) } unset _flags_getopt_cmd_ } # shellcheck disable=SC2119 setglobal __FLAGS_GETOPT_VERS = $[_flags_getopt_vers] # getopt optstring lengths setglobal __FLAGS_OPTSTR_SHORT = '0' setglobal __FLAGS_OPTSTR_LONG = '1' setglobal __FLAGS_NULL = ''~'' # Flag info strings. setglobal __FLAGS_INFO_DEFAULT = ''default'' setglobal __FLAGS_INFO_HELP = ''help'' setglobal __FLAGS_INFO_SHORT = ''short'' setglobal __FLAGS_INFO_TYPE = ''type'' # Flag lengths. setglobal __FLAGS_LEN_SHORT = '0' setglobal __FLAGS_LEN_LONG = '1' # Flag types. setglobal __FLAGS_TYPE_NONE = '0' setglobal __FLAGS_TYPE_BOOLEAN = '1' setglobal __FLAGS_TYPE_FLOAT = '2' setglobal __FLAGS_TYPE_INTEGER = '3' setglobal __FLAGS_TYPE_STRING = '4' # Set the constants readonly. setglobal __flags_constants = $[set |awk -F= '/^FLAGS_/ || /^__FLAGS_/ {print $1}] for __flags_const in [$(__flags_constants)] { # Skip certain flags. match $(__flags_const) { with FLAGS_HELP continue with FLAGS_PARENT continue } # Set flag readonly. if test -z $(ZSH_VERSION:-) { readonly $(__flags_const) continue } match $(ZSH_VERSION) { with [123].* readonly $(__flags_const) with * readonly -g $(__flags_const) # Declare readonly constants globally. } } unset __flags_const __flags_constants # # Internal variables. # # Space separated lists. setglobal __flags_boolNames = '' '' # Boolean flag names. setglobal __flags_longNames = '' '' # Long flag names. setglobal __flags_shortNames = '' '' # Short flag names. setglobal __flags_definedNames = '' '' # Defined flag names (used for validation). setglobal __flags_columns = '''' # Screen width in columns. setglobal __flags_level = '0' # Default logging level. setglobal __flags_opts = '''' # Temporary storage for parsed getopt flags. #------------------------------------------------------------------------------ # Private functions. # # Logging functions. proc _flags_debug { test $(__flags_level) -le $(FLAGS_LEVEL_DEBUG) || return echo "flags:DEBUG $ifsjoin(Argv)" > !2 } proc _flags_info { test $(__flags_level) -le $(FLAGS_LEVEL_INFO) || return echo "flags:INFO $ifsjoin(Argv)" > !2 } proc _flags_warn { test $(__flags_level) -le $(FLAGS_LEVEL_WARN) || return echo "flags:WARN $ifsjoin(Argv)" > !2 } proc _flags_error { test $(__flags_level) -le $(FLAGS_LEVEL_ERROR) || return echo "flags:ERROR $ifsjoin(Argv)" > !2 } proc _flags_fatal { test $(__flags_level) -le $(FLAGS_LEVEL_FATAL) || return echo "flags:FATAL $ifsjoin(Argv)" > !2 exit ${FLAGS_ERROR} } # Get the logging level. proc flags_loggingLevel { echo $(__flags_level); } # Set the logging level. # # Args: # _flags_level_: integer: new logging level # Returns: # nothing proc flags_setLoggingLevel { test $Argc -ne 1 && _flags_fatal "flags_setLevel(): logging level missing" setglobal _flags_level_ = $1 test $(_flags_level_) -ge $(FLAGS_LEVEL_DEBUG) \ -a $(_flags_level_) -le $(FLAGS_LEVEL_FATAL) \ || _flags_fatal "Invalid logging level '$(_flags_level_)' specified." setglobal __flags_level = $1 unset _flags_level_ } # Define a flag. # # Calling this function will define the following info variables for the # specified flag: # FLAGS_flagname - the name for this flag (based upon the long flag name) # __flags__default - the default value # __flags_flagname_help - the help string # __flags_flagname_short - the single letter alias # __flags_flagname_type - the type of flag (one of __FLAGS_TYPE_*) # # Args: # _flags_type_: integer: internal type of flag (__FLAGS_TYPE_*) # _flags_name_: string: long flag name # _flags_default_: default flag value # _flags_help_: string: help string # _flags_short_: string: (optional) short flag name # Returns: # integer: success of operation, or error proc _flags_define { if test $Argc -lt 4 { setglobal flags_error = ''DEFINE error: too few arguments'' setglobal flags_return = $(FLAGS_ERROR) _flags_error $(flags_error) return ${flags_return} } setglobal _flags_type_ = $1 setglobal _flags_name_ = $2 setglobal _flags_default_ = $3 setglobal _flags_help_ = $(4:-§) # Special value '§' indicates no help string provided. setglobal _flags_short_ = $(5:-${__FLAGS_NULL}) _flags_debug "type:$(_flags_type_) name:$(_flags_name_)" \ "default:'$(_flags_default_)' help:'$(_flags_help_)'" \ "short:$(_flags_short_)" setglobal _flags_return_ = $(FLAGS_TRUE) setglobal _flags_usName_ = $[_flags_underscoreName $(_flags_name_)] # Check whether the flag name is reserved. _flags_itemInList $(_flags_usName_) $(__FLAGS_RESERVED_LIST) if test $Status -eq $(FLAGS_TRUE) { setglobal flags_error = ""flag name ($(_flags_name_)) is reserved"" setglobal _flags_return_ = $(FLAGS_ERROR) } # Require short option for getopt that don't support long options. if test $(_flags_return_) -eq $(FLAGS_TRUE) \ -a $(__FLAGS_GETOPT_VERS) -ne $(__FLAGS_GETOPT_VERS_ENH) \ -a $(_flags_short_) = $(__FLAGS_NULL) { setglobal flags_error = ""short flag required for ($(_flags_name_)) on this platform"" setglobal _flags_return_ = $(FLAGS_ERROR) } # Check for existing long name definition. if test $(_flags_return_) -eq $(FLAGS_TRUE) { if _flags_itemInList $(_flags_usName_) $(__flags_definedNames) { setglobal flags_error = ""definition for ([no]$(_flags_name_)) already exists"" _flags_warn $(flags_error) setglobal _flags_return_ = $(FLAGS_FALSE) } } # Check for existing short name definition. if test $(_flags_return_) -eq $(FLAGS_TRUE) \ -a $(_flags_short_) != $(__FLAGS_NULL) { if _flags_itemInList $(_flags_short_) $(__flags_shortNames) { setglobal flags_error = ""flag short name ($(_flags_short_)) already defined"" _flags_warn $(flags_error) setglobal _flags_return_ = $(FLAGS_FALSE) } } # Handle default value. Note, on several occasions the 'if' portion of an # if/then/else contains just a ':' which does nothing. A binary reversal via # '!' is not done because it does not work on all shells. if test $(_flags_return_) -eq $(FLAGS_TRUE) { match $(_flags_type_) { with ${__FLAGS_TYPE_BOOLEAN} if _flags_validBool $(_flags_default_) { match $(_flags_default_) { with true|t|0 setglobal _flags_default_ = $(FLAGS_TRUE) with false|f|1 setglobal _flags_default_ = $(FLAGS_FALSE) } } else { setglobal flags_error = ""invalid default flag value '$(_flags_default_)'"" setglobal _flags_return_ = $(FLAGS_ERROR) } with ${__FLAGS_TYPE_FLOAT} if _flags_validFloat $(_flags_default_) { : } else { setglobal flags_error = ""invalid default flag value '$(_flags_default_)'"" setglobal _flags_return_ = $(FLAGS_ERROR) } with ${__FLAGS_TYPE_INTEGER} if _flags_validInt $(_flags_default_) { : } else { setglobal flags_error = ""invalid default flag value '$(_flags_default_)'"" setglobal _flags_return_ = $(FLAGS_ERROR) } with ${__FLAGS_TYPE_STRING} # Everything in shell is a valid string. with * setglobal flags_error = ""unrecognized flag type '$(_flags_type_)'"" setglobal _flags_return_ = $(FLAGS_ERROR) } } if test $(_flags_return_) -eq $(FLAGS_TRUE) { # Store flag information. eval "FLAGS_$(_flags_usName_)='$(_flags_default_)'" eval "__flags_$(_flags_usName_)_$(__FLAGS_INFO_TYPE)=$(_flags_type_)" eval "__flags_$(_flags_usName_)_$(__FLAGS_INFO_DEFAULT)=\ \"$(_flags_default_)\"" eval "__flags_$(_flags_usName_)_$(__FLAGS_INFO_HELP)=\"$(_flags_help_)\"" eval "__flags_$(_flags_usName_)_$(__FLAGS_INFO_SHORT)='$(_flags_short_)'" # append flag names to name lists setglobal __flags_shortNames = ""$(__flags_shortNames)$(_flags_short_) "" setglobal __flags_longNames = ""$(__flags_longNames)$(_flags_name_) "" test $(_flags_type_) -eq $(__FLAGS_TYPE_BOOLEAN) && \ setglobal __flags_boolNames = ""$(__flags_boolNames)no$(_flags_name_) "" # Append flag names to defined names for later validation checks. setglobal __flags_definedNames = ""$(__flags_definedNames)$(_flags_usName_) "" test $(_flags_type_) -eq $(__FLAGS_TYPE_BOOLEAN) && \ setglobal __flags_definedNames = ""$(__flags_definedNames)no$(_flags_usName_) "" } setglobal flags_return = $(_flags_return_) unset _flags_default_ _flags_help_ _flags_name_ _flags_return_ \ _flags_short_ _flags_type_ _flags_usName_ test $(flags_return) -eq $(FLAGS_ERROR) && _flags_error $(flags_error) return ${flags_return} } # Underscore a flag name by replacing dashes with underscores. # # Args: # unnamed: string: log flag name # Output: # string: underscored name proc _flags_underscoreName { echo $1 |tr '-' '_' } # Return valid getopt options using currently defined list of long options. # # This function builds a proper getopt option string for short (and long) # options, using the current list of long options for reference. # # Args: # _flags_optStr: integer: option string type (__FLAGS_OPTSTR_*) # Output: # string: generated option string for getopt # Returns: # boolean: success of operation (always returns True) proc _flags_genOptStr { setglobal _flags_optStrType_ = $1 setglobal _flags_opts_ = '''' for _flags_name_ in [$(__flags_longNames)] { setglobal _flags_usName_ = $[_flags_underscoreName $(_flags_name_)] setglobal _flags_type_ = $[_flags_getFlagInfo $(_flags_usName_) $(__FLAGS_INFO_TYPE)] test $Status -eq $(FLAGS_TRUE) || _flags_fatal 'call to _flags_type_ failed' match $(_flags_optStrType_) { with ${__FLAGS_OPTSTR_SHORT} setglobal _flags_shortName_ = $[_flags_getFlagInfo \ $(_flags_usName_) $(__FLAGS_INFO_SHORT)] if test $(_flags_shortName_) != $(__FLAGS_NULL) { setglobal _flags_opts_ = ""$(_flags_opts_)$(_flags_shortName_)"" # getopt needs a trailing ':' to indicate a required argument. test $(_flags_type_) -ne $(__FLAGS_TYPE_BOOLEAN) && \ setglobal _flags_opts_ = ""$(_flags_opts_):"" } with ${__FLAGS_OPTSTR_LONG} setglobal _flags_opts_ = ""$(_flags_opts_:+${_flags_opts_},)$(_flags_name_)"" # getopt needs a trailing ':' to indicate a required argument test $(_flags_type_) -ne $(__FLAGS_TYPE_BOOLEAN) && \ setglobal _flags_opts_ = ""$(_flags_opts_):"" } } echo $(_flags_opts_) unset _flags_name_ _flags_opts_ _flags_optStrType_ _flags_shortName_ \ _flags_type_ _flags_usName_ return ${FLAGS_TRUE} } # Returns flag details based on a flag name and flag info. # # Args: # string: underscored flag name # string: flag info (see the _flags_define function for valid info types) # Output: # string: value of dereferenced flag variable # Returns: # integer: one of FLAGS_{TRUE|FALSE|ERROR} proc _flags_getFlagInfo { # Note: adding gFI to variable names to prevent naming conflicts with calling # functions setglobal _flags_gFI_usName_ = $1 setglobal _flags_gFI_info_ = $2 # Example: given argument usName (underscored flag name) of 'my_flag', and # argument info of 'help', set the _flags_infoValue_ variable to the value of # ${__flags_my_flag_help}, and see if it is non-empty. setglobal _flags_infoVar_ = ""__flags_$(_flags_gFI_usName_)_$(_flags_gFI_info_)"" setglobal _flags_strToEval_ = ""_flags_infoValue_=\"\${$(_flags_infoVar_):-}"\"" eval $(_flags_strToEval_) if test -n $(_flags_infoValue_) { # Special value '§' indicates no help string provided. test $(_flags_gFI_info_) = $(__FLAGS_INFO_HELP) \ -a $(_flags_infoValue_) = '§' && setglobal _flags_infoValue_ = '''' setglobal flags_return = $(FLAGS_TRUE) } else { # See if the _flags_gFI_usName_ variable is a string as strings can be # empty... # Note: the DRY principle would say to have this function call itself for # the next three lines, but doing so results in an infinite loop as an # invalid _flags_name_ will also not have the associated _type variable. # Because it doesn't (it will evaluate to an empty string) the logic will # try to find the _type variable of the _type variable, and so on. Not so # good ;-) # # Example cont.: set the _flags_typeValue_ variable to the value of # ${__flags_my_flag_type}, and see if it equals '4'. setglobal _flags_typeVar_ = ""__flags_$(_flags_gFI_usName_)_$(__FLAGS_INFO_TYPE)"" setglobal _flags_strToEval_ = ""_flags_typeValue_=\"\${$(_flags_typeVar_):-}"\"" eval $(_flags_strToEval_) # shellcheck disable=SC2154 if test $(_flags_typeValue_) = $(__FLAGS_TYPE_STRING) { setglobal flags_return = $(FLAGS_TRUE) } else { setglobal flags_return = $(FLAGS_ERROR) setglobal flags_error = ""missing flag info variable ($(_flags_infoVar_))"" } } echo $(_flags_infoValue_) unset _flags_gFI_usName_ _flags_gfI_info_ _flags_infoValue_ _flags_infoVar_ \ _flags_strToEval_ _flags_typeValue_ _flags_typeVar_ test $(flags_return) -eq $(FLAGS_ERROR) && _flags_error $(flags_error) return ${flags_return} } # Check for presence of item in a list. # # Passed a string (e.g. 'abc'), this function will determine if the string is # present in the list of strings (e.g. ' foo bar abc '). # # Args: # _flags_str_: string: string to search for in a list of strings # unnamed: list: list of strings # Returns: # boolean: true if item is in the list proc _flags_itemInList { setglobal _flags_str_ = $1 shift match " $(*:-) " { with *\ ${_flags_str_}\ * setglobal flags_return = $(FLAGS_TRUE) with * setglobal flags_return = $(FLAGS_FALSE) } unset _flags_str_ return ${flags_return} } # Returns the width of the current screen. # # Output: # integer: width in columns of the current screen. proc _flags_columns { if test -z $(__flags_columns) { if eval stty size >/dev/null !2 > !1 { # stty size worked :-) # shellcheck disable=SC2046 set -- $[stty size] setglobal __flags_columns = $(2:-) } } if test -z $(__flags_columns) { if eval tput cols >/dev/null !2 > !1 { # shellcheck disable=SC2046 set -- $[tput cols] setglobal __flags_columns = $(1:-) } } echo $(__flags_columns:-80) } # Validate a boolean. # # Args: # _flags__bool: boolean: value to validate # Returns: # bool: true if the value is a valid boolean proc _flags_validBool { setglobal _flags_bool_ = $1 setglobal flags_return = $(FLAGS_TRUE) match $(_flags_bool_) { with true|t|0 with false|f|1 with * setglobal flags_return = $(FLAGS_FALSE) } unset _flags_bool_ return ${flags_return} } # Validate a float. # # Args: # _flags_float_: float: value to validate # Returns: # bool: true if the value is a valid integer proc _flags_validFloat { setglobal flags_return = $(FLAGS_FALSE) test -n $1 || return ${flags_return} setglobal _flags_float_ = $1 if _flags_validInt $(_flags_float_) { setglobal flags_return = $(FLAGS_TRUE) } elif _flags_useBuiltin { setglobal _flags_float_whole_ = $(_flags_float_%.*) setglobal _flags_float_fraction_ = $(_flags_float_#*.) if _flags_validInt $(_flags_float_whole_:-0) -a \ _flags_validInt $(_flags_float_fraction_) { setglobal flags_return = $(FLAGS_TRUE) } unset _flags_float_whole_ _flags_float_fraction_ } else { setglobal flags_return = $(FLAGS_TRUE) match $(_flags_float_) { with -* # Negative floats. setglobal _flags_test_ = $[$(FLAGS_EXPR_CMD) $(_flags_float_) :\ '\(-[0-9]*\.[0-9]*\)] with * # Positive floats. setglobal _flags_test_ = $[$(FLAGS_EXPR_CMD) $(_flags_float_) :\ '\([0-9]*\.[0-9]*\)] } test $(_flags_test_) != $(_flags_float_) && setglobal flags_return = $(FLAGS_FALSE) unset _flags_test_ } unset _flags_float_ _flags_float_whole_ _flags_float_fraction_ return ${flags_return} } # Validate an integer. # # Args: # _flags_int_: integer: value to validate # Returns: # bool: true if the value is a valid integer proc _flags_validInt { setglobal flags_return = $(FLAGS_FALSE) test -n $1 || return ${flags_return} setglobal _flags_int_ = $1 match $(_flags_int_) { with -*.* # Ignore negative floats (we'll invalidate them later). with -* # Strip possible leading negative sign. if _flags_useBuiltin { setglobal _flags_int_ = $(_flags_int_#-) } else { setglobal _flags_int_ = $[$(FLAGS_EXPR_CMD) $(_flags_int_) : '-\([0-9][0-9]*\)] } } match $(_flags_int_) { with *[!0-9]* setglobal flags_return = $(FLAGS_FALSE) with * setglobal flags_return = $(FLAGS_TRUE) } unset _flags_int_ return ${flags_return} } # Parse command-line options using the standard getopt. # # Note: the flag options are passed around in the global __flags_opts so that # the formatting is not lost due to shell parsing and such. # # Args: # @: varies: command-line options to parse # Returns: # integer: a FLAGS success condition proc _flags_getoptStandard { setglobal flags_return = $(FLAGS_TRUE) setglobal _flags_shortOpts_ = $[_flags_genOptStr $(__FLAGS_OPTSTR_SHORT)] # Check for spaces in passed options. for _flags_opt_ in [@Argv] { # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06. setglobal _flags_match_ = $[echo "x$(_flags_opt_)x" |sed 's/ //g] if test $(_flags_match_) != "x$(_flags_opt_)x" { setglobal flags_error = ''the available getopt does not support spaces in options'' setglobal flags_return = $(FLAGS_ERROR) break } } if test $(flags_return) -eq $(FLAGS_TRUE) { setglobal __flags_opts = $[getopt $(_flags_shortOpts_) @Argv !2 > !1] setglobal _flags_rtrn_ = $Status if test $(_flags_rtrn_) -ne $(FLAGS_TRUE) { _flags_warn $(__flags_opts) setglobal flags_error = ''unable to parse provided options with getopt.'' setglobal flags_return = $(FLAGS_ERROR) } } unset _flags_match_ _flags_opt_ _flags_rtrn_ _flags_shortOpts_ return ${flags_return} } # Parse command-line options using the enhanced getopt. # # Note: the flag options are passed around in the global __flags_opts so that # the formatting is not lost due to shell parsing and such. # # Args: # @: varies: command-line options to parse # Returns: # integer: a FLAGS success condition proc _flags_getoptEnhanced { setglobal flags_return = $(FLAGS_TRUE) setglobal _flags_shortOpts_ = $[_flags_genOptStr $(__FLAGS_OPTSTR_SHORT)] setglobal _flags_boolOpts_ = $[echo $(__flags_boolNames) \ |sed 's/^ *//;s/ *$//;s/ /,/g] setglobal _flags_longOpts_ = $[_flags_genOptStr $(__FLAGS_OPTSTR_LONG)] setglobal __flags_opts = $[$(FLAGS_GETOPT_CMD) \ -o $(_flags_shortOpts_) \ -l "$(_flags_longOpts_),$(_flags_boolOpts_)" \ -- @Argv !2 > !1] setglobal _flags_rtrn_ = $Status if test $(_flags_rtrn_) -ne $(FLAGS_TRUE) { _flags_warn $(__flags_opts) setglobal flags_error = ''unable to parse provided options with getopt.'' setglobal flags_return = $(FLAGS_ERROR) } unset _flags_boolOpts_ _flags_longOpts_ _flags_rtrn_ _flags_shortOpts_ return ${flags_return} } # Dynamically parse a getopt result and set appropriate variables. # # This function does the actual conversion of getopt output and runs it through # the standard case structure for parsing. The case structure is actually quite # dynamic to support any number of flags. # # Args: # argc: int: original command-line argument count # @: varies: output from getopt parsing # Returns: # integer: a FLAGS success condition proc _flags_parseGetopt { setglobal _flags_argc_ = $1 shift setglobal flags_return = $(FLAGS_TRUE) if test $(__FLAGS_GETOPT_VERS) -ne $(__FLAGS_GETOPT_VERS_ENH) { # The @$ must be unquoted as it needs to be re-split. # shellcheck disable=SC2068 set -- $ifsjoin(Argv) } else { # Note the quotes around the `$@' -- they are essential! eval set -- @Argv } # Provide user with the number of arguments to shift by later. # NOTE: the FLAGS_ARGC variable is obsolete as of 1.0.3 because it does not # properly give user access to non-flag arguments mixed in between flag # arguments. Its usage was replaced by FLAGS_ARGV, and it is being kept only # for backwards compatibility reasons. setglobal FLAGS_ARGC = $[_flags_math "$Argc - 1 - $(_flags_argc_)] export FLAGS_ARGC # Handle options. note options with values must do an additional shift. while true { setglobal _flags_opt_ = $1 setglobal _flags_arg_ = $(2:-) setglobal _flags_type_ = $(__FLAGS_TYPE_NONE) setglobal _flags_name_ = '''' # Determine long flag name. match $(_flags_opt_) { with -- shift; break # Discontinue option parsing. with --* # Long option. if _flags_useBuiltin { setglobal _flags_opt_ = $(_flags_opt_#*--) } else { setglobal _flags_opt_ = $[$(FLAGS_EXPR_CMD) $(_flags_opt_) : '--\(.*\)] } setglobal _flags_len_ = $(__FLAGS_LEN_LONG) if _flags_itemInList $(_flags_opt_) $(__flags_longNames) { setglobal _flags_name_ = $(_flags_opt_) } else { # Check for negated long boolean version. if _flags_itemInList $(_flags_opt_) $(__flags_boolNames) { if _flags_useBuiltin { setglobal _flags_name_ = $(_flags_opt_#*no) } else { setglobal _flags_name_ = $[$(FLAGS_EXPR_CMD) $(_flags_opt_) : 'no\(.*\)] } setglobal _flags_type_ = $(__FLAGS_TYPE_BOOLEAN) setglobal _flags_arg_ = $(__FLAGS_NULL) } } with -* # Short option. if _flags_useBuiltin { setglobal _flags_opt_ = $(_flags_opt_#*-) } else { setglobal _flags_opt_ = $[$(FLAGS_EXPR_CMD) $(_flags_opt_) : '-\(.*\)] } setglobal _flags_len_ = $(__FLAGS_LEN_SHORT) if _flags_itemInList $(_flags_opt_) $(__flags_shortNames) { # Yes. Match short name to long name. Note purposeful off-by-one # (too high) with awk calculations. setglobal _flags_pos_ = $[echo $(__flags_shortNames) \ |awk 'BEGIN{RS=" ";rn=0}$0==e{rn=NR}END{print rn}' \ e="$(_flags_opt_)] setglobal _flags_name_ = $[echo $(__flags_longNames) \ |awk 'BEGIN{RS=" "}rn==NR{print $0}' rn="$(_flags_pos_)] } } # Die if the flag was unrecognized. if test -z $(_flags_name_) { setglobal flags_error = ""unrecognized option ($(_flags_opt_))"" setglobal flags_return = $(FLAGS_ERROR) break } # Set new flag value. setglobal _flags_usName_ = $[_flags_underscoreName $(_flags_name_)] test $(_flags_type_) -eq $(__FLAGS_TYPE_NONE) && \ setglobal _flags_type_ = $[_flags_getFlagInfo \ $(_flags_usName_) $(__FLAGS_INFO_TYPE)] match $(_flags_type_) { with ${__FLAGS_TYPE_BOOLEAN} if test $(_flags_len_) -eq $(__FLAGS_LEN_LONG) { if test $(_flags_arg_) != $(__FLAGS_NULL) { eval "FLAGS_$(_flags_usName_)=$(FLAGS_TRUE)" } else { eval "FLAGS_$(_flags_usName_)=$(FLAGS_FALSE)" } } else { setglobal _flags_strToEval_ = ""_flags_val_=\ \${__flags_$(_flags_usName_)_$(__FLAGS_INFO_DEFAULT)}"" eval $(_flags_strToEval_) # shellcheck disable=SC2154 if test $(_flags_val_) -eq $(FLAGS_FALSE) { eval "FLAGS_$(_flags_usName_)=$(FLAGS_TRUE)" } else { eval "FLAGS_$(_flags_usName_)=$(FLAGS_FALSE)" } } with ${__FLAGS_TYPE_FLOAT} if _flags_validFloat $(_flags_arg_) { eval "FLAGS_$(_flags_usName_)='$(_flags_arg_)'" } else { setglobal flags_error = ""invalid float value ($(_flags_arg_))"" setglobal flags_return = $(FLAGS_ERROR) break } with ${__FLAGS_TYPE_INTEGER} if _flags_validInt $(_flags_arg_) { eval "FLAGS_$(_flags_usName_)='$(_flags_arg_)'" } else { setglobal flags_error = ""invalid integer value ($(_flags_arg_))"" setglobal flags_return = $(FLAGS_ERROR) break } with ${__FLAGS_TYPE_STRING} eval "FLAGS_$(_flags_usName_)='$(_flags_arg_)'" } # Handle special case help flag. if test $(_flags_usName_) = 'help' { # shellcheck disable=SC2154 if test $(FLAGS_help) -eq $(FLAGS_TRUE) { flags_help setglobal flags_error = ''help requested'' setglobal flags_return = $(FLAGS_FALSE) break } } # Shift the option and non-boolean arguments out. shift test $(_flags_type_) != $(__FLAGS_TYPE_BOOLEAN) && shift } # Give user back non-flag arguments. setglobal FLAGS_ARGV = '''' while test $Argc -gt 0 { setglobal FLAGS_ARGV = ""$(FLAGS_ARGV:+${FLAGS_ARGV} )'$1'"" shift } unset _flags_arg_ _flags_len_ _flags_name_ _flags_opt_ _flags_pos_ \ _flags_strToEval_ _flags_type_ _flags_usName_ _flags_val_ return ${flags_return} } # Perform some path using built-ins. # # Args: # $@: string: math expression to evaluate # Output: # integer: the result # Returns: # bool: success of math evaluation proc _flags_math { if test $Argc -eq 0 { setglobal flags_return = $(FLAGS_FALSE) } elif _flags_useBuiltin { # Variable assignment is needed as workaround for Solaris Bourne shell, # which cannot parse a bare $((expression)). # shellcheck disable=SC2016 setglobal _flags_expr_ = ''$(($@))'' eval echo $(_flags_expr_) setglobal flags_return = $Status unset _flags_expr_ } else { eval expr @Argv setglobal flags_return = $Status } return ${flags_return} } # Cross-platform strlen() implementation. # # Args: # _flags_str: string: to determine length of # Output: # integer: length of string # Returns: # bool: success of strlen evaluation proc _flags_strlen { setglobal _flags_str_ = $(1:-) if test -z $(_flags_str_) { setglobal flags_output = '0' } elif _flags_useBuiltin { setglobal flags_output = $(#_flags_str_) } else { setglobal flags_output = $[$(FLAGS_EXPR_CMD) $(_flags_str_) : '.*] } setglobal flags_return = $Status unset _flags_str_ echo $(flags_output) return ${flags_return} } # Use built-in helper function to enable unit testing. # # Args: # None # Returns: # bool: true if built-ins should be used proc _flags_useBuiltin { return ${__FLAGS_USE_BUILTIN}; } #------------------------------------------------------------------------------ # public functions # # A basic boolean flag. Boolean flags do not take any arguments, and their # value is either 1 (false) or 0 (true). For long flags, the false value is # specified on the command line by prepending the word 'no'. With short flags, # the presence of the flag toggles the current value between true and false. # Specifying a short boolean flag twice on the command results in returning the # value back to the default value. # # A default value is required for boolean flags. # # For example, lets say a Boolean flag was created whose long name was 'update' # and whose short name was 'x', and the default value was 'false'. This flag # could be explicitly set to 'true' with '--update' or by '-x', and it could be # explicitly set to 'false' with '--noupdate'. proc DEFINE_boolean { _flags_define $(__FLAGS_TYPE_BOOLEAN) @Argv; } # Other basic flags. proc DEFINE_float { _flags_define $(__FLAGS_TYPE_FLOAT) @Argv; } proc DEFINE_integer { _flags_define $(__FLAGS_TYPE_INTEGER) @Argv; } proc DEFINE_string { _flags_define $(__FLAGS_TYPE_STRING) @Argv; } # Parse the flags. # # Args: # unnamed: list: command-line flags to parse # Returns: # integer: success of operation, or error proc FLAGS { # Define a standard 'help' flag if one isn't already defined. test -z $(__flags_help_type:-) && \ DEFINE_boolean 'help' false 'show this help' 'h' # Parse options. if test $Argc -gt 0 { if test $(__FLAGS_GETOPT_VERS) -ne $(__FLAGS_GETOPT_VERS_ENH) { _flags_getoptStandard @Argv } else { _flags_getoptEnhanced @Argv } setglobal flags_return = $Status } else { # Nothing passed; won't bother running getopt. setglobal __flags_opts = ''--'' setglobal flags_return = $(FLAGS_TRUE) } if test $(flags_return) -eq $(FLAGS_TRUE) { _flags_parseGetopt $Argc $(__flags_opts) setglobal flags_return = $Status } test $(flags_return) -eq $(FLAGS_ERROR) && _flags_fatal $(flags_error) return ${flags_return} } # This is a helper function for determining the 'getopt' version for platforms # where the detection isn't working. It simply outputs debug information that # can be included in a bug report. # # Args: # none # Output: # debug info that can be included in a bug report # Returns: # nothing proc flags_getoptInfo { # Platform info. _flags_debug "uname -a: $[uname -a]" _flags_debug "PATH: $(PATH)" # Shell info. if test -n $(BASH_VERSION:-) { _flags_debug 'shell: bash' _flags_debug "BASH_VERSION: $(BASH_VERSION)" } elif test -n $(ZSH_VERSION:-) { _flags_debug 'shell: zsh' _flags_debug "ZSH_VERSION: $(ZSH_VERSION)" } # getopt info. $(FLAGS_GETOPT_CMD) >/dev/null setglobal _flags_getoptReturn = $Status _flags_debug "getopt return: $(_flags_getoptReturn)" _flags_debug "getopt --version: $[$(FLAGS_GETOPT_CMD) --version !2 > !1]" unset _flags_getoptReturn } # Returns whether the detected getopt version is the enhanced version. # # Args: # none # Output: # none # Returns: # bool: true if getopt is the enhanced version proc flags_getoptIsEnh { test $(__FLAGS_GETOPT_VERS) -eq $(__FLAGS_GETOPT_VERS_ENH) } # Returns whether the detected getopt version is the standard version. # # Args: # none # Returns: # bool: true if getopt is the standard version proc flags_getoptIsStd { test $(__FLAGS_GETOPT_VERS) -eq $(__FLAGS_GETOPT_VERS_STD) } # This is effectively a 'usage()' function. It prints usage information and # exits the program with ${FLAGS_FALSE} if it is ever found in the command line # arguments. Note this function can be overridden so other apps can define # their own --help flag, replacing this one, if they want. # # Args: # none # Returns: # integer: success of operation (always returns true) proc flags_help { if test -n $(FLAGS_HELP:-) { echo $(FLAGS_HELP) > !2 } else { echo "USAGE: $(FLAGS_PARENT:-$0) [flags] args" > !2 } if test -n $(__flags_longNames) { echo 'flags:' > !2 for flags_name_ in [$(__flags_longNames)] { setglobal flags_flagStr_ = '''' setglobal flags_boolStr_ = '''' setglobal flags_usName_ = $[_flags_underscoreName $(flags_name_)] setglobal flags_default_ = $[_flags_getFlagInfo \ $(flags_usName_) $(__FLAGS_INFO_DEFAULT)] setglobal flags_help_ = $[_flags_getFlagInfo \ $(flags_usName_) $(__FLAGS_INFO_HELP)] setglobal flags_short_ = $[_flags_getFlagInfo \ $(flags_usName_) $(__FLAGS_INFO_SHORT)] setglobal flags_type_ = $[_flags_getFlagInfo \ $(flags_usName_) $(__FLAGS_INFO_TYPE)] test $(flags_short_) != $(__FLAGS_NULL) && \ setglobal flags_flagStr_ = ""-$(flags_short_)"" if test $(__FLAGS_GETOPT_VERS) -eq $(__FLAGS_GETOPT_VERS_ENH) { test $(flags_short_) != $(__FLAGS_NULL) && \ setglobal flags_flagStr_ = ""$(flags_flagStr_),"" # Add [no] to long boolean flag names, except the 'help' flag. test $(flags_type_) -eq $(__FLAGS_TYPE_BOOLEAN) \ -a $(flags_usName_) != 'help' && \ setglobal flags_boolStr_ = ''[no]'' setglobal flags_flagStr_ = ""$(flags_flagStr_)--$(flags_boolStr_)$(flags_name_):"" } match $(flags_type_) { with ${__FLAGS_TYPE_BOOLEAN} if test $(flags_default_) -eq $(FLAGS_TRUE) { setglobal flags_defaultStr_ = ''true'' } else { setglobal flags_defaultStr_ = ''false'' } with ${__FLAGS_TYPE_FLOAT}|${__FLAGS_TYPE_INTEGER} setglobal flags_defaultStr_ = $(flags_default_) with ${__FLAGS_TYPE_STRING} setglobal flags_defaultStr_ = ""'$(flags_default_)'"" } setglobal flags_defaultStr_ = ""(default: $(flags_defaultStr_))"" setglobal flags_helpStr_ = "" $(flags_flagStr_) $(flags_help_:+${flags_help_} )$(flags_defaultStr_)"" _flags_strlen $(flags_helpStr_) >/dev/null setglobal flags_helpStrLen_ = $(flags_output) setglobal flags_columns_ = $[_flags_columns] if test $(flags_helpStrLen_) -lt $(flags_columns_) { echo $(flags_helpStr_) > !2 } else { echo " $(flags_flagStr_) $(flags_help_)" > !2 # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06 # because it doesn't like empty strings when used in this manner. setglobal flags_emptyStr_ = $[echo '"'x$(flags_flagStr_)x'"' \ |awk '{printf "%"length($0)-2"s", ""}] setglobal flags_helpStr_ = "" $(flags_emptyStr_) $(flags_defaultStr_)"" _flags_strlen $(flags_helpStr_) >/dev/null setglobal flags_helpStrLen_ = $(flags_output) if test $(__FLAGS_GETOPT_VERS) -eq $(__FLAGS_GETOPT_VERS_STD) \ -o $(flags_helpStrLen_) -lt $(flags_columns_) { # Indented to match help string. echo $(flags_helpStr_) > !2 } else { # Indented four from left to allow for longer defaults as long flag # names might be used too, making things too long. echo " $(flags_defaultStr_)" > !2 } } } } unset flags_boolStr_ flags_default_ flags_defaultStr_ flags_emptyStr_ \ flags_flagStr_ flags_help_ flags_helpStr flags_helpStrLen flags_name_ \ flags_columns_ flags_short_ flags_type_ flags_usName_ return ${FLAGS_TRUE} } # Reset shflags back to an uninitialized state. # # Args: # none # Returns: # nothing proc flags_reset { for flags_name_ in [$(__flags_longNames)] { setglobal flags_usName_ = $[_flags_underscoreName $(flags_name_)] setglobal flags_strToEval_ = ""unset FLAGS_$(flags_usName_)"" for flags_type_ in [\ $(__FLAGS_INFO_DEFAULT) \ $(__FLAGS_INFO_HELP) \ $(__FLAGS_INFO_SHORT) \ $(__FLAGS_INFO_TYPE)] { setglobal flags_strToEval_ = "\ "$(flags_strToEval_) __flags_$(flags_usName_)_$(flags_type_)"" } eval $(flags_strToEval_) } # Reset internal variables. setglobal __flags_boolNames = '' '' setglobal __flags_longNames = '' '' setglobal __flags_shortNames = '' '' setglobal __flags_definedNames = '' '' # Reset logging level back to default. flags_setLoggingLevel $(__FLAGS_LEVEL_DEFAULT) unset flags_name_ flags_type_ flags_strToEval_ flags_usName_ } # # Initialization # # Set the default logging level. flags_setLoggingLevel $(__FLAGS_LEVEL_DEFAULT)