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