bash getopts behavior different in while loop vs commandline - bash

Commandline
$ getopts ":mnopq:rs" Option -q
$ echo $Option
?
$ echo $OPTARG
Given
$ cat getopt.sh
#!/bin/bash
echo '$#' is $#
while getopts ":mnopq:rs" Option
do
echo Option is $Option
echo OPTARG is $OPTARG
case $Option in
m ) echo "Scenario #1: option -m- [OPTIND=${OPTIND}]";;
n | o ) echo "Scenario #2: option -$Option- [OPTIND=${OPTIND}]";;
p ) echo "Scenario #3: option -p- [OPTIND=${OPTIND}]";;
q ) echo "Scenario #4: option -q-\
with argument \"$OPTARG\" [OPTIND=${OPTIND}]";;
# Note that option 'q' must have an associated argument,
#+ otherwise it falls through to the default.
r | s ) echo "Scenario #5: option -$Option-";;
* ) echo "Unimplemented option chosen.";; # Default.
esac
done
I get
Script
$ ./getopt.sh -q
$# is -q
Option is :
OPTARG is q
Unimplemented option chosen.
Why is there a difference in output between Commandline and Script ?

Have you run the "Commandline" test multiple times in the same shell? getopts uses the variable OPTIND to keep track of where it is in the argument list, so it doesn't just process the same option over and over. As a result, if you run the test multiple times it'll skip over what it processed last time. In your case, I suspect this is making it think it's at the end of the argument list (in which case it'll have an exit status of 1). Here's an excerpt from the bash man page:
[...] Each time it is invoked, getopts places [...] 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.
[...] 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.
Here's an example:
$ getopts ":mnopq:rs" Option -q
$ echo "status=$?, Option='$Option', OPTARG='$OPTARG', OPTIND=$OPTIND"
status=0, Option=':', OPTARG='q', OPTIND=2
$
$ getopts ":mnopq:rs" Option -q
$ echo "status=$?, Option='$Option', OPTARG='$OPTARG', OPTIND=$OPTIND"
status=1, Option='?', OPTARG='', OPTIND=2
$
$ unset OPTIND
$ getopts ":mnopq:rs" Option -q
$ echo "status=$?, Option='$Option', OPTARG='$OPTARG', OPTIND=$OPTIND"
status=0, Option=':', OPTARG='q', OPTIND=2
The first time it does what you expect. The second time it indicates it's out of options to process, which matches what you're seeing. The third time OPTIND has been unset, so it defaults back to the beginning of the argument list.

Related

Shell script not reading getopts arguments on MacOS

I have written a script that performs tasks according to switches used on command line. Here is my script:
Expects the git branch name and two optional switches
-r for resetting database
-t for merging training data
test.sh
merge_training=false
reset_database=false
while getopts ":tr" opt; do
case ${opt} in
t ) # process option t
echo "one"
$merge_training=true
;;
r ) # process option r
echo "two"
$reset_database=true
;;
esac
done
echo $reset_database
echo $merge_training
When I run this script with command:
sh test.sh branchname -r -t
It does not print one or two and the last statements prints:
false
false
What is wrong here?
What is wrong is your assignments. Your should have got 2 error messages saying:
false=true: command not found
Hint: Never put a $ on the left-side of an assignment:
The convention is to put options first, then extra parameters, so your command-line should be:
bash test.sh -r -t branchname
Use bash not sh. sh is a POSIX shell and is roughly a subset of bash (its complicated). Don't confuse the two.
merge_training=false
reset_database=false
while getopts ":tr" opt; do
case ${opt} in
t ) # process option t
echo "one"
merge_training=true # <<<<<<<<<<<<<<<
;;
r ) # process option r
echo "two"
reset_database=true # <<<<<<<<<<<<<<<
;;
esac
done
shift $(( OPTIND-1 ))
extra="$1"
echo $reset_database
echo $merge_training
echo $extra
when you remove the branchname as a param, then it works (- but you have to also remove the $ from $merge_training=true, else you try to false=true..)
So what you could do is to save the $1 param in a variable and simply shift.
Here the code:
#!/bin/bash
merge_training=false
reset_database=false
branchname="$1"
shift
while getopts ":tr" opt; do
case $opt in
t ) # process option t
echo "one"
merge_training=true
;;
r ) # process option r
echo "two"
reset_database=true
;;
esac
done
echo $branchname
echo $reset_database
echo $merge_training
Some of the points you could enhance on your script,
Variable assignments don't take $ on the left-hand-side
The command-line arguments are seeing an extra argument branchname before processing the actual OPTSTRING, you need to exclude the first parameter before getting into the getopts() call.
Always set the shell interpreter to the one you want to run your script with. In bash do #!/usr/bin/env bash
Always quote your shell variables unless you have a good reason not to.
The updated script should look like below if you are passing arguments as bash test.sh branchname -r -t
#!/usr/bin/env bash
merge_training=false
reset_database=false
branchname="$1"
shift 1
while getopts ":tr" opt; do
case "${opt}" in
t ) # process option t
echo "one"
merge_training=true
;;
r ) # process option r
echo "two"
reset_database=true
;;
esac
done
echo "$reset_database"
echo "$merge_training"

UNIX - getopts for multiple switches

The below while loop exists within a shell of mine called test.sh
I want to run the following command
ksh -x test.sh -c -e file1 file2
I want the while loop to perform both the c) case first then e) case within the loop, however at the moment it is only performing the c) case
Can someone advise how to get it to perform both? First c) then e)
while getopts ":c:e:" opt; do
case $opt in
c)
gzip -9 archivedfile
;;
e)
ksh test2.sh archivedfile
;;
esac
shift
done
Based on your example script invocation:
ksh -x test.sh -c -e file1 file2
the '-c' option does not have an argument
the '-e' option does have an argument (ie, 'file1')
If this is the case, try removing the colon (:) after the letter 'c' in your getopts option string, eg:
while getopts ":ce:" opt; do
If this fixes your issue ... keep reading for more details ...
When a colon (:) follows a letter in the getopts option string, this signifies that the option has an argument, which in turn means OPTARG will be set to the next item read from the command line.
When there is no colon following a letter in the getopts option string, this signifies the option does not have an argument, which in turn means OPTARG will be unset.
We can see this behavior with the following sample scripts:
For this first script we're expecting an argument after each of our options (c,e) - notice the colon (:) after each letter (c,e) in the getopts option string:
$ cat test1.sh
#!/bin/ksh
while getopts :c:e: opt
do
case $opt in
c) echo "c: OPTARG='${OPTARG:-undefined}'" ;;
e) echo "e: OPTARG='${OPTARG:-undefined}'" ;;
*) echo "invalid flag" ;;
esac
done
A couple test runs:
$ test1.sh -c -e file1
c: OPTARG='-e'
because our 'c' option/flag is followed by a colon (c:), the '-e' is treated as the argument to the '-c' option; since the '-e' is consumed on this pass through getopts ...
there are no more option flags to process and therefore the getopts case for the 'e' option is never accessed
$ test1.sh -c filex -e file1
c: OPTARG='filex'
e: OPTARG='file1'
because we've provided an argument for both of our options/flags (c,e), we see that both getopts cases are processed as desired
Now, if we don't want the '-c' option to have an argument, then we need to remove the colon (:) that follows the letter 'c' in our getopts option string:
$ cat test2.sh
#!/bin/ksh
while getopts :ce: opt
do
case $opt in
c) echo "c: OPTARG='${OPTARG:-undefined}'" ;;
e) echo "e: OPTARG='${OPTARG:-undefined}'" ;;
*) echo "invalid flag" ;;
esac
done
$ test2.sh -c -e file1
c: OPTARG='undefined'
e: OPTARG='file1'
because our 'c' option/flag is not followed by a colon, getopts does not read any more items from the command line, which means ...
for the next pass through getopts the '-e' option/flag is processed and OPTARG is set to 'file1'
Use ;&
case c in
c)
echo got c
;&
c|e)
echo got c or e
;;
esac
See man ksh
If ;& is used in place of ;; the next subsequent list, if any, is executed.

Using getopts to read one optional parameter placed as final place

I wrote a bash script that takes flexible number of parameters and now I would like to add an optional argument (-l) to each of them.
I am currently having difficulty getting the desired behavior.
I want all of the following to execute correctly:
./Script.sh arg1 arg2 arg3 -l opt
./Script.sh arg1 arg2 arg3
./Script.sh arg1 arg2 arg3 arg4 -l opt
./Script.sh arg1 arg2 arg3 arg4 arg5
The problem is that $OPTIND cannot be set.
The following loop works if the -l opt is placed before first argument.
while getopts ":l:" option
do
case "$option" in
t)
F_NAME=$OPTARG
;;
esac
done
shift $((OPTIND - 1))
However, place the optional -l as last parameter is a requirement.
What's the easiest way to achieve this?
Here is a trick I have found to use arguments with optional parameters with getopts.
The way to manage the case where the optional parameter is inside the command is given by Jan Schampera in is reply on bash-hackers.org :
if [[ $OPTARG = -* ]]; then
((OPTIND--))
continue
fi
(see : http://wiki.bash-hackers.org/howto/getopts_tutorial deep in the page)
But it does not manage the case where the option is given at the end of the command.
In that case, that is considered wrong because no parameter is given, getopts set the opt variable to ':' (colon) and OPTARG to the fault option value.
So we have to manage the ':' case, with a case $OPTARG.
Let us say we write a script having three options :
a : without parameter
b : with a required parameter
v : to set the verbosity with a value from 0 to 2. The default value is 0 and a preset value is used when the script is called with -v without parameter or with a bad value.
Here is the code :
#!/bin/bash
VERBOSITY=0 # default verbosity set to 0
PRESET_VERBOSITY=1 # preset verbosity when asked
while getopts :ab:v: opt; do
case $opt in
a)
echo "manage option a"
;;
b)
echo "manage option b with value '$OPTARG'"
;;
v)
if [[ $OPTARG = -* ]]; then # Jan Schampera reply
echo "set verbosity to PRESET (no value given, not last position)"
VERBOSITY=$PRESET_VERBOSITY
((OPTIND--))
continue
fi
if [[ "$OPTARG" =~ ^[0-2]$ ]]; then
echo "set verbosity to $OPTARG (good value given)"
VERBOSITY=$OPTARG
else
echo "set verbosity to PRESET (bad value given)"
VERBOSITY=$PRESET_VERBOSITY
fi
;;
:)
case $OPTARG in
v)
echo "set verbosity to PRESET (no value given, last option)"
VERBOSITY=$PRESET_VERBOSITY
;;
esac
;;
\?)
echo "WTF!"
;;
esac
done
echo "**verbosity is set to $VERBOSITY**"
getopts conforms to the posix-standard command-line syntax where flag options come first. So it's not easy to use for non-standard cases.
However, you may have the Gnu implementation of getopt(1) (see man 1 getopt), which can handle permuted option flags as well as long options. However, it's not as easy an interface.
Or you can just interpret the argument yourself.
for ((i=1; i<=$#; ++i)); do
if [[ ${!i} == "-l" ]]; then
((++i))
OPT_L=${!i}
else
# handle the argument (in "${!i]")
fi
done
(Note: the above does not throw an error if the -l appears right at the end of the argument list; it just sets the option value to an empty string. If that's not appropriate, which it probably isn't, then insert some error checking.)

Changing no args default case in getopts - bash

Whenever I use getopts and i don't give any argument for a given flag I get the following message: "option requires an argument -- d"
I would like to remove this message and allow the user to retype the options using the read command.
Here is my case in the getopts:
if [ $# -lt $OPTIND ]; then
echo "Option -d argument missing: needs 2 args"
echo "Please enter two args: <arg1> <arg2>"
read d_ID d_SIZE
echo "disc $d_ID $d_SIZE" >> $FILENAME
else
d_ID=$OPTARG
eval d_SIZE=\$$OPTIND
echo "disc $d_ID $d_SIZE" >> $FILENAME
fi
;;
I think the behavior you want is a bad idea; there exist programs that take options with optional arguments, and they work nothing like what you describe. Your approach will likely confuse your users; it will make it difficult for other programs to interoperate with it; and it will limit the future extensibility of your script. (Imagine you want to add another option later on. your_script.sh -d -e will pass -e as an argument to -d, even if the user wanted to use -d with no argument and intended -e as a separate option.) And it's especially bizarre to expect one argument if it's on the command line, but two arguments from standard input.
That said . . .
To achieve some of the effect of an optional option-argument, you can tell getopts to be "silent" (that is, to use "silent error reporting") by putting a colon : at the beginning of the option string. (For example, instead of getopts d: ..., you would write getopts :d: ....) Then, when an option's argument is missing, it will set the option-name to : (instead of d) and OPTARG to the option-name (namely d, instead of the option-argument).
For example, the below script can be called either as script.sh -d foo or as script.sh -d, with the latter causing the user to be prompted to type a value:
#!/bin/bash
if getopts :d: arg ; then
if [[ "$arg" == d ]] ; then
d="$OPTARG"
else
read -p 'Enter d: ' d
fi
echo "d is: $d"
else
echo 'no d'
fi
Resulting in:
$ ./script.sh
no d
$ ./script.sh -d
Enter d: x
d is: x
$ ./script.sh -d y
d is: y

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""

Resources