Optional option argument with getopts

75,860

Solution 1

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

Solution 2

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.

Solution 3

This workaround defines 'R' with no argument (no ':'), tests for any argument after the '-R' (manage last option on the command line) and tests if an existing argument starts with a dash.

# No : after R
while getopts "hd:R" arg; do
  case $arg in
  (...)
  R)
    # Check next positional parameter
    eval nextopt=\${$OPTIND}
    # existing or starting with dash?
    if [[ -n $nextopt && $nextopt != -* ]] ; then
      OPTIND=$((OPTIND + 1))
      level=$nextopt
    else
      level=1
    fi
    ;;
  (...)
  esac
done

Solution 4

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

Solution 5

This is actually pretty easy. Just drop the trailing colon after the R and use OPTIND

while getopts "hRd:" opt; do
   case $opt in
      h) echo -e $USAGE && exit
      ;;
      d) DIR="$OPTARG"
      ;;
      R)       
        if [[ ${@:$OPTIND} =~ ^[0-9]+$ ]];then
          LEVEL=${@:$OPTIND}
          OPTIND=$((OPTIND+1))
        else
          LEVEL=1
        fi
      ;;
      \?) echo "Invalid option -$OPTARG" >&2
      ;;
   esac
done
echo $LEVEL $DIR

count.sh -d test

test

count.sh -d test -R

1 test

count.sh -R -d test

1 test

count.sh -d test -R 2

2 test

count.sh -R 2 -d test

2 test

Share:
75,860
iverson
Author by

iverson

Updated on July 09, 2022

Comments

  • iverson
    iverson almost 2 years
    while getopts "hd:R:" arg; do
      case $arg in
        h)
          echo "usage" 
          ;;
        d)
          dir=$OPTARG
          ;;
        R)
          if [[ $OPTARG =~ ^[0-9]+$ ]];then
            level=$OPTARG
          else
            level=1
          fi
          ;;
        \?)
          echo "WRONG" >&2
          ;;
      esac
    done
    
    • level refers to the parameter of -R, dir refers to parameters of -d

    • when I input ./count.sh -R 1 -d test/ it works correctly

    • when I input ./count.sh -d test/ -R 1 it works correctly

    • but I want to have it work when I input ./count.sh -d test/ -R or ./count.sh -R -d test/

    This means that I want -R to have a default value and for the sequence of commands to be more flexible.

  • gerardw
    gerardw almost 11 years
    Not in a meaningful way. If -R is the last argument it's not processed
  • Admin
    Admin over 9 years
    From playing around with getopts it it looks like what does the job here is let OPTIND=$OPTIND-1 not sure what is going on with the level tracking. Maybe its for another use from your code?
  • calandoa
    calandoa about 8 years
    No. YOU are wrong. ./count.sh -R -d test/ is not working because -d is taken as the argument of -R (which is not at all optional).
  • johnnyB
    johnnyB almost 8 years
    The example given from wiki.bash-hackers.org does not work if the last option is the "optional" OPTARG. $ prog.bash -a -b -c will not even know about -c option when $ prog.bash -a -c -b will. How can this work if its the last arg?
  • Rohit
    Rohit almost 8 years
    Please see answer below from Andreas Spindler , it is supported
  • user2141130
    user2141130 over 7 years
    This answer is misleading at best. As pointed out by @calandoa, any option with an "optional" argument only "works" if it is the last option given. Otherwise, it will consume the next option as its argument. e.g. in this usage ./count.sh -R -d test/ '-R' takes '-d' as its argument and '-d' is not recognized as an option. I'm only restating what has already been said (by @calandoa) because this incorrect answer has 20 net upvotes.
  • user2141130
    user2141130 over 7 years
    Maybe a previous version of bash (or sh) "does the right thing" but bash 4.3.30(1) Debian Jessie fails as described in the previous comment.
  • Otiel
    Otiel about 6 years
    @Rohit Please note that Andreas Spindler answer is wrong in most ways, as described in the comments below his answer.
  • Chuck Wilbur
    Chuck Wilbur almost 6 years
    This answer is a strange case in terms of earning upvotes in that it doesn't answer the rather specific question asked, but it does answer a (I'm guessing) much more common question: What is the syntax to get getopts to ignore arguments it doesn't recognize without reporting an error? That's the question I came seeking an answer to and this was the answer I was after.
  • nandilugio
    nandilugio almost 5 years
    Last test (count.sh -R 2 -d test) gives me 1 as a result, not 2 test (bash 5.0.3). All others work. This is because ${@:$OPTIND} evaluates to all the rest of the arguments, not just the next.
  • nandilugio
    nandilugio almost 5 years
    This is in fact the only of the answers here that works! Please upvote it.
  • nandilugio
    nandilugio almost 5 years
    Fixing it using @calandoa's answer makes it pass all the tests.
  • nandilugio
    nandilugio almost 5 years
    Inspired in this answer (the only one that actually works!), I've made a simple function that can make it easy to be used multiple times. See my answer here
  • Christoph
    Christoph almost 3 years
    eval nextopt=\${$OPTIND} is a creative solution, but Bash already has a special syntax for indirect expansion: nextopt=${!OPTIND}.
  • tripleee
    tripleee over 2 years
    (There is no "above" or "below"; each visitor has their personal sorting preferences to decide in which order answers are displayed.)