LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   bash getopts - or how to gracefully error when encountering invalid arguments (https://www.linuxquestions.org/questions/programming-9/bash-getopts-or-how-to-gracefully-error-when-encountering-invalid-arguments-4175604596/)

binnsr 04-25-2017 02:21 PM

bash getopts - or how to gracefully error when encountering invalid arguments
 
I've been unable to find a way for getopts to gracefully handle spaces.

Code:

while getopts a:b:c o ; do
  case ${o} in
    a) export varA="${OPTARG}" ;;
    b) export varB="${OPTARG}" ;;
    c) export varC=true ;;
    *) echo oopsy && exit 1 ;;
  esac
done

echo varA=${varA}
echo varB=${varB}
echo varC=${varC}

If, for instance, the script is invoked as:
Code:

./script.sh -a "one two" -b three four -c
the -a option should succeed - which it does, but the -b option should throw an error, but still parce out -c..
What I actually get is:
Code:

varA=one two
varB=three
varC=

getopts exits prematurely when it hits 'four' in the arglist and never processes the -c argument.

This has stumped me for quite some time now.. any help would be greatly appreciated.

astrogeek 04-25-2017 03:16 PM

Quote:

Originally Posted by binnsr (Post 5702049)
the -a option should succeed - which it does, but the -b option should throw an error, but still parce out -c...

No, that is not how it works.

From man bash:

Code:

getopts optstring name [args]
      getopts is used by shell procedures to  parse  positional  parameters.  optstring  contains  the
      option characters to be recognized; if a character is followed by a colon, the option is expected
      to have an argument, which should be separated from it by white space.  The  colon  and  question
      mark  characters  may  not be used as option characters.  Each time it is invoked, getopts places
      the next option in the shell variable name, initializing name if it does not exist, and the index
      of  the  next argument to be processed into the variable OPTIND.  OPTIND is initialized to 1 each
      time the shell or a shell script is invoked.  When an option requires an argument, getopts places
      that  argument  into the variable OPTARG.  The shell does not reset OPTIND automatically; it must
      be manually reset between multiple calls to getopts within the same shell invocation if a new set
      of parameters is to be used.

      When  the  end  of  options  is encountered, getopts exits with a return value greater than zero.
      OPTIND is set to the index of the first non-option argument, and name is set to ?.

The first non-option argument is where options end, and in your example that is the word four, so getopts never sees the -c, it is treated as another non-option argument.

If you were to quote "three four", these would be seen as a single argument to -b, so it would then proceed to -c.

binnsr 04-25-2017 03:57 PM

I know that's how getopts works. I'm trying to either work around that, or, alternatively, work around user's failure to follow directions.

I think I just figured it out..

Code:

#!/bin/bash

export options="bx:yz:"

function is_flag {
  # Check if $1 is a flag; e.g. "-b"
  [[ "$1" =~ -.* ]] && return 0 || return 1
} #/is_flag{}

function do_args {
  local OPTIND=-1
  while getopts ${options} o ; do
    x=0
    eval "a1=\$$((OPTIND))"
    while ! is_flag ${a1} ;
    do
        [[ $((OPTIND + 1)) -gt $# ]] && break
        if [[ ! "${a1}" =~ -.* ]]; then
            echo "ignoring extra parameter for $o"
            OPTIND=$((OPTIND+1))
            break
        fi
    done
    OPTIND=$((OPTIND+x))
    unset x a1
    case ${o} in
      b) export B='true' ;;
      x) export X="${OPTARG}" ;;
      y) export Y=true ;;
      z) export Z="${OPTARG}" ;;
    \?) echo oopsy 1 ;;
      ?) echo oopsy 2 ;;
      *) echo oopsy 3 ; exit 1 ;;
    esac || echo oopsy 4
  done || echo oopsy 5
} #/do_args{}

function do_args_multi {
  local OPTIND=-1
  while getopts ${options} o ; do
    x=0
    eval "a1=\$$((OPTIND))"
    while ! is_flag ${a1} ;
    do
        [[ ! "${a1}" =~ -.* ]] && OPTARG="${OPTARG} ${a1}"
        x=$((x+1))
        [[ $((OPTIND + x)) -gt $# ]] && break
        eval "a1=\$$((OPTIND+x))"
    done
    OPTIND=$((OPTIND+x))
  unset x a1
    case ${o} in
      b) export B='true' ;;
      x) export X="${OPTARG}" ;;
      y) export Y=true ;;
      z) export Z="${OPTARG}" ;;
    \?) echo oopsy 1 ;;
      ?) echo oopsy 2 ;;
      *) echo oopsy 3 ; exit 1 ;;
    esac || echo oopsy 4
  done || echo oopsy 5
} #/do_args_multi{}



printf "checking spaces in getopts\n"
printf " -- disallowing extras\n"
do_args "$@"
echo B=${B}
echo X=${X}
echo Y=${Y}
echo Z=${Z}

unset B X Y Z

printf " -- allowing multiples\n"
do_args_multi "$@"
echo B=${B}
echo X=${X}
echo Y=${Y}
echo Z=${Z}

Gives me output like such:
Code:

> ./x.sh -x "one two" -y  -z three four -b
checking spaces in getopts
 -- disallowing extras
ignoring extra parameter for z
B=true
X=one two
Y=true
Z=three
 -- allowing multiples
B=true
X=one two
Y=true
Z=three four

It's a bit messy.. but seems to work

edit: found the idea for the OPTIND workaround here.

astrogeek 04-25-2017 04:16 PM

You would probably be better served then, to use GNU getopt (it isn't actually GNU but is known by that name).

It will canonicalize all the args and reorder them so that all non-option args come at the end of the list, no matter what order the user enters them, and offers some very useful error checking and additional options. (See man getopt as always).

Something like this based on your original post using -a, -b and -c

Code:

#!/bin/bash

TEMP=`getopt -o a:b:c \
-n "$0" -- "$@"`

if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

while true; do
  case "$1" in
    -a) varA=$2;
        shift ;;
    -b) varB=$2;
        shift ;;
    -c) varC=true ;;
    -- ) shift; break ;;
    * ) break ;;
  esac
  shift
done

echo "varA=${varA}"
echo "varB=${varB}"
echo "varC=${varC}"
echo "Leftovers = $#"

Which produces this...

Code:

./mytest.sh -a "one two" -b three four -c
varA=one two
varB=three
varC=true
Leftovers = 1

You could then trigger an error based on unexpected Leftovers, or simply ignore them, or take whatever action you want.

binnsr 04-25-2017 04:31 PM

Quote:

Originally Posted by astrogeek (Post 5702100)
You would probably be better served then, to use GNU getopt (it isn't actually GNU but is known by that name).

I had considered GNU getopt when originally writing this script framework a few years ago, but we have a large enterprise environment with mixed access to the util-linux package (although it's getting better, there was a very minimalist approach for a while).
As this is a "quick-fix" to existing scripts, I'm going to leave the getopts in with this code in the functions that we need to error-check. I do, however, like that the GNU getopt code is cleaner, so will add an issue to our tracker for looking into a full re-vamp of the argument logic throughout the system.

Thanks!


All times are GMT -5. The time now is 01:51 AM.