Optional option argument with getopts

前端 未结 11 1250
while getopts \"hd:R:\" arg; do
  case $arg in
    h)
      echo \"usgae\" 
      ;;
    d)
      dir=$OPTARG
      ;;
    R)
      if [[ $OPTARG =~ ^[0-9]+$ ]];then         


        
相关标签:
11条回答
  • 2020-11-28 08:56

    I just ran into this myself and felt that none of the existing solutions were really clean. After working on it a bit and trying various things, I found that leveraging getopts SILENT mode with :) ... appears to have done the trick along with keeping OPTIND in sync.


    usage: test.sh [-abst] [-r [DEPTH]] filename
    *NOTE: -r (recursive) with no depth given means full recursion
    
    #!/usr/bin/env bash
    
    depth='-d 1'
    
    while getopts ':abr:st' opt; do
        case "${opt}" in
            a) echo a;;
            b) echo b;;
            r) if [[ "${OPTARG}" =~ ^[0-9]+$ ]]; then
                   depth="-d ${OPTARG}"
               else
                   depth=
                   (( OPTIND-- ))
               fi
               ;;
            s) echo s;;
            t) echo t;;
            :) [[ "${OPTARG}" = 'r' ]] && depth=;;
            *) echo >&2 "Invalid option: ${opt}"; exit 1;;
        esac
    done
    shift $(( OPTIND - 1 ))
    
    filename="$1"
    ...
    
    0 讨论(0)
  • 2020-11-28 08:57

    Inspired in @calandoa's answer (the only one that actually works!), I've made a simple function that can make it easy to be used multiple times.

    getopts_get_optional_argument() {
      eval next_token=\${$OPTIND}
      if [[ -n $next_token && $next_token != -* ]]; then
        OPTIND=$((OPTIND + 1))
        OPTARG=$next_token
      else
        OPTARG=""
      fi
    }
    

    An example usage:

    while getopts "hdR" option; do
      case $option in
      d)
        getopts_get_optional_argument $@
        dir=${OPTARG}
        ;;
      R)
        getopts_get_optional_argument $@
        level=${OPTARG:-1}
        ;;
      h)
        show_usage && exit 0
        ;;
      \?)
        show_usage && exit 1
        ;;
      esac
    done
    

    This gives us a practical way to get "that missing feature" in getopts :)

    NOTE that nevertheless command-line options with optional args seems to be discouraged explicitly

    Guideline 7: Option-arguments should not be optional.

    but I have no intuitive way to implement my case without this: I have 2 modes that are activated by either using one flag or other, and those have both an argument with a clear default. Introducing a third flag just to disambiguate makes it look a bad CLI style.

    I've tested this with many combinations, including all in @aaron-sua's answer and works well.

    0 讨论(0)
  • 2020-11-28 08:59

    Wrong. Actually getopts does support optional arguments! From the bash man page:

    If  a  required  argument is not found, and getopts is not silent, 
    a question mark (?) is placed in name, OPTARG is unset, and a diagnostic
    message is printed.  If getopts is silent, then a colon (:) is placed in name 
    and OPTARG is set to the option character found.
    

    When the man page says "silent" it means silent error reporting. To enable it, the first character of optstring needs to be a colon:

    while getopts ":hd:R:" arg; do
        # ...rest of iverson's loop should work as posted 
    done
    

    Since Bash's getopt does not recognize -- to end the options list, it may not work when -R is the last option, followed by some path argument.

    P.S.: Traditionally, getopt.c uses two colons (::) to specify an optional argument. However, the version used by Bash doesn't.

    0 讨论(0)
  • 2020-11-28 09:08

    getopts doesn't really support this; but it's not hard to write your own replacement.

    while true; do
        case $1 in
          -R) level=1
                shift
                case $1 in
                  *[!0-9]* | "") ;;
                  *) level=$1; shift ;;
                esac ;;
            # ... Other options ...
            -*) echo "$0: Unrecognized option $1" >&2
                exit 2;;
            *) break ;;
        esac
    done
    
    0 讨论(0)
  • 2020-11-28 09:08

    The following code solves this problem by checking for a leading dash and if found decrements OPTIND to point back to the skipped option for processing. This generally works fine except that you do not know the order the user will place options on the command line - if your optional argument option is last and does not provide an argument getopts will want to error out.

    To fix the problem of the final argument missing, the "$@" array simply has an empty string "$@ " appended so that getopts will be satisfied that it has gobbled up yet another option argument. To fix this new empty argument a variable is set that holds the total count of all options to be processed - when the last option is being processed a helper function called trim is called and removes the empty string prior to the value being utilized.

    This is not working code, it has only place holders but you can easily modify it and with a little bit of care it can be useful to build a robust system.

    #!/usr/bin/env bash 
    declare  -r CHECK_FLOAT="%f"  
    declare  -r CHECK_INTEGER="%i"  
    
     ## <arg 1> Number - Number to check
     ## <arg 2> String - Number type to check
     ## <arg 3> String - Error message
    function check_number() {
      local NUMBER="${1}"
      local NUMBER_TYPE="${2}"
      local ERROR_MESG="${3}"
      local FILTERED_NUMBER=$(sed 's/[^.e0-9+\^]//g' <<< "${NUMBER}")
      local -i PASS=1
      local -i FAIL=0
        if [[ -z "${NUMBER}" ]]; then 
            echo "Empty number argument passed to check_number()." 1>&2
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"          
      elif [[ -z "${NUMBER_TYPE}" ]]; then 
            echo "Empty number type argument passed to check_number()." 1>&2
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"          
      elif [[ ! "${#NUMBER}" -eq "${#FILTERED_NUMBER}" ]]; then 
            echo "Non numeric characters found in number argument passed to check_number()." 1>&2
            echo "${ERROR_MESG}" 1>&2
            echo "${FAIL}"          
      else  
       case "${NUMBER_TYPE}" in
         "${CHECK_FLOAT}")
             if ((! $(printf "${CHECK_FLOAT}" "${NUMBER}" &>/dev/random;echo $?))); then
                echo "${PASS}"
             else
                echo "${ERROR_MESG}" 1>&2
                echo "${FAIL}"
             fi
             ;;
         "${CHECK_INTEGER}")
             if ((! $(printf "${CHECK_INTEGER}" "${NUMBER}" &>/dev/random;echo $?))); then
                echo "${PASS}"
             else
                echo "${ERROR_MESG}" 1>&2
                echo "${FAIL}"
             fi
             ;;
                          *)
             echo "Invalid number type format: ${NUMBER_TYPE} to check_number()." 1>&2
             echo "${FAIL}"
             ;;
        esac
     fi 
    }
    
     ## Note: Number can be any printf acceptable format and includes leading quotes and quotations, 
     ##       and anything else that corresponds to the POSIX specification. 
     ##       E.g. "'1e+03" is valid POSIX float format, see http://mywiki.wooledge.org/BashFAQ/054
     ## <arg 1> Number - Number to print
     ## <arg 2> String - Number type to print
    function print_number() { 
      local NUMBER="${1}" 
      local NUMBER_TYPE="${2}" 
      case "${NUMBER_TYPE}" in 
          "${CHECK_FLOAT}") 
               printf "${CHECK_FLOAT}" "${NUMBER}" || echo "Error printing Float in print_number()." 1>&2
            ;;                 
        "${CHECK_INTEGER}") 
               printf "${CHECK_INTEGER}" "${NUMBER}" || echo "Error printing Integer in print_number()." 1>&2
            ;;                 
                         *) 
            echo "Invalid number type format: ${NUMBER_TYPE} to print_number()." 1>&2
            ;;                 
       esac
    } 
    
     ## <arg 1> String - String to trim single ending whitespace from
    function trim_string() { 
     local STRING="${1}" 
     echo -En $(sed 's/ $//' <<< "${STRING}") || echo "Error in trim_string() expected a sensible string, found: ${STRING}" 1>&2
    } 
    
     ## This a hack for getopts because getopts does not support optional
     ## arguments very intuitively. E.g. Regardless of whether the values
     ## begin with a dash, getopts presumes that anything following an
     ## option that takes an option argument is the option argument. To fix  
     ## this the index variable OPTIND is decremented so it points back to  
     ## the otherwise skipped value in the array option argument. This works
     ## except for when the missing argument is on the end of the list,
     ## in this case getopts will not have anything to gobble as an
     ## argument to the option and will want to error out. To avoid this an
     ## empty string is appended to the argument array, yet in so doing
     ## care must be taken to manage this added empty string appropriately.
     ## As a result any option that doesn't exit at the time its processed
     ## needs to be made to accept an argument, otherwise you will never
     ## know if the option will be the last option sent thus having an empty
     ## string attached and causing it to land in the default handler.
    function process_options() {
    local OPTIND OPTERR=0 OPTARG OPTION h d r s M R S D
    local ERROR_MSG=""  
    local OPTION_VAL=""
    local EXIT_VALUE=0
    local -i NUM_OPTIONS
    let NUM_OPTIONS=${#@}+1
    while getopts “:h?d:DM:R:S:s:r:” OPTION "$@";
     do
         case "$OPTION" in
             h)
                 help | more
                 exit 0
                 ;;
             r)
                 OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 ERROR_MSG="Invalid input: Integer or floating point number required."
                 if [[ -z "${OPTION_VAL}" ]]; then
                   ## can set global flags here 
                   :;
                 elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
                   let OPTIND=${OPTIND}-1
                   ## can set global flags here 
                 elif [ "${OPTION_VAL}" = "0" ]; then
                   ## can set global flags here 
                   :;               
                 elif (($(check_number "${OPTION_VAL}" "${CHECK_FLOAT}" "${ERROR_MSG}"))); then
                   :; ## do something really useful here..               
                 else
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 fi
                 ;;
             d)
                 OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 [[  ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]] && let OPTIND=${OPTIND}-1            
                 DEBUGMODE=1
                 set -xuo pipefail
                 ;;
             s)
                 OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 if [[ ! -z "${OPTION_VAL}" && "${OPTION_VAL}" =~ ^-. ]]; then ## if you want a variable value that begins with a dash, escape it
                   let OPTIND=${OPTIND}-1
                 else
                  GLOBAL_SCRIPT_VAR="${OPTION_VAL}"
                    :; ## do more important things
                 fi
                 ;;
             M)  
                 OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 ERROR_MSG=$(echo "Error - Invalid input: ${OPTION_VAL}, Integer required"\
                                  "retry with an appropriate option argument.")
                 if [[ -z "${OPTION_VAL}" ]]; then
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
                   let OPTIND=${OPTIND}-1
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 elif (($(check_number "${OPTION_VAL}" "${CHECK_INTEGER}" "${ERROR_MSG}"))); then
                 :; ## do something useful here
                 else
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 fi
                 ;;                      
             R)  
                 OPTION_VAL=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 ERROR_MSG=$(echo "Error - Invalid option argument: ${OPTION_VAL},"\
                                  "the value supplied to -R is expected to be a "\
                                  "qualified path to a random character device.")            
                 if [[ -z "${OPTION_VAL}" ]]; then
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 elif [[ "${OPTION_VAL}" =~ ^-. ]]; then
                   let OPTIND=${OPTIND}-1
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 elif [[ -c "${OPTION_VAL}" ]]; then
                   :; ## Instead of erroring do something useful here..  
                 else
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 fi
                 ;;                      
             S)  
                 STATEMENT=$(((${NUM_OPTIONS}==${OPTIND})) && trim_string "${OPTARG##*=}" || echo -En "${OPTARG##*=}")
                 ERROR_MSG="Error - Default text string to set cannot be empty."
                 if [[ -z "${STATEMENT}" ]]; then
                   ## Instead of erroring you could set a flag or do something else with your code here..  
                 elif [[ "${STATEMENT}" =~ ^-. ]]; then ## if you want a statement that begins with a dash, escape it
                   let OPTIND=${OPTIND}-1
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                   echo "${ERROR_MSG}" 1>&2 && exit -1
                 else
                    :; ## do something even more useful here you can modify the above as well 
                 fi
                 ;;                      
             D)  
                 ## Do something useful as long as it is an exit, it is okay to not worry about the option arguments 
                 exit 0
                 ;;          
             *)
                 EXIT_VALUE=-1
                 ;&                  
             ?)
                 usage
                 exit ${EXIT_VALUE}
                 ;;
         esac
    done
    }
    
    process_options "$@ " ## extra space, so getopts can find arguments  
    
    0 讨论(0)
  • 2020-11-28 09:09

    I agree with tripleee, getopts does not support optional argument handling.

    The compromised solution I have settled on is to use the upper case/lower case combination of the same option flag to differentiate between the option that takes an argument and the other that does not.

    Example:

    COMMAND_LINE_OPTIONS_HELP='
    Command line options:
        -I          Process all the files in the default dir: '`pwd`'/input/
        -i  DIR     Process all the files in the user specified input dir
        -h          Print this help menu
    
    Examples:
        Process all files in the default input dir
            '`basename $0`' -I
    
        Process all files in the user specified input dir
            '`basename $0`' -i ~/my/input/dir
    
    '
    
    VALID_COMMAND_LINE_OPTIONS="i:Ih"
    INPUT_DIR=
    
    while getopts $VALID_COMMAND_LINE_OPTIONS options; do
        #echo "option is " $options
        case $options in
            h)
                echo "$COMMAND_LINE_OPTIONS_HELP"
                exit $E_OPTERROR;
            ;;
            I)
                INPUT_DIR=`pwd`/input
                echo ""
                echo "***************************"
                echo "Use DEFAULT input dir : $INPUT_DIR"
                echo "***************************"
            ;;
            i)
                INPUT_DIR=$OPTARG
                echo ""
                echo "***************************"
                echo "Use USER SPECIFIED input dir : $INPUT_DIR"
                echo "***************************"
            ;;
            \?)
                echo "Usage: `basename $0` -h for help";
                echo "$COMMAND_LINE_OPTIONS_HELP"
                exit $E_OPTERROR;
            ;;
        esac
    done
    
    0 讨论(0)
提交回复
热议问题