Bash getopts, short options only, all require values, own validation

bashgetopts

I'm trying to build up a shell script that accepts various options and getopts seems like a good solution as it can handle the variable ordering of the options and arguments (I think!).

I'll only be using short options and each short options will require a corresponding value eg: ./command.sh -a arga -g argg -b argb but I would like to allow the options to be entered in a non-specific order, as is the way most people are accustomed to working with shell commands.

The other point is that I would like to do my own checking of option argument values, ideally within the case statements. The reason for this is that my testing of :) in my case statement has yielded inconsistent results (probably through lack of understanding on my part).
For example:

#!/bin/bash
OPTIND=1 # Reset if getopts used previously
if (($# == 0)); then
        echo "Usage"
        exit 2
fi
while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                :)
                       echo "Option -$OPTARG requires an argument" >&2
                       exit 2;;
        esac
done
shift $((OPTIND-1))
echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Was failing for occurrences like this… ./command.sh -d -h
When I want it to flag -d as requiring an argument but I get the value of -d=-h which is not what I need.

So I figured it would be easier to run my own validation within the case statements to ensure that each option is set and set only once.

I'm trying to do the following but my if [ ! "$MYSQL_HOST" ]; then blocks are not triggered.

OPTIND=1 # Reset if getopts used previously

if (($# == 0)); then
        echo "Usage"
        exit 2
fi

while getopts ":h:u:p:d:" opt; do
        case "$opt" in

                h)
                        MYSQL_HOST=$OPTARG
                        if [ ! "$MYSQL_HOST" ]; then
                                echo "host not set"
                                exit 2
                        fi
                        ;;
                u)
                        MYSQL_USER=$OPTARG
                        if [ ! "$MYSQL_USER" ]; then
                                echo "username not set"
                                exit 2
                        fi
                        ;;
                p)
                        MYSQL_PASS=$OPTARG
                        if [ ! "$MYSQL_PASS" ]; then
                                echo "password not set"
                                exit 2
                        fi
                        ;;
                d)
                        BACKUP_DIR=$OPTARG
                        if [ ! "$BACKUP_DIR" ]; then
                                echo "backup dir not set"
                                exit 2
                        fi
                        ;;
                \?)
                        echo "Invalid option: -$OPTARG" >&2
                        exit 2;;
                #:)
                #       echo "Option -$opt requires an argument" >&2
                #       exit 2;;
        esac
done
shift $((OPTIND-1))

echo "MYSQL_HOST='$MYSQL_HOST'  MYSQL_USER='$MYSQL_USER'  MYSQL_PASS='$MYSQL_PASS'  BACKUP_DIR='$BACKUP_DIR' Additionals: $@"

Is there a reason that I'm unable to check if an OPTARG has zero-length from within getopts ... while ... case?

What's the better way to run my own argument validation with getopts in a case where I don't want to be relying on the :). Perform my argument validation outside of the while ... case ... esac?
Then I could end up with argument values of -d etc and not catching a missing option.

Best Answer

When you call your second script (I saved it as getoptit) with:

getoptit -d -h

This will print:

MYSQL_HOST=''  MYSQL_USER=''  MYSQL_PASS=''  BACKUP_DIR='-h' Additionals: 

So BACKUP_DIR is set, and you are testing with if [ ! "$BACKUP_DIR" ]; then if it is not set, so it is normal that the code inside of it is not triggered.

If you want to test if each option is set once, you have to do that before you do the assigment from the $OPTARG value. And you should probably also check for the $OPTARG to start with a '-' (for the -d -h error) before assigning:

...
            d)
                    if [ ! -z "$BACKUP_DIR" ]; then
                            echo "backup dir already set"
                            exit 2
                    fi
                    if [ z"${OPTARG:0:1}" == "z-" ]; then
                            echo "backup dir starts with option string"
                            exit 2
                    fi
                    BACKUP_DIR=$OPTARG
                    ;;
...
Related Question