bash getopts with multiple and mandatory options - bash

Is it possible to use getopts to process multiple options together? For example, myscript -iR or myscript -irv.
Also, I have a situation where based on a condition script would need mandatory option. For example, if argument to script is a directory, I will need to specify -R or -r option along with any other options (myscript -iR mydir or myscript -ir mydir or myscript -i -r mydir or myscript -i -R mydir), in case of file only -i is sufficient (myscript -i myfile).
I tried to search but didn't get any answers.

You can concatenate the options you provide and getopts will separate them. In your case statement you will handle each option individually.
You can set a flag when options are seen and check to make sure mandatory "options" (!) are present after the getopts loop has completed.
Here is an example:
#!/bin/bash
rflag=false
small_r=false
big_r=false
usage () { echo "How to use"; }
options=':ij:rRvhm'
while getopts $options option
do
case "$option" in
i ) i_func;;
j ) j_arg=$OPTARG;;
r ) rflag=true; small_r=true;;
R ) rflag=true; big_r=true;;
v ) v_func; other_func;;
h ) usage; exit;;
\? ) echo "Unknown option: -$OPTARG" >&2; exit 1;;
: ) echo "Missing option argument for -$OPTARG" >&2; exit 1;;
* ) echo "Unimplemented option: -$option" >&2; exit 1;;
esac
done
if ((OPTIND == 1))
then
echo "No options specified"
fi
shift $((OPTIND - 1))
if (($# == 0))
then
echo "No positional arguments specified"
fi
if ! $rflag && [[ -d $1 ]]
then
echo "-r or -R must be included when a directory is specified" >&2
exit 1
fi
This represents a complete reference implementation of a getopts function, but is only a sketch of a larger script.

Related

Is there a way to continue in a Flag when there is no $OPTARG set in Bash, GETOPTS?

I would like to build a script with getopts, that continues in the flag, when an $OPTARG isn't set.
My script looks like this:
OPTIONS=':dBhmtb:P:'
while getopts $OPTIONS OPTION
do
case "$OPTION" in
m ) echo "m"
t ) echo "t"
d ) echo "d";;
h ) echo "h";;
B ) echo "b";;
r ) echo "r";;
b ) echo "b"
P ) echo hi;;
#continue here
\? ) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
My aim is to continue at my comment, when there is no $OPTARG set for -P.
All I get after running ./test -P is :
test -P requieres an argument
and then it continues after the loop but I want to continue in the -P flag.
All clear?
Any Ideas?
First, fix the missing ;; in some of the case branches.
I don't think you can: you told getopts that -P requires an argument: two error cases
-P without an argument is the last option. In this case getops sees that nothing follows -P and sets the OPTION variable to :, which you handle in the case statement.
-P is followed by another option: getopts will simply take the next word, even if the next word is another option, as OPTARG.
Change the case branch to
P ) echo "P: '$OPTARG'";;
Then:
invoking the script like bash script.sh -P -m -t, the output is
P: '-m'
t
invoking the script like bash script.sh -Pmt, the output is
P: 'mt'
This is clearly difficult to work around. How do you know if the user intended the option argument to be literally "mt" and not the options -m and -t?
You might be able to work around this using getopt (see the canonical example) using an optional argument for a long option (those require an equal sign like --long=value) so it's maybe easier to check if the option argument is missing or not.
Translating getopts parsing to getopt -- it's more verbose, but you have finer-grained control
die() { echo "$*" >&2; exit 1; }
tmpArgs=$(getopt -o 'dBhmt' \
--long 'b::,P::' \
-n "$(basename "$0")" \
-- "$#"
)
(( $? == 0 )) || die 'Problem parsing options'
eval set -- "$tmpArgs"
while true; do
case "$1" in
-d) echo d; shift ;;
-B) echo B; shift ;;
-h) echo h; shift ;;
-m) echo m; shift ;;
-t) echo t; shift ;;
--P) case "$2" in
'') echo "P with no argument" ;;
*) echo "P: $2" ;;
esac
shift 2
;;
--b) case "$2" in
'') echo "b with no argument" ;;
*) echo "b: $2" ;;
esac
shift 2
;;
--) shift; break ;;
*) printf "> %q\n" "$#"
die 'getopt internal error: $*' ;;
esac
done
echo "Remaining arguments:"
for ((i=1; i<=$#; i++)); do
echo "$i: ${!i}"
done
Successfully invoking the program with --P:
$ ./myscript.sh --P -mt foo bar
P with no argument
m
t
Remaining arguments:
1: foo
2: bar
$ ./myscript.sh --P=arg -mt foo bar
P: arg
m
t
Remaining arguments:
1: foo
2: bar
This does impose higher overhead on your users, because -P (with one dash) is invalid, and the argument must be given with =
$ ./myscript.sh --P arg -mt foo bar
P with no argument
m
t
Remaining arguments:
1: arg
2: foo
3: bar
$ ./myscript.sh --Parg mt foo bar
myscript.sh: unrecognized option `--Parg'
Problem parsing options
$ ./myscript.sh -P -mt foo bar
myscript.sh: invalid option -- P
Problem parsing options
$ ./myscript.sh -P=arg -mt foo bar
myscript.sh: invalid option -- P
myscript.sh: invalid option -- =
myscript.sh: invalid option -- a
myscript.sh: invalid option -- r
myscript.sh: invalid option -- g
Problem parsing options
Do not mix logic with arguments parsing.
Prefer lower case variables.
My aim is to continue at my comment, when there is no $OPTARG set for -P
I advise not to. The less you do at one scope, the less you have to think about. Split parsing options and executing actions in separate stages. I advise to:
# set default values for options
do_something_related_to_P=false
recursive=false
tree_output=false
# parse arguments
while getopts ':dBhmtb:P:' option; do
case "$option" in
t) tree_output=true; ;;
r) recursive="$OPTARG"; ;;
P) do_something_related_to_P="$OPTARG"; ;;
\?) echo "?";;
:) echo "test -$OPTARG requieres an argument" >&2
esac
done
# application logic
if "$do_something_related_to_P"; then
do something related to P
if "$recursive"; then
do it in recursive style
fi
fi |
if "$tree_output"; then
output_as_tree
else
cat
fi
Example of "don't put programming application logic in the case branches" -- the touch command can take a -t timespec option or a -r referenceFile option but not both:
$ touch -t 202010100000 -r file1 file2
touch: cannot specify times from more than one source
Try 'touch --help' for more information.
I would implement that like (ignoring other options):
while getopts t:r: opt; do
case $opt in
t) timeSpec=$OPTARG ;;
r) refFile=$OPTARG ;;
esac
done
shift $((OPTIND-1))
if [[ -n $timeSpec && -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
I would not do this:
while getopts t:r: opt; do
case $opt in
t) if [[ -n $refFile ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
timeSpec=$OPTARG ;;
r) if [[ -n $timeSpec ]]; then
echo "touch: cannot specify times from more than one source" >&2
exit 1
fi
refFile=$OPTARG ;;
esac
done
You can see if the logic gets more complicated (as I mentioned, exactly one of -a or -b or -c), that the case statement size can easily balloon unmaintainably.

Pass wildcard to scp in a wrapper bash script

I am writing a bash wrapper for scp'ing into and from a certain host with a certain username, like:
johny#bonjour:~/bin$ cat scpphcl
#!/bin/bash
download=false
upload=false
local=""
remote=""
usage()
{
echo "Usage: $0 -d[-u] -l <LocalPath> -r <RemotePath>"
exit 1
}
while getopts "h?dul:r:" opt; do
case "$opt" in
h|\?)
usage
;;
d)
download=true
upload=false
;;
u)
download=false
upload=true
;;
l)
local=$OPTARG
;;
r)
remote=$OPTARG
;;
esac
done
if [[ -z $local || -z $remote ]]; then
echo "Need to provide local and remote path."
usage
fi
if $download; then
scp somebody#somehost:"$remote" $local
elif $upload; then
scp $local somebody#somehost:"$remote"
else
echo "Neither download nor upload?"
exit 1
fi
if [[ $? -ne 0 ]]; then
echo "Something wrong happened in the scp process."
exit 1
fi
exit 0
It works well with the usual filenames, but if there is any wildcard in the local filename field, it will not work right.
johny#bonjour:~/test$ scpphcl -u -l * -r /u/somebody/temp
Need to provide local and remote path.
Usage: /Users/johny/bin/scpphcl -d[-u] -l <LocalPath> -r <RemotePath>
There is a walkaround, using sinqle quotes around the local file argument if there is a wildcard in it:
johny#bonjour:~/test$ scpphcl -u -l '*' -r /u/somebody/temp
But even this walkaround will not work, if the command is issued outside the folder test:
johny#bonjour:~/test$ cd ..
johny#bonjour:~$ scpphcl -u -l 'test/*' -r /u/somebody/temp
This doesn't work and will hang in the scp process.
Any help in how to pass the wildcard in local filenames with the bash wrapper?
It's probably best not to require your users to quote wildcard patterns. I'd instead change the interface of your program to accept any number of local paths, after the option arguments:
echo "Usage: $0 [-d|-u] [-r <RemotePath>] <LocalPath>..."
When reading options, consume them with shift:
while getopts "h?dur:" opt; do
case "$opt" in
h|\?)
usage
exit 0
;;
d)
download=true
upload=false
;;
u)
download=false
upload=true
;;
r)
remote="$OPTARG"
;;
*)
usage >&2
exit 1
;;
esac
done
shift $((OPTIND-1))
Now the remaining positional arguments are the local filenames (and can be accessed with "$#" - note the all-important double-quotes there):
if test -z "$*" # no LocalPath arguments!
then usage >&2; exit 1
elif $download
then exec scp somebody#somehost:"$remote" "$#"
elif $upload
then exec scp "$#" somebody#somehost:"$remote"
fi

Bash getopts doesn't show error for second option

I want a script to take in two options, both are required. if I pass one in, the script doesn't print an error requesting you to pass in a second one.
-bash-4.2$ bash test.sh -b
Invalid option: b requires an argument
-bash-4.2$ bash test.sh -p
Invalid option: p requires an argument
-bash-4.2$ bash test.sh -b sdfsfd
-bash-4.2$ bash test.sh -p sdfsfd
-bash-4.2$ bash test.sh -b sdfsfd -s sfd
Invalid option: s
Code
showHelp()
{
cat << EOF
Find files in client's folder and upload to S3 bucket.
Usage: $(basename $0) [-p PATH_TO_SEARCH] [-b S3 bucket]
OPTIONS:
-h Show this help message
-p Path to search
-b S3 Bucket
EOF
exit 1
}
while getopts ":p:b:h" o; do
case "${o}" in
h)
showHelp
;;
p)
p=${OPTARG}
;;
b)
b=${OPTARG}
;;
\? )
echo "Invalid option: $OPTARG";;
: )
echo "Invalid option: ${OPTARG} requires an argument";;
esac
done
shift $((OPTIND-1))
if [ -z "${p}" ]; then
showHelp
fi
if [ -z "${b}" ]; then
showHelp
fi
If you want to ensure you get both options, you can use something like:
no_p=1
no_b=1
while getopts ":p:b:h" o; do
case "${o}" in
h)
showHelp
;;
p)
p=${OPTARG}
no_p=0
;;
b)
b=${OPTARG}
no_b=0
;;
\? )
echo "Invalid option: $OPTARG";;
: )
echo "Invalid option: ${OPTARG} requires an argument";;
esac
done
[[ $no_p -eq 1 ]] && echo "No -p provided" && exit 1
[[ $no_b -eq 1 ]] && echo "No -b provided" && exit 1

How to pass in shell scripts mandatory and optional flags in command line using getopts?

I want to pass 3 parameters with getopts to my shell script. The script requires at least the first 2, the third parameter is optional. If it is not set, its default is used. So that the following would both work:
sh script.sh -a "/home/dir" -b 3
sh script.sh -a "/home/dir" -b 3 -c "String"
I tried to do it like the following, but it constantly ignores my entered parameters.
usage() {
echo "Usage: Script -a <homedir> -b <threads> -c <string>"
echo "options:"
echo "-h show brief help"
1>&2; exit 1;
}
string="bla"
while getopts h?d:t:a: args; do
case $args in
-h|\?)
usage;
exit;;
-a ) homedir=d;;
-b ) threads=${OPTARG};;
-c ) string=${OPTARG}
((string=="bla" || string=="blubb")) || usage;;
: )
echo "Missing option argument for -$OPTARG" >&2; exit 1;;
* )
echo "Unimplemented option: -$OPTARG" >&2; exit 1;;
esac
done
I'm new to this getopts, before I just added the parameters in a specific order, which I don't want to do here. I have read a lot questions in here, but unfortenatly didn't find it the way I need it.
I really would appriciate your help here. Thanks:)
You have several mistakes in your script. Most importantly, $args only contains the letter of the option, without the leading dash. Also the option string you gave to getopts (h?d:t:a:) doesn't fit to the options you actually handle (h, ?, a, b, c). Here is a corrected version of the loop:
while getopts "h?c:b:a:" args; do
case $args in
h|\?)
usage;
exit;;
a ) homedir=d;;
b ) threads=${OPTARG};;
c ) string=${OPTARG}
echo "Entered string: $string"
[[ $string=="bla" || $string=="blubb" ]] && usage;;
: )
echo "Missing option argument for -$OPTARG" >&2; exit 1;;
* )
echo "Unimplemented option: -$OPTARG" >&2; exit 1;;
esac
done

How to create a bash script with optional parameters for a flag

I'm trying to create a script which will have a flag with optional options. With getopts it's possible to specify a mandatory argument (using a colon) after the flag, but I want to keep it optional.
It will be something like this:
./install.sh -a 3
or
./install.sh -a3
where 'a' is the flag and '3' is the optional parameter that follows a.
Thanks in advance.
The getopt external program allows options to have a single optional argument by adding a double-colon to the option name.
# Based on a longer example in getopt-parse.bash, included with
# getopt
TEMP=$(getopt -o a:: -- "$#")
eval set -- "$TEMP"
while true ; do
case "$1" in
-a)
case "$2" in
"") echo "Option a, no argument"; shift 2 ;;
*) echo "Option a, argument $2"; shift 2;;
esac ;;
--) shift; break ;;
*) echo "Internal error!"; exit 1 ;;
esac
done
The following is without getopt and it takes an optional argument with the -a flag:
for WORD; do
case $WORD in
-a?) echo "single arg Option"
SEP=${WORD:2:1}
echo $SEP
shift ;;
-a) echo "split arg Option"
if [[ ${2:0:1} != "-" && ${2:0:1} != ""]] ; then
SEP=$2
shift 2
echo "arg present"
echo $SEP
else
echo "optional arg omitted"
fi ;;
-a*) echo "arg Option"
SEP=${WORD:2}
echo $SEP
shift ;;
-*) echo "Unrecognized Short Option"
echo "Unrecognized argument"
;;
esac
done
Other options/flags also can be added easily.
Use the getopt feature. On most systems, man getopt will yield documentation for it, and even examples of using it in a script. From the man page on my system:
The following code fragment shows how one might process the arguments
for a command that can take the options -a and -b, and the option -o,
which requires an argument.
args=`getopt abo: $*`
# you should not use `getopt abo: "$#"` since that would parse
# the arguments differently from what the set command below does.
if [ $? != 0 ]
then
echo 'Usage: ...'
exit 2
fi
set -- $args
# You cannot use the set command with a backquoted getopt directly,
# since the exit code from getopt would be shadowed by those of set,
# which is zero by definition.
for i
do
case "$i"
in
-a|-b)
echo flag $i set; sflags="${i#-}$sflags";
shift;;
-o)
echo oarg is "'"$2"'"; oarg="$2"; shift;
shift;;
--)
shift; break;;
esac
done
echo single-char flags: "'"$sflags"'"
echo oarg is "'"$oarg"'"
This code will accept any of the following as equivalent:
cmd -aoarg file file
cmd -a -o arg file file
cmd -oarg -a file file
cmd -a -oarg -- file file
In bash there is some implicit variable:
$#: contains number of arguments for a called script/function
$0: contains names of script/function
$1: contains first argument
$2: contains second argument
...
$n: contains n-th argument
For example:
#!/bin/ksh
if [ $# -ne 2 ]
then
echo "Wrong number of argument - expected 2 : $#"
else
echo "Argument list:"
echo "\t$0"
echo "\t$1"
echo "\t$2"
fi
My solution:
#!/bin/bash
count=0
skip=0
flag="no flag"
list=($#) #put args in array
for arg in $# ; do #iterate over array
count=$(($count+1)) #update counter
if [ $skip -eq 1 ]; then #check if we have to skip this args
skip=0
continue
fi
opt=${arg:0:2} #get only first 2 chars as option
if [ $opt == "-a" ]; then #check if option equals "-a"
if [ $opt == $arg ] ; then #check if this is only the option or has a flag
if [ ${list[$count]:0:1} != "-" ]; then #check if next arg is an option
skip=1 #skip next arg
flag=${list[$count]} #use next arg as flag
fi
else
flag=${arg:2} #use chars after "-a" as flag
fi
fi
done
echo $flag

Resources