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 bar
options 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