# 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 setvar FLAGS_VERSION = ''1.2.3pre'' # Return values that scripts can use. setvar FLAGS_TRUE = '0' setvar FLAGS_FALSE = '1' setvar FLAGS_ERROR = '2' # Logging levels. setvar FLAGS_LEVEL_DEBUG = '0' setvar FLAGS_LEVEL_INFO = '1' setvar FLAGS_LEVEL_WARN = '2' setvar FLAGS_LEVEL_ERROR = '3' setvar FLAGS_LEVEL_FATAL = '4' setvar __FLAGS_LEVEL_DEFAULT = ${FLAGS_LEVEL_WARN} # Determine some reasonable command defaults. setvar __FLAGS_EXPR_CMD = ''expr --'' setvar __FLAGS_UNAME_S = $(uname -s) if test ${__FLAGS_UNAME_S} = 'BSD' { setvar __FLAGS_EXPR_CMD = ''gexpr --'' } else { setvar _flags_output_ = $(${__FLAGS_EXPR_CMD} ) if test $? -eq ${FLAGS_TRUE} -a ${_flags_output_} = '--' { # We are likely running inside BusyBox. setvar __FLAGS_EXPR_CMD = ''expr'' } unset _flags_output_ } # Commands a user can override if desired. setvar FLAGS_EXPR_CMD = ${FLAGS_EXPR_CMD:-${__FLAGS_EXPR_CMD}} setvar FLAGS_GETOPT_CMD = ${FLAGS_GETOPT_CMD:-getopt} # Specific shell checks. if test -n ${ZSH_VERSION:-} { setopt |grep "^shwordsplit$" >/dev/null if test $? -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 $? -eq ${FLAGS_TRUE} { setvar __FLAGS_USE_BUILTIN = ${FLAGS_TRUE} } else { setvar __FLAGS_USE_BUILTIN = ${FLAGS_FALSE} } # # Constants. # # Reserved flag names. setvar __FLAGS_RESERVED_LIST = '' ARGC ARGV ERROR FALSE GETOPT_CMD HELP PARENT TRUE '' setvar __FLAGS_RESERVED_LIST = ""${__FLAGS_RESERVED_LIST} VERSION "" # Determined getopt version (standard or enhanced). setvar __FLAGS_GETOPT_VERS_STD = '0' setvar __FLAGS_GETOPT_VERS_ENH = '1' # shellcheck disable=SC2120 proc _flags_getopt_vers { setvar _flags_getopt_cmd_ = ${1:-${FLAGS_GETOPT_CMD}} case{ ' -- --foo' { echo ${__FLAGS_GETOPT_VERS_STD} } ' --foo --' { echo ${__FLAGS_GETOPT_VERS_ENH} } # Unrecognized output. Assuming standard getopt version. * { echo ${__FLAGS_GETOPT_VERS_STD} } } unset _flags_getopt_cmd_ } # shellcheck disable=SC2119 setvar __FLAGS_GETOPT_VERS = $(_flags_getopt_vers) # getopt optstring lengths setvar __FLAGS_OPTSTR_SHORT = '0' setvar __FLAGS_OPTSTR_LONG = '1' setvar __FLAGS_NULL = ''~'' # Flag info strings. setvar __FLAGS_INFO_DEFAULT = ''default'' setvar __FLAGS_INFO_HELP = ''help'' setvar __FLAGS_INFO_SHORT = ''short'' setvar __FLAGS_INFO_TYPE = ''type'' # Flag lengths. setvar __FLAGS_LEN_SHORT = '0' setvar __FLAGS_LEN_LONG = '1' # Flag types. setvar __FLAGS_TYPE_NONE = '0' setvar __FLAGS_TYPE_BOOLEAN = '1' setvar __FLAGS_TYPE_FLOAT = '2' setvar __FLAGS_TYPE_INTEGER = '3' setvar __FLAGS_TYPE_STRING = '4' # Set the constants readonly. setvar __flags_constants = $(set |awk -F= '/^FLAGS_/ || /^__FLAGS_/ {print $1}) for __flags_const in ${__flags_constants} { # Skip certain flags. case{ FLAGS_HELP { continue } FLAGS_PARENT { continue } } # Set flag readonly. if test -z ${ZSH_VERSION:-} { readonly ${__flags_const} continue } case{ [123].* { readonly ${__flags_const} } * { readonly -g ${__flags_const} } # Declare readonly constants globally. } } unset __flags_const __flags_constants # # Internal variables. # # Space separated lists. setvar __flags_boolNames = '' '' # Boolean flag names. setvar __flags_longNames = '' '' # Long flag names. setvar __flags_shortNames = '' '' # Short flag names. setvar __flags_definedNames = '' '' # Defined flag names (used for validation). setvar __flags_columns = '''' # Screen width in columns. setvar __flags_level = '0' # Default logging level. setvar __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 $[join(ARGV)]" >&2 } proc _flags_info { test ${__flags_level} -le ${FLAGS_LEVEL_INFO} || return echo "flags:INFO $[join(ARGV)]" >&2 } proc _flags_warn { test ${__flags_level} -le ${FLAGS_LEVEL_WARN} || return echo "flags:WARN $[join(ARGV)]" >&2 } proc _flags_error { test ${__flags_level} -le ${FLAGS_LEVEL_ERROR} || return echo "flags:ERROR $[join(ARGV)]" >&2 } proc _flags_fatal { test ${__flags_level} -le ${FLAGS_LEVEL_FATAL} || return echo "flags:FATAL $[join(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" setvar _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." setvar __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 { setvar flags_error = ''DEFINE error: too few arguments'' setvar flags_return = ${FLAGS_ERROR} _flags_error ${flags_error} return ${flags_return} } setvar _flags_type_ = "$1" setvar _flags_name_ = "$2" setvar _flags_default_ = "$3" setvar _flags_help_ = ${4:-§} # Special value '§' indicates no help string provided. setvar _flags_short_ = ${5:-${__FLAGS_NULL}} _flags_debug "type:${_flags_type_} name:${_flags_name_}" \ "default:'${_flags_default_}' help:'${_flags_help_}'" \ "short:${_flags_short_}" setvar _flags_return_ = ${FLAGS_TRUE} setvar _flags_usName_ = "$(_flags_underscoreName ${_flags_name_})" # Check whether the flag name is reserved. _flags_itemInList ${_flags_usName_} ${__FLAGS_RESERVED_LIST} if test $? -eq ${FLAGS_TRUE} { setvar flags_error = ""flag name (${_flags_name_}) is reserved"" setvar _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} { setvar flags_error = ""short flag required for (${_flags_name_}) on this platform"" setvar _flags_return_ = ${FLAGS_ERROR} } # Check for existing long name definition. if test ${_flags_return_} -eq ${FLAGS_TRUE} { if _flags_itemInList ${_flags_usName_} ${__flags_definedNames} { setvar flags_error = ""definition for ([no]${_flags_name_}) already exists"" _flags_warn ${flags_error} setvar _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} { setvar flags_error = ""flag short name (${_flags_short_}) already defined"" _flags_warn ${flags_error} setvar _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} { case{ ${__FLAGS_TYPE_BOOLEAN} { if _flags_validBool ${_flags_default_} { case{ true|t|0 { setvar _flags_default_ = ${FLAGS_TRUE} } false|f|1 { setvar _flags_default_ = ${FLAGS_FALSE} } } } else { setvar flags_error = ""invalid default flag value '${_flags_default_}'"" setvar _flags_return_ = ${FLAGS_ERROR} } } ${__FLAGS_TYPE_FLOAT} { if _flags_validFloat ${_flags_default_} { : } else { setvar flags_error = ""invalid default flag value '${_flags_default_}'"" setvar _flags_return_ = ${FLAGS_ERROR} } } ${__FLAGS_TYPE_INTEGER} { if _flags_validInt ${_flags_default_} { : } else { setvar flags_error = ""invalid default flag value '${_flags_default_}'"" setvar _flags_return_ = ${FLAGS_ERROR} } } ${__FLAGS_TYPE_STRING} { } # Everything in shell is a valid string. * { setvar flags_error = ""unrecognized flag type '${_flags_type_}'"" setvar _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 setvar __flags_shortNames = ""${__flags_shortNames}${_flags_short_} "" setvar __flags_longNames = ""${__flags_longNames}${_flags_name_} "" test ${_flags_type_} -eq ${__FLAGS_TYPE_BOOLEAN} && \ setvar __flags_boolNames = ""${__flags_boolNames}no${_flags_name_} "" # Append flag names to defined names for later validation checks. setvar __flags_definedNames = ""${__flags_definedNames}${_flags_usName_} "" test ${_flags_type_} -eq ${__FLAGS_TYPE_BOOLEAN} && \ setvar __flags_definedNames = ""${__flags_definedNames}no${_flags_usName_} "" } setvar 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 { setvar _flags_optStrType_ = "$1" setvar _flags_opts_ = '''' for _flags_name_ in ${__flags_longNames} { setvar _flags_usName_ = "$(_flags_underscoreName ${_flags_name_})" setvar _flags_type_ = "$(_flags_getFlagInfo ${_flags_usName_} ${__FLAGS_INFO_TYPE})" test $? -eq ${FLAGS_TRUE} || _flags_fatal 'call to _flags_type_ failed' case{ ${__FLAGS_OPTSTR_SHORT} { setvar _flags_shortName_ = "$(_flags_getFlagInfo \ ${_flags_usName_} ${__FLAGS_INFO_SHORT})" if test ${_flags_shortName_} != ${__FLAGS_NULL} { setvar _flags_opts_ = ""${_flags_opts_}${_flags_shortName_}"" # getopt needs a trailing ':' to indicate a required argument. test ${_flags_type_} -ne ${__FLAGS_TYPE_BOOLEAN} && \ setvar _flags_opts_ = ""${_flags_opts_}:"" } } ${__FLAGS_OPTSTR_LONG} { setvar _flags_opts_ = ""${_flags_opts_:+${_flags_opts_},}${_flags_name_}"" # getopt needs a trailing ':' to indicate a required argument test ${_flags_type_} -ne ${__FLAGS_TYPE_BOOLEAN} && \ setvar _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 setvar _flags_gFI_usName_ = "$1" setvar _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. setvar _flags_infoVar_ = ""__flags_${_flags_gFI_usName_}_${_flags_gFI_info_}"" setvar _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_} = '§' && setvar _flags_infoValue_ = '''' setvar 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'. setvar _flags_typeVar_ = ""__flags_${_flags_gFI_usName_}_${__FLAGS_INFO_TYPE}"" setvar _flags_strToEval_ = ""_flags_typeValue_=\"\${${_flags_typeVar_}:-}"\"" eval ${_flags_strToEval_} # shellcheck disable=SC2154 if test ${_flags_typeValue_} = ${__FLAGS_TYPE_STRING} { setvar flags_return = ${FLAGS_TRUE} } else { setvar flags_return = ${FLAGS_ERROR} setvar 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 { setvar _flags_str_ = "$1" shift case{ *\ ${_flags_str_}\ * { setvar flags_return = ${FLAGS_TRUE} } * { setvar 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) setvar __flags_columns = "${2:-}" } } if test -z ${__flags_columns} { if eval tput cols >/dev/null 2>&1 { # shellcheck disable=SC2046 set -- $(tput cols) setvar __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 { setvar _flags_bool_ = "$1" setvar flags_return = ${FLAGS_TRUE} case{ true|t|0 { } false|f|1 { } * { setvar 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 { setvar flags_return = ${FLAGS_FALSE} test -n $1 || return ${flags_return} setvar _flags_float_ = "$1" if _flags_validInt ${_flags_float_} { setvar flags_return = ${FLAGS_TRUE} } elif _flags_useBuiltin { setvar _flags_float_whole_ = ${_flags_float_%.*} setvar _flags_float_fraction_ = ${_flags_float_#*.} if _flags_validInt ${_flags_float_whole_:-0} -a \ _flags_validInt ${_flags_float_fraction_} { setvar flags_return = ${FLAGS_TRUE} } unset _flags_float_whole_ _flags_float_fraction_ } else { setvar flags_return = ${FLAGS_TRUE} case{ -* { # Negative floats. setvar _flags_test_ = $(${FLAGS_EXPR_CMD} ${_flags_float_} :\ '\(-[0-9]*\.[0-9]*\)) } * { # Positive floats. setvar _flags_test_ = $(${FLAGS_EXPR_CMD} ${_flags_float_} :\ '\([0-9]*\.[0-9]*\)) } } test ${_flags_test_} != ${_flags_float_} && setvar 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 { setvar flags_return = ${FLAGS_FALSE} test -n $1 || return ${flags_return} setvar _flags_int_ = "$1" case{ -*.* { } # Ignore negative floats (we'll invalidate them later). -* { # Strip possible leading negative sign. if _flags_useBuiltin { setvar _flags_int_ = ${_flags_int_#-} } else { setvar _flags_int_ = $(${FLAGS_EXPR_CMD} ${_flags_int_} : '-\([0-9][0-9]*\)) } } } case{ *[!0-9]* { setvar flags_return = ${FLAGS_FALSE} } * { setvar 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 { setvar flags_return = ${FLAGS_TRUE} setvar _flags_shortOpts_ = $(_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}) # Check for spaces in passed options. for _flags_opt_ in "$@" { # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06. setvar _flags_match_ = $(echo "x${_flags_opt_}x" |sed 's/ //g) if test ${_flags_match_} != "x${_flags_opt_}x" { setvar flags_error = ''the available getopt does not support spaces in options'' setvar flags_return = ${FLAGS_ERROR} break } } if test ${flags_return} -eq ${FLAGS_TRUE} { setvar __flags_opts = $(getopt ${_flags_shortOpts_} @ARGV ) setvar _flags_rtrn_ = ""$? if test ${_flags_rtrn_} -ne ${FLAGS_TRUE} { _flags_warn ${__flags_opts} setvar flags_error = ''unable to parse provided options with getopt.'' setvar 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 { setvar flags_return = ${FLAGS_TRUE} setvar _flags_shortOpts_ = $(_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}) setvar _flags_boolOpts_ = $(echo ${__flags_boolNames} \ |sed 's/^ *//;s/ *$//;s/ /,/g) setvar _flags_longOpts_ = $(_flags_genOptStr ${__FLAGS_OPTSTR_LONG}) setvar __flags_opts = $(${FLAGS_GETOPT_CMD} \ -o ${_flags_shortOpts_} \ -l "${_flags_longOpts_},${_flags_boolOpts_}" \ -- @ARGV ) setvar _flags_rtrn_ = ""$? if test ${_flags_rtrn_} -ne ${FLAGS_TRUE} { _flags_warn ${__flags_opts} setvar flags_error = ''unable to parse provided options with getopt.'' setvar 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 { setvar _flags_argc_ = "$1" shift setvar 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 -- $[join(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. setvar FLAGS_ARGC = $(_flags_math "$Argc - 1 - ${_flags_argc_}) export FLAGS_ARGC # Handle options. note options with values must do an additional shift. while true { setvar _flags_opt_ = "$1" setvar _flags_arg_ = ${2:-} setvar _flags_type_ = ${__FLAGS_TYPE_NONE} setvar _flags_name_ = '''' # Determine long flag name. case{ -- { shift; break } # Discontinue option parsing. --* { # Long option. if _flags_useBuiltin { setvar _flags_opt_ = ${_flags_opt_#*--} } else { setvar _flags_opt_ = $(${FLAGS_EXPR_CMD} ${_flags_opt_} : '--\(.*\)) } setvar _flags_len_ = ${__FLAGS_LEN_LONG} if _flags_itemInList ${_flags_opt_} ${__flags_longNames} { setvar _flags_name_ = ${_flags_opt_} } else { # Check for negated long boolean version. if _flags_itemInList ${_flags_opt_} ${__flags_boolNames} { if _flags_useBuiltin { setvar _flags_name_ = ${_flags_opt_#*no} } else { setvar _flags_name_ = $(${FLAGS_EXPR_CMD} ${_flags_opt_} : 'no\(.*\)) } setvar _flags_type_ = ${__FLAGS_TYPE_BOOLEAN} setvar _flags_arg_ = ${__FLAGS_NULL} } } } -* { # Short option. if _flags_useBuiltin { setvar _flags_opt_ = ${_flags_opt_#*-} } else { setvar _flags_opt_ = $(${FLAGS_EXPR_CMD} ${_flags_opt_} : '-\(.*\)) } setvar _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. setvar _flags_pos_ = $(echo ${__flags_shortNames} \ |awk 'BEGIN{RS=" ";rn=0}$0==e{rn=NR}END{print rn}' \ e="${_flags_opt_}) setvar _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_} { setvar flags_error = ""unrecognized option (${_flags_opt_})"" setvar flags_return = ${FLAGS_ERROR} break } # Set new flag value. setvar _flags_usName_ = $(_flags_underscoreName ${_flags_name_}) test ${_flags_type_} -eq ${__FLAGS_TYPE_NONE} && \ setvar _flags_type_ = $(_flags_getFlagInfo \ ${_flags_usName_} ${__FLAGS_INFO_TYPE}) case{ ${__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 { setvar _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}" } } } ${__FLAGS_TYPE_FLOAT} { if _flags_validFloat ${_flags_arg_} { eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" } else { setvar flags_error = ""invalid float value (${_flags_arg_})"" setvar flags_return = ${FLAGS_ERROR} break } } ${__FLAGS_TYPE_INTEGER} { if _flags_validInt ${_flags_arg_} { eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" } else { setvar flags_error = ""invalid integer value (${_flags_arg_})"" setvar flags_return = ${FLAGS_ERROR} break } } ${__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 setvar flags_error = ''help requested'' setvar 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. setvar FLAGS_ARGV = '''' while test $Argc -gt 0 { setvar 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 { setvar 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 setvar _flags_expr_ = ''$(($@))'' eval echo ${_flags_expr_} setvar flags_return = ""$? unset _flags_expr_ } else { eval expr @ARGV setvar flags_return = ""$? } 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 { setvar _flags_str_ = ${1:-} if test -z ${_flags_str_} { setvar flags_output = '0' } elif _flags_useBuiltin { setvar flags_output = ${#_flags_str_} } else { setvar flags_output = $(${FLAGS_EXPR_CMD} ${_flags_str_} : '.*) } setvar flags_return = ""$? 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 } setvar flags_return = ""$? } else { # Nothing passed; won't bother running getopt. setvar __flags_opts = ''--'' setvar flags_return = ${FLAGS_TRUE} } if test ${flags_return} -eq ${FLAGS_TRUE} { _flags_parseGetopt $Argc ${__flags_opts} setvar flags_return = ""$? } 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 setvar _flags_getoptReturn = ""$? _flags_debug "getopt return: ${_flags_getoptReturn}" _flags_debug "getopt --version: $(${FLAGS_GETOPT_CMD} --version )" 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} { setvar flags_flagStr_ = '''' setvar flags_boolStr_ = '''' setvar flags_usName_ = $(_flags_underscoreName ${flags_name_}) setvar flags_default_ = $(_flags_getFlagInfo \ ${flags_usName_} ${__FLAGS_INFO_DEFAULT}) setvar flags_help_ = $(_flags_getFlagInfo \ ${flags_usName_} ${__FLAGS_INFO_HELP}) setvar flags_short_ = $(_flags_getFlagInfo \ ${flags_usName_} ${__FLAGS_INFO_SHORT}) setvar flags_type_ = $(_flags_getFlagInfo \ ${flags_usName_} ${__FLAGS_INFO_TYPE}) test ${flags_short_} != ${__FLAGS_NULL} && \ setvar flags_flagStr_ = ""-${flags_short_}"" if test ${__FLAGS_GETOPT_VERS} -eq ${__FLAGS_GETOPT_VERS_ENH} { test ${flags_short_} != ${__FLAGS_NULL} && \ setvar 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' && \ setvar flags_boolStr_ = ''[no]'' setvar flags_flagStr_ = ""${flags_flagStr_}--${flags_boolStr_}${flags_name_}:"" } case{ ${__FLAGS_TYPE_BOOLEAN} { if test ${flags_default_} -eq ${FLAGS_TRUE} { setvar flags_defaultStr_ = ''true'' } else { setvar flags_defaultStr_ = ''false'' } } ${__FLAGS_TYPE_FLOAT}|${__FLAGS_TYPE_INTEGER} { setvar flags_defaultStr_ = ${flags_default_} } ${__FLAGS_TYPE_STRING} { setvar flags_defaultStr_ = ""'${flags_default_}'"" } } setvar flags_defaultStr_ = ""(default: ${flags_defaultStr_})"" setvar flags_helpStr_ = "" ${flags_flagStr_} ${flags_help_:+${flags_help_} }${flags_defaultStr_}"" _flags_strlen ${flags_helpStr_} >/dev/null setvar flags_helpStrLen_ = ${flags_output} setvar 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. setvar flags_emptyStr_ = "$(echo '"'x${flags_flagStr_}x'"' \ |awk '{printf "%"length($0)-2"s", ""})" setvar flags_helpStr_ = "" ${flags_emptyStr_} ${flags_defaultStr_}"" _flags_strlen ${flags_helpStr_} >/dev/null setvar 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} { setvar flags_usName_ = $(_flags_underscoreName ${flags_name_}) setvar flags_strToEval_ = ""unset FLAGS_${flags_usName_}"" for flags_type_ in \ ${__FLAGS_INFO_DEFAULT} \ ${__FLAGS_INFO_HELP} \ ${__FLAGS_INFO_SHORT} \ ${__FLAGS_INFO_TYPE} { setvar flags_strToEval_ = "\ "${flags_strToEval_} __flags_${flags_usName_}_${flags_type_}"" } eval ${flags_strToEval_} } # Reset internal variables. setvar __flags_boolNames = '' '' setvar __flags_longNames = '' '' setvar __flags_shortNames = '' '' setvar __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}