Bash optarg fails to spot missing arguments - bash

I am inexperienced with bash shell scripting, and have run into a problem with bash optarg
Here's a small script to reproduce the problem:
#!/bin/sh
while getopts ":a:b:" opt; do
case ${opt} in
a ) echo "a=$OPTARG"
;;
b ) echo "b=$OPTARG"
;;
\? ) echo "Invalid option: $OPTARG" 1>&2
;;
: ) echo "Invalid option: $OPTARG requires an argument" 1>&2
esac
done
When I try this:
./args.sh -a av -b bv
I get the expected result:
a=av
b=bv
But when I omit the argument for -a:
/args.sh -a -b bv
I get this unfortunate result:
a=-b
When I would expect an error to show that the value of -a is missing.
It seems to have taken the -b argument as the value for -a.
Have I done something wrong & how can I achieve the expected behaviour?

The only positive advice is how do you treat But when I omit the argument for '-a', you cannot just skip to the next subsequent option. By convention getopts a: means you are expecting to an provide an arg value for the flag defined.
So even for the omitting case, you need to define an empty string which means the value for the arg is not defined i.e.
-a '' -b bv
Or if you don't expect the -a to get any arg values, better change the option string to not receive any as :ab:.
Any other ways of working around by checking if the OPTARG for -a is does not contain - or other hacks are not advised as it does not comply with the getopts() work flow.

getopts doesn't support such detection. So there's no way to do that with getopts.
You can probably write a loop around the arguments instead. something like:
#!/bin/sh
check_option()
{
case $1 in
-*)
return 1
;;
esac
return 0
}
for opt in $#; do
case ${opt} in
-a) shift
if check_option $1; then
echo "arg for -a: $1"
shift
else
echo "Invalid option -a"
fi
;;
-b) shift
if check_option $1; then
echo "arg for -b: $1"
shift
else
echo "Invalid option -b"
fi
;;
esac
done

Related

Pass parameters as option in custom getopts script in bash

I'd like to pass options as a parameter. E.g.:
mycommand -a 1 -t '-q -w 111'
The script cannot recognize a string in quotes. I.e it gets only part of the string.
getopts works the same - it see only -q.
For custom getopts I use similar script (example):
while :
do
case $1 in
-h | --help | -\?)
# Show some help
;;
-p | --project)
PROJECT="$2"
shift 2
;;
-*)
printf >&2 'WARN: Unknown option (ignored): %s\n' "$1"
shift
;;
*) # no more options. Stop while loop
break
;;
--) # End of all options
echo "End of all options"
shift
break
;;
esac
done
Maybe I misunderstand the question, but getopts seems to work for me:
while getopts a:t: arg
do
case $arg in
a) echo "option a, argument <$OPTARG>"
;;
t) echo "option t, argument <$OPTARG>"
;;
esac
done
Run:
bash gash.sh -a 1 -t '-q -w 111'
option a, argument <1>
option t, argument <-q -w 111>
Isn't that what you want? Maybe you missed the : after the options with arguments?

Bash interprets too long arguments for an option

I'm writing a bash script, that can take three options: c, p, and m; c can't have arguments, but p and m must have arguments. In my script I have these lines of code:
while getopts ":cp:m:" opt
do
case $opt in
c ) capitals=1
;;
m ) length=$OPTARG
;;
p ) number=$OPTARG
;;
\? ) echo "ERROR" 1>&2
exit 1
;;
esac
done
shift `expr $OPTIND - 1`
Now, when I write in the command line
myScript.sh -c -p 15 -m 12
all goes perfect. But when I write
myScript.sh -cp15m12
it goes wrong. Bash interprets the argument of the option 'p' as "15m12", while it should just be "15".
How can I solve this?
Since command-line arguments are not typed, there is no way to tell bash that the argument to -p must be an integer, and therefore no way for bash to infer that the correct way to parse -cp15m12 is as -c -p15 -m12 rather than as -c -p15m12. Since -c does not take an argument and getopts cannot define multi-character options, bash can infer that -cp15 is split into two separate options, -c and -p.
The value of getopts is fairly limited -- it's not hard to write your own replacement which supports option clustering. Here's a quick ad-hoc attempt.
#!/bin/sh
while true; do
case $1 in
-*) opt=${1#-}; shift;;
*) break;;
esac
while [ "$opt" ]; do
case $opt in
'-') opt=""; break 2;;
c*) capitals=1; opt=${opt#c};;
m[0-9]*)
opt=${opt#m}; length=${opt%%[!0-9]*}; opt=${opt#$length};;
m) opt=""; length=$1; shift;;
p[0-9]*)
opt=${opt#p}; number=${opt%%[!0-9]*}; opt=${opt#$number};;
p) opt=""; number=$1; shift;;
h|\?) echo "Syntax: $0 [-c] [-m length] [-p number]" >&2; exit 1;;
*) echo "Invalid option '$opt'" >&2; exit 2;;
esac
done
done
cat <<HERE
capitals: '$capitals'
length: '$length'
number: '$number'
args: '$#'
HERE
(Not sure if break 2 is entirely compatible with stock sh. Works for me on dash.)

Reading $OPTARG for optional flags?

I'd like to be able to accept both mandatory and optional flags in my script. Here's what I have so far.
#!bin/bash
while getopts ":a:b:cdef" opt; do
case $opt in
a ) APPLE="$OPTARG";;
b ) BANANA="$OPTARG";;
c ) CHERRY="$OPTARG";;
d ) DFRUIT="$OPTARG";;
e ) EGGPLANT="$OPTARG";;
f ) FIG="$OPTARG";;
\?) echo "Invalid option: -"$OPTARG"" >&2
exit 1;;
: ) echo "Option -"$OPTARG" requires an argument." >&2
exit 1;;
esac
done
echo "Apple is "$APPLE""
echo "Banana is "$BANANA""
echo "Cherry is "$CHERRY""
echo "Dfruit is "$DFRUIT""
echo "Eggplant is "$EGGPLANT""
echo "Fig is "$FIG""
However, the output for the following:
bash script.sh -a apple -b banana -c cherry -d dfruit -e eggplant -f fig
...outputs this:
Apple is apple
Banana is banana
Cherry is
Dfruit is
Eggplant is
Fig is
As you can see, the optional flags are not pulling the arguments with $OPTARG as it does with the required flags. Is there a way to read $OPTARG on optional flags without getting rid of the neat ":)" error handling?
=======================================
EDIT: I wound up following the advice of Gilbert below. Here's what I did:
#!/bin/bash
if [[ "$1" =~ ^((-{1,2})([Hh]$|[Hh][Ee][Ll][Pp])|)$ ]]; then
print_usage; exit 1
else
while [[ $# -gt 0 ]]; do
opt="$1"
shift;
current_arg="$1"
if [[ "$current_arg" =~ ^-{1,2}.* ]]; then
echo "WARNING: You may have left an argument blank. Double check your command."
fi
case "$opt" in
"-a"|"--apple" ) APPLE="$1"; shift;;
"-b"|"--banana" ) BANANA="$1"; shift;;
"-c"|"--cherry" ) CHERRY="$1"; shift;;
"-d"|"--dfruit" ) DFRUIT="$1"; shift;;
"-e"|"--eggplant" ) EGGPLANT="$1"; shift;;
"-f"|"--fig" ) FIG="$1"; shift;;
* ) echo "ERROR: Invalid option: \""$opt"\"" >&2
exit 1;;
esac
done
fi
if [[ "$APPLE" == "" || "$BANANA" == "" ]]; then
echo "ERROR: Options -a and -b require arguments." >&2
exit 1
fi
Thanks so much, everyone. This works perfectly so far.
: means "takes an argument", not "mandatory argument". That is, an option character not followed by : means a flag-style option (no argument), whereas an option character followed by : means an option with an argument.
Thus, you probably want
getopts "a:b:c:d:e:f:" opt
If you want "mandatory" options (a bit of an oxymoron), you can check after argument parsing that your mandatory option values were all set.
It isn't easy... Any "optional" option arguments must actually be required as far as getopts will know. Of course, an optional argument must be a part of the same argument to the script as the option it goes with. Otherwise an option -f with an optional argument and an option -a with a required argument can get confused:
# Is -a an option or an argument?
./script.sh -f -a foo
# -a is definitely an argument
./script.sh -f-a foo
The only way to do this is to test whether the option and its argument are in the same argument to the script. If so, OPTARG is the argument to the option. Otherwise, OPTIND must be decremented by one. Of course, the option is now required to have an argument, meaning a character will be found when an option is missing an argument. Just use another case to determine if any options are required:
while getopts ":a:b:c:d:e:f:" opt; do
case $opt in
a) APPLE="$OPTARG";;
b) BANANA="$OPTARG";;
c|d|e|f)
if test "$OPTARG" = "$(eval echo '$'$((OPTIND - 1)))"; then
OPTIND=$((OPTIND - 1))
else
case $opt in
c) CHERRY="$OPTARG";;
d) DFRUIT="$OPTARG";;
...
esac
fi ;;
\?) ... ;;
:)
case "$OPTARG" in
c|d|e|f) ;; # Ignore missing arguments
*) echo "option requires an argument -- $OPTARG" >&2 ;;
esac ;;
esac
done
This has worked for me so far.
For bash, this is my favorite way to parse/support cli args. I used getopts and it was too frustrating that it wouldn't support long options. I do like how it works otherwise - especially for built-in functionality.
usage()
{
echo "usage: $0 -OPT1 <opt1_arg> -OPT2"
}
while [ "`echo $1 | cut -c1`" = "-" ]
do
case "$1" in
-OPT1)
OPT1_ARGV=$2
OPT1_BOOL=1
shift 2
;;
-OPT2)
OPT2_BOOL=1
shift 1
;;
*)
usage
exit 1
;;
esac
done
Short, simple. An engineer's best friend!
I think this can be modified to support "--" options as well...
Cheers =)
Most shell getopts have been annoying me for a long time, including lack of support of optional arguments.
But if you are willing to use "--posix" style arguments, visit bash argument case for args in $#
Understanding bash's getopts
The bash manual page (quoting the version 4.1 manual) for getopts says:
getopts optstring name[args]
getopts is used by shell scripts 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 (‘?’) 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 ‘?’.
getopts normally parses the positional parameters, but if more arguments are
given in args, getopts parses those instead.
getopts can report errors in two ways. If the first character of optstring is a
colon, silent error reporting is used. In normal operation diagnostic messages
are printed when invalid options or missing option arguments are encountered.
If the variable OPTERR is set to 0, no error messages will be displayed, even if
the first character of optstring is not a colon.
If an invalid option is seen, getopts places ‘?’ into name and, if not silent,
prints an error message and unsets OPTARG. If getopts is silent, the option
character found is placed in OPTARG and no diagnostic message is printed.
If a required argument is not found, and getopts is not silent, a question mark
(‘?’) is placed in name, OPTARG is unset, and a diagnostic message is printed. If
getopts is silent, then a colon (‘:’) is placed in name and OPTARG is set to the
option character found.
Note that:
The leading colon in the option string puts getopts into silent mode; it does not generate any error messages.
The description doesn't mention anything about optional option arguments.
I'm assuming that you are after functionality akin to:
script -ffilename
script -f
where the flag f (-f) optionally accepts an argument. This is not supported by bash's getopts command. The POSIX function getopt() barely supports that notation. In effect, only the last option on a command line can have an optional argument under POSIX.
What are the alternatives?
In part, consult Using getopts in bash shell script to get long and short command-line options.
The GNU getopt (singular!) program is a complex beastie that supports long and short options and supports optional arguments for long options (and uses GNU getopt(3). Tracking its source is entertaining; the link on the page at die.net is wrong; you'll find it in a sub-directory under ftp://ftp.kernel.org/pub/linux/utils/util-linux (without the -ng). I've not tracked down a location at http://www.gnu.org/ or http://www.fsf.org/ that contains it.
#!/bin/bash
while getopts ":a:b:c:d:e:f:" opt; do
case $opt in
a ) APPLE="$OPTARG";;
b ) BANANA="$OPTARG";;
c ) CHERRY="$OPTARG";;
d ) DFRUIT="$OPTARG";;
e ) EGGPLANT="$OPTARG";;
f ) FIG="$OPTARG";;
\?) echo "Invalid option: -"$OPTARG"" >&2
exit 1;;
: ) echo "Option -"$OPTARG" requires an argument." >&2
exit 1;;
esac
done
echo "Apple is "$APPLE""
echo "Banana is "$BANANA""
echo "Cherry is "$CHERRY""
echo "Dfruit is "$DFRUIT""
echo "Eggplant is "$EGGPLANT""
echo "Fig is "$FIG""

Bash case statement

I'm trying to learn case as I was to write a fully functional script.
I'm starting off with the below
#!/bin/sh
case $# in
-h|--help)
echo "You have selected Help"
;;
-B|-b)
echo "You have selected B"
;;
-C|-c)
echo "You have selected C"
;;
*)
echo "Valid Choices are A,B,C"
exit 1
;;
esac
I want to use two of these options:
./getopts.sh -h -c
But i get this result
Valid Choices are A,B,C
Please can you help out and let me know what I'm doing wrong?
I want to build a script that will do something if you enter one option but do multiple things if you enter multiple.
Also how would i parse $1 to this script as surley which ever option i enter first (-h) will be $1 ??
Thanks!
Try this
#!/bin/sh
usage() {
echo `basename $0`: ERROR: $* 1>&2
echo usage: `basename $0` '[-a] [-b] [-c]
[file ...]' 1>&2
exit 1
}
while :
do
case "$1" in
-a|-A) echo you picked A;;
-b|-B) echo you picked B;;
-c|-C) echo you picked C;;
-*) usage "bad argument $1";;
*) break;;
esac
shift
done
Using getopt or getopts is the better solution. But to answer your immediate question, $# is all of your arguments, so -h -c, which doesn't match any of the single-argument patterns in your case statement. You would still need to iterate over your arguments like so
for arg in "$#"; do
case $arg in
....
esac
done
to parse the positional arguments like ... $1 , just use $1 in the case stmt and then at the end ... use shift to pust the 2nd arg to $1 and likewise .
also i would put the case stmt in a while loop or better a fxn so that i can run it twice for the two options or the number of options ..........
$# will let you know how many options/arguments were there .

What is the best way to check the getopts status in bash?

I am using following script :
#!/bin/bash
#script to print quality of man
#unnikrishnan 24/Nov/2010
shopt -s -o nounset
declare -rx SCRIPT=${0##*/}
declare -r OPTSTRING="hm:q:"
declare SWITCH
declare MAN
declare QUALITY
if [ $# -eq 0 ];then
printf "%s -h for more information\n" "$SCRIPT"
exit 192
fi
while getopts "$OPTSTRING" SWITCH;do
case "$SWITCH" in
h) printf "%s\n" "Usage $SCRIPT -h -m MAN-NAME -q MAN-QUALITY"
exit 0
;;
m) MAN="$OPTARG"
;;
q) QUALITY="$OPTARG"
;;
\?) printf "%s\n" "Invalid option"
printf "%s\n" "$SWITCH"
exit 192
;;
*) printf "%s\n" "Invalid argument"
exit 192
;;
esac
done
printf "%s is a %s boy\n" "$MAN" "$QUALITY"
exit 0
In this if I am giving the junk option :
./getopts.sh adsas
./getopts.sh: line 32: MAN: unbound variable
you can see it fails. it seems while is not working. What is the best way to solve it.
The getopts builtin returns 1 ("false") when there are no option arguments.
So, your while never executes unless you have option arguments beginning with a -.
Note the last paragraph in the getopts section of bash(1):
getopts returns true if an option, specified or unspecified, is
found. It returns false if the end of options is encountered or
an error occurs.
If you absolutely require MAN, then i suggest you don't make it an option parameter, but a positional parameter. Options are supposed to be optional.
However, if you want to do it as an option, then do:
# initialise MAN to the empty string
MAN=
# loop as rewritten by DigitalRoss
while getopts "$OPTSTRING" SWITCH "$#"; do
case "$SWITCH" in
m) MAN="$OPTARG" ;;
esac
done
# check that you have a value for MAN
[[ -n "$MAN" ]] || { echo "You must supply a MAN's name with -m"; exit 1; }
Even better, print the usage message before exiting - pull it out into a function so you can share it with the -h option's case.
The "best" solution is subjective. One solution would be to give default values to those variables that can be set by options.

Resources