• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# vim:et:ft=sh:sts=2:sw=2
2#
3# Copyright 2008-2023 Kate Ward. All Rights Reserved.
4# Released under the Apache License 2.0 license.
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# shFlags is an advanced command-line flag library for Unix shell scripts.
8#
9# Author: kate.ward@forestent.com (Kate Ward)
10# https://github.com/kward/shflags
11#
12# This module implements something like the gflags library available
13# from https://github.com/gflags/gflags.
14#
15# FLAG TYPES: This is a list of the DEFINE_*'s that you can do.  All flags take
16# a name, default value, help-string, and optional 'short' name (one-letter
17# name). Some flags have other arguments, which are described with the flag.
18#
19# DEFINE_string: takes any input, and interprets it as a string.
20#
21# DEFINE_boolean: does not take any arguments. Say --myflag to set
22#   FLAGS_myflag to true, or --nomyflag to set FLAGS_myflag to false. For short
23#   flags, passing the flag on the command-line negates the default value, i.e.
24#   if the default is true, passing the flag sets the value to false.
25#
26# DEFINE_float: takes an input and interprets it as a floating point number. As
27#   shell does not support floats per-se, the input is merely validated as
28#   being a valid floating point value.
29#
30# DEFINE_integer: takes an input and interprets it as an integer.
31#
32# SPECIAL FLAGS: There are a few flags that have special meaning:
33#   --help (or -?)  prints a list of all the flags in a human-readable fashion
34#   --flagfile=foo  read flags from foo.  (not implemented yet)
35#   --              as in getopt(), terminates flag-processing
36#
37# EXAMPLE USAGE:
38#
39#   -- begin hello.sh --
40#   #! /bin/sh
41#   . ./shflags
42#   DEFINE_string name 'world' "somebody's name" n
43#   FLAGS "$@" || exit $?
44#   eval set -- "${FLAGS_ARGV}"
45#   echo "Hello, ${FLAGS_name}."
46#   -- end hello.sh --
47#
48#   $ ./hello.sh -n Kate
49#   Hello, Kate.
50#
51# CUSTOMIZABLE BEHAVIOR:
52#
53# A script can override the default 'getopt' command by providing the path to
54# an alternate implementation by defining the FLAGS_GETOPT_CMD variable.
55#
56# NOTES:
57#
58# * Not all systems include a getopt version that supports long flags. On these
59#   systems, only short flags are recognized.
60
61#==============================================================================
62# shFlags
63#
64# Shared attributes:
65#   flags_error:  last error message
66#   flags_output: last function output (rarely valid)
67#   flags_return: last return value
68#
69#   __flags_longNames: list of long names for all flags
70#   __flags_shortNames: list of short names for all flags
71#   __flags_boolNames: list of boolean flag names
72#
73#   __flags_opts: options parsed by getopt
74#
75# Per-flag attributes:
76#   FLAGS_<flag_name>: contains value of flag named 'flag_name'
77#   __flags_<flag_name>_default: the default flag value
78#   __flags_<flag_name>_help: the flag help string
79#   __flags_<flag_name>_short: the flag short name
80#   __flags_<flag_name>_type: the flag type
81#
82# Notes:
83# - lists of strings are space separated, and a null value is the '~' char.
84#
85### ShellCheck (http://www.shellcheck.net/)
86# expr may be antiquated, but it is the only solution in some cases.
87#   shellcheck disable=SC2003
88# $() are not fully portable (POSIX != portable).
89#   shellcheck disable=SC2006
90# [ p -a q ] are well defined enough (vs [ p ] && [ q ]).
91#   shellcheck disable=SC2166
92
93# Return if FLAGS already loaded.
94if [ -n "${FLAGS_VERSION:-}" ]; then return 0; fi
95FLAGS_VERSION='1.4.0pre'
96
97# Return values that scripts can use.
98FLAGS_TRUE=0
99FLAGS_FALSE=1
100FLAGS_ERROR=2
101
102# shlib_expr_cmd determines a reasonable default `expr` command.
103# https://github.com/kward/shlib
104#
105# Use like:
106#   EXPR_CMD=$(shlib_expr_cmd)
107#   ${EXPR_CMD} 1 + 1
108#
109# Args:
110#   none
111# Output:
112#   string: expr command
113# Return
114#   int: 0 upon success
115shlib_expr_cmd() {
116  if [ "$(uname -s)" = 'BSD' ]; then
117    echo 'gexpr --'
118    return 0
119  fi
120
121  _shlib_expr_cmd_='expr --'
122  # shellcheck disable=SC2003
123  if _shlib_output_=$(${_shlib_expr_cmd_} 2>&1); then
124    if [ "${_shlib_output_}" = '--' ]; then
125      # We are likely running inside BusyBox.
126      _shlib_expr_cmd_='expr'
127    fi
128  fi
129
130  echo "${_shlib_expr_cmd_}"
131  unset _shlib_expr_cmd_ _shlib_output_
132}
133__FLAGS_EXPR_CMD=`shlib_expr_cmd`
134
135# Commands a user can override if desired.
136FLAGS_EXPR_CMD=${FLAGS_EXPR_CMD:-${__FLAGS_EXPR_CMD}}
137FLAGS_GETOPT_CMD=${FLAGS_GETOPT_CMD:-getopt}
138
139#
140# Logging functions.
141#
142
143# Logging levels.
144FLAGS_LEVEL_DEBUG=0
145FLAGS_LEVEL_INFO=1
146FLAGS_LEVEL_WARN=2
147FLAGS_LEVEL_ERROR=3
148FLAGS_LEVEL_FATAL=4
149__FLAGS_LEVEL_DEFAULT=${FLAGS_LEVEL_WARN}
150__flags_level=${__FLAGS_LEVEL_DEFAULT} # Current logging level.
151
152_flags_debug() {
153  if [ "${__flags_level}" -le "${FLAGS_LEVEL_DEBUG}" ]; then echo "flags:DEBUG $*" >&2; fi
154}
155_flags_info() {
156  if [ "${__flags_level}" -le "${FLAGS_LEVEL_INFO}" ]; then echo "flags:INFO $*" >&2; fi
157}
158_flags_warn() {
159  if [ "${__flags_level}" -le "${FLAGS_LEVEL_WARN}" ]; then echo "flags:WARN $*" >&2; fi
160}
161_flags_error() {
162  if [ "${__flags_level}" -le "${FLAGS_LEVEL_ERROR}" ]; then echo "flags:ERROR $*" >&2; fi
163}
164_flags_fatal() {
165  echo "flags:FATAL $*" >&2
166  exit ${FLAGS_ERROR}
167}
168
169# Get the logging level.
170flags_loggingLevel() { echo "${__flags_level}"; }
171
172# Set the logging level by overriding the `__flags_level` variable.
173#
174# Args:
175#   _flags_level_: integer: new logging level
176# Returns:
177#   nothing
178flags_setLoggingLevel() {
179  [ $# -ne 1 ] && _flags_fatal "flags_setLevel(): logging level missing"
180  _flags_level_=$1
181  if ! [ "${_flags_level_}" -ge "${FLAGS_LEVEL_DEBUG}" -a "${_flags_level_}" -le "${FLAGS_LEVEL_FATAL}" ]; then
182    _flags_fatal "Invalid logging level '${_flags_level_}' specified."
183  fi
184  __flags_level=$1
185  unset _flags_level_
186}
187
188#
189# Shell checks.
190#
191
192if [ -n "${ZSH_VERSION:-}" ]; then
193  setopt |grep "^shwordsplit$" >/dev/null
194  if [ $? -ne ${FLAGS_TRUE} ]; then
195    _flags_fatal 'zsh shwordsplit option is required for proper zsh operation'
196  fi
197  if [ -z "${FLAGS_PARENT:-}" ]; then
198    _flags_fatal "zsh does not pass \$0 through properly. please declare' \
199\"FLAGS_PARENT=\$0\" before calling shFlags"
200  fi
201fi
202
203# Can we use built-ins?
204( echo "${FLAGS_TRUE#0}"; ) >/dev/null 2>&1
205if [ $? -eq ${FLAGS_TRUE} ]; then
206  __FLAGS_USE_BUILTIN=${FLAGS_TRUE}
207else
208  __FLAGS_USE_BUILTIN=${FLAGS_FALSE}
209fi
210
211#
212# Constants.
213#
214
215# Reserved flag names.
216__FLAGS_RESERVED_LIST=' ARGV ERROR FALSE GETOPT_CMD HELP PARENT TRUE '
217__FLAGS_RESERVED_LIST="${__FLAGS_RESERVED_LIST} VERSION "
218
219# Determined getopt version (standard or enhanced).
220__FLAGS_GETOPT_VERS_STD=0
221__FLAGS_GETOPT_VERS_ENH=1
222
223# shellcheck disable=SC2120
224_flags_getopt_vers() {
225  _flags_getopt_cmd_=${1:-${FLAGS_GETOPT_CMD}}
226  case "`${_flags_getopt_cmd_} -lfoo '' --foo 2>&1`" in
227    ' -- --foo') echo ${__FLAGS_GETOPT_VERS_STD} ;;
228    ' --foo --') echo ${__FLAGS_GETOPT_VERS_ENH} ;;
229    # Unrecognized output. Assuming standard getopt version.
230    *) echo ${__FLAGS_GETOPT_VERS_STD} ;;
231  esac
232  unset _flags_getopt_cmd_
233}
234# shellcheck disable=SC2119
235__FLAGS_GETOPT_VERS=`_flags_getopt_vers`
236
237# getopt optstring lengths
238__FLAGS_OPTSTR_SHORT=0
239__FLAGS_OPTSTR_LONG=1
240
241__FLAGS_NULL='~'
242
243# Flag info strings.
244__FLAGS_INFO_DEFAULT='default'
245__FLAGS_INFO_HELP='help'
246__FLAGS_INFO_SHORT='short'
247__FLAGS_INFO_TYPE='type'
248
249# Flag lengths.
250__FLAGS_LEN_SHORT=0
251__FLAGS_LEN_LONG=1
252
253# Flag types.
254__FLAGS_TYPE_NONE=0
255__FLAGS_TYPE_BOOLEAN=1
256__FLAGS_TYPE_FLOAT=2
257__FLAGS_TYPE_INTEGER=3
258__FLAGS_TYPE_STRING=4
259
260# Set the constants readonly.
261__flags_constants=`set |awk -F= '/^FLAGS_/ || /^__FLAGS_/ {print $1}'`
262for __flags_const in ${__flags_constants}; do
263  # Skip certain flags.
264  case ${__flags_const} in
265    FLAGS_HELP) continue ;;
266    FLAGS_PARENT) continue ;;
267  esac
268  # Set flag readonly.
269  if [ -z "${ZSH_VERSION:-}" ]; then
270    readonly "${__flags_const}"
271    continue
272  fi
273  case ${ZSH_VERSION} in
274    [123].*) readonly "${__flags_const}" ;;
275    *)
276      # Declare readonly constants globally.
277      # shellcheck disable=SC2039,SC3045
278      readonly -g "${__flags_const}" ;;
279  esac
280done
281unset __flags_const __flags_constants
282
283#
284# Internal variables.
285#
286
287# Space separated lists.
288__flags_boolNames=' '     # Boolean flag names.
289__flags_longNames=' '     # Long flag names.
290__flags_shortNames=' '    # Short flag names.
291__flags_definedNames=' '  # Defined flag names (used for validation).
292
293__flags_columns=''  # Screen width in columns.
294__flags_opts=''     # Temporary storage for parsed getopt flags.
295
296# Define a flag.
297#
298# Calling this function will define the following info variables for the
299# specified flag:
300#   FLAGS_flagname - the name for this flag (based upon the long flag name)
301#   __flags_<flag_name>_default - the default value
302#   __flags_flagname_help - the help string
303#   __flags_flagname_short - the single letter alias
304#   __flags_flagname_type - the type of flag (one of __FLAGS_TYPE_*)
305#
306# Args:
307#   _flags_type_: integer: internal type of flag (__FLAGS_TYPE_*)
308#   _flags_name_: string: long flag name
309#   _flags_default_: default flag value
310#   _flags_help_: string: help string
311#   _flags_short_: string: (optional) short flag name
312# Returns:
313#   integer: success of operation, or error
314_flags_define() {
315  if [ $# -lt 4 ]; then
316    flags_error='DEFINE error: too few arguments'
317    flags_return=${FLAGS_ERROR}
318    _flags_error "${flags_error}"
319    return ${flags_return}
320  fi
321
322  _flags_type_=$1
323  _flags_name_=$2
324  _flags_default_=$3
325  _flags_help_=${4:-§}  # Special value '§' indicates no help string provided.
326  _flags_short_=${5:-${__FLAGS_NULL}}
327
328  _flags_debug "type:${_flags_type_} name:${_flags_name_}" \
329      "default:'${_flags_default_}' help:'${_flags_help_}'" \
330      "short:${_flags_short_}"
331
332  _flags_return_=${FLAGS_TRUE}
333  _flags_usName_="`_flags_underscoreName "${_flags_name_}"`"
334
335  # Check whether the flag name is reserved.
336  if _flags_itemInList "${_flags_usName_}" "${__FLAGS_RESERVED_LIST}"; then
337    flags_error="flag name (${_flags_name_}) is reserved"
338    _flags_return_=${FLAGS_ERROR}
339  fi
340
341  # Require short option for getopt that don't support long options.
342  if [ ${_flags_return_} -eq ${FLAGS_TRUE} \
343      -a "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" \
344      -a "${_flags_short_}" = "${__FLAGS_NULL}" ]
345  then
346    flags_error="short flag required for (${_flags_name_}) on this platform"
347    _flags_return_=${FLAGS_ERROR}
348  fi
349
350  # Check for existing long name definition.
351  if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then
352    if _flags_itemInList "${_flags_usName_}" "${__flags_definedNames}"; then
353      flags_error="definition for ([no]${_flags_name_}) already exists"
354      _flags_warn "${flags_error}"
355      _flags_return_=${FLAGS_FALSE}
356    fi
357  fi
358
359  # Check for existing short name definition.
360  if [ ${_flags_return_} -eq ${FLAGS_TRUE} -a "${_flags_short_}" != "${__FLAGS_NULL}" ]; then
361    if _flags_itemInList "${_flags_short_}" "${__flags_shortNames}"; then
362      flags_error="flag short name (${_flags_short_}) already defined"
363      _flags_warn "${flags_error}"
364      _flags_return_=${FLAGS_FALSE}
365    fi
366  fi
367
368  # Handle default value. Note, on several occasions the 'if' portion of an
369  # if/then/else contains just a ':' which does nothing. A binary reversal via
370  # '!' is not done because it does not work on all shells.
371  if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then
372    case ${_flags_type_} in
373      "${__FLAGS_TYPE_BOOLEAN}")
374        if _flags_validBool "${_flags_default_}"; then
375          case ${_flags_default_} in
376            true|t|0) _flags_default_=${FLAGS_TRUE} ;;
377            false|f|1) _flags_default_=${FLAGS_FALSE} ;;
378          esac
379        else
380          flags_error="invalid default flag value '${_flags_default_}'"
381          _flags_return_=${FLAGS_ERROR}
382        fi
383        ;;
384
385      "${__FLAGS_TYPE_FLOAT}")
386        if _flags_validFloat "${_flags_default_}"; then
387          :
388        else
389          flags_error="invalid default flag value '${_flags_default_}'"
390          _flags_return_=${FLAGS_ERROR}
391        fi
392        ;;
393
394      "${__FLAGS_TYPE_INTEGER}")
395        if _flags_validInt "${_flags_default_}"; then
396          :
397        else
398          flags_error="invalid default flag value '${_flags_default_}'"
399          _flags_return_=${FLAGS_ERROR}
400        fi
401        ;;
402
403      "${__FLAGS_TYPE_STRING}") ;;  # Everything in shell is a valid string.
404
405      *)
406        flags_error="unrecognized flag type '${_flags_type_}'"
407        _flags_return_=${FLAGS_ERROR}
408        ;;
409    esac
410  fi
411
412  if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then
413    # Store flag information.
414    eval "FLAGS_${_flags_usName_}='${_flags_default_}'"
415    eval "__flags_${_flags_usName_}_${__FLAGS_INFO_TYPE}=${_flags_type_}"
416    eval "__flags_${_flags_usName_}_${__FLAGS_INFO_DEFAULT}=\
417\"${_flags_default_}\""
418    eval "__flags_${_flags_usName_}_${__FLAGS_INFO_HELP}=\"${_flags_help_}\""
419    eval "__flags_${_flags_usName_}_${__FLAGS_INFO_SHORT}='${_flags_short_}'"
420
421    # append flag names to name lists
422    __flags_shortNames="${__flags_shortNames}${_flags_short_} "
423    __flags_longNames="${__flags_longNames}${_flags_name_} "
424    [ "${_flags_type_}" -eq "${__FLAGS_TYPE_BOOLEAN}" ] && \
425        __flags_boolNames="${__flags_boolNames}no${_flags_name_} "
426
427    # Append flag names to defined names for later validation checks.
428    __flags_definedNames="${__flags_definedNames}${_flags_usName_} "
429    [ "${_flags_type_}" -eq "${__FLAGS_TYPE_BOOLEAN}" ] && \
430        __flags_definedNames="${__flags_definedNames}no${_flags_usName_} "
431  fi
432
433  flags_return=${_flags_return_}
434  unset _flags_default_ _flags_help_ _flags_name_ _flags_return_ \
435      _flags_short_ _flags_type_ _flags_usName_
436  [ ${flags_return} -eq ${FLAGS_ERROR} ] && _flags_error "${flags_error}"
437  return ${flags_return}
438}
439
440# Underscore a flag name by replacing dashes with underscores.
441#
442# Args:
443#   unnamed: string: log flag name
444# Output:
445#   string: underscored name
446_flags_underscoreName() {
447  echo "$1" |tr z- z_
448}
449
450# Return valid getopt options using currently defined list of long options.
451#
452# This function builds a proper getopt option string for short (and long)
453# options, using the current list of long options for reference.
454#
455# Args:
456#   _flags_optStr: integer: option string type (__FLAGS_OPTSTR_*)
457# Output:
458#   string: generated option string for getopt
459# Returns:
460#   boolean: success of operation (always returns True)
461_flags_genOptStr() {
462  _flags_optStrType_=$1
463
464  _flags_opts_=''
465
466  for _flags_name_ in ${__flags_longNames}; do
467    _flags_usName_="`_flags_underscoreName "${_flags_name_}"`"
468    _flags_type_="`_flags_getFlagInfo "${_flags_usName_}" "${__FLAGS_INFO_TYPE}"`"
469    if [ $? -ne ${FLAGS_TRUE} ]; then
470      _flags_fatal 'call to _flags_type_ failed'
471    fi
472    case ${_flags_optStrType_} in
473      "${__FLAGS_OPTSTR_SHORT}")
474        _flags_shortName_="`_flags_getFlagInfo \
475            "${_flags_usName_}" "${__FLAGS_INFO_SHORT}"`"
476        if [ "${_flags_shortName_}" != "${__FLAGS_NULL}" ]; then
477          _flags_opts_="${_flags_opts_}${_flags_shortName_}"
478          # getopt needs a trailing ':' to indicate a required argument.
479          [ "${_flags_type_}" -ne "${__FLAGS_TYPE_BOOLEAN}" ] && \
480              _flags_opts_="${_flags_opts_}:"
481        fi
482        ;;
483
484      "${__FLAGS_OPTSTR_LONG}")
485        _flags_opts_="${_flags_opts_:+${_flags_opts_},}${_flags_name_}"
486        # getopt needs a trailing ':' to indicate a required argument
487        [ "${_flags_type_}" -ne "${__FLAGS_TYPE_BOOLEAN}" ] && \
488            _flags_opts_="${_flags_opts_}:"
489        ;;
490    esac
491  done
492
493  echo "${_flags_opts_}"
494  unset _flags_name_ _flags_opts_ _flags_optStrType_ _flags_shortName_ \
495      _flags_type_ _flags_usName_
496  return ${FLAGS_TRUE}
497}
498
499# Returns flag details based on a flag name and flag info.
500#
501# Args:
502#   string: underscored flag name
503#   string: flag info (see the _flags_define function for valid info types)
504# Output:
505#   string: value of dereferenced flag variable
506# Returns:
507#   integer: one of FLAGS_{TRUE|FALSE|ERROR}
508_flags_getFlagInfo() {
509  # Note: adding gFI to variable names to prevent naming conflicts with calling
510  # functions
511  _flags_gFI_usName_=$1
512  _flags_gFI_info_=$2
513
514  # Example: given argument usName (underscored flag name) of 'my_flag', and
515  # argument info of 'help', set the _flags_infoValue_ variable to the value of
516  # ${__flags_my_flag_help}, and see if it is non-empty.
517  _flags_infoVar_="__flags_${_flags_gFI_usName_}_${_flags_gFI_info_}"
518  _flags_strToEval_="_flags_infoValue_=\"\${${_flags_infoVar_}:-}\""
519  eval "${_flags_strToEval_}"
520  if [ -n "${_flags_infoValue_}" ]; then
521    # Special value '§' indicates no help string provided.
522    [ "${_flags_gFI_info_}" = "${__FLAGS_INFO_HELP}" \
523        -a "${_flags_infoValue_}" = '§' ] && _flags_infoValue_=''
524    flags_return=${FLAGS_TRUE}
525  else
526    # See if the _flags_gFI_usName_ variable is a string as strings can be
527    # empty...
528    # Note: the DRY principle would say to have this function call itself for
529    # the next three lines, but doing so results in an infinite loop as an
530    # invalid _flags_name_ will also not have the associated _type variable.
531    # Because it doesn't (it will evaluate to an empty string) the logic will
532    # try to find the _type variable of the _type variable, and so on. Not so
533    # good ;-)
534    #
535    # Example cont.: set the _flags_typeValue_ variable to the value of
536    # ${__flags_my_flag_type}, and see if it equals '4'.
537    _flags_typeVar_="__flags_${_flags_gFI_usName_}_${__FLAGS_INFO_TYPE}"
538    _flags_strToEval_="_flags_typeValue_=\"\${${_flags_typeVar_}:-}\""
539    eval "${_flags_strToEval_}"
540    # shellcheck disable=SC2154
541    if [ "${_flags_typeValue_}" = "${__FLAGS_TYPE_STRING}" ]; then
542      flags_return=${FLAGS_TRUE}
543    else
544      flags_return=${FLAGS_ERROR}
545      flags_error="missing flag info variable (${_flags_infoVar_})"
546    fi
547  fi
548
549  echo "${_flags_infoValue_}"
550  unset _flags_gFI_usName_ _flags_gfI_info_ _flags_infoValue_ _flags_infoVar_ \
551      _flags_strToEval_ _flags_typeValue_ _flags_typeVar_
552  [ ${flags_return} -eq ${FLAGS_ERROR} ] && _flags_error "${flags_error}"
553  return ${flags_return}
554}
555
556# Check for presence of item in a list.
557#
558# Passed a string (e.g. 'abc'), this function will determine if the string is
559# present in the list of strings (e.g.  ' foo bar abc ').
560#
561# Args:
562#   _flags_str_: string: string to search for in a list of strings
563#   unnamed: list: list of strings
564# Returns:
565#   boolean: true if item is in the list
566_flags_itemInList() {
567  _flags_str_=$1
568  shift
569
570  case " ${*:-} " in
571    *\ ${_flags_str_}\ *) flags_return=${FLAGS_TRUE} ;;
572    *) flags_return=${FLAGS_FALSE} ;;
573  esac
574
575  unset _flags_str_
576  return ${flags_return}
577}
578
579# Returns the width of the current screen.
580#
581# Output:
582#   integer: width in columns of the current screen.
583_flags_columns() {
584  if [ -z "${__flags_columns}" ]; then
585    if eval stty size >/dev/null 2>&1; then
586      # stty size worked :-)
587      # shellcheck disable=SC2046
588      set -- `stty size`
589      __flags_columns="${2:-}"
590    fi
591  fi
592  if [ -z "${__flags_columns}" ]; then
593   if eval tput cols >/dev/null 2>&1; then
594      # shellcheck disable=SC2046
595      set -- `tput cols`
596      __flags_columns="${1:-}"
597    fi
598  fi
599  echo "${__flags_columns:-80}"
600}
601
602# Validate a boolean.
603#
604# Args:
605#   _flags__bool: boolean: value to validate
606# Returns:
607#   bool: true if the value is a valid boolean
608_flags_validBool() {
609  _flags_bool_=$1
610
611  flags_return=${FLAGS_TRUE}
612  case "${_flags_bool_}" in
613    true|t|0) ;;
614    false|f|1) ;;
615    *) flags_return=${FLAGS_FALSE} ;;
616  esac
617
618  unset _flags_bool_
619  return ${flags_return}
620}
621
622# Validate a float.
623#
624# Args:
625#   _flags_float_: float: value to validate
626# Returns:
627#   bool: true if the value is a valid integer
628_flags_validFloat() {
629  flags_return=${FLAGS_FALSE}
630  if [ -z "$1" ]; then
631    return ${flags_return}
632  fi
633  _flags_float_=$1
634
635  if _flags_validInt "${_flags_float_}"; then
636    flags_return=${FLAGS_TRUE}
637  elif _flags_useBuiltin; then
638    _flags_float_whole_=${_flags_float_%.*}
639    _flags_float_fraction_=${_flags_float_#*.}
640    [ "${_flags_float_whole_}" = '-' ] && _flags_float_whole_='-0'
641    if _flags_validInt "${_flags_float_whole_:-0}" -a \
642      _flags_validInt "${_flags_float_fraction_}"; then
643      flags_return=${FLAGS_TRUE}
644    fi
645    unset _flags_float_whole_ _flags_float_fraction_
646  else
647    flags_return=${FLAGS_TRUE}
648    case ${_flags_float_} in
649      -*)  # Negative floats.
650        _flags_test_=`${FLAGS_EXPR_CMD} "${_flags_float_}" :\
651            '\(-[0-9]*\.[0-9]*\)'`
652        ;;
653      *)  # Positive floats.
654        _flags_test_=`${FLAGS_EXPR_CMD} "${_flags_float_}" :\
655            '\([0-9]*\.[0-9]*\)'`
656        ;;
657    esac
658    [ "${_flags_test_}" != "${_flags_float_}" ] && flags_return=${FLAGS_FALSE}
659    unset _flags_test_
660  fi
661
662  unset _flags_float_ _flags_float_whole_ _flags_float_fraction_
663  return ${flags_return}
664}
665
666# Validate an integer.
667#
668# Args:
669#   _flags_int_: integer: value to validate
670# Returns:
671#   bool: true if the value is a valid integer
672_flags_validInt() {
673  expr \( "$1" + '0' \) '=' "$1" >/dev/null 2>&1
674}
675
676# Parse command-line options using the standard getopt.
677#
678# Note: the flag options are passed around in the global __flags_opts so that
679# the formatting is not lost due to shell parsing and such.
680#
681# Args:
682#   @: varies: command-line options to parse
683# Returns:
684#   integer: a FLAGS success condition
685_flags_getoptStandard() {
686  flags_return=${FLAGS_TRUE}
687  _flags_shortOpts_=`_flags_genOptStr "${__FLAGS_OPTSTR_SHORT}"`
688
689  # Check for spaces in passed options.
690  for _flags_opt_ in "$@"; do
691    # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06.
692    _flags_match_=`echo "x${_flags_opt_}x" |sed 's/ //g'`
693    if [ "${_flags_match_}" != "x${_flags_opt_}x" ]; then
694      flags_error='the available getopt does not support spaces in options'
695      flags_return=${FLAGS_ERROR}
696      break
697    fi
698  done
699
700  if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then
701    __flags_opts=`getopt "${_flags_shortOpts_}" "$@" 2>&1`
702    _flags_rtrn_=$?
703    if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then
704      _flags_warn "${__flags_opts}"
705      flags_error='unable to parse provided options with getopt.'
706      flags_return=${FLAGS_ERROR}
707    fi
708  fi
709
710  unset _flags_match_ _flags_opt_ _flags_rtrn_ _flags_shortOpts_
711  return ${flags_return}
712}
713
714# Parse command-line options using the enhanced getopt.
715#
716# Note: the flag options are passed around in the global __flags_opts so that
717# the formatting is not lost due to shell parsing and such.
718#
719# Args:
720#   @: varies: command-line options to parse
721# Returns:
722#   integer: a FLAGS success condition
723_flags_getoptEnhanced() {
724  flags_return=${FLAGS_TRUE}
725  _flags_shortOpts_=`_flags_genOptStr "${__FLAGS_OPTSTR_SHORT}"`
726  _flags_boolOpts_=`echo "${__flags_boolNames}" \
727      |sed 's/^ *//;s/ *$//;s/ /,/g'`
728  _flags_longOpts_=`_flags_genOptStr "${__FLAGS_OPTSTR_LONG}"`
729
730  __flags_opts=`${FLAGS_GETOPT_CMD} \
731      -o "${_flags_shortOpts_}" \
732      -l "${_flags_longOpts_},${_flags_boolOpts_}" \
733      -- "$@" 2>&1`
734  _flags_rtrn_=$?
735  if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then
736    _flags_warn "${__flags_opts}"
737    flags_error='unable to parse provided options with getopt.'
738    flags_return=${FLAGS_ERROR}
739  fi
740
741  unset _flags_boolOpts_ _flags_longOpts_ _flags_rtrn_ _flags_shortOpts_
742  return ${flags_return}
743}
744
745# Dynamically parse a getopt result and set appropriate variables.
746#
747# This function does the actual conversion of getopt output and runs it through
748# the standard case structure for parsing. The case structure is actually quite
749# dynamic to support any number of flags.
750#
751# Args:
752#   @: varies: output from getopt parsing
753# Returns:
754#   integer: a FLAGS success condition
755_flags_parseGetopt() {
756  flags_return=${FLAGS_TRUE}
757
758  if [ "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" ]; then
759    # The @$ must be unquoted as it needs to be re-split.
760    #   shellcheck disable=SC2068
761    set -- $@
762  else
763    # Note the quotes around the `$@` -- they are essential!
764    #  shellcheck disable=SC2294
765    eval set -- "$@"
766  fi
767
768  # Handle options. note options with values must do an additional shift.
769  while true; do
770    _flags_opt_=$1
771    _flags_arg_=${2:-}
772    _flags_type_=${__FLAGS_TYPE_NONE}
773    _flags_name_=''
774
775    # Determine long flag name.
776    case "${_flags_opt_}" in
777      --) shift; break ;;  # Discontinue option parsing.
778
779      --*)  # Long option.
780        if _flags_useBuiltin; then
781          _flags_opt_=${_flags_opt_#*--}
782        else
783          _flags_opt_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : '--\(.*\)'`
784        fi
785        _flags_len_=${__FLAGS_LEN_LONG}
786        if _flags_itemInList "${_flags_opt_}" "${__flags_longNames}"; then
787          _flags_name_=${_flags_opt_}
788        else
789          # Check for negated long boolean version.
790          if _flags_itemInList "${_flags_opt_}" "${__flags_boolNames}"; then
791            if _flags_useBuiltin; then
792              _flags_name_=${_flags_opt_#*no}
793            else
794              _flags_name_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : 'no\(.*\)'`
795            fi
796            _flags_type_=${__FLAGS_TYPE_BOOLEAN}
797            _flags_arg_=${__FLAGS_NULL}
798          fi
799        fi
800        ;;
801
802      -*)  # Short option.
803        if _flags_useBuiltin; then
804          _flags_opt_=${_flags_opt_#*-}
805        else
806          _flags_opt_=`${FLAGS_EXPR_CMD} "${_flags_opt_}" : '-\(.*\)'`
807        fi
808        _flags_len_=${__FLAGS_LEN_SHORT}
809        if _flags_itemInList "${_flags_opt_}" "${__flags_shortNames}"; then
810          # Yes. Match short name to long name. Note purposeful off-by-one
811          # (too high) with awk calculations.
812          _flags_pos_=`echo "${__flags_shortNames}" \
813              |awk 'BEGIN{RS=" ";rn=0}$0==e{rn=NR}END{print rn}' \
814                  e="${_flags_opt_}"`
815          _flags_name_=`echo "${__flags_longNames}" \
816              |awk 'BEGIN{RS=" "}rn==NR{print $0}' rn="${_flags_pos_}"`
817        fi
818        ;;
819    esac
820
821    # Die if the flag was unrecognized.
822    if [ -z "${_flags_name_}" ]; then
823      flags_error="unrecognized option (${_flags_opt_})"
824      flags_return=${FLAGS_ERROR}
825      break
826    fi
827
828    # Set new flag value.
829    _flags_usName_=`_flags_underscoreName "${_flags_name_}"`
830    [ "${_flags_type_}" -eq "${__FLAGS_TYPE_NONE}" ] && \
831        _flags_type_=`_flags_getFlagInfo "${_flags_usName_}" "${__FLAGS_INFO_TYPE}"`
832    case ${_flags_type_} in
833      "${__FLAGS_TYPE_BOOLEAN}")
834        if [ "${_flags_len_}" -eq "${__FLAGS_LEN_LONG}" ]; then
835          if [ "${_flags_arg_}" != "${__FLAGS_NULL}" ]; then
836            eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}"
837          else
838            eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}"
839          fi
840        else
841          _flags_strToEval_="_flags_val_=\
842\${__flags_${_flags_usName_}_${__FLAGS_INFO_DEFAULT}}"
843          eval "${_flags_strToEval_}"
844          # shellcheck disable=SC2154
845          if [ "${_flags_val_}" -eq ${FLAGS_FALSE} ]; then
846            eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}"
847          else
848            eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}"
849          fi
850        fi
851        ;;
852
853      "${__FLAGS_TYPE_FLOAT}")
854        if _flags_validFloat "${_flags_arg_}"; then
855          eval "FLAGS_${_flags_usName_}='${_flags_arg_}'"
856        else
857          flags_error="invalid float value (${_flags_arg_})"
858          flags_return=${FLAGS_ERROR}
859          break
860        fi
861        ;;
862
863      "${__FLAGS_TYPE_INTEGER}")
864        if _flags_validInt "${_flags_arg_}"; then
865          eval "FLAGS_${_flags_usName_}='${_flags_arg_}'"
866        else
867          flags_error="invalid integer value (${_flags_arg_})"
868          flags_return=${FLAGS_ERROR}
869          break
870        fi
871        ;;
872
873      "${__FLAGS_TYPE_STRING}")
874        eval "FLAGS_${_flags_usName_}='${_flags_arg_}'"
875        ;;
876    esac
877
878    # Handle special case help flag.
879    if [ "${_flags_usName_}" = 'help' ]; then
880      # shellcheck disable=SC2154
881      if [ "${FLAGS_help}" -eq ${FLAGS_TRUE} ]; then
882        flags_help
883        flags_error='help requested'
884        flags_return=${FLAGS_FALSE}
885        break
886      fi
887    fi
888
889    # Shift the option and non-boolean arguments out.
890    shift
891    [ "${_flags_type_}" != "${__FLAGS_TYPE_BOOLEAN}" ] && shift
892  done
893
894  # Give user back non-flag arguments.
895  FLAGS_ARGV=''
896  while [ $# -gt 0 ]; do
897    FLAGS_ARGV="${FLAGS_ARGV:+${FLAGS_ARGV} }'$1'"
898    shift
899  done
900
901  unset _flags_arg_ _flags_len_ _flags_name_ _flags_opt_ _flags_pos_ \
902      _flags_strToEval_ _flags_type_ _flags_usName_ _flags_val_
903  return ${flags_return}
904}
905
906# Perform some path using built-ins.
907#
908# Args:
909#   $@: string: math expression to evaluate
910# Output:
911#   integer: the result
912# Returns:
913#   bool: success of math evaluation
914_flags_math() {
915  if [ $# -eq 0 ]; then
916    flags_return=${FLAGS_FALSE}
917  elif _flags_useBuiltin; then
918    # Variable assignment is needed as workaround for Solaris Bourne shell,
919    # which cannot parse a bare $((expression)).
920    # shellcheck disable=SC2016
921    _flags_expr_='$(($@))'
922    eval echo ${_flags_expr_}
923    flags_return=$?
924    unset _flags_expr_
925  else
926    #  shellcheck disable=SC2294
927    eval expr "$@"
928    flags_return=$?
929  fi
930
931  return ${flags_return}
932}
933
934# Cross-platform strlen() implementation.
935#
936# Args:
937#   _flags_str: string: to determine length of
938# Output:
939#   integer: length of string
940# Returns:
941#   bool: success of strlen evaluation
942_flags_strlen() {
943  _flags_str_=${1:-}
944
945  if [ -z "${_flags_str_}" ]; then
946    flags_output=0
947  elif _flags_useBuiltin; then
948    flags_output=${#_flags_str_}
949  else
950    flags_output=`${FLAGS_EXPR_CMD} "${_flags_str_}" : '.*'`
951  fi
952  flags_return=$?
953
954  unset _flags_str_
955  echo "${flags_output}"
956  return ${flags_return}
957}
958
959# Use built-in helper function to enable unit testing.
960#
961# Args:
962#   None
963# Returns:
964#   bool: true if built-ins should be used
965_flags_useBuiltin() { return "${__FLAGS_USE_BUILTIN}"; }
966
967#------------------------------------------------------------------------------
968# public functions
969#
970# A basic boolean flag. Boolean flags do not take any arguments, and their
971# value is either 1 (false) or 0 (true). For long flags, the false value is
972# specified on the command line by prepending the word 'no'. With short flags,
973# the presence of the flag toggles the current value between true and false.
974# Specifying a short boolean flag twice on the command results in returning the
975# value back to the default value.
976#
977# A default value is required for boolean flags.
978#
979# For example, lets say a Boolean flag was created whose long name was 'update'
980# and whose short name was 'x', and the default value was 'false'. This flag
981# could be explicitly set to 'true' with '--update' or by '-x', and it could be
982# explicitly set to 'false' with '--noupdate'.
983DEFINE_boolean() { _flags_define "${__FLAGS_TYPE_BOOLEAN}" "$@"; }
984
985# Other basic flags.
986DEFINE_float()   { _flags_define "${__FLAGS_TYPE_FLOAT}" "$@"; }
987DEFINE_integer() { _flags_define "${__FLAGS_TYPE_INTEGER}" "$@"; }
988DEFINE_string()  { _flags_define "${__FLAGS_TYPE_STRING}" "$@"; }
989
990# Parse the flags.
991#
992# Args:
993#   unnamed: list: command-line flags to parse
994# Returns:
995#   integer: success of operation, or error
996FLAGS() {
997  # Define a standard 'help' flag if one isn't already defined.
998  if [ -z "${__flags_help_type:-}" ]; then
999    DEFINE_boolean 'help' false 'show this help' 'h'
1000  fi
1001
1002  # Parse options.
1003  if [ $# -gt 0 ]; then
1004    if [ "${__FLAGS_GETOPT_VERS}" -ne "${__FLAGS_GETOPT_VERS_ENH}" ]; then
1005      _flags_getoptStandard "$@"
1006    else
1007      _flags_getoptEnhanced "$@"
1008    fi
1009    flags_return=$?
1010  else
1011    # Nothing passed; won't bother running getopt.
1012    __flags_opts='--'
1013    flags_return=${FLAGS_TRUE}
1014  fi
1015
1016  if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then
1017    _flags_parseGetopt "${__flags_opts}"
1018    flags_return=$?
1019  fi
1020
1021  if [ ${flags_return} -eq ${FLAGS_ERROR} ]; then
1022    _flags_fatal "${flags_error}"
1023  fi
1024  return ${flags_return}
1025}
1026
1027# This is a helper function for determining the 'getopt' version for platforms
1028# where the detection isn't working. It simply outputs debug information that
1029# can be included in a bug report.
1030#
1031# Args:
1032#   none
1033# Output:
1034#   debug info that can be included in a bug report
1035# Returns:
1036#   nothing
1037flags_getoptInfo() {
1038  # Platform info.
1039  _flags_debug "uname -a: `uname -a`"
1040  _flags_debug "PATH: ${PATH}"
1041
1042  # Shell info.
1043  if [ -n "${BASH_VERSION:-}" ]; then
1044    _flags_debug 'shell: bash'
1045    _flags_debug "BASH_VERSION: ${BASH_VERSION}"
1046  elif [ -n "${ZSH_VERSION:-}" ]; then
1047    _flags_debug 'shell: zsh'
1048    _flags_debug "ZSH_VERSION: ${ZSH_VERSION}"
1049  fi
1050
1051  # getopt info.
1052  ${FLAGS_GETOPT_CMD} >/dev/null
1053  _flags_getoptReturn=$?
1054  _flags_debug "getopt return: ${_flags_getoptReturn}"
1055  _flags_debug "getopt --version: `${FLAGS_GETOPT_CMD} --version 2>&1`"
1056
1057  unset _flags_getoptReturn
1058}
1059
1060# Returns whether the detected getopt version is the enhanced version.
1061#
1062# Args:
1063#   none
1064# Output:
1065#   none
1066# Returns:
1067#   bool: true if getopt is the enhanced version
1068flags_getoptIsEnh() {
1069  test "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_ENH}"
1070}
1071
1072# Returns whether the detected getopt version is the standard version.
1073#
1074# Args:
1075#   none
1076# Returns:
1077#   bool: true if getopt is the standard version
1078flags_getoptIsStd() {
1079  test "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_STD}"
1080}
1081
1082# This is effectively a 'usage()' function. It prints usage information and
1083# exits the program with ${FLAGS_FALSE} if it is ever found in the command line
1084# arguments. Note this function can be overridden so other apps can define
1085# their own --help flag, replacing this one, if they want.
1086#
1087# Args:
1088#   none
1089# Returns:
1090#   integer: success of operation (always returns true)
1091flags_help() {
1092  if [ -n "${FLAGS_HELP:-}" ]; then
1093    echo "${FLAGS_HELP}" >&2
1094  else
1095    echo "USAGE: ${FLAGS_PARENT:-$0} [flags] args" >&2
1096  fi
1097  if [ -n "${__flags_longNames}" ]; then
1098    echo 'flags:' >&2
1099    for flags_name_ in ${__flags_longNames}; do
1100      flags_flagStr_=''
1101      flags_boolStr_=''
1102      flags_usName_=`_flags_underscoreName "${flags_name_}"`
1103
1104      flags_default_=`_flags_getFlagInfo \
1105          "${flags_usName_}" "${__FLAGS_INFO_DEFAULT}"`
1106      flags_help_=`_flags_getFlagInfo \
1107          "${flags_usName_}" "${__FLAGS_INFO_HELP}"`
1108      flags_short_=`_flags_getFlagInfo \
1109          "${flags_usName_}" "${__FLAGS_INFO_SHORT}"`
1110      flags_type_=`_flags_getFlagInfo \
1111          "${flags_usName_}" "${__FLAGS_INFO_TYPE}"`
1112
1113      [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \
1114          flags_flagStr_="-${flags_short_}"
1115
1116      if [ "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_ENH}" ]; then
1117        [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \
1118            flags_flagStr_="${flags_flagStr_},"
1119        # Add [no] to long boolean flag names, except the 'help' flag.
1120        [ "${flags_type_}" -eq "${__FLAGS_TYPE_BOOLEAN}" \
1121          -a "${flags_usName_}" != 'help' ] && \
1122            flags_boolStr_='[no]'
1123        flags_flagStr_="${flags_flagStr_}--${flags_boolStr_}${flags_name_}:"
1124      fi
1125
1126      case ${flags_type_} in
1127        "${__FLAGS_TYPE_BOOLEAN}")
1128          if [ "${flags_default_}" -eq ${FLAGS_TRUE} ]; then
1129            flags_defaultStr_='true'
1130          else
1131            flags_defaultStr_='false'
1132          fi
1133          ;;
1134        "${__FLAGS_TYPE_FLOAT}"|"${__FLAGS_TYPE_INTEGER}")
1135          flags_defaultStr_=${flags_default_} ;;
1136        "${__FLAGS_TYPE_STRING}") flags_defaultStr_="'${flags_default_}'" ;;
1137      esac
1138      flags_defaultStr_="(default: ${flags_defaultStr_})"
1139
1140      flags_helpStr_="  ${flags_flagStr_}  ${flags_help_:+${flags_help_} }${flags_defaultStr_}"
1141      _flags_strlen "${flags_helpStr_}" >/dev/null
1142      flags_helpStrLen_=${flags_output}
1143      flags_columns_=`_flags_columns`
1144
1145      if [ "${flags_helpStrLen_}" -lt "${flags_columns_}" ]; then
1146        echo "${flags_helpStr_}" >&2
1147      else
1148        echo "  ${flags_flagStr_}  ${flags_help_}" >&2
1149        # Note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06
1150        # because it doesn't like empty strings when used in this manner.
1151        flags_emptyStr_="`echo \"x${flags_flagStr_}x\" \
1152            |awk '{printf "%"length($0)-2"s", ""}'`"
1153        flags_helpStr_="  ${flags_emptyStr_}  ${flags_defaultStr_}"
1154        _flags_strlen "${flags_helpStr_}" >/dev/null
1155        flags_helpStrLen_=${flags_output}
1156
1157        if [ "${__FLAGS_GETOPT_VERS}" -eq "${__FLAGS_GETOPT_VERS_STD}" \
1158            -o "${flags_helpStrLen_}" -lt "${flags_columns_}" ]; then
1159          # Indented to match help string.
1160          echo "${flags_helpStr_}" >&2
1161        else
1162          # Indented four from left to allow for longer defaults as long flag
1163          # names might be used too, making things too long.
1164          echo "    ${flags_defaultStr_}" >&2
1165        fi
1166      fi
1167    done
1168  fi
1169
1170  unset flags_boolStr_ flags_default_ flags_defaultStr_ flags_emptyStr_ \
1171      flags_flagStr_ flags_help_ flags_helpStr flags_helpStrLen flags_name_ \
1172      flags_columns_ flags_short_ flags_type_ flags_usName_
1173  return ${FLAGS_TRUE}
1174}
1175
1176# Reset shflags back to an uninitialized state.
1177#
1178# Args:
1179#   none
1180# Returns:
1181#   nothing
1182flags_reset() {
1183  for flags_name_ in ${__flags_longNames}; do
1184    flags_usName_=`_flags_underscoreName "${flags_name_}"`
1185    flags_strToEval_="unset FLAGS_${flags_usName_}"
1186    for flags_type_ in \
1187        ${__FLAGS_INFO_DEFAULT} \
1188        ${__FLAGS_INFO_HELP} \
1189        ${__FLAGS_INFO_SHORT} \
1190        ${__FLAGS_INFO_TYPE}
1191    do
1192      flags_strToEval_=\
1193"${flags_strToEval_} __flags_${flags_usName_}_${flags_type_}"
1194    done
1195    eval "${flags_strToEval_}"
1196  done
1197
1198  # Reset internal variables.
1199  __flags_boolNames=' '
1200  __flags_longNames=' '
1201  __flags_shortNames=' '
1202  __flags_definedNames=' '
1203
1204  # Reset logging level back to default.
1205  flags_setLoggingLevel "${__FLAGS_LEVEL_DEFAULT}"
1206
1207  unset flags_name_ flags_type_ flags_strToEval_ flags_usName_
1208}
1209
1210#-----------------------------------------------------------------------------
1211# Initialization
1212#
1213
1214# Set the default logging level.
1215flags_setLoggingLevel ${__FLAGS_LEVEL_DEFAULT}
1216