Using getopts with multiple arguments - linux

I have the following code:
while getopts ":vh" opt; do
case $opt in
(h)
helpMe
exit 0
;;
(v)
version1
exit 0
;;
\?)
echo -e "Invalid option: -$OPTARG\ntype -h for more help.\n" >&2
exit 1
;;
esac
done
This code should get flags 'h' and 'v' only but when I give flag for example 'hg' I still get the help output. Also, when I give flag "va" I still get the version output.
What I am trying to say is that my script should ignore all of the letters after the required one.
Why does it happen? Can someone please advise?

Options are parsed and handled one at a time by the loop and -hg is treated as two individual options, -h and -g. Since the loop encounters -h first it triggers helpMe and exits without ever parsing -g. If you were to run your script with -gh you would see that it fails when it hits the -g option since it comes first in this case.
If you don't like this behaviour you can use a variable to store the state, let getopts finish parsing and test for -h afterwards:
while getopts ":vh" opt; do
case $opt in
h)
opt_h=true
;;
[...]
esac
done
if [ "${opt_h}" = true ]; then
helpMe
exit 0
fi

It's the calls to exit in both your choices. getopts will process the arguments one at a time so, when it strikes the h in -h -g (the expansion of -hg), it runs helpMe and then exits.
That means it will never find the -g in the options.
The simplest way to handle this is to simply state that, on -h, it will output the help text and exit, regardless of whatever other parameters there are.
If you want to ensure the arguments are valid, simply store their state in the option checking code and process them later.
That way, the checks will be done before any actions. Something like this:
flag_v=0
flag_h=0
while getopts ":vh" opt; do
case $opt in
(h)
flag_h=1
;;
(v)
flag_v=1
;;
\?)
echo -e "Invalid option: -$OPTARG\ntype -h for more help.\n" >&2
exit 1
;;
esac
done
if [[ $flag_h -eq 1 ]] ; then
helpMe
exit 0
fi
if [[ $flag_v -eq 1 ]] ; then
showVersion
exit 0
fi
# Now continue, neither h nor v was specified.
That will give help and exit if you specify -h or -hv, version information and exit if you specify -v, and exit with an error if you provide any other option.
If you specify no options, it will simply carry on beyond the comment to do whatever it is your script is meant to do.

Related

Defining flag parameters in bash

I wrote bash script, in which I included some logging, to see what is going on in each step of exectuion.
Now, I can split those logs in debuggin info and user info (that something has completed, etc.). So I'd like to have some flag parameter, like --verbose, which I saw in some other bash functions to enable full logging and usage was like:
some_function --verbose
or
some_function -v
I call it flag parameters and don't know what's the right name, thus I can't find anything useful.
How to define such parameters for bash script?
Case suits better for this
while [[ "$#" ]]; do
case "$1" in
-v|--verbose) verbose="true";;
esac
shift
done
Same can be done in function, but note that in this case it'll process parameters passed to function some_function -v.
some_function () {
while [[ "$#" ]]; do
case "$1" in
-v|--verbose) verbose="true";;
esac
shift
done
}
Then somewhere in script you can check if verbose is set
[[ "$verbose" ]] && echo "verbose mode" || echo "silent mode"
For now, I used workaround and take it as normal positional parameter (as $n). To be exact, I have list of four parameters, so I collect this flag like this:
verbose=$4
if [ ! "$verbose" == "--verbose" ]; then
verbose=""
fi
So, if parameter is not matching a flag, then I leave it empty and if I want to use it, I just compare it against empty string.
There's one big disadvantage for me though: it has to be at 4th position in parameters list.

shell getopts parameters collection issue

I have below code in my shell script.
show_help()
{
cat <<EOF
Usage: ${0##*/} [-h help] [-g GATEWAY_HOSTID] [-t TIMEZONE]
-h display this help and exit
-g GATEWAY_HOSTID zabbix gateway identifier (e.g. '20225')
-t Time Zone TimeZone against which you want to test
EOF
}
OPTIND=1
while getopts "g:h:t" opt; do
case "$opt" in
h)
show_help
exit 0
;;
g)
gateway_hostid=$OPTARG
;;
t)
timezone=$OPTARG
;;
esac
done
shift $((OPTIND-1))
if [[ ! $timezone ]]; then
timezone="UTC"
fi
if [[ ! $gateway_hostid ]]; then
echo "hostid is missing!!! Exiting now."
exit
fi
When I execute script it only takes parameter gateway_hostid and ignores timezone parameter. I am not sure what I am doing wrong here. Also it doesn't show help function as well. Can someone help. below is the syntax for calling script.
./script_name.sh -g 20225 -t Europe/Zurich
./script_name.sh -g 20225 -t CEST
Your problem is with the optstring. You're specifying h: which means that -h requires an option. You are also specifying t without a : meaning t does not expect an option.
The optstring to have g and t take options and h not need one is hg:t:

User defined output redirection not working as expected

I am using a KSH script to execute a binary (program) that has the following syntax to execute correctly:
myprog [-v | --verbose (optional)] [input1] [input2]
The program prints nothing & returns exit code 0 (zero) on success. On failure it prints ERROR messages to STDERR & returns exit status > 0. If -v option is specified it prints verbose details to STDOUT both in case of success and failure.
To make this usable and reduce chances of argument swapping and user controlled logging I used a ksh shell script to invoke this binary. The syntax to run the ksh shell script is as:
myshell.sh [-v (optional)] [-a input1] [-b input2]
If -v option is specified, ksh redirects STDOUT to <execution_date_time>_out.log and STDERR to <execution_date_time>_err.log. My ksh script is as follows:
myshell.sh :
#! /bun/ksh
verbopt=""
log=""
arg1=""
arg2=""
dateTime=`date +%y-%m-%d_%H:%M:%S`
while getopts "va:b:" arg
do
case $arg in
v) # verbose output
verbopt="-v"
log="1>${dateTime}_out.log 2>${dateTime}_err.log"
;;
a) # Input 1
arg1=$OPTARG
;;
b) # Input 2
arg2=$OPTARG
;;
*) # usage
echo "USAGE: myshell.sh [-v] [-a input1] [-b input2]"
exit 2
;;
esac
done
if [[ -z $arg1|| -z $arg2]]
then
echo "Missing arguments"
exit 2
fi
myprog $verbopt $arg1 $arg2 $log
exit $?
The problem here is, all the output STDERR & STDOUT is printed on the screen (i.e, No redirection took place) as well as no *.log files were created after successful or unsuccessful execution (i.e, exit status: 0 or >0 respectively).
Can anyone help me out on this?
Thanks.
Rather than trying to monkey patch redirections into the command line, just redirect the streams when you parse the flags. That is:
while getopts "va:b:" arg
do
case $arg in
v) # verbose output
verbopt="-v"
exec 1>${dateTime}_out.log 2>${dateTime}_err.log
;;
...
You need to be a little careful, since you do some error checking after this and you probably don't want your later error messages going to the *_err.log, but that's fairly trivial to fix. (eg, error check sooner, or do a test -n "$verbopt" && exec > ... after the error check, or similar)
The problem is that > is not expanded in the value of $log.
I'm afraid you will need to use a conditional for this, for example:
cmd="myprog $verbopt $arg1 $arg2"
if [ "$log" ]; then
$cmd 1>${dateTime}_out.log 2>${dateTime}_err.log
else
$cmd
fi
I would use the idiom exec redirection, which runs the rest of the script as if the given redirection had been supplied when it was run:
if need_to_log; then
exec >stdout_file 2>stderr_file
fi
this command will be logged if the above if statement was true
If you need to restore stdout and stderr afterward for the script to do more unlogged things, you can just run the logging part in a subshell:
(
if need_to_log; then
exec >stdout_file 2>stderr_file
fi
this command will be logged if the above if statement was true
)
this command will not be logged regardless
I would also build the command in an array, so you can add things like -v to it without having to have a separate variable for each possible parameter. If the order in which the -a and -b arguments are supplied to myprog doesn't matter, you can just add those to the array instead of having separate variables as well.
You can see my version below. Besides the above changes, I also don't bother getting the timestamp if not logging, since it's unneeded, and send error messages to standard error instead of standard out using the ksh builtin print.
Here's what I put together:
#!/usr/bin/env ksh
# new array syntax requires ksh93+; for older ksh, use this:
# set -A cmd myprog
cmd=(myprog) # build up the command to run in an array
log_flag=0 # nonzero if the command should be logged
input_a= # the two input filenames
input_b=
while getopts 'va:b:' arg; do
case $arg in
v) # verbose output
# older ksh: set -A cmd "${cmd[#]}" -v
cmd+=(-v)
log_flag=1
;;
a) # Input 1
input_a=$OPTARG
;;
b) # Input 2
input_b=$OPTARG
;;
*) # usage
print -u2 "USAGE: $0 [-v] [-a input1] [-b input2]"
exit 2
;;
esac
done
if [[ -z $input_a || -z $input_b ]]; then
print -u2 "$0: Missing arguments"
exit 2
fi
if (( log_flag )); then
timestamp=$(date +%y-%m-%d_%H:%M:%S)
exec >"${timestamp}_out.log" 2>"${timestamp}_err.log"
fi
"${cmd[#]}" "$input_a" "$input_b"
Your timestamp uses the two-digit year (%y); that and the underscore between the components are the only deviations from the ISO 8601 standard, so I would recommend you go ahead and adopt the standard format. That'd be %Y-%m-%dT%H:%M:%S, or, in C libraries with newer versions of strftime, %FT%T.
You could also be a little more clever and make log_flag a string that is either empty or -q, pass that to the command, and test it against the empty string to determine whether or not to open the log files, but I find the logic easier to follow with the simple 0/1 value treated as a Boolean.
Take a look at the eval command.
Replace ...
myprog $verbopt $arg1 $arg2 $log
with:
eval myprog $verbopt $arg1 $arg2 $log
I don't know what your myprog does but here's a simple example using eval to run date (valid command) and date xyz (invalid command), redirecting output to log.stdout/log.stderr accordingly:
$ cat logout
log='1>log.stdout 2>log.stderr'
'rm' -rf log.std* > /dev/null 2>&1
echo ""
echo 'eval date ${log}'
eval date ${log}
echo ""
echo "++++++++++++ log.stdout"
cat log.stdout
echo "++++++++++++ log.stderr"
cat log.stderr
echo "++++++++++++"
'rm' -rf log.std* > /dev/null 2>&1
echo ""
echo 'eval date xyz ${log}'
eval date xyz ${log}
echo ""
echo "++++++++++++ log.stdout"
cat log.stdout
echo "++++++++++++ log.stderr"
cat log.stderr
echo "++++++++++++"
Now run the script:
$ logout
eval date ${log}
++++++++++++ log.stdout
Sun Jul 23 15:56:01 CDT 2017
++++++++++++ log.stderr
++++++++++++
eval date xyz ${log}
++++++++++++ log.stdout
++++++++++++ log.stderr
date: invalid date `xyz'
++++++++++++

How to add to your bash script help option "yourscript --help"

is there any possibility to add "help" to written by you bash script in Linux (Debian)? I mean specifically, by using command yourscript --help or yourscript -h
It doesn't have to be harder than this.
case $1 in
-[h?] | --help)
cat <<-____HALP
Usage: ${0##*/} [ --help ]
Outputs a friendly help message if you can figure out how.
____HALP
exit 0;;
esac
If you use getopts for option processing, use that to identify the option; but the action is going to look more or less similar (and IMNSHO getopts doesn't really offer anything over a simple while ... shift loop).
getopt
#!/bin/bash
args=$(getopt -n "$(basename "$0")" -o h --longoptions help -- "$#") || exit 1
eval set -- "$args"
while :; do
case $1 in
-h|--help) echo offer help here ; exit ;;
--) shift; break ;;
*) echo "error: $1"; exit 1;;
esac
done
echo "hello world, $*"
There are many ways to do this. Over time I have come to prefer separate usage and help functions. The help is provided in response to a request for either --help or -h and it provides extended help/option information in a heredoc format. The usage function is provided in response to an invalid input. It is short and provides a quick reminder of what the script needs. Both functions take a string as the first argument that allows you to pass an error message to be displayed along with the help or usage. Both also allow you to pass an exit code as the second argument.
The following is an example I pulled from an existing script. You can ignore the contents, but it was left by way of example:
function help {
local ecode=${2:-0}
[[ -n $1 ]] && printf "\n $1\n" >&2
cat >&2 << helpMessage
Usage: ${0##*/} <ofile> <file.c> [ <cflags> ... --log [ \$(<./bldflags)]]
${0##*/} calls 'gcc -Wall -o <ofile> <file.c> <cflags> <\$(<./bldflags)>'
If the file './bldflags' exists in the present directory, its contents are
read into the script as additional flags to pass to gcc. It is intended to
provide a simple way of specifying additional libraries common to the source
files to be built. (e.g. -lssl -lcrypto).
If the -log option is given, then the compile string and compiler ouput are
written to a long file in ./log/<ofile>_gcc.log
Options:
-h | --help program help (this file)
-l | --log write compile string and compiler ouput to ./log/<ofile>_gcc.log
helpMessage
exit $ecode
}
function usage {
local ecode=${2:-0}
[[ -n $1 ]] && printf "\n $1\n" >&2
printf "\n Usage: %s <ofile> <file.c> [ <cflags> ... --log [ \$(<./bldflags)]]\n\n" "${0##*/}"
exit $ecode
}
I generally test for help when looking at all arguments, e.g.:
## test for help and log flags and parse remaining args as cflags
for i in $*; do
test "$i" == "-h" || test "$i" == "--help" && help
...
done
Usage is provided in response to an invalid input, e.g.:
[ -f "$1" ] || usage "error: first argument is not a file." 1
They come in handy and I've preferred this approach to getopts.

issue in getopt , Unix shell script

Hi can someone fix this issue, i am not able to get outpt.
I am not able to get output of -p.
#!/bin/bash
args=`getopt c:m:p $*`
if [ $? != 0 -o $# == 0 ]
then
echo 'Usage: -c <current-dir> -m <my dir> -p <argument>'
exit 1
fi
set -- $args
for i
do
case "$i" in
-c) shift;CURRDIR=$1;shift;shift ;;
-m) MYDIR=$1;shift;;
-p) ARGVAL=$OPTARG;;
esac
done
echo "CURRDIR = $CURRDIR"
echo "MYDIR = $MYDIR"
echo "ARGVAL = $ARGVAL"
./1.sh -c "def" -m "ref" -p "ref -k ref"
Expected output
output -c = "def"
-m ="ref"
-p ="ref -k ref"
getopt
args=`getopt c:m:p $*`
You need to add a colon after the p to indicate that -p takes an argument. Also you should change $* to "$#" for better handling of spaces.
args=`getopt c:m:p: "$#"`
You are also mixing up getopt and getopts. $OPTARG is a getopts feature. With plain getopt and set you should simply use $2 and then shift off the argument.
-p) ARGVAL=$2; shift 2;;
At this point you've done as good as you can with getopt. Unfortunately it doesn't handle the multi-word argument to -p no matter what you do. For that, we need to use getopts.
getopts
From getopt and getopts:
Easier to use and generally better than getopt, though of course not available in csh-like shells. You shouldn't be using those anyway.
This works rather differently than "getopt". First, because it's a built-in, you usually won't find a separate man page for it, though "help getopts" may give you what you need.
The old "getopt" is called once, and it modifies the environment as we saw above. The builtin "getopts" is called each time you want to process an argument, and it doesn't change the original arguments .
Using getopts is a lot simpler. Your entire loop can be simplified to this:
while getopts c:m:p: flag
do
case "$flag" in
c) CURRDIR=$OPTARG;;
m) MYDIR=$OPTARG;;
p) ARGVAL=$OPTARG;;
esac
done
No shifting needed, you just read $OPTARG each time to get each option's value.

Resources