Search code examples
bashshellcommand-line-argumentsgetoptgetopts

Bash getopts: recognizing negative options (-x- or +x)?


(Similar to this, but in bash.)

I have an existing bash script that uses the builtin getopts to recognize -s (flag only - no argument). I find I use that option every time, so I would like to make it default unless -s- or +s is specified on the command line. However, although ksh getopts can handle +s, I can't find that capability in the bash getopts manual.

My current workarounds are:

  1. to use s: so I can recognize -s- by $OPTARG="-"; or
  2. to replace -s with a different option, e.g., -d (for "don't").

However, #1 has the problem that it swallows the next argument if I accidentally specify -s, and #2 has the problem that it uses a different switch letter than the one I already have in my muscle memory. I am hoping there might be a straightforward way to parse -s- or +s in bash.

  • The util-linux getopt(1) also doesn't appear to have +s. It can handle optional arguments, so might be able to take -s-. However, I've not used getopt(1) before so would appreciate pointers on how not to shoot myself in the foot.
  • BashFAQ 035 says "parse it yourself." if you have a routine already written that does +s or -s-, I'd love to see it.

My current arg-parsing loop is very basic:

while getopts "nthps" opt ; do
    case "$opt" in
        <other cases cut>
        (s)     saw_s="yes"
                ;;
    esac
done
shift $((OPTIND-1))

Solution

  • The sequence of negative flags (+abcdef +g) differs from a normal sequence (-abcdef -g) with just the plus character. So you can simply revert the flag value for + prefix.

    The second form of negative sequence is as simple. Just strip the last character (-), and negate the normal flag value.

    Example

    The following script accepts all of the mentioned formats, e.g. -ns -n -s -n- -ns- +ns +n +s.

    arglist='ns'
    
    while (( $# )); do
      arg="$1"
    
      # Parse -abcdef- (negative form of -abcdef) options
      if [ "${arg:0:1}" = '-' -a "${arg#${arg%?}}" = '-' ]; then
        flag_val=no
        arg="${arg%?}" # -abcdef- becomes -abcdef
      elif [ "${arg:0:1}" = '+' ]; then
        flag_val=no
        arg="${arg/#+/-}"
      else
        flag_val=yes
      fi
    
      # OPTIND is the index of the next argument to be processed.
      # We are going to parse "$arg" from the beginning, so we need
      # to reset it to 1 before calling getopts.
      OPTIND=1
    
      while getopts "$arglist" opt "$arg"; do
        case "$opt" in
          s) saw_s="$flag_val" ;;
          n) saw_n="$flag_val" ;;
        esac
      done
    
      shift
    done
    
    # Set default values
    : ${saw_s:=yes}
    : ${saw_n:=no}
    
    printf "saw_s='%s'\nsaw_n='%s'\n" "$saw_s" "$saw_n"
    

    Testing

    $ ./pargs.sh
    saw_s='yes'
    saw_n='no'
    $ ./pargs.sh -s
    saw_s='yes'
    saw_n='no'
    $ ./pargs.sh +s
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh -s-
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh -s- +s -s
    saw_s='yes'
    saw_n='no'
    $ ./pargs.sh -s +s
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh +s -s
    saw_s='yes'
    saw_n='no'
    $ ./pargs.sh -s -s-
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh -sn
    saw_s='yes'
    saw_n='yes'
    $ ./pargs.sh -sn -s-
    saw_s='no'
    saw_n='yes'
    $ ./pargs.sh -sn +s
    saw_s='no'
    saw_n='yes'
    $ ./pargs.sh +sn
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh -sn-
    saw_s='no'
    saw_n='no'
    $ ./pargs.sh -sn- -n
    saw_s='no'
    saw_n='yes'