Using getopts to process long and short command line options

后端 未结 30 1577
佛祖请我去吃肉
佛祖请我去吃肉 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:10

    Long options can be parsed by the standard getopts builtin as “arguments” to the - “option”

    This is portable and native POSIX shell – no external programs or bashisms are needed.

    This guide implements long options as arguments to the - option, so --alpha is seen by getopts as - with argument alpha and --bravo=foo is seen as - with argument bravo=foo. The true argument can be harvested with a simple replacement: ${OPTARG#*=}.

    In this example, -b and -c (and their long forms, --bravo and --charlie) have mandatory arguments. Arguments to long options come after equals signs, e.g. --bravo=foo (space delimiters for long options would be hard to implement, see below).

    Because this uses the getopts builtin, this solution supports usage like cmd --bravo=foo -ac FILE (which has combined options -a and -c and interleaves long options with standard options) while most other answers here either struggle or fail to do that.

    die() { echo "$*" >&2; exit 2; }  # complain to STDERR and exit with error
    needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
    
    while getopts ab:c:-: OPT; do
      # support long options: https://stackoverflow.com/a/28466267/519360
      if [ "$OPT" = "-" ]; then   # long option: reformulate OPT and OPTARG
        OPT="${OPTARG%%=*}"       # extract long option name
        OPTARG="${OPTARG#$OPT}"   # extract long option argument (may be empty)
        OPTARG="${OPTARG#=}"      # if long option argument, remove assigning `=`
      fi
      case "$OPT" in
        a | alpha )    alpha=true ;;
        b | bravo )    needs_arg; bravo="$OPTARG" ;;
        c | charlie )  needs_arg; charlie="$OPTARG" ;;
        ??* )          die "Illegal option --$OPT" ;;  # bad long option
        ? )            exit 2 ;;  # bad short option (error reported via getopts)
      esac
    done
    shift $((OPTIND-1)) # remove parsed options and args from $@ list
    

    When the option is a dash (-), it is a long option. getopts will have parsed the actual long option into $OPTARG, e.g. --bravo=foo originally sets OPT='-' and OPTARG='bravo=foo'. The if stanza sets $OPT to the contents of $OPTARG before the first equals sign (bravo in our example) and then removes that from the beginning of $OPTARG (yielding =foo in this step, or an empty string if there is no =). Finally, we strip the argument's leading =. At this point, $OPT is either a short option (one character) or a long option (2+ characters).

    The case then matches either short or long options. For short options, getopts automatically complains about options and missing arguments, so we have to replicate those manually using the needs_arg function, which fatally exits when $OPTARG is empty. The ??* condition will match any remaining long option (? matches a single character and * matches zero or more, so ??* matches 2+ characters), allowing us to issue the "Illegal option" error before exiting.

    Minor bug: if somebody gives an invalid single-character long option (and it's not also a short option), this will exit with an error but without a message. This is because this implementation assumes it was a short option. You could track that with an extra variable in the long option stanza preceding the case and then test for it in the final case condition, but I consider that too much of a corner case to bother.

    (A note about all-uppercase variable names: Generally, the advice is to reserve all-uppercase variables for system use. I'm keeping $OPT as all-uppercase to keep it in line with $OPTARG, but this does break that convention. I think it fits because this is something the system should have done, and it should be safe because there are no standards (afaik) that use such a variable.)


    To complain about unexpected arguments to long options, mimic what we did for mandatory arguments: use a helper function. Just flip the test around to complain about an argument when one isn't expected:

    no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }
    

    An older version of this answer had an attempt at accepting long options with space-delimited arguments, but it was not reliable; getopts could prematurely terminate on the assumption that the argument was beyond its scope and manually incrementing $OPTIND doesn't work in all shells.

    This would be accomplished using one of these techniques depending on your shell:

    • POSIX eval: eval "bravo=\"\$$OPTIND\""
    • Bash indirect expansion: bravo="${!OPTIND}"
    • Zsh parameter expansion P flag: bravo="${(P)OPTIND}"

    and then concluded with something like [ $# -gt $OPTIND ] && OPTIND=$((OPTIND+1))

提交回复
热议问题