# 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. [ -n "${FLAGS_VERSION:-}" ] && return 0 FLAGS_VERSION='1.2.3pre' # Return values that scripts can use. FLAGS_TRUE=0 FLAGS_FALSE=1 FLAGS_ERROR=2 # Logging levels. FLAGS_LEVEL_DEBUG=0 FLAGS_LEVEL_INFO=1 FLAGS_LEVEL_WARN=2 FLAGS_LEVEL_ERROR=3 FLAGS_LEVEL_FATAL=4 __FLAGS_LEVEL_DEFAULT=${FLAGS_LEVEL_WARN} # Determine some reasonable command defaults. __FLAGS_EXPR_CMD='expr --' __FLAGS_UNAME_S=`uname -s` if [ "${__FLAGS_UNAME_S}" = 'BSD' ]; then __FLAGS_EXPR_CMD='gexpr --' else _flags_output_=`${__FLAGS_EXPR_CMD} 2>&1` if [ $? -eq ${FLAGS_TRUE} -a "${_flags_output_}" = '--' ]; then # We are likely running inside BusyBox. __FLAGS_EXPR_CMD='expr' fi unset _flags_output_ fi # Commands a user can override if desired. FLAGS_EXPR_CMD=${FLAGS_EXPR_CMD:-${__FLAGS_EXPR_CMD}} FLAGS_GETOPT_CMD=${FLAGS_GETOPT_CMD:-getopt} # Specific shell checks. if [ -n "${ZSH_VERSION:-}" ]; then setopt |grep "^shwordsplit$" >/dev/null if [ $? -ne ${FLAGS_TRUE} ]; then _flags_fatal 'zsh shwordsplit option is required for proper zsh operation' fi if [ -z "${FLAGS_PARENT:-}" ]; then _flags_fatal "zsh does not pass \$0 through properly. please declare' \ \"FLAGS_PARENT=\$0\" before calling shFlags" fi fi # Can we use built-ins? ( echo "${FLAGS_TRUE#0}"; ) >/dev/null 2>&1 if [ $? -eq ${FLAGS_TRUE} ]; then __FLAGS_USE_BUILTIN=${FLAGS_TRUE} else __FLAGS_USE_BUILTIN=${FLAGS_FALSE} fi # # Constants. # # Reserved flag names. __FLAGS_RESERVED_LIST=' ARGC ARGV ERROR FALSE GETOPT_CMD HELP PARENT TRUE ' __FLAGS_RESERVED_LIST="${__FLAGS_RESERVED_LIST} VERSION " # Determined getopt version (standard or enhanced). __FLAGS_GETOPT_VERS_STD=0 __FLAGS_GETOPT_VERS_ENH=1 # shellcheck disable=SC2120 _flags_getopt_vers() { _flags_getopt_cmd_=${1:-${FLAGS_GETOPT_CMD}} case "`${_flags_getopt_cmd_} -lfoo '' --foo 2>&1`" in ' -- --foo') echo ${__FLAGS_GETOPT_VERS_STD} ;; ' --foo --') echo ${__FLAGS_GETOPT_VERS_ENH} ;; # Unrecognized output. Assuming standard getopt version. *) echo ${__FLAGS_GETOPT_VERS_STD} ;; esac unset _flags_getopt_cmd_ } # shellcheck disable=SC2119 __FLAGS_GETOPT_VERS=`_flags_getopt_vers` # getopt optstring lengths __FLAGS_OPTSTR_SHORT=0 __FLAGS_OPTSTR_LONG=1 __FLAGS_NULL='~' # Flag info strings. __FLAGS_INFO_DEFAULT='default' __FLAGS_INFO_HELP='help' __FLAGS_INFO_SHORT='short' __FLAGS_INFO_TYPE='type' # Flag lengths. __FLAGS_LEN_SHORT=0 __FLAGS_LEN_LONG=1 # Flag types. __FLAGS_TYPE_NONE=0 __FLAGS_TYPE_BOOLEAN=1 __FLAGS_TYPE_FLOAT=2 __FLAGS_TYPE_INTEGER=3 __FLAGS_TYPE_STRING=4 # Set the constants readonly. __flags_constants=`set |awk -F= '/^FLAGS_/ || /^__FLAGS_/ {print $1}'` for __flags_const in ${__flags_constants}; do # Skip certain flags. case ${__flags_const} in FLAGS_HELP) continue ;; FLAGS_PARENT) continue ;; esac # Set flag readonly. if [ -z "${ZSH_VERSION:-}" ]; then readonly "${__flags_const}" continue fi case ${ZSH_VERSION} in [123].*) readonly "${__flags_const}" ;; *) readonly -g "${__flags_const}" ;; # Declare readonly constants globally. esac done unset __flags_const __flags_constants # # Internal variables. # # Space separated lists. __flags_boolNames=' ' # Boolean flag names. __flags_longNames=' ' # Long flag names. __flags_shortNames=' ' # Short flag names. __flags_definedNames=' ' # Defined flag names (used for validation). __flags_columns='' # Screen width in columns. __flags_level=0 # Default logging level. __flags_opts='' # Temporary storage for parsed getopt flags. #------------------------------------------------------------------------------ # Private functions. # # Logging functions. _flags_debug() { [ ${__flags_level} -le ${FLAGS_LEVEL_DEBUG} ] || return echo "flags:DEBUG $*" >&2 } _flags_info() { [ ${__flags_level} -le ${FLAGS_LEVEL_INFO} ] || return echo "flags:INFO $*" >&2 } _flags_warn() { [ ${__flags_level} -le ${FLAGS_LEVEL_WARN} ] || return echo "flags:WARN $*" >&2 } _flags_error() { [ ${__flags_level} -le ${FLAGS_LEVEL_ERROR} ] || return echo "flags:ERROR $*" >&2 } _flags_fatal() { [ ${__flags_level} -le ${FLAGS_LEVEL_FATAL} ] || return echo "flags:FATAL $*" >&2 exit ${FLAGS_ERROR} } # Get the logging level. flags_loggingLevel() { echo ${__flags_level}; } # Set the logging level. # # Args: # _flags_level_: integer: new logging level # Returns: # nothing flags_setLoggingLevel() { [ $# -ne 1 ] && _flags_fatal "flags_setLevel(): logging level missing" _flags_level_=$1 [ "${_flags_level_}" -ge "${FLAGS_LEVEL_DEBUG}" \ -a "${_flags_level_}" -le "${FLAGS_LEVEL_FATAL}" ] \ || _flags_fatal "Invalid logging level '${_flags_level_}' specified." __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 _flags_define() { if [ $# -lt 4 ]; then flags_error='DEFINE error: too few arguments' flags_return=${FLAGS_ERROR} _flags_error "${flags_error}" return ${flags_return} fi _flags_type_=$1 _flags_name_=$2 _flags_default_=$3 _flags_help_=${4:-§} # Special value '§' indicates no help string provided. _flags_short_=${5:-${__FLAGS_NULL}} _flags_debug "type:${_flags_type_} name:${_flags_name_}" \ "default:'${_flags_default_}' help:'${_flags_help_}'" \ "short:${_flags_short_}" _flags_return_=${FLAGS_TRUE} _flags_usName_="`_flags_underscoreName "${_flags_name_}"`" # Check whether the flag name is reserved. _flags_itemInList "${_flags_usName_}" "${__FLAGS_RESERVED_LIST}" if [ $? -eq ${FLAGS_TRUE} ]; then flags_error="flag name (${_flags_name_}) is reserved" _flags_return_=${FLAGS_ERROR} fi # Require short option for getopt that don't support long options. if [ ${_flags_return_} -eq ${FLAGS_TRUE} \ -a "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" \ -a "${_flags_short_}" = "${__FLAGS_NULL}" ] then flags_error="short flag required for (${_flags_name_}) on this platform" _flags_return_=${FLAGS_ERROR} fi # Check for existing long name definition. if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then if _flags_itemInList "${_flags_usName_}" "${__flags_definedNames}"; then flags_error="definition for ([no]${_flags_name_}) already exists" _flags_warn "${flags_error}" _flags_return_=${FLAGS_FALSE} fi fi # Check for existing short name definition. if [ ${_flags_return_} -eq ${FLAGS_TRUE} \ -a "${_flags_short_}" != "${__FLAGS_NULL}" ] then if _flags_itemInList "${_flags_short_}" "${__flags_shortNames}"; then flags_error="flag short name (${_flags_short_}) already defined" _flags_warn "${flags_error}" _flags_return_=${FLAGS_FALSE} fi fi # 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 [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then case ${_flags_type_} in ${__FLAGS_TYPE_BOOLEAN}) if _flags_validBool "${_flags_default_}"; then case ${_flags_default_} in true|t|0) _flags_default_=${FLAGS_TRUE} ;; false|f|1) _flags_default_=${FLAGS_FALSE} ;; esac else flags_error="invalid default flag value '${_flags_default_}'" _flags_return_=${FLAGS_ERROR} fi ;; ${__FLAGS_TYPE_FLOAT}) if _flags_validFloat "${_flags_default_}"; then : else flags_error="invalid default flag value '${_flags_default_}'" _flags_return_=${FLAGS_ERROR} fi ;; ${__FLAGS_TYPE_INTEGER}) if _flags_validInt "${_flags_default_}"; then : else flags_error="invalid default flag value '${_flags_default_}'" _flags_return_=${FLAGS_ERROR} fi ;; ${__FLAGS_TYPE_STRING}) ;; # Everything in shell is a valid string. *) flags_error="unrecognized flag type '${_flags_type_}'" _flags_return_=${FLAGS_ERROR} ;; esac fi if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then # 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 __flags_shortNames="${__flags_shortNames}${_flags_short_} " __flags_longNames="${__flags_longNames}${_flags_name_} " [ "${_flags_type_}" -eq "${__FLAGS_TYPE_BOOLEAN}" ] && \ __flags_boolNames="${__flags_boolNames}no${_flags_name_} " # Append flag names to defined names for later validation checks. __flags_definedNames="${__flags_definedNames}${_flags_usName_} " [ "${_flags_type_}" -eq "${__FLAGS_TYPE_BOOLEAN}" ] && \ __flags_definedNames="${__flags_definedNames}no${_flags_usName_} " fi flags_return=${_flags_return_} unset _flags_default_ _flags_help_ _flags_name_ _flags_return_ \ _flags_short_ _flags_type_ _flags_usName_ [ ${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 _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) _flags_genOptStr() { _flags_optStrType_=$1 _flags_opts_='' for _flags_name_ in ${__flags_longNames}; do _flags_usName_="`_flags_underscoreName "${_flags_name_}"`" _flags_type_="`_flags_getFlagInfo "${_flags_usName_}" "${__FLAGS_INFO_TYPE}"`" [ $? -eq ${FLAGS_TRUE} ] || _flags_fatal 'call to _flags_type_ failed' case ${_flags_optStrType_} in ${__FLAGS_OPTSTR_SHORT}) _flags_shortName_="`_flags_getFlagInfo \ "${_flags_usName_}" "${__FLAGS_INFO_SHORT}"`" if [ "${_flags_shortName_}" != "${__FLAGS_NULL}" ]; then _flags_opts_="${_flags_opts_}${_flags_shortName_}" # getopt needs a trailing ':' to indicate a required argument. [ "${_flags_type_}" -ne "${__FLAGS_TYPE_BOOLEAN}" ] && \ _flags_opts_="${_flags_opts_}:" fi ;; ${__FLAGS_OPTSTR_LONG}) _flags_opts_="${_flags_opts_:+${_flags_opts_},}${_flags_name_}" # getopt needs a trailing ':' to indicate a required argument [ "${_flags_type_}" -ne "${__FLAGS_TYPE_BOOLEAN}" ] && \ _flags_opts_="${_flags_opts_}:" ;; esac done 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} _flags_getFlagInfo() { # Note: adding gFI to variable names to prevent naming conflicts with calling # functions _flags_gFI_usName_=$1 _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. _flags_infoVar_="__flags_${_flags_gFI_usName_}_${_flags_gFI_info_}" _flags_strToEval_="_flags_infoValue_=\"\${${_flags_infoVar_}:-}\"" eval "${_flags_strToEval_}" if [ -n "${_flags_infoValue_}" ]; then # Special value '§' indicates no help string provided. [ "${_flags_gFI_info_}" = ${__FLAGS_INFO_HELP} \ -a "${_flags_infoValue_}" = '§' ] && _flags_infoValue_='' 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'. _flags_typeVar_="__flags_${_flags_gFI_usName_}_${__FLAGS_INFO_TYPE}" _flags_strToEval_="_flags_typeValue_=\"\${${_flags_typeVar_}:-}\"" eval "${_flags_strToEval_}" # shellcheck disable=SC2154 if [ "${_flags_typeValue_}" = "${__FLAGS_TYPE_STRING}" ]; then flags_return=${FLAGS_TRUE} else flags_return=${FLAGS_ERROR} flags_error="missing flag info variable (${_flags_infoVar_})" fi fi echo "${_flags_infoValue_}" unset _flags_gFI_usName_ _flags_gfI_info_ _flags_infoValue_ _flags_infoVar_ \ _flags_strToEval_ _flags_typeValue_ _flags_typeVar_ [ ${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 _flags_itemInList() { _flags_str_=$1 shift case " ${*:-} " in *\ ${_flags_str_}\ *) flags_return=${FLAGS_TRUE} ;; *) flags_return=${FLAGS_FALSE} ;; esac unset _flags_str_ return ${flags_return} } # Returns the width of the current screen. # # Output: # integer: width in columns of the current screen. _flags_columns() { if [ -z "${__flags_columns}" ]; then if eval stty size >/dev/null 2>&1; then # stty size worked :-) # shellcheck disable=SC2046 set -- `stty size` __flags_columns="${2:-}" fi fi if [ -z "${__flags_columns}" ]; then if eval tput cols >/dev/null 2>&1; then # shellcheck disable=SC2046 set -- `tput cols` __flags_columns="${1:-}" fi fi echo "${__flags_columns:-80}" } # Validate a boolean. # # Args: # _flags__bool: boolean: value to validate # Returns: # bool: true if the value is a valid boolean _flags_validBool() { _flags_bool_=$1 flags_return=${FLAGS_TRUE} case "${_flags_bool_}" in true|t|0) ;; false|f|1) ;; *) flags_return=${FLAGS_FALSE} ;; esac 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 _flags_validFloat() { flags_return=${FLAGS_FALSE} [ -n "$1" ] || return ${flags_return} _flags_float_=$1 if _flags_validInt "${_flags_float_}"; then flags_return=${FLAGS_TRUE} elif _flags_useBuiltin; then _flags_float_whole_=${_flags_float_%.*} _flags_float_fraction_=${_flags_float_#*.} if _flags_validInt "${_flags_float_whole_:-0}" -a \ _flags_validInt "${_flags_float_fraction_}"; then flags_return=${FLAGS_TRUE} fi unset _flags_float_whole_ _flags_float_fraction_ else flags_return=${FLAGS_TRUE} case ${_flags_float_} in -*) # Negative floats. _flags_test_=`${FLAGS_EXPR_CMD} "${_flags_float_}" :\ '\(-[0-9]*\.[0-9]*\)'` ;; *) # Positive floats. _flags_test_=`${FLAGS_EXPR_CMD} "${_flags_float_}" :\ '\([0-9]*\.[0-9]*\)'` ;; esac [ "${_flags_test_}" != "${_flags_float_}" ] && flags_return=${FLAGS_FALSE} unset _flags_test_ fi 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 _flags_validInt() { flags_return=${FLAGS_FALSE} [ -n "$1" ] || return ${flags_return} _flags_int_=$1 case ${_flags_int_} in -*.*) ;; # Ignore negative floats (we'll invalidate them later). -*) # Strip possible leading negative sign. if _flags_useBuiltin; then _flags_int_=${_flags_int_#-} else _flags_int_=`${FLAGS_EXPR_CMD} "${_flags_int_}" : '-\([0-9][0-9]*\)'` fi ;; esac case ${_flags_int_} in *[!0-9]*) flags_return=${FLAGS_FALSE} ;; *) flags_return=${FLAGS_TRUE} ;; esac 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 _flags_getoptStandard() { flags_return=${FLAGS_TRUE} _flags_shortOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}` # Check for spaces in passed options. for _flags_opt_ in "$@"; do # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06. _flags_match_=`echo "x${_flags_opt_}x" |sed 's/ //g'` if [ "${_flags_match_}" != "x${_flags_opt_}x" ]; then flags_error='the available getopt does not support spaces in options' flags_return=${FLAGS_ERROR} break fi done if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then __flags_opts=`getopt "${_flags_shortOpts_}" "$@" 2>&1` _flags_rtrn_=$? if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then _flags_warn "${__flags_opts}" flags_error='unable to parse provided options with getopt.' flags_return=${FLAGS_ERROR} fi fi 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 _flags_getoptEnhanced() { flags_return=${FLAGS_TRUE} _flags_shortOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}` _flags_boolOpts_=`echo "${__flags_boolNames}" \ |sed 's/^ *//;s/ *$//;s/ /,/g'` _flags_longOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_LONG}` __flags_opts=`${FLAGS_GETOPT_CMD} \ -o "${_flags_shortOpts_}" \ -l "${_flags_longOpts_},${_flags_boolOpts_}" \ -- "$@" 2>&1` _flags_rtrn_=$? if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then _flags_warn "${__flags_opts}" flags_error='unable to parse provided options with getopt.' flags_return=${FLAGS_ERROR} fi 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 _flags_parseGetopt() { _flags_argc_=$1 shift flags_return=${FLAGS_TRUE} if [ "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" ]; then # The @$ must be unquoted as it needs to be re-split. # shellcheck disable=SC2068 set -- $@ else # Note the quotes around the `$@' -- they are essential! eval set -- "$@" fi # 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. FLAGS_ARGC=`_flags_math "$# - 1 - ${_flags_argc_}"` export FLAGS_ARGC # Handle options. note options with values must do an additional shift. while true; do _flags_opt_=$1 _flags_arg_=${2:-} _flags_type_=${__FLAGS_TYPE_NONE} _flags_name_='' # Determine long flag name. case "${_flags_opt_}" in --) shift; break ;; # Discontinue option parsing. --*) # Long option. if _flags_useBuiltin; then _flags_opt_=${_flags_opt_#*--} else _flags_opt_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : '--\(.*\)'` fi _flags_len_=${__FLAGS_LEN_LONG} if _flags_itemInList "${_flags_opt_}" "${__flags_longNames}"; then _flags_name_=${_flags_opt_} else # Check for negated long boolean version. if _flags_itemInList "${_flags_opt_}" "${__flags_boolNames}"; then if _flags_useBuiltin; then _flags_name_=${_flags_opt_#*no} else _flags_name_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : 'no\(.*\)'` fi _flags_type_=${__FLAGS_TYPE_BOOLEAN} _flags_arg_=${__FLAGS_NULL} fi fi ;; -*) # Short option. if _flags_useBuiltin; then _flags_opt_=${_flags_opt_#*-} else _flags_opt_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : '-\(.*\)'` fi _flags_len_=${__FLAGS_LEN_SHORT} if _flags_itemInList "${_flags_opt_}" "${__flags_shortNames}"; then # Yes. Match short name to long name. Note purposeful off-by-one # (too high) with awk calculations. _flags_pos_=`echo "${__flags_shortNames}" \ |awk 'BEGIN{RS=" ";rn=0}$0==e{rn=NR}END{print rn}' \ e="${_flags_opt_}"` _flags_name_=`echo "${__flags_longNames}" \ |awk 'BEGIN{RS=" "}rn==NR{print $0}' rn="${_flags_pos_}"` fi ;; esac # Die if the flag was unrecognized. if [ -z "${_flags_name_}" ]; then flags_error="unrecognized option (${_flags_opt_})" flags_return=${FLAGS_ERROR} break fi # Set new flag value. _flags_usName_=`_flags_underscoreName "${_flags_name_}"` [ ${_flags_type_} -eq ${__FLAGS_TYPE_NONE} ] && \ _flags_type_=`_flags_getFlagInfo \ "${_flags_usName_}" ${__FLAGS_INFO_TYPE}` case ${_flags_type_} in ${__FLAGS_TYPE_BOOLEAN}) if [ ${_flags_len_} -eq ${__FLAGS_LEN_LONG} ]; then if [ "${_flags_arg_}" != "${__FLAGS_NULL}" ]; then eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}" else eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}" fi else _flags_strToEval_="_flags_val_=\ \${__flags_${_flags_usName_}_${__FLAGS_INFO_DEFAULT}}" eval "${_flags_strToEval_}" # shellcheck disable=SC2154 if [ "${_flags_val_}" -eq ${FLAGS_FALSE} ]; then eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}" else eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}" fi fi ;; ${__FLAGS_TYPE_FLOAT}) if _flags_validFloat "${_flags_arg_}"; then eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" else flags_error="invalid float value (${_flags_arg_})" flags_return=${FLAGS_ERROR} break fi ;; ${__FLAGS_TYPE_INTEGER}) if _flags_validInt "${_flags_arg_}"; then eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" else flags_error="invalid integer value (${_flags_arg_})" flags_return=${FLAGS_ERROR} break fi ;; ${__FLAGS_TYPE_STRING}) eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" ;; esac # Handle special case help flag. if [ "${_flags_usName_}" = 'help' ]; then # shellcheck disable=SC2154 if [ "${FLAGS_help}" -eq ${FLAGS_TRUE} ]; then flags_help flags_error='help requested' flags_return=${FLAGS_FALSE} break fi fi # Shift the option and non-boolean arguments out. shift [ "${_flags_type_}" != ${__FLAGS_TYPE_BOOLEAN} ] && shift done # Give user back non-flag arguments. FLAGS_ARGV='' while [ $# -gt 0 ]; do FLAGS_ARGV="${FLAGS_ARGV:+${FLAGS_ARGV} }'$1'" shift done 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 _flags_math() { if [ $# -eq 0 ]; then flags_return=${FLAGS_FALSE} elif _flags_useBuiltin; then # Variable assignment is needed as workaround for Solaris Bourne shell, # which cannot parse a bare $((expression)). # shellcheck disable=SC2016 _flags_expr_='$(($@))' eval echo ${_flags_expr_} flags_return=$? unset _flags_expr_ else eval expr "$@" flags_return=$? fi 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 _flags_strlen() { _flags_str_=${1:-} if [ -z "${_flags_str_}" ]; then flags_output=0 elif _flags_useBuiltin; then flags_output=${#_flags_str_} else flags_output=`${FLAGS_EXPR_CMD} "${_flags_str_}" : '.*'` fi 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 _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'. DEFINE_boolean() { _flags_define ${__FLAGS_TYPE_BOOLEAN} "$@"; } # Other basic flags. DEFINE_float() { _flags_define ${__FLAGS_TYPE_FLOAT} "$@"; } DEFINE_integer() { _flags_define ${__FLAGS_TYPE_INTEGER} "$@"; } DEFINE_string() { _flags_define ${__FLAGS_TYPE_STRING} "$@"; } # Parse the flags. # # Args: # unnamed: list: command-line flags to parse # Returns: # integer: success of operation, or error FLAGS() { # Define a standard 'help' flag if one isn't already defined. [ -z "${__flags_help_type:-}" ] && \ DEFINE_boolean 'help' false 'show this help' 'h' # Parse options. if [ $# -gt 0 ]; then if [ "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" ]; then _flags_getoptStandard "$@" else _flags_getoptEnhanced "$@" fi flags_return=$? else # Nothing passed; won't bother running getopt. __flags_opts='--' flags_return=${FLAGS_TRUE} fi if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then _flags_parseGetopt $# "${__flags_opts}" flags_return=$? fi [ ${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 flags_getoptInfo() { # Platform info. _flags_debug "uname -a: `uname -a`" _flags_debug "PATH: ${PATH}" # Shell info. if [ -n "${BASH_VERSION:-}" ]; then _flags_debug 'shell: bash' _flags_debug "BASH_VERSION: ${BASH_VERSION}" elif [ -n "${ZSH_VERSION:-}" ]; then _flags_debug 'shell: zsh' _flags_debug "ZSH_VERSION: ${ZSH_VERSION}" fi # getopt info. ${FLAGS_GETOPT_CMD} >/dev/null _flags_getoptReturn=$? _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 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 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) flags_help() { if [ -n "${FLAGS_HELP:-}" ]; then echo "${FLAGS_HELP}" >&2 else echo "USAGE: ${FLAGS_PARENT:-$0} [flags] args" >&2 fi if [ -n "${__flags_longNames}" ]; then echo 'flags:' >&2 for flags_name_ in ${__flags_longNames}; do flags_flagStr_='' flags_boolStr_='' flags_usName_=`_flags_underscoreName "${flags_name_}"` flags_default_=`_flags_getFlagInfo \ "${flags_usName_}" ${__FLAGS_INFO_DEFAULT}` flags_help_=`_flags_getFlagInfo \ "${flags_usName_}" ${__FLAGS_INFO_HELP}` flags_short_=`_flags_getFlagInfo \ "${flags_usName_}" ${__FLAGS_INFO_SHORT}` flags_type_=`_flags_getFlagInfo \ "${flags_usName_}" ${__FLAGS_INFO_TYPE}` [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \ flags_flagStr_="-${flags_short_}" if [ "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_ENH}" ]; then [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \ flags_flagStr_="${flags_flagStr_}," # Add [no] to long boolean flag names, except the 'help' flag. [ "${flags_type_}" -eq ${__FLAGS_TYPE_BOOLEAN} \ -a "${flags_usName_}" != 'help' ] && \ flags_boolStr_='[no]' flags_flagStr_="${flags_flagStr_}--${flags_boolStr_}${flags_name_}:" fi case ${flags_type_} in ${__FLAGS_TYPE_BOOLEAN}) if [ "${flags_default_}" -eq ${FLAGS_TRUE} ]; then flags_defaultStr_='true' else flags_defaultStr_='false' fi ;; ${__FLAGS_TYPE_FLOAT}|${__FLAGS_TYPE_INTEGER}) flags_defaultStr_=${flags_default_} ;; ${__FLAGS_TYPE_STRING}) flags_defaultStr_="'${flags_default_}'" ;; esac flags_defaultStr_="(default: ${flags_defaultStr_})" flags_helpStr_=" ${flags_flagStr_} ${flags_help_:+${flags_help_} }${flags_defaultStr_}" _flags_strlen "${flags_helpStr_}" >/dev/null flags_helpStrLen_=${flags_output} flags_columns_=`_flags_columns` if [ "${flags_helpStrLen_}" -lt "${flags_columns_}" ]; then 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. flags_emptyStr_="`echo \"x${flags_flagStr_}x\" \ |awk '{printf "%"length($0)-2"s", ""}'`" flags_helpStr_=" ${flags_emptyStr_} ${flags_defaultStr_}" _flags_strlen "${flags_helpStr_}" >/dev/null flags_helpStrLen_=${flags_output} if [ "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_STD}" \ -o "${flags_helpStrLen_}" -lt "${flags_columns_}" ]; then # 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 fi fi done fi 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 flags_reset() { for flags_name_ in ${__flags_longNames}; do flags_usName_=`_flags_underscoreName "${flags_name_}"` flags_strToEval_="unset FLAGS_${flags_usName_}" for flags_type_ in \ ${__FLAGS_INFO_DEFAULT} \ ${__FLAGS_INFO_HELP} \ ${__FLAGS_INFO_SHORT} \ ${__FLAGS_INFO_TYPE} do flags_strToEval_=\ "${flags_strToEval_} __flags_${flags_usName_}_${flags_type_}" done eval "${flags_strToEval_}" done # Reset internal variables. __flags_boolNames=' ' __flags_longNames=' ' __flags_shortNames=' ' __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}