Can a shell script flag have optional arguments if parsing with getopts? - shell

I have a script that I want to run in three ways:
Without a flag -- ./script.sh
With a flag but no parameter -- ./script.sh -u
With a flag that takes a parameter -- ./script.sh -u username
Is there a way to do this?
After reading some guides (examples here and here) it doesn't seem like this is a possibility, especially if I want to use getopts.
Can I do this with getopts or will I need to parse my options another way? My goal is to continue using getopts if I can.

The non-getopts example in BashFAQ #35 can cover the use case:
user_set=0 # 1 if any -u is given
user= # set to specific string for -u, if provided
while :; do
case $1 in
-u=*) user_set=1; user=${1#*=} ;;
-u) user_set=1
if [ -n "$2" ]; then
user=$2
shift
fi ;;
--) shift; break ;;
*) break ;;
esac
shift
done

Related

Using getopts in Bash

I want to use getopts in a Bash script as follows:
while getopts ":hXX:h" opt; do
case ${opt} in
hXX ) Usage
;;
h ) echo "You pressed Hey"
;;
\? ) echo "Usage: cmd [-h] [-p]"
;;
esac
done
The idea behind is that I want to have two flags -h or --help for allowing user to be able to use HELP in order to be guided how to use the script and another second flag which starts with h but its like -hxx where x is whatever.
How can I distinguish these two since even when I press --hxx flag it automatically executes help flag. I think the order of presenting them in getopt has nothing to do with this.
The 'external' getopt program (NOT the bash built in getopts) has support for '--longoptions'. It can be used as a pre-procssor to the command line options, making it possible to consume long options with the bash built-in getopt (or to other programs that do not support long options).
See: Using getopts to process long and short command line options for more details.
#! /bin/bash
TEMP=$(getopt -l help -- h "$#")
eval set -- "$TEMP"
while getopts h-: opt ; do
case "$opt" in
h) echo "single" ;;
-) case "$OPTARG" in
-help) echo "Double" ;;
*) echo "Multi: $OPTARG" ;;
esac ;;
*) echo "ERROR: $opt" ;;
esac
done

getopt erroneously caches arguments

I've created a script in my bash_aliases to make SSH'ing onto servers easier. However, I'm getting some odd behavior that I don't understand. The below script works as you'd expect, except for when it's re-used.
If I use it like this for this first time in a shell, it works exactly as expected:
$>sdev -s myservername
ssh -i ~/.ssh/id_rsa currentuser#myservername.devdomain.com
However, if I run that a second time, without specifying -s|--server, it will use the server name from the last time I ran this, having seemingly cached it:
$>sdev
ssh -i ~/.ssh/id_rsa currentuser#myservername.devdomain.com
It should have exited with an error and output this message: /bin/bash: A server name (-s|--server) is required.
This happens with any of the arguments; that is, if I specify an argument, and then the next time I don't, this method will use the argument from the last time it was supplied.
Obviously, this is not the behavior I want. What's responsible in my script for doing that, and how do I fix it?
#!/bin/bash
sdev() {
getopt --test > /dev/null
if [[ $? -ne 4 ]]; then
echo "`getopt --test` failed in this environment"
exit 1
fi
OPTIONS=u:,k:,p,s:
LONGOPTIONS=user:,key:,prod,server:
# -temporarily store output to be able to check for errors
# -e.g. use “--options” parameter by name to activate quoting/enhanced mode
# -pass arguments only via -- "$#" to separate them correctly
PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTIONS --name "$0" -- "$#")
if [[ $? -ne 0 ]]; then
# e.g. $? == 1
# then getopt has complained about wrong arguments to stdout
exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"
domain=devdomain
user="$(whoami)"
key=id_rsa
# now enjoy the options in order and nicely split until we see --
while true; do
case "$1" in
-u|--user)
user="$2"
shift 2
;;
-k|--key)
key="$2".pem
shift 2
;;
-p|--prod)
domain=proddomain
shift
;;
-s|--server)
server="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Programming error"
exit 3
;;
esac
done
if [ -z "$server" ]; then
echo "$0: A server name (-s|--server) is required."
kill -INT $$
fi
echo "ssh -i ~/.ssh/$key.pem $user#$server.$domain.com"
ssh -i ~/.ssh/$key $user#$server.$domain.com
}
server is a global shell variable, so it's shared between runs of the function (as long as they're run in the same shell). That is, when you run sdev -s myservername, it sets the variable server to "myservername". Later, when you run just sdev, it checks to see if $server is empty, finds it's not, and goes ahead and uses it.
Solution: use local variables! Actually, it'd be best to declare all of the variables you use in the function as local; that way, you don't run the risk of interfering with something else that's trying to use the same variable name. I'd also recommend avoiding all-caps variable names (like OPTIONS, LONGOPTIONS, and PARSED) -- there are a bunch of all-caps variables that have special meanings to the shell and/or other programs, and if you use one of those by mistake it can cause weird problems.
Anyway, here's the simple solution: add this near the beginning of the script:
local server=""

Getopts in sourced Bash function works interactively, but not in test script?

I have a Bash function library and one function is proving problematic for testing. prunner is a function that is meant to provide some of the functionality of GNU Parallel, and avoid the scoping issues of trying to use other Bash functions in Perl. It supports setting a command to run against the list of arguments with -c, and setting the number of background jobs to run concurrently with -t.
In testing it, I have ended up with the following scenario:
prunner -c "gzip -fk" *.out - works as expected in test.bash and interactively.
find . -maxdepth 1 -name "*.out" | prunner -c echo -t 6 - does not work, seemingly ignoring -c echo.
Testing was performed on Ubuntu 16.04 with Bash 4.3 and on Mac OS X with Bash 4.4.
What appears to be happening with the latter in test.bash is that getopts is refusing to process -c, and thus prunner will try to directly execute the argument without the prefix command it was given. The strange part is that I am able to observe it accepting the -t option, so getopts is at least partially working. Bash debugging with set -x has not been able to shed any light on why this is happening for me.
Here is the function in question, lightly modified to use echo instead of log and quit so that it can be used separately from the rest of my library:
prunner () {
local PQUEUE=()
while getopts ":c:t:" OPT ; do
case ${OPT} in
c) local PCMD="${OPTARG}" ;;
t) local THREADS="${OPTARG}" ;;
:) echo "ERROR: Option '-${OPTARG}' requires an argument." ;;
*) echo "ERROR: Option '-${OPTARG}' is not defined." ;;
esac
done
shift $(($OPTIND-1))
for ARG in "$#" ; do
PQUEUE+=("$ARG")
done
if [ ! -t 0 ] ; then
while read -r LINE ; do
PQUEUE+=("$LINE")
done
fi
local QCOUNT="${#PQUEUE[#]}"
local INDEX=0
echo "Starting parallel execution of $QCOUNT jobs with ${THREADS:-8} threads using command prefix '$PCMD'."
until [ ${#PQUEUE[#]} == 0 ] ; do
if [ "$(jobs -rp | wc -l)" -lt "${THREADS:-8}" ] ; then
echo "Starting command in parallel ($(($INDEX+1))/$QCOUNT): ${PCMD} ${PQUEUE[$INDEX]}"
eval "${PCMD} ${PQUEUE[$INDEX]}" || true &
unset PQUEUE[$INDEX]
((INDEX++)) || true
fi
done
wait
echo "Parallel execution finished for $QCOUNT jobs."
}
Can anyone please help me to determine why -c options are not working correctly for prunner when lines are piped to stdin?
My guess is that you are executing the two commands in the same shell. In that case, in the second invocation, OPTIND will have the value 3 (which is where it got to on the first invocation) and that is where getopts will start scanning.
If you use getopts to parse arguments to a function (as opposed to a script), declare local OPTIND=1 to avoid invocations from interfering with each other.
Perhaps you are already doing this, but make sure to pass the top-level shell parameters to your function. The function will receive the parameters via the call, for example:
xyz () {
echo "First arg: ${1}"
echo "Second arg: ${2}"
}
xyz "This is" "very simple"
In your example, you should always be calling the function with the standard args so that they can be processed in the method via getopts.
prunner "$#"
Note that pruner will not modify the standard args outside of the function.

How make bash getopts not recognize option as a option argument

In my bash script, I would like to use getopts to parse command-line options.
My first attempt, to learn how to use it, is as follows:
#!/bin/bash
v_option_arg=""
r_option_arg=""
h_option_arg=""
function get_opt_args() {
while getopts ":v:r:h:" opt
do
case $opt in
"v")
v_option_arg="$OPTARG"
;;
"h")
h_option_arg="$OPTARG"
;;
"r")
r_option_arg="$OPTARG"
;;
"?")
echo "Unknown option -$OPTARG"
exit 1
;;
":")
echo "No argument value for option -$OPTARG"
;;
*)
# Should not occur
echo "Unknown error while processing options"
;;
esac
done
shift $((OPTIND-1))
}
get_opt_args $#
if [ ! -z "$v_option_arg" ]; then
echo "Argnument value for option -v: $v_option_arg"
fi
if [ ! -z "$r_option_arg" ]; then
echo "Argnument value for option -r: $r_option_arg"
fi
if [ ! -z "$h_option_arg" ]; then
echo "Argnument value for option -h: $h_option_arg"
fi
$ bash testopts.sh -v 1
Argnument value for option -v: 1
$ bash testopts.sh -r 2
Argnument value for option -r: 2
$ bash testopts.sh -h 3
Argnument value for option -h: 3
$ bash testopts.sh -v 1 -r 2 -h 3
Argnument value for option -v: 1
Argnument value for option -r: 2
Argnument value for option -h: 3
$ bash testopts.sh -v
No argument value for option -v
$ bash testopts.sh -a
Unknown option -a
This seems to work successfully.
Next, I test my script's robustness by omitting an argument:
$ bash testopts.sh -v -r 2
Argnument value for option -v: -r
This is not what I was expecting. How can I make it distinguish the differences of one option and one option argument?
I want to make my script more robust, so that if one option is given without its argument, I can emit a suitable error message.
Note: Each option must have a option argument.
Can I do this with just getopts?
From man bash, SHELL BUILTIN COMMANDS, getopts:
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.
From your script:
while getopts ":v:r:h:" opt
You told bash explicitly that -v expects an argument. In the case of -v -r 2, -r is the argument to -v, with 2 remaining as non-option argument to the script.
Works as designed, and this is the limit of getopts abilities.
What you can do is checking if the argument to -v is numeric (as it seems that is what your script expects), and in the given case inform the user that -v does require a number, and that -r isn't it. But that is something your script needs to do in the "v") case, not something getopts can handle.
case $opt in
"v")
v_option_arg="$OPTARG"
if [[ ! "${v_option_arg}" =~ ^[0-9]*$ ]]
then
echo "Error: Option '-v' expects numeric argument, you gave: ${v_option_arg}"
exit 1
fi
;;

Parsing mixed arguments in a script bash

I need to implement a script called with mixed (optional and non-optional) arguments for example -
./scriptfile -m "(argument of -m)" file1 -p file2 -u "(argument of -u)"
in a random order. I've read a lot about the getopts builtin command, but I think it doesn't solve my problem. I can't change the order of arguments, so I don't understand how I can read the arguments one by one.
Someone have any ideas?
You should really give a try to getopts, it is designed for that purpose :
Ex :
#!/bin/bash
while getopts ":a:x:" opt; do
case $opt in
a)
echo "-a was triggered with $OPTARG" >&2
;;
x)
echo "-x was triggered with $OPTARG" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
;;
esac
done
Running the script with different switches ordering :
$ bash /tmp/l.sh -a foo -x bar
-a was triggered with foo
-x was triggered with bar
$ bash /tmp/l.sh -x bar -a foo
-x was triggered with bar
-a was triggered with foo
As you can see, there's no problem to change the order of the switches
See http://wiki.bash-hackers.org/howto/getopts_tutorial
Consider using Python and its excellent built-in library argparse. It will support almost any reasonable and conventional command line options, and with less hassle than bash (which is, strangely, a fairly poor language when it comes to command line argument processing).

Resources