How to use getopt with long options in Bash? - bash

I have the following code in Bash:
declare {BPM_USERNAME,BPM_PASSWORD,HOST,TARGET_IP,OVERRIDE_STATUS}=''
OPTS=`getopt -a --longoptions username:,password:,csc:,ip:,override: -n "$0" -- "$#"`
eval set -- "$OPTS"
if [ $? != 0 ] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi
while true; do
echo ""
echo $OPTS
echo $1
echo $2
case "$1" in
--username )
BPM_USERNAME=$2
shift 2
;;
--password )
BPM_PASSWORD=$2
shift 2
;;
--csc )
HOST=$2
shift 2
;;
--ip )
TARGET_IP=$2
shift 2
;;
--override )
OVERRIDE_STATUS=$2
shift 2
;;
--)
shift
echo "Breaking While loop"
break
;;
*)
echo ""
echo "Error in given Parameters. Undefined: "
echo $*
echo ""
echo "Usage: $0 [--username BPM_USERNAME] [--password BPM_PASSWORD] [--ip IP ADDRESS_OF_VyOS/BPM] [--csc CLIENT_SHORT_CODE] [--override TRUE/FALSE]"
exit 1
esac
done
I give Bash the following command (name of script is UpdateSSL.sh):
./UpdateSSL.sh -username bpmadmin -password bpmadmin -ip 10.91.201.99 -csc xyz -override False
But instead of parsing the options, I get back the following result (showing that the while loop goes to the *) case):
'bpmadmin' --password 'bpmadmin' --ip '10.91.201.99' --csc 'xyz' --override 'False' --
bpmadmin
--password
Error in given Parameters. Undefined:
bpmadmin --password bpmadmin --ip 10.91.201.99 --csc xyz --override False --
Usage: ./UpdateSSL.sh [--username BPM_USERNAME] [--password BPM_PASSWORD] [--ip IP ADDRESS_OF_VyOS/BPM] [--csc CLIENT_SHORT_CODE] [--override TRUE/FALSE]
I don't know what I'm doing wrong.

The answer is actually at the very end of the man page:
The syntax if you do not want any short option variables at all is not very intuitive (you have to set them explicitly to the empty string).
In order to make getopt run with no short options, you have to manually specify -o '' as the first argument. I made some other changes, and the below works on my system (see *** markers):
#!/bin/bash
# *** Make sure you have a new enough getopt to handle long options (see the man page)
getopt -T &>/dev/null
if [[ $? -ne 4 ]]; then echo "Getopt is too old!" >&2 ; exit 1 ; fi
declare {BPM_USERNAME,BPM_PASSWORD,HOST,TARGET_IP,OVERRIDE_STATUS}=''
OPTS=$(getopt -o '' -a --longoptions 'username:,password:,csc:,ip:,override:' -n "$0" -- "$#")
# *** Added -o '' ; surrounted the longoptions by ''
if [[ $? -ne 0 ]] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi
# *** This has to be right after the OPTS= assignment or $? will be overwritten
set -- $OPTS
# *** As suggested by chepner
while true; do
# ... no changes in the while loop
done

Related

Giving a bash script the option to accept flags like a command [duplicate]

This question already has answers here:
How do I parse command line arguments in Bash?
(40 answers)
Closed 2 years ago.
I'm writing a simple bash script and I would like it to accept parameters from the command line in any order.
I've browsed around the web and wrote a simple function with a case statement in a while loop. Right now, the 'any order' part works - but it only picks up the first parameter I set. I'm certainly doing something wrong but scripting is quite new to me and I hadn't been able to figure it out - your help would be greatly appreciated. The flags part of the script is as follows:
#Parameters - source,destination,credentials,bandwidth,timeout,port,help
flags () {
while test $# -gt 0; do
case "$1" in
-s|--source)
shift
if test $# -gt 0; then
export SOURCE=$1
else
echo "No source directory specified!"
exit 1
fi
;;
-d|--destination)
shift
if test $# -gt 0; then
export DESTINATION=$1
fi
;;
-c|--credentials)
shift
if test $# -gt 0; then
export CREDENTIALS=$1
fi
;;
-b|--bandwidth)
shift
if test $# -gt 0; then
export BANDWIDTH=$1
fi
;;
-t|--timeout)
shift
if test $# -gt 0; then
export TIMEOUT=$1
fi
;;
-p|--port)
shift
if test $# -gt 0; then
export PORT=$1
fi
;;
-h|--help)
shift
if test $# -gt 0; then
echo "Help goes here"
fi
;;
-l|--compression-level)
shift
if test $# -gt 0; then
export COMPRESS_LEVEL=$1
fi
;;
*)
break
;;
esac
done
}
flags "$#"
echo "source is $SOURCE, destination is $DESTINATION, credentials are $CREDENTIALS, bandwidth is $BANDWIDTH, timeout is $TIMEOUT, port is $PORT"
Ideally, some of those parameters would be mandatory, and others optional - but that's not a must.
How can I fix this script to accept any of those parameters (both long and short forms, ideally) in any order?
As noted in the comments, after you consume the argument (for example for credentials), you need another shift. You should be consistent in your error reporting for non-existent arguments. If you get -h or --help, you should simply print the help and exit; you should not test for more arguments. If help is requested, you give it and do nothing else. You should also echo errors to standard error: echo "message" >&2. Your messages should be prefixed with the script/program name: arg0=$(basename "$0" .sh) and echo "$arg0: message" >&2 etc.
Putting the changes together, you might come up with a script like this:
#!/bin/sh
arg0=$(basename "$0" .sh)
blnk=$(echo "$arg0" | sed 's/./ /g')
usage_info()
{
echo "Usage: $arg0 [{-s|--source} source] [{-d|--destination} destination] \\"
echo " $blnk [{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \\"
echo " $blnk [{-t|--timeout} timeout] [{-p|--port} port] \\"
echo " $blnk [-h|--help] [{-l|--compression-level} level]"
}
usage()
{
exec 1>2 # Send standard output to standard error
usage_info
exit 1
}
error()
{
echo "$arg0: $*" >&2
exit 1
}
help()
{
usage_info
echo
echo " {-s|--source} source -- Set source directory (default: .)"
echo " {-d|--destination} destination -- Set destination"
echo " {-c|--credentials} credentials -- Set credentials"
echo " {-b|--bandwidth} bandwidth -- Set maximum bandwidth"
echo " {-t|--timeout} timeout -- Set timeout (default: 60s)"
echo " {-p|--port} port -- Set port number (default: 1234)"
echo " {-l|--compression-level} level -- Set compression level (default: 1)"
echo " {-h|--help} -- Print this help message and exit"
# echo " {-V|--version} -- Print version information and exit"
exit 0
}
flags()
{
while test $# -gt 0
do
case "$1" in
(-s|--source)
shift
[ $# = 0 ] && error "No source directory specified"
export SOURCE="$1"
shift;;
(-d|--destination)
shift
[ $# = 0 ] && error "No destination specified"
export DESTINATION="$1"
shift;;
(-c|--credentials)
shift
[ $# = 0 ] && error "No credentials specified"
export CREDENTIALS="$1"
shift;;
(-b|--bandwidth)
shift
[ $# = 0 ] && error "No bandwidth specified"
export BANDWIDTH="$1"
shift;;
(-t|--timeout)
shift
[ $# = 0 ] && error "No timeout specified"
export TIMEOUT="$1"
shift;;
(-p|--port)
shift
[ $# = 0 ] && error "No port specified"
export PORT="$1"
shift;;
(-l|--compression-level)
shift
[ $# = 0 ] && error "No compression level specified"
export COMPRESS_LEVEL="$1"
shift;;
(-h|--help)
help;;
# (-V|--version)
# version_info;;
(*) usage;;
esac
done
}
flags "$#"
echo "source is $SOURCE"
echo "destination is $DESTINATION"
echo "credentials are $CREDENTIALS"
echo "bandwidth is $BANDWIDTH"
echo "timeout is $TIMEOUT"
echo "port is $PORT"
Sample run (script name: flags53.sh):
$ sh flags53.sh -c XYZ -d PQR -s 123 -l 4 -t 99 -b 12 -p 56789
source is 123
destination is PQR
credentials are XYZ
bandwidth is 12
timeout is 99
port is 56789
$ sh flags53.sh -c XYZ --destination PQR -s 123 -l 4 --timeout 99 -b 12 --port 56789
source is 123
destination is PQR
credentials are XYZ
bandwidth is 12
timeout is 99
port is 56789
$ sh flags53.sh -c XYZ -h
Usage: flags53 [{-s|--source} source] [{-d|--destination} destination] \
[{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \
[{-t|--timeout} timeout] [{-p|--port} port] \
[-h|--help] [{-l|--compression-level} level]
{-s|--source} source -- Set source directory (default: .)
{-d|--destination} destination -- Set destination
{-c|--credentials} credentials -- Set credentials
{-b|--bandwidth} bandwidth -- Set maximum bandwidth
{-t|--timeout} timeout -- Set timeout (default: 60s)
{-p|--port} port -- Set port number (default: 1234)
{-l|--compression-level} level -- Set compression level (default: 1)
{-h|--help} -- Print this help message and exit
$
Note that requested help can go to standard output instead of standard error, though sending the help to standard error would not be an egregious crime. The help gets the usage message and extra information about the meaning of each option. Noting defaults (and setting them) is a good idea too. It may not be necessary to export the settings — you could simply set the variables without an explicit export. You should really set the variables to their defaults before calling the flags function, or at the start of the flags function. This avoids accidentally inheriting exported values (environment variables). Unless, of course, you want to accept environment variables, but then your names should probably be given a systematic prefix appropriate for the script name. Most programs should have a --version or -V option (use -v for 'verbose', not for version). If the command does not accept any non-option (file name) arguments, add a check after the parsing loop and complain about unwanted arguments. If the command must have at least one non-option argument, check that instead. Do not report an error on receiving -- as an argument; terminate the checking loop and treat any remaining arguments as non-option arguments.
One residual problem — the shifts in the function affect the function's argument list, not the global "$#". You'd have to work out how to deal with that from this skeleton. I think I'd probably create an analogue to $OPTIND that reports how many arguments to shift to get to the non-option arguments. The code in the flags function should keep track of how many arguments it shifts.
That leads to the revised code:
#!/bin/sh
arg0=$(basename "$0" .sh)
blnk=$(echo "$arg0" | sed 's/./ /g')
usage_info()
{
echo "Usage: $arg0 [{-s|--source} source] [{-d|--destination} destination] \\"
echo " $blnk [{-c|--credentials} credentials] [{-b|--bandwidth} bandwidth] \\"
echo " $blnk [{-t|--timeout} timeout] [{-p|--port} port] \\"
echo " $blnk [-h|--help] [{-l|--compression-level} level]"
}
usage()
{
exec 1>2 # Send standard output to standard error
usage_info
exit 1
}
error()
{
echo "$arg0: $*" >&2
exit 1
}
help()
{
usage_info
echo
echo " {-s|--source} source -- Set source directory (default: .)"
echo " {-d|--destination} destination -- Set destination"
echo " {-c|--credentials} credentials -- Set credentials"
echo " {-b|--bandwidth} bandwidth -- Set maximum bandwidth"
echo " {-t|--timeout} timeout -- Set timeout (default: 60s)"
echo " {-p|--port} port -- Set port number (default: 1234)"
echo " {-l|--compression-level} level -- Set compression level (default: 1)"
echo " {-h|--help} -- Print this help message and exit"
# echo " {-V|--version} -- Print version information and exit"
exit 0
}
flags()
{
OPTCOUNT=0
while test $# -gt 0
do
case "$1" in
(-s|--source)
shift
[ $# = 0 ] && error "No source directory specified"
export SOURCE="$1"
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-d|--destination)
shift
[ $# = 0 ] && error "No destination specified"
export DESTINATION=$1
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-c|--credentials)
shift
[ $# = 0 ] && error "No credentials specified"
export CREDENTIALS=$1
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-b|--bandwidth)
shift
[ $# = 0 ] && error "No bandwidth specified"
export BANDWIDTH=$1
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-t|--timeout)
shift
[ $# = 0 ] && error "No timeout specified"
export TIMEOUT="$1"
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-p|--port)
shift
[ $# = 0 ] && error "No port specified"
export PORT=$1
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-l|--compression-level)
shift
[ $# = 0 ] && error "No compression level specified"
export COMPRESS_LEVEL="$1"
shift
OPTCOUNT=$(($OPTCOUNT + 2));;
(-h|--help)
help;;
# (-V|--version)
# version_info;;
(--)
shift
OPTCOUNT=$(($OPTCOUNT + 1))
break;;
(*) usage;;
esac
done
echo "DEBUG-1: [$*]" >&2
echo "OPTCOUNT=$OPTCOUNT" >&2
}
flags "$#"
echo "DEBUG-2: [$*]" >&2
echo "OPTCOUNT=$OPTCOUNT" >&2
shift $OPTCOUNT
echo "DEBUG-3: [$*]" >&2
echo "source is $SOURCE"
echo "destination is $DESTINATION"
echo "credentials are $CREDENTIALS"
echo "bandwidth is $BANDWIDTH"
echo "timeout is $TIMEOUT"
echo "port is $PORT"
There are other ways of writing the arithmetic if you wish to experiment. Don't use expr though.

Shell getopt first parameter error

Running the following code, I find host_ip is empty, I don't know what the reason is?
TEMP=`getopt --long hostip:,hostport: -n 'javawrap' -- "$#"`
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
eval set -- "$TEMP"
host_ip=
host_port=
while true; do
case "$1" in
--hostip ) host_ip="$2"; shift 2;;
--hostport ) host_port="$2"; shift 2 ;;
* ) break ;;
esac
done
echo $host_ip
echo $host_port
It seems you need to specify the short options to getopt otherwise it (IMO) messes up the parsing. From man getopt:
If this option is not found, the first parameter of getopt that does not start with a '-' (and is not an option argument) is used as the short options string.
This works:
$ getopt --options '' --longoptions hostip:,hostport: -n 'javawrap' -- --hostip foo --hostport bar
--hostip 'foo' --hostport 'bar' --

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 detect if an argument was passed to the script?

I am a novice at shell scripting. I have written a script that takes zero or more options and an optional path parameter. I want to use the current directory if a path parameter is not set.
This is the argument parsing section of the script:
OPTIONS=$(getopt -o dhlv -l drop-databases,help,learner-portal,verifier-portal -- "$#")
if [ $? -ne 0 ]; then
echo "getopt error"
exit 1
fi
eval set -- $OPTIONS
while true; do
case "$1" in
-d|--drop-databases) RESETDB=1
;;
-h|--help) echo "$usage"
exit
;;
-l|--learner-portal) LERPOR=1
;;
-v|--verifier-portal) VERPOR=1
;;
--) shift
break;;
*) echo -e "\e[31munknown option: $1\e[0m"
echo "$usage"
exit 1
;;
esac
shift
done
# Set directory of module
if [[ -n $BASH_ARGV ]]
then
MOD_DIR=$(readlink -f $BASH_ARGV)
fi
if [[ -n $MOD_DIR ]]
then
cd $MOD_DIR
fi
The script works as intended when called without and arguments, or when called with both options and a path.
However, when I run the script and only specify options, I get an error from readlink like so
$ rebuild_module -dv
readlink: invalid option -- 'd'
Try 'readlink --help' for more information.
Obviously, it's parsing the options wrong, but I'm not sure how to detect that I haven't passed a path, and therefore avoid calling readlink. How can I go about correcting this behaviour?
You can do [ $# -ne 0 ] instead of [[ -n $BASH_ARGV ]]. The former is affected by shift/set, but the latter isn't:
$ cat test.sh
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
eval set -- foo bar
shift
echo "$#"
echo "${BASH_ARGV[#]}"
echo "$#"
$ bash test.sh x y z
3
z y x
x y z
1
z y x
bar

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