Using getopts to process long and short command line options

后端 未结 30 1637
佛祖请我去吃肉
佛祖请我去吃肉 2020-11-21 22:52

I wish to have long and short forms of command line options invoked using my shell script.

I know that getopts can be used, but like in Perl, I have not

30条回答
  •  臣服心动
    2020-11-21 23:11

    I only write shell scripts now and then and fall out of practice, so any feedback is appreciated.

    Using the strategy proposed by @Arvid Requate, we noticed some user errors. A user who forgets to include a value will accidentally have the next option's name treated as a value:

    ./getopts_test.sh --loglevel= --toc=TRUE
    

    will cause the value of "loglevel" to be seen as "--toc=TRUE". This can be avoided.

    I adapted some ideas about checking user error for CLI from http://mwiki.wooledge.org/BashFAQ/035 discussion of manual parsing. I inserted error checking into handling both "-" and "--" arguments.

    Then I started fiddling around with the syntax, so any errors in here are strictly my fault, not the original authors.

    My approach helps users who prefer to enter long with or without the equal sign. That is, it should have same response to "--loglevel 9" as "--loglevel=9". In the --/space method, it is not possible to know for sure if the user forgets an argument, so some guessing is needed.

    1. if the user has the long/equal sign format (--opt=), then a space after = triggers an error because an argument was not supplied.
    2. if user has long/space arguments (--opt ), this script causes a fail if no argument follows (end of command) or if argument begins with dash)

    In case you are starting out on this, there is an interesting difference between "--opt=value" and "--opt value" formats. With the equal sign, the command line argument is seen as "opt=value" and the work to handle that is string parsing, to separate at the "=". In contrast, with "--opt value", the name of the argument is "opt" and we have the challenge of getting the next value supplied in the command line. That's where @Arvid Requate used ${!OPTIND}, the indirect reference. I still don't understand that, well, at all, and comments in BashFAQ seem to warn against that style (http://mywiki.wooledge.org/BashFAQ/006). BTW, I don't think previous poster's comments about importance of OPTIND=$(( $OPTIND + 1 )) are correct. I mean to say, I see no harm from omitting it.

    In newest version of this script, flag -v means VERBOSE printout.

    Save it in a file called "cli-5.sh", make executable, and any of these will work, or fail in the desired way

    ./cli-5.sh  -v --loglevel=44 --toc  TRUE
    ./cli-5.sh  -v --loglevel=44 --toc=TRUE
    ./cli-5.sh --loglevel 7
    ./cli-5.sh --loglevel=8
    ./cli-5.sh -l9
    
    ./cli-5.sh  --toc FALSE --loglevel=77
    ./cli-5.sh  --toc=FALSE --loglevel=77
    
    ./cli-5.sh   -l99 -t yyy
    ./cli-5.sh   -l 99 -t yyy
    

    Here is example output of the error-checking on user intpu

    $ ./cli-5.sh  --toc --loglevel=77
    ERROR: toc value must not have dash at beginning
    $ ./cli-5.sh  --toc= --loglevel=77
    ERROR: value for toc undefined
    

    You should consider turning on -v, because it prints out internals of OPTIND and OPTARG

    #/usr/bin/env bash
    
    ## Paul Johnson
    ## 20171016
    ##
    
    ## Combines ideas from
    ## https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
    ## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035
    
    # What I don't understand yet: 
    # In @Arvid REquate's answer, we have 
    # val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
    # this works, but I don't understand it!
    
    
    die() {
        printf '%s\n' "$1" >&2
        exit 1
    }
    
    printparse(){
        if [ ${VERBOSE} -gt 0 ]; then
            printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
        fi
    }
    
    showme(){
        if [ ${VERBOSE} -gt 0 ]; then
            printf 'VERBOSE: %s\n' "$1" >&2;
        fi
    }
    
    
    VERBOSE=0
    loglevel=0
    toc="TRUE"
    
    optspec=":vhl:t:-:"
    while getopts "$optspec" OPTCHAR; do
    
        showme "OPTARG:  ${OPTARG[*]}"
        showme "OPTIND:  ${OPTIND[*]}"
        case "${OPTCHAR}" in
            -)
                case "${OPTARG}" in
                    loglevel) #argument has no equal sign
                        opt=${OPTARG}
                        val="${!OPTIND}"
                        ## check value. If negative, assume user forgot value
                        showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                        if [[ "$val" == -* ]]; then
                            die "ERROR: $opt value must not have dash at beginning"
                        fi
                        ## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
                        printparse "--${OPTARG}" "  " "${val}"
                        loglevel="${val}"
                        shift
                        ;;
                    loglevel=*) #argument has equal sign
                        opt=${OPTARG%=*}
                        val=${OPTARG#*=}
                        if [ "${OPTARG#*=}" ]; then
                            printparse "--${opt}" "=" "${val}"
                            loglevel="${val}"
                            ## shift CAUTION don't shift this, fails othewise
                        else
                            die "ERROR: $opt value must be supplied"
                        fi
                        ;;
                    toc) #argument has no equal sign
                        opt=${OPTARG}
                        val="${!OPTIND}"
                        ## check value. If negative, assume user forgot value
                        showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
                        if [[ "$val" == -* ]]; then
                            die "ERROR: $opt value must not have dash at beginning"
                        fi
                        ## OPTIND=$(( $OPTIND + 1 )) #??
                        printparse "--${opt}" " " "${val}"
                        toc="${val}"
                        shift
                        ;;
                    toc=*) #argument has equal sign
                        opt=${OPTARG%=*}
                        val=${OPTARG#*=}
                        if [ "${OPTARG#*=}" ]; then
                            toc=${val}
                            printparse "--$opt" " -> " "$toc"
                            ##shift ## NO! dont shift this
                        else
                            die "ERROR: value for $opt undefined"
                        fi
                        ;;
    
                    help)
                        echo "usage: $0 [-v] [--loglevel[=]] [--toc[=]]" >&2
                        exit 2
                        ;;
                    *)
                        if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
                            echo "Unknown option --${OPTARG}" >&2
                        fi
                        ;;
                esac;;
            h|-\?|--help)
                ## must rewrite this for all of the arguments
                echo "usage: $0 [-v] [--loglevel[=]] [--toc[=]]" >&2
                exit 2
                ;;
            l)
                loglevel=${OPTARG}
                printparse "-l" " "  "${loglevel}"
                ;;
            t)
                toc=${OPTARG}
                ;;
            v)
                VERBOSE=1
                ;;
    
            *)
                if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
                    echo "Non-option argument: '-${OPTARG}'" >&2
                fi
                ;;
        esac
    done
    
    
    
    echo "
    After Parsing values
    "
    echo "loglevel  $loglevel" 
    echo "toc  $toc"
    

提交回复
热议问题