bashcommand-linecommand-line-argumentsgetopt

bash: -- being caught by first option, rather than by -- case


I have a script that parses command line arguments after using getopt. The issue is that the -- before my positional arguments is caught by my --usage) case, thus showing usage text erroneously. Is there a solution to this other than placing the --) case at the top of the case block?

Say, I have a script like the following (my example is more complex, but the essence is captured here).

SHORT_OPTIONS="?::,h::,a::,b::"
LONG_OPTIONS="help::,usage::apple::,banana::"
ARGV="$(getopt -o $SHORT_OPTIONS -l $LONG_OPTIONS -- "$@")"

eval set -- "$ARGV"

APPLE=false
BANANA=false
CARROT=false
DURIAN=false

while [[ $# > 0 ]]; do
  case "$1" in
    -?)
      ;&
    -h)
      ;&
    --help)
      ;&
    --usage)
      echo "Usage:"
      echo "  script.sh <args> ..."
      echo "Arguments:"
      echo "  [-? | -h | --help | --usage]:  Show this usage text"
      echo "  [-a | --apple]:  Process an apple"
      echo "  [-b | --banana]:  Process a banana"
      echo "  carrot:  Process the carrot"
      echo "  durian:  Process the durian"
      break;;
    -a)
      ;&
    --apple)
      APPLE=true
      shift 2;;
    -b)
      ;&
    --banana)
      BANANA=true
      shift 2;;
    *)
      case "$1" in
        carrot)
          CARROT=true
          ;;
        durian)
          DURIAN=true
          ;;
        *)
          ;;
      esac
      shift 1;;
  esac
done

if [[ "$APPLE" == true ]]; then
  echo "I have an apple"
fi

if [[ "$BANANA" == true ]]; then
  echo "I have an banana"
fi

if [[ "$CARROT" == true ]]; then
  echo "Yay!  I got my carrot."
else
  echo "ERROR:  I need a carrot"
  return 1 2>/dev/null
  exit 1
fi

if [[ "$DURIAN" == true ]]; then
  echo "Yay!  I got my durian."
else
  echo "ERROR:  I need a durian"
  return 1 2>/dev/null
  exit 1
fi

I call the script bash script.sh -a --apple -b --banana carrot durian, it will print the usage text, instead of continuing with the script.


Solution

  • the -- before my positional arguments is caught by my --usage) case

    Technically the -- is being caught by your -? case.
    The ? is acting as a single character wildcard.
    Put it in quotes. :)

    $ tst() { case $1 in -?) echo HIT;; esac; }
    $ tst --
    HIT
    $ tst -x
    HIT
    
    $ tst() { case $1 in '-?') echo HIT;; esac; }
    $ tst -x
    $ tst --
    

    Addendum -

    allowing -? sets up a surprise when someone accidentally creates a file named -x.

    $ tst -?       # works great
    HIT
    $ touch ./-x   # but if we make a file that matches
    $ tst -?       # now it gets "-x" instead of "-?"
    $ rm ./-x      # remove it
    $ tst -?       # and it works again
    HIT
    

    They think they passed in -? - it's what they typed - but the interpreter sees the local file named -x which matches the given glob -? so it passes it instead.

    That's why most *NIX systems just use -h instead.

    Also, in a couple places you have

      return 1 2>/dev/null
      exit 1
    

    return usually has no output. The one case I know of where it will have an error message is because you are calling it from an invalid context -

    bash: return: can only `return' from a function or sourced script
    

    You probably just want one or the other.

    I can imagine that maybe you are expecting this code to possibly be sourced, and you want it to let the calling context decide what to do with the return code, but you think it could also be run directly in which case you want the error message thrown away, and want it to just exit. That's a pretty contrived case.

    In general, one or the other. It does kind of work, but if you actually have a situation that complicated it's probably better to check the call stack.

    $ tst() ( set -x; cat script; . script; echo $?; ./script; echo $?; )
    $ tst
    + cat script
    #! /bin/bash -x
    printf '%s\n' 'call stack:' "${FUNCNAME[@]}"
    return 1
    exit 1
    
    + . script
    ++ printf '%s\n' 'call stack:' source tst
    call stack:
    source
    tst
    ++ return 1
    + echo 1
    1
    + ./script
    + printf '%s\n' 'call stack:'
    call stack:
    + return 1
    ./script: line 3: return: can only `return' from a function or sourced script
    + exit 1
    + echo 1
    1
    

    Same function after editing the script -

    $ tst
    + cat script
    #! /bin/bash -x
    if ((${#FUNCNAME[@]}));
    then printf '%s\n' 'call stack:' "${FUNCNAME[@]}"
         return 1
    else echo "Direct call"
         exit 1
    fi
    + . script
    ++ (( 2 ))
    ++ printf '%s\n' 'call stack:' source tst
    call stack:
    source
    tst
    ++ return 1
    + echo 1
    1
    + ./script
    + (( 0 ))
    + echo 'Direct call'
    Direct call
    + exit 1
    + echo 1
    1