Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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.

share|improve this question
add comment

5 Answers 5

up vote 3 down vote accepted

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
                    ;;
...
share|improve this answer
    
Excellent explanation. Makes perfect sense once I thought about the fact that there is actually a while loop running over the options. This is the method I've gone with as although it's verbose compared to others, it's very clear what's going on in the script. And maintainability by others is important in this case. –  benbradley May 15 '13 at 13:46
add comment

I would continue to use getopts but do some extra checking after: (untested)

errs=0
declare -A option=(
    [MYSQL_HOST]="-h"
    [MYSQL_USER]="-u"
    [MYSQL_PASS]="-p"
    [BACKUP_DIR]="-d" 
)
for var in "${!option[@]}"; do
    if [[ -z "${!var}" ]]; then
        echo "error: specify a value for $var with ${option[var]}"
        ((errs++))
    fi
done
((errs > 0)) && exit 1

requires bash version 4

share|improve this answer
add comment

The Gnu non-shell built-in getopt does less work for you but allows greater flexibility as you get to determine what happens after a flag is found, including shifting off the arguments yourself.

I found an article comparing getopts and getopt which will probably be helpful as the manual is kinda hard to make sense of (at least before coffee).

share|improve this answer
add comment

First, you should be using if [ -z "$MYSQL_USER" ].

Secondly, there is no need for the assignment - if [ -z "$OPTARG" ] will work fine.

Thirdly, I suspect what you actually want is if [ ${#OPTARG} = 0 ]. ${#x} is a bash thing returns the length of the string $x (see more here).

Fourthly, if you are doing your own validation, I would recommend getopt instead of getopts, as it provides a lot more flexibility.

Finally, to answer your first question, you can detect when a flag is passed as an argument by putting a list of flags at the top, like this:

args=( -h -u -p -d )

And then having a check in the the option where you want to check whether the argument supplied is an option, something like this:

d)
    BACKUP_DIR=$OPTARG
    if [ "$(echo ${args[@]/$OPTARG/})" = "$(echo ${args[@]})" ]
    then
        echo "Argument is an option! Error!"
    fi

Not a perfect answer, but it works! Your problem is that "-h" is a perfectly valid argument, and you give no way for the shell to know that it is only looking for parameters that are not also valid flags.

share|improve this answer
add comment

Since the other answers don't actually answer your question so much: This is expected behavior and based on how POSIX is being interpreted:

When the option requires an option-argument, the getopts utility shall place it in the shell variable OPTARG . If no option was found, or if the option that was found does not have an option-argument, OPTARG shall be unset.)

This is all that is being stated, and to put a (not so long) story short: getopts does not magically know that the perfectly valid argument it sees for the option that you require to have an argument is actually not an option argument but another option. Nothing prevents you from having -h as a valid argument to the -d option, which in practice means that getopts will only throw an error if your option that requires an argument comes last on the command line, e.g.:

test.sh:

#!/bin/sh

while getopts ":xy:" o; do
    case "${o}" in
        :) echo "${OPTARG} requires an argument"; exit 1;
    esac
done

Example:

$ ./test.sh -y -x
$

$ ./test.sh -x -y
y requires an argument

The reason why your second approach does not work is because the parsing that getopts does is still the same, so once you are inside the loop the "damage" is already done.

If you absolutely want to forbid this then, as Benubird pointed out, you will have to check your option arguments yourself and throw an error if they equal a valid option.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.