I am using getopts to parse arguments in a bash script. I want to do two things:
"$@" "$@"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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With