4

I ran into an interesting scenario last night and, so far, my google foo has been unable to find a work around. I have a script that supports a number of arguments. A user (damn those users) didn't specify an argument for an option and the results were ... unexpected.

The code:

while getopts "a:c:d:De:rs:" arg
do
  case ${arg} in
    a)  app=${OPTARG} ;;
    c)  cmd=${OPTARG} ;;
    d)  domain=${OPTARG} ;;
    D)  Debug=1 ;;
    e)  env=${OPTARG} ;;
    r)  Doit=1 ;;
    s)  subapp=${OPTARG} ;;
    *)  echo "You are DISCREPANT!!";;
    # *)  usage "Invalid argument ${arg}::${OPTARG}" ;;
  esac
done

if [ ${Debug} -gt 0 ]
then
  echo "Env:    ${env}"
  echo "App:    ${app}"
  echo "Subapp: ${subapp}"
  echo "Cmd:    ${cmd}"
  echo "Doit:   ${Doit}"
  echo "Debug:  ${Debug}"
  exit 1
fi

Specifying all the args correctly results in:

$ ./mwctl -a weblogic -c start -s admin -e trn -r -D
Env:    trn
App:    weblogic
Subapp: admin
Cmd:    start
Doit:   1
Debug:  1

Forgetting the '-s' results in:

$ ./mwctl -D -a weblogic -c start admin -e trn -r
Env:
App:    weblogic
Subapp:
Cmd:    start
Doit:   0
Debug:  1

Similar results for skipping other args with options. It seems that 'case' loses its mind when presented with an OPTARG that doesn't have an OPT...

I'm at a bit of a loss as to how to catch this.

  • 3
    The parsing of the command line stops when a non-option is found. That's why. – Kusalananda Jun 22 '17 at 13:27
4

I would use getopt instead of getopts:

#!/usr/bin/env bash

OPT=$(getopt \
    --options a:c:d:De:rs: \
    --name "$0" \
    -- "$@"
)

if [ $? -ne 0 ]; then
    echo You are doing it wrong!
    exit 1
fi

eval set -- "${OPT}"

while true; do
    case "$1" in
        -a)  app=${2}; shift 2;;
        -c)  cmd=${2}; shift 2;;
        -d)  domain=${2}; shift 2;;
        -D)  Debug=1; shift;;
        -e)  env=${2}; shift 2;;
        -r)  Doit=1; shift;;
        -s)  subapp=${2}; shift 2;;
        --)  break;;
    esac
done

echo "Env:    ${env}"
echo "App:    ${app}"
echo "Subapp: ${subapp}"
echo "Cmd:    ${cmd}"
echo "Doit:   ${Doit}"
echo "Debug:  ${Debug}"

$ ./mwctl -a weblogic -c start -s admin -e trn -r -D
> Env:    trn
> App:    weblogic
> Subapp: admin
> Cmd:    start
> Doit:   1
> Debug:  1

$ ./mwctl -D -a weblogic -c start admin -e trn -r   
> Env:    trn
> App:    weblogic
> Subapp: 
> Cmd:    start
> Doit:   1
> Debug:  1

Note that when you are googling getopts vs. getopt, you will find many people complaining about getopt. As far as I can tell, this is always about an older version of getopt, which indeed was very buggy. My experience is, that getopt has more options and is also more robust than getopts.

To check if you have the enhanced getopt version, you can run

getopt -T
echo $?

If the output is 4, you have the enhanced version.

  • Well, that does a lot better job of catching invalid arguments. Thanks alot! I appreciate the help. Doug – dkoleary Jun 22 '17 at 13:38
  • why not put the shift 2 as the last statement in the while true instead of putting it after almost every case in the case? – cat Jun 22 '17 at 13:39
  • If you have a situation in which every arg has an option, then 'shift 2' after the case would work. in this scenario, though, at least two of the options dont' take args. – dkoleary Jun 22 '17 at 14:33
3

The traditional Unix convention is that a command line has options (including arguments of options), then non-option arguments of the command. In mwctl -D -a weblogic -c start admin -e trn -r, the options are -D (no argument), -a (argument: weblogic), and -c (argument: start). The next word is admin, which isn't an option, so the options are over. The non-option arguments are therefore admin, -e, trn and -r. The getopt builtin implements this convention.

The GNU convention is that an argument that begins with - anywhere on the command line is an option, unless there is a -- argument before it, with the caveat that arguments of options don't count. Under the GNU convention, in mwctl -D -a weblogic -c start admin -e trn -r, there is an option -D, an option -a with the argument weblogic, an option -c with the argument start, a non-option argument admin, an option -e with the argument trn and an option -r.

case doesn't “lose its mind”, your problem has nothing to do with case. Your code is missing the extraction of non-option arguments:

case $arg in …
esac
shift $((OPTIND - 1))
echo "There are $# non-option arguments, the first is $1"

If the command doesn't support any non-option arguments, you need to state this explicitly.

…
shift $((OPTIND - 1))
if [ $# -ne 0 ]; then
  echo >&2 "Extraneous argument: $1"
  exit 3
fi

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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