UNIX - getopts for multiple switches - shell

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.

Related

bash getopts behavior different in while loop vs commandline

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.

Bash script - how to read file line by line while using getopts

I want to do two things in this script:
1) pass a file name to the script
2) pass options to the script
example 1:
$./test_script.sh file_name_to_be_read
pass only file names to script
example 2:
$./test_script.sh -a -b file_name_to_be_read
pass file name and options to script
I am able to get example 1 to work using the following codes:
while read -r line ; do
echo $line
done
In example 2, I want to add additional flags like these:
while getopts "abc opt; do
case "$opt" in
a) a=1
echo "a is enabled"
;;
b) b=1
echo "b is enabled"
;;
esac
done
but how do I make it so that the file_name to be mandatory and be used with or without options?
getopts only parses options (arguments starting with -); the other arguments are left alone. The parameter OPTIND tells you the index of the first argument not yet looked at; typically you discard the options before this.
while getopts "ab" opt; do
case "$opt" in
a) a=1
echo "a is enabled"
;;
b) b=1
echo "b is enabled"
;;
esac
done
shift $((OPTIND - 1))
echo "$# arguments remaining"
for arg in "$#"; do
echo "$arg"
done
The preceding, if called as bash tmp.bash -a -b c d e, produces
$ bash tmp.bash -a -b c d e
a is enabled
b is enabled
3 arguments remaining:
c
d
e

bash: getopts: parse two options with arguments

I am trying to parse two options which both need an argument.
#!/bin/bash
while getopts "a:b:" opt; do
case $opt in
a)
echo "a has the argument $OPTARG."
shift
;;
b)
echo "b has the argument $OPTARG."
shift
;;
esac
done
What I expect is that this script prints out the arguments of a and b. But the output is only:
$ sh ./cmd_parse_test.sh -a foo -b bar
a has the argument foo.
What am I doing wrong?
You don't have to shift to get the next argument. Simply dump whatever you want, and continue for the next iteration, as in:
#!/bin/bash
while getopts "a:b:" opt; do
case $opt in
a)
echo "a has the argument $OPTARG."
;;
b)
echo "b has the argument $OPTARG."
;;
esac
done
Which outputs:
$ ./cmd_parse_test.sh -a foo -b bar
a has the argument foo.
b has the argument bar.
Notice also that you don't have to run the script with sh, since you've already set the shbang to use bash.

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

getopts printing help when no cmd. line argument was matched

I'm trying to use getopts in bash to parse command line arguments, but I couldn't figure out how to implement "default" action, if no argument was matched (or no cmdline argument given).
This is silghtly simplified version of what I've tried so far:
while getopts v:t:r:s:c: name;
do
case $name in
v) VALIDATE=1;;
t) TEST=1;;
r) REPORT=1;;
s) SYNC=1;;
c) CLEAR=1;;
*) print_help; exit 2;;
\?) print_help; exit 2;;
esac
done
Is there any (simple) way to make it call print_help; exit 2; on non matching input?
Looking between your question and the comments on Aditya's answer, I'd recommend the following:
[getopts]$ cat go
#!/bin/bash
function print_help { echo "Usage" >&2 ; }
while getopts vtrsc name; do
case $name in
v) VALIDATE=1;;
t) TEST=1;;
r) REPORT=1;;
s) SYNC=1;;
c) CLEAR=1;;
?) print_help; exit 2;;
esac
done
echo "OPTIND: $OPTIND"
echo ${##}
shift $((OPTIND - 1))
while (( "$#" )); do
if [[ $1 == -* ]] ; then
echo "All opts up front, please." >&2 ; print_help ; exit 2
fi
echo $1
shift
done
Since each of those are boolean flag options, you don't need (and in fact, do not want) the arguments, so we get rid of the colons. None of those characters are in IFS, so we don't need to wrap that in quotes, it will be one token for getopts anyway.
Next, we change the \? to a single ? and get rid of the *, as the * would match before the literal \?, and we might as well combine the rules into a single default match. This is a good thing, since any option specified with a - prefix should be an option, and users will expect the program to fail if they specify an option you don't expect.
getopts will parse up to the first thing that isn't an argument, and set OPTIND to that position's value. In this case, we'll shift OPTIND - 1 (since opts are 0-indexed) off the front. We'll then loop through those args by shifting them off, echoing them or failing if they start with a -.
And to test:
[getopts]$ ./go
OPTIND: 1
0
[getopts]$ ./go -t -v go go
OPTIND: 3
4
go
go
[getopts]$ ./go -d -v go go
./go: illegal option -- d
Usage
[getopts]$ ./go -t go -v go -d
OPTIND: 2
5
go
All opts up front, please.
Usage
Try the following workaround:
# Parse the arguments.
while getopts ':h?f:' opts; do
case ${opts} in
f) # Foo argument.
;;
# My other arguments.
\? | h | *) # Prints help.
grep " .)\ #" $0
exit 0
;;
esac
done
So basically -?/-h would print the parameters with comments based on its own source. Specifying : before options will print help for any other unknown argument also.
v:t:r:s:c: should be in double quotes
"v:t:r:s:c:"
Based on the script you posted, maybe you don't require all those colons :
Also you don't need *)
You need to provide a leading colon in the getopts option string if you want to enable ? to match an invalid option -- :vtrsc. Also you don't need the backslash before the ?

Resources