How do I iterate over bash script flags and use them? - bash

I have a code that I wrap with a bash script, and I want to know if a certain flag was given (-b), and if so, to update some variable I have in the script (called "x"). Unfortunately, I need this to be done on the bash script, and the synthax here drives me nuts. A small example of what I did is (without the call to the code I wrap):
#!/bin/bash
x=0
while getopts :b opt; do
case $opt in
b) x=1;;
?) echo "";;
esac
done
echo "x is ${x}"
My problem is with having more than one flag I want to pass to the command line.
I tried to use:
while getopts :b:f:c opt; do..., but it looks like it fails to work when I do not give the flags in the -b -f ... -c ... order (I get x is 0 when I expect it to be x is 1).
Moreover, this does not work when I want to give flags with the -- prefix (--frame instead of -f).
What should be the code for this purpose? I tried a few options but I do not manage to figure out what should the synthax exactly.
Thank you in advance.

TL;DR
Options with argument have a colon after their character in the optstring. So getopts :b:c:f means:
you have a -b option that takes an argument (b:)
you have a -c option that takes an argument (c:)
you have a -f option that takes no argument (f)
use silent error reporting (the leading colon)
If you use while getopts :b:f:c opt; do... in your script and you type:
./script.sh -c -b -f
-b is considered as the argument of option -c, not as an option itself.
Details
Considering this part of your question: "it looks like it fails to work when I do not give the flags in the -b -f ... -c ... order", let's assume you want the b option to have no argument, and the f and c options to have one.
If your option has no argument don't put a colon after it in the optstring parameter; else put the colon after the option character. With your simple example you could try:
#!/bin/bash
x=0
fopt=""
copt=""
while getopts bf:c: opt; do
case "$opt" in
b) x=1;;
f) echo "f option found"; fopt="$OPTARG";;
c) echo "c option found"; copt="$OPTARG";;
?) echo "";;
esac
done
echo "x is ${x}"
echo "fopt is ${fopt}"
echo "copt is ${copt}"
And then:
$ ./script.sh -b -c foo -f bar
c option found
f option found
x is 1
fopt is bar
copt is foo
$ ./script.sh -c foo -f bar
c option found
f option found
x is 0
fopt is bar
copt is foo
$ ./script.sh -c foo -b
c option found
x is 1
fopt is
copt is foo
But be careful: with the last example, if you omit the foo argument of the -c option:
./script.sh -c -b
c option found
x is 0
fopt is
copt is -b
See? -b became the missing argument and has not been considered as an option. Is it the cause of your problem?
Note: If you want to also use long options don't use the bash getopts builtin, use the getopt utility.

Related

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.

Can getopts parse a subset of a bash script's arguments and leave the rest intact?

I am using getopts to parse arguments in a bash script. I want to do two things:
remove processed options from "$#"
leave unprocessed options in "$#"
consider the command-line
$ foo -a val_a -b val_b -c -d -e -f val_f positional_l positional_2 ...
Where foo uses getopts to parse options defined by a optstring of 'b:c' and afterwards needs to leave "$#" as
`-a val_a -d -e -f val_f positional_l positional_2 ...`
I need to do two things:
parse a subset of options that may be given
leave all other opptions intact
The reason for this is because foo must use the options it recognises to determine another script bar to which it must pass the remaining "#".
Normally getopts stops when it encounters an unrecognised option but I need it to continue (up to any --). I need it to proceess and remove the options it recognises and leave alone those that it doesn't.
I did try to work around my problem using -- between the foo options and the bar options but getopts seems to baulk if the text following -- begins with a - (I tried but could not escape the hyphen).
Anyway I would prefer not to have to use -- because I want the existence of bar to be effectively transparent to the caller of foo, and I'd like the caller of foo to be able to present the options in any order.
I also tried listing all baroptions in foo (i.e. using 'a:b:cdef:'for the optstring) without processing them but I need to delete the processed ones from "$#" as they occur. I could not work out how to do that (shift doesn't allow a position to be specified).
I can manually reconstruct a new options list (see my own answer) but I wondered if there was a better way to do it.
Try the following, which only requires the script's own options to be known in advance:
#!/usr/bin/env bash
passThru=() # init. pass-through array
while getopts ':cb:' opt; do # look only for *own* options
case "$opt" in
b)
file="$OPTARG";;
c) ;;
*) # pass-thru option, possibly followed by an argument
passThru+=( "-$OPTARG" ) # add to pass-through array
# see if the next arg is an option, and, if not,
# add it to the pass-through array and skip it
if [[ ${#: OPTIND:1} != -* ]]; then
passThru+=( "${#: OPTIND:1}" )
(( ++OPTIND ))
fi
;;
esac
done
shift $((OPTIND - 1))
passThru+=( "$#" ) # append remaining args. (operands), if any
./"$file" "${passThru[#]}"
Caveats: There are two types of ambiguities that cannot be resolved this way:
For pass-thru options with option-arguments, this approach only works if the argument isn't directly appended to the option.
E.g., -a val_a works, but -aval_a wouldn't (in the absence of a: in the getopts argument, this would be interpreted as an option group and turn it into multiple options -a, -v, -a, -l, -_, -a).
As chepner points out in a comment on the question, -a -b could be option -a with option-argument -b (that just happens to look like an option itself), or it could be distinct options -a and -b; the above approach will do the latter.
To resolve these ambiguities, you must stick with your own approach, which has the down-side of requiring knowledge of all possible pass-thru options in advance.
You can manually rebuild the options list like this example which processes the -b and -c options and passes anything left intact.
#!/bin/bash
while getopts ":a:b:cdef:" opt
do
case "${opt}" in
b) file="$OPTARG" ;;
c) ;;
*) opts+=("-${opt}"); [[ -n "$OPTARG" ]] && opts+=("$OPTARG") ;;
esac
done
shift "$((OPTIND-1))"
./$file "${opts[#]}" "$#"
So
./foo -a 'foo bar' -b bar -c -d -e -f baz one two 'three and four' five
would invoke bar, given as the argument to option b, as
./bar -a 'foo bar' -d -e -f baz one two 'three and four' five
This solution suffers the disadvantage that the optstring must include the pass-through options (i.e. ":a:b:cdef:" instead of the preferable ":b:c").
Replacing the argument list with the reconstructed one can be done like this:
set -- "${opts[#]}" "$#"
which would leave "$#" containing the unprocessed arguments as specified in the question.

How to quote bash flag arguments to pass through one getopts call and be interpreted by a second?

Script nerf calls script herd, which calls script er. nerf uses a flag on herd that explicitly takes arguments needing to be passed to er.
This was not a problem before nerf existed - when herd was just called from the command line, we could single-quote the arguments to the -p flag, and they would never be interpreted by herd's getopts, but instead they would be interpreted by er's getopts.
But now we have generated values in the flags that eventually need to go to er, so I need to expand the variable $file_contents in nerf, but not let them be interpreted by getopts until they get to er.
Any of these three scripts can be modified.
$ cat nerf
#!/bin/bash
file_contents="`cat one_liner_file`"
er_args="-jkl -m $file_contents"
./herd -p "$er_args" # <-- the problem
$ cat herd
#!/bin/bash
passthru_args=""
while getopts "p:a:b:cde" opt
do
case $opt in
p) passthru_args="$OPTARGS" ;;
...
esac
done
./er "$passthru_args"
$ cat er
#!/bin/bash
while getopts "jklm:" opt
do
case $opt in
...
esac
done
If I use single quotes on the marked line above, I get the literal string "$er_args" passed through. Using double quotes, the flags are directly interpreted by herd. Using single inside double quotes, the flags aren't interpreted by ANY getopts.
I'm thinking there's no elegant solution here, but please let me know if I'm wrong. The only solutions I can think of are crappy:
Expose all of er's flags explicitly through herd.
Remove the er call from herd and place it directly into nerf.
???
What many tools do is when passed -p "-jkl -m something", they split up the string using pseudo-shell syntax. This is a bad idea because it makes space and quote handling unpredictable.
Instead, the better way is to have a way to pass individual words to the command. This is what find -exec does -- all arguments after -exec and up until + or ; are passed literally as separate arguments.
Here's a simple example of a herd with the same semantics:
#!/bin/bash
passthru_args=()
while getopts "pa:b:cde" opt
do
case $opt in
p)
while [[ ${!OPTIND} != ';' ]]
do
passthru_args+=("${!OPTIND}")
let OPTIND++
done
let OPTIND++
;;
*) echo "herd: $opt is $OPTARG"
;;
esac
done
./er "${passthru_args[#]}"
You can now run ./herd -p -jkl -m "some stuff" \; -a foo
This will run ./er -jkl -m "some stuff" safely without any space issues (but you'll have a hard time nesting multiple calls that use ; as an argument terminator).

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.

How to wrap another shell still passing $OPTIND as-is?

I'm trying to wrap a bash script b with a script a.
However I want to pass the options passed to a also to b as they are.
#!/bin/bash
# script a
./b ${#:$OPTIND}
This will also print $1 (if any). What's the simplest way not to?
So calling:
./a -c -d 5 first-arg
I want b to execute:
./b -c -d 5 # WITHOUT first-arg
In bash, you can build an array containing the options, and use that array to call the auxiliary program.
call_b () {
typeset -i i=0
typeset -a a; a=()
while ((++i <= OPTIND)); do # for i=1..$OPTIND
a+=("${!i}") # append parameter $i to $a
done
./b "${a[#]}"
}
call_b "$#"
In any POSIX shell (ash, bash, ksh, zsh under sh or ksh emulation, …), you can build a list with "$1" "$2" … and use eval to set different positional parameters.
call_b () {
i=1
while [ $i -le $OPTIND ]; do
a="$a \"\$$i\""
i=$(($i+1))
done
eval set -- $a
./b "$#"
}
call_b "$#"
As often, this is rather easier in zsh.
./b "${(#)#[1,$OPTIND]}"
Why are you using ${#:$OPTIND} and not just $# or $*?
The ${parameter:index} syntax says to use index to parse $parameter. If you're using $#, it'll use index as an index into the parameters.
$ set one two three four #Sets "$#"
$ echo $#
one two three four
$ echo ${#:0}
one two three four
$ echo ${#:1}
one two three four
$ echo ${#:2}
two three four
$OPTIND is really only used if you're using getopts. This counts the number of times getopts processes the parameters in $#. According to the bash manpage:
OPTIND is initialized to 1 each time the shell or a shell script is invoked.
Which may explain why you're constantly getting the value of 1.
EDITED IN RESPONSE TO EDITED QUESTION
#David - "./b $# " still prints the arguments of passed to a (see Q edit). I want to pass only the options of a and not the args
So, if I executed:
$ a -a foo -b bar -c fubar barfu barbar
You want to pass to b:
$ b -a foo -b bar -c fubar
but not
$ b -arg1 foo -arg2 bar -arg3 fubar barfu barbar
That's going to be tricky...
Is there a reason why you can't pass the whole line to b and just ignore it?
I believe it might be possible to use regular expressions:
$ echo "-a bar -b foo -c barfoo foobar" | sed 's/\(-[a-z] [^- ][^- ]*\) *\([^-][^-]*\)$/\1/'
-a bar -b foo -c barfoo
I can't vouch that this regular expression will work in all situations (i.e. what if there are no parameters?). Basically, I'm anchoring it to the end of the line, and then matching for the last parameter and argument and the rest of the line. I do a replace with just the last parameter and argument.
I've tested it in a few situations, but you might simply be better off using getopts to capture the arguments and then passing those to b yourself, or simply have b ignore those extra arguments if possible.
In order to separate the command options from the regular arguments, you need to know which options take arguments, and which stand alone.
In the example command:
./a -c -d 5 first-arg
-c and -d might be standalone options and 5 first-arg the regular arguments
5 might be an argument to the -d option (this seems to be what you mean)
-d might be an argument to the -c option and (as in the first case) 5 first-arg the regular arguments.
Here's how I'd handle it, assuming -a, -b, -c and -d are the only options, and that -b and -d is the only ones that take an option argument. Note that it is necessary to parse all of the options in order to figure out where they end.
#!/bin/bash
while getopts ab:cd: OPT; do
case "$OPT" in
a|b|c|d) : ;; # Don't do anything, we're just here for the parsing
?) echo "Usage: $0 [-ac] [-b something] [-d something] [args...]" >&2
exit 1 ;;
esac
done
./b "${#:1:$((OPTIND-1))}"
The entire while loop is there just to compute OPTIND. The ab:cd: in the getopts command defines what options are allowed and which ones take arguments (indicated by colons). The cryptic final expression means "elements 1 through OPTIND-1 of the argument array, passed as separate words".

Resources