Failing to read value multi-word command line argument in bash - bash

I am working on writing a bash shell script that has 4 command-line arguments. 3/4 arguments are one word and 1/4 is multi-word. I managed to get values for 2 of them, but can't get right values for 3rd and fourth. If I remove multi-work argumnent however, it works.
options=$(getopt -l "help,env:,site:,cluster:,cluster-group:" -o "he:s:c:cg:" -a -- "$#")
eval set -- "$options"
while true
do
case $1 in
-h|--help)
showHelp
exit 0
;;
-e|--env)
shift
export environment=$1
;;
-s|--site)
shift
export site=$1
;;
-c|--cluster)
shift
export cluster=$1
;;
-cg|--cluster-group)
shift
export cluster_group=$1
;;
--)
shift
break;;
esac
shift
done
echo $environment
echo $site
echo $cluster
echo $cluster_group
When ran sh b.sh -s S1 -e E1 -c C1 -cg CG1, output is
E1
S1
g
What am I doing wrong here?

As per man getopt(1): -o recognizes only one-character options.
-o, --options shortopts
The short (one-character) options to be recognized. 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. Each short
option character in shortopts may be followed by one colon
to indicate it has a required argument, and by two colons
to indicate it has an optional argument. The first
character of shortopts may be '+' or '-' to influence the
way options are parsed and output is generated (see
section SCANNING MODES for details).
So in your case ,you could mention only a single letter, something like :g after -o option for the cluster-group.
options=$(getopt -l "help:,env:,site:,cluster:,cluster-group:" -o "h:e:s:c:g:" -a -- "$#")

Related

How to access multiple options for a command line flag in bash

I want to access multiple command line inputs for flags, but I can't get it to work. The input order is out of my control, with the format being (# are numbers, not comments)
./program.sh -a -b # #
./program.sh -b # # -a
./program.sh -b # #
-a has no options, it is just a toggle on/off
-b is always followed by two numbers.
I have tried using getopts, and that works for -a and the first number of -b, but i cannot access the second number. As sometimes -a comes after -b, treating the 'remainder' of the input as a string doesn't work as intended.
I tried using a loop that when it found -b, looked at the next two values to set, shown below:
for i in "$#"; do
case "$i" in
-a)
upperCase=true;
;;
-b)
first=$(($i+1));
second=$(($i+2));
;;
*)
;;
esac
done
output should be the letters from # to # in both directions printed, but I have got that working, my only issue is actually receiving the input.
Maybe this loop would work, instead:
while [[ $# -gt 0 ]]
do
case "$1" in
-a)
upperCase=true;
;;
-b)
first=$2; # Take the next two arguments
second=$3;
shift 2 # And shift twice to account for them
;;
*)
;;
esac
shift # Shift each argument out after processing them
done
$(($i+1)) is just adding one to the variable i, instead of taking the next positional parameter as you wanted.

Getopt generates a double-dash (--) even if there's none on the command line, and doesn't validate an extraneous argument

I'm learning the getopt command and using the following diagnostic script to study its workings:
$ cat test-getopt.sh
#!/bin/bash
args=`getopt ab:c $*`
set -- $args
for i
do
echo "-->$i"
done
echo $#
I cannot understand its behavour in the following cases. Could you clarify?
1st case:
$ ./test-getopt.sh -ab arg -c
-->-a
-->-b
-->arg
-->-c
-->--
5
Why does getopt add -- as $5? What does it mean here? To point out the end of options?
2nd case:
$ ./test-getopt.sh -ab arg c
-->-a
-- -b
-->arg
-->--
-->c
5
Now, getopt adds c as $5's value, after that --. It is not a option, what does c mean here?
Which kind of element is it -- option, or option's argument, or positional argument?
It's not defined in getopt's parameter specifying valid options, why doesn't the program raise an error?
I've already skimmed through the getopt man page as well as some tutorials but couldn't quite work out a clear explanation.
According to getopt manpage:
Normally, no non-option parameters output is generated until all
options and their arguments have been generated. Then '--' is
generated as a single parameter, and after it the non-option
parameters in the order they were found, each as a separate parameter.
I.e. -- by itself is generated to signify the end of options. (And after it, positional parameters are generated if there are any.)
I guess this is done for uniformity -- to use the same code logic regardless of whether the user specified -- on the command line or not.
In the 2nd case, c is a positional argument. Positional arguments are not checked by getopt in any way and are rather passed as-is. The manpage doesn't say anything about validating non-option arguments:
getopt is used to break up (parse) options in command lines for easy
parsing by shell procedures, and to check for legal options.
Finally, note that to correctly process arguments with whitespace, you need to: use $# instead of $*; quoting; eval with set; and use the enhanced mode of getopt -- as per Example of how to parse options with bash/getopt. Also should use bash -e mode to quit the program on an invalid option:
#!/bin/bash -e
args=`getopt -o ab:c -- "$#"`
eval set -- "$args"
for i
do
echo "-->$i"
done
echo $#
$ ./test-getopt.sh -b "arg ument"
-->-b
-->arg ument
-->--
3
$ ./test-getopt.sh -d ; echo $?
getopt: unknown option -- d
1
Also, a while loop with shift as per the same example could be more convenient that for as it: makes it easy to get the next argument -- to get the option's argument and check if there is an argument if it's optional; check the number of the remaining (positional) arguments when you're done with options.
I normally use constructs like this to run getopts:
# Set defaults
opt_a=0; opt_b=""; opt_c=false
# Step through options
while getopts ab:c opt; do
case "$opt" in
a) opt_a=1 ;;
b) opt_b="${OPTARG:?The -b option requires an argument.}" ;;
c) opt_c=true ;;
*) usage; exit 64 ;;
esac
done
shift $((OPTIND - 1))
Use of shift like this at the end causes your positional arguments to be shifted back such that the first argument that getopts can't process becomes $1. For example, if the above snippet was part of a script named foo, one might run:
$ foo -ab meh smoo blarg
which would set $opt_a to 1, $opt_b to "meh", $1 to "smoo" and $2 to "blarg" for the portion of the script following the snippet.

Passing command line options to invoked script in bash

Suppose I have a script a.sh to be invoked with options
a.sh -a1 a1option -a2 a2option
Suppose also I have a script b.sh, which invokes a.sh and uses its own options. So user executes the scripts as follows:
b.sh -b1 b1option -b2 b2option -a1 a1option -a2 a2option
Now I wonder how to parse the command line options in b.sh.
I do not need to parse the entire command line. I do not want b.sh to be aware of options a1 and a2. I would like to get only options b1 and b2 and pass the rest to a.sh.
How would you do it ?
As requested, this method avoids parsing the entire command line. Only the arguments up to -- are collected for b.sh. Then the arguments for b are stripped and only the remaining arguments are passed to a.sh.
b.sh is invoked with b.sh -b b1option -B b2option -- -a1 a1option -a2 a2option. In this line, the double dash -- indicates the end of options for b.sh. The following parses the options before the -- for use by b.sh, then removes the b arguments from the $# so you can pass it to a.sh without worrying about what errors a.sh might give you.
while getopts ":b:B:" opt; do
case $opt in
b) B1=${OPTARG}
;;
B) B2=${OPTARG}
;;
esac
done
## strips off the b options (which must be placed before the --)
shift $(({OPTIND}-1))
a.sh "$#"
A note: This method utilizes the bash builtin getopts. Getopts (as opposed to getopt, no s) takes only single-character options; hence, I have used b and B instead of b1 and b2.
My favorite getopts reference.
You can do something like this:
#!/bin/bash
while [[ $# -gt 0 ]]; do
case "$1" in
-b1)
B1=true
B1OPT=$2
shift
;;
-b2)
B2=true
B2OPT=$2
shift
;;
--)
shift
break
;;
*)
echo "Invalid option: $1"
exit 1 ## Could be optional.
;;
esac
shift
done
bash a2.sh "$#"
Note that you should place your variable $# inside doublequotes to prevent word splitting when expanded.
If a.sh can ignore options it doesn't know you can just call it with all the options b.sh was called:
a.sh "${#}"

Parsing a flag with a list of values

I'm creating a bash script which involves parsing arguments. The usage would be:
$ ./my_script.sh -a ARG_1 -b ARG_2 [-c LIST_OF_ARGS...]
Using getopts I'm able to parse -a and -b and get their respective values ARG_1 and ARG_2. If and only if user places -c as last argument, then I'm also able to get -c and create a list with all values in LIST_OF_ARGS....
But I would not like to force user to insert -c as the last flag. For instance, it would be great if the script can be invoked this way:
$ ./my_script.sh -b ARG_2 -c V1 V2 V3 -a ARG_1
Here is my current code:
while getopts a:b:c opt
do
case $opt in
a)
A_FLAG=$OPTARG
;;
b)
B_FLAG=$OPTARG
;;
c)
# Handle values as regular expressions
args=("$#")
C_LIST=()
for (( i=$OPTIND-1 ; i <= $#-1 ; i++ ))
do
C_LIST=("${C_LIST[#]}" ${args[$i]})
done
;;
?)
usage
;;
esac
done
You need to separate your detection of the -c flag with the processing associated with it. For example, something like:
while getopts a:b:c opt
do
case $opt in
a)
A_FLAG=$OPTARG
;;
b)
B_FLAG=$OPTARG
;;
c)
C_FLAG=1
;;
?)
usage
;;
esac
done
# discard all of our options.
shift `expr $OPTIND - 1`
if [ "$C_FLAG" = 1 ]; then
# Handle values as regular expressions
args=("$#")
C_LIST=()
for (( i=0 ; i <= $#-1 ; i++ ))
do
C_LIST=("${C_LIST[#]}" ${args[$i]})
done
fi
This script doesn't collect all the non-option arguments until after processing all the command line options.
Here's a question: why have a -c option at all?
If the full usage involves a list of values, why not just have no -c option and allow the -a and -b options only while the rest are regular args as in ./myscript.sh -a ARG_1 -b ARG_2 [argument ...], where any arguments are optional (like the -c option and its arguments are in your usage example?
Then your question becomes "how do I intersperse program options and arguments", to which I would respond: "You shouldn't do this, but to achieve this anyway, parse the command line yourself; getopts won't work the way you want it to otherwise."
Of course, parsing is the hard way. Another possibility involves adding the values after -c to a list, so long as you don't encounter another option or the end of the options:
C_LIST=()
while getopts a:b:c: opt; do
#Skipping code...
c)
C_LIST+="$OPTARG"
shift $(expr $OPTIND - 1)
while [ -n "$1" ] && [ $(printf "%s" "$1" | grep -- '^[^-]') ]; do
C_LIST+="$1"
shift
done
OPTIND=1
;;
The behaviour of getopts is mimicked: even if OPTARG begins with a '-' character, it is still kept, but after OPTARG, any string starting with the '-' character may simply be an invalid option such as -n. I used printf instead of echo because some versions of echo, such as the one that bash has built-in, have a -e option that may or may not allow the loop to continue, which isn't desired. The grep expression should prevent this, but who knows if that version of echo allows for -e'hello', which would cause grep to succeed because it sees "hello"? While possibly unnecessary, why take chances?
Personally, I'd avoid this behaviour if you can, but I also don't understand why you're asking for this behaviour in the first place. If I were to recommend anything, I'd suggest the more common /path/to/script -a ARG_1 -b ARG_2 [argument ...] style above any other possible choice of implementation.
On my system, I haven a /usr/share/doc/util-linux/examples/getopt-parse.bash file. It puts the result of getopt into a variable, and set the positional parameters to that variable. Then uses a switch similar to yours, but uses shift to remove arguments when found.
You could do something similar, but for your -c option use shift until you get an option or run out of arguments.
Or it might be enough for you to use your current solution, but remember to set the OPTIND variable after the loop.

Pass a list of variables to a Bash script

I need to be able to read a list of variables that follow certain parameters (similar to, say, mysqldump --databases db1 db2 db3)
Basically the script should be invoked like this:
./charge.sh --notify --target aig wfc msft --amount 1bln
In the script itself I need to assign "aig wfc msft" either to a single variable or create an array out of them.
What would be a good way of doing that?
I often find the shift statement to be really useful in situations like this. In a while loop, you can test for expected options in a case statement, popping argument 0 off during every iteration with shift, until you either get to the end, or the first positional parameter.
When you get to the --target argument in the loop, you can use shift, to pop it off the argument list, then in a loop, append each argument to a list (in this case $TARGET_LIST) and shift, until you get to the end of the argument list, or the next option (when '$1' starts with '-').
NOTIFY=0
AMOUNT=''
TARGET_LIST=''
while :; do
case "$1" in
-h|--help)
echo "$HELP"
exit 0
;;
--notify)
NOTIFY=1
shift
;;
--amount)
shift; AMOUNT="$1"; shift
;;
--target)
shift
while ! echo "$1" | egrep '^-' > /dev/null 2>&1 && [ ! -z "$1" ]; do
TARGET_LIST="$TARGET_LIST $1"
shift
done
;;
-*)
# Unexpected option
echo $USAGE
exit 2
;;
*)
break
;;
esac
done
If you can invoke the script like this (note the quotes):
./charge.sh --notify --target "aig wfc msft" --amount 1bln
You can assign "aig wcf msft" to a single variable.
If you cannot change the way the script is invoked and if you can guarantee that the --target option arguments are always followed by another option or other delimiter, you could grab the arguments between them and store them in a variable.
var=$(echo $* | sed -e 's/.*--target\(.*\)--.*/\1/')

Resources