Is there a way to continue in a Flag when there is no $OPTARG set in Bash, GETOPTS?

913 Views Asked by At

I would like to build a script with getopts, that continues in the flag, when an $OPTARG isn't set. My script looks like this:

OPTIONS=':dBhmtb:P:'
while getopts $OPTIONS OPTION
do
        case "$OPTION" in
                m ) echo "m"          
                t ) echo "t"     
                d ) echo "d";;     
                h ) echo "h";; 
                B ) echo "b";;
                r ) echo "r";; 
                b ) echo "b"                                 
                P ) echo hi;; 
                    #continue here                    
                \? ) echo "?";;
                :) echo "test -$OPTARG requieres an argument" >&2
                         
        esac
done

My aim is to continue at my comment, when there is no $OPTARG set for -P. All I get after running ./test -P is : test -P requieres an argument and then it continues after the loop but I want to continue in the -P flag. All clear? Any Ideas?

3

There are 3 best solutions below

1
On BEST ANSWER

First, fix the missing ;; in some of the case branches.

I don't think you can: you told getopts that -P requires an argument: two error cases

  1. -P without an argument is the last option. In this case getops sees that nothing follows -P and sets the OPTION variable to :, which you handle in the case statement.

  2. -P is followed by another option: getopts will simply take the next word, even if the next word is another option, as OPTARG.

    Change the case branch to

    P ) echo "P: '$OPTARG'";;
    

    Then:

    • invoking the script like bash script.sh -P -m -t, the output is
      P: '-m'
      t
      
    • invoking the script like bash script.sh -Pmt, the output is
      P: 'mt'
      

    This is clearly difficult to work around. How do you know if the user intended the option argument to be literally "mt" and not the options -m and -t?

You might be able to work around this using getopt (see the canonical example) using an optional argument for a long option (those require an equal sign like --long=value) so it's maybe easier to check if the option argument is missing or not.


Translating getopts parsing to getopt -- it's more verbose, but you have finer-grained control

die() { echo "$*" >&2; exit 1; }

tmpArgs=$(getopt -o 'dBhmt' \
                 --long 'b::,P::' \
                 -n "$(basename "$0")" \
                 -- "$@"
        )
(( $? == 0 )) || die 'Problem parsing options'
eval set -- "$tmpArgs"

while true; do
    case "$1" in
       -d)  echo d; shift ;;
       -B)  echo B; shift ;;
       -h)  echo h; shift ;;
       -m)  echo m; shift ;;
       -t)  echo t; shift ;;
      --P)  case "$2" in
               '')  echo "P with no argument" ;;
                *)  echo "P: $2" ;;
            esac
            shift 2
            ;;
      --b)  case "$2" in
               '')  echo "b with no argument" ;;
                *)  echo "b: $2" ;;
            esac
            shift 2
            ;;
       --)  shift; break ;;
        *)  printf "> %q\n" "$@"
            die 'getopt internal error: $*' ;;
    esac
done

echo "Remaining arguments:"
for ((i=1; i<=$#; i++)); do
    echo "$i: ${!i}"
done

Successfully invoking the program with --P:

$ ./myscript.sh --P -mt foo bar
P with no argument
m
t
Remaining arguments:
1: foo
2: bar
$ ./myscript.sh --P=arg -mt foo bar
P: arg
m
t
Remaining arguments:
1: foo
2: bar

This does impose higher overhead on your users, because -P (with one dash) is invalid, and the argument must be given with =

$ ./myscript.sh --P arg -mt foo bar
P with no argument
m
t
Remaining arguments:
1: arg
2: foo
3: bar
$ ./myscript.sh --Parg mt foo bar
myscript.sh: unrecognized option `--Parg'
Problem parsing options
$ ./myscript.sh -P -mt foo bar
myscript.sh: invalid option -- P
Problem parsing options
$ ./myscript.sh -P=arg -mt foo bar
myscript.sh: invalid option -- P
myscript.sh: invalid option -- =
myscript.sh: invalid option -- a
myscript.sh: invalid option -- r
myscript.sh: invalid option -- g
Problem parsing options
1
On
  • Do not mix logic with arguments parsing.
  • Prefer lower case variables.

My aim is to continue at my comment, when there is no $OPTARG set for -P

I advise not to. The less you do at one scope, the less you have to think about. Split parsing options and executing actions in separate stages. I advise to:

# set default values for options
do_something_related_to_P=false
recursive=false
tree_output=false

# parse arguments
while getopts ':dBhmtb:P:' option; do
        case "$option" in
                t) tree_output=true; ;;
                r) recursive="$OPTARG"; ;;
                P) do_something_related_to_P="$OPTARG"; ;;
                \?) echo "?";;
                :) echo "test -$OPTARG requieres an argument" >&2
                         
        esac
done

# application logic
if "$do_something_related_to_P"; then
    do something related to P
    if "$recursive"; then
        do it in recursive style
    fi
fi |
if "$tree_output"; then
    output_as_tree
else
    cat
fi
2
On

Example of "don't put programming application logic in the case branches" -- the touch command can take a -t timespec option or a -r referenceFile option but not both:

$ touch -t 202010100000 -r file1 file2
touch: cannot specify times from more than one source
Try 'touch --help' for more information.

I would implement that like (ignoring other options):

while getopts t:r: opt; do
    case $opt in
        t) timeSpec=$OPTARG ;;
        r) refFile=$OPTARG ;;
    esac
done
shift $((OPTIND-1))

if [[ -n $timeSpec && -n $refFile ]]; then
    echo "touch: cannot specify times from more than one source" >&2
    exit 1
fi

I would not do this:

while getopts t:r: opt; do
    case $opt in
        t)  if [[ -n $refFile ]]; then
                echo "touch: cannot specify times from more than one source" >&2
                exit 1
            fi
            timeSpec=$OPTARG ;;
        r)  if [[ -n $timeSpec ]]; then
                echo "touch: cannot specify times from more than one source" >&2
                exit 1
            fi
            refFile=$OPTARG ;;
    esac
done

You can see if the logic gets more complicated (as I mentioned, exactly one of -a or -b or -c), that the case statement size can easily balloon unmaintainably.