4

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.

starfry
  • 9,273
  • 7
  • 66
  • 96
  • 1
    One (seemingly insurmountable) problem: if you see `-a -b 3`, is that an unknown option `-a` with no argument followed by the expected `-b` option, or is it an unknown option `-a` with an argument `-b` followed by a positional argument `3`? – chepner Oct 17 '16 at 15:45
  • @chepner: Good point. The other problem: is `-ab` option `-a` with option-argument `b`, or an option group to be interpreted as `-a` and `-b`? – mklement0 Oct 17 '16 at 15:52
  • 1
    That looks even more insurmountabler :) – chepner Oct 17 '16 at 15:59

2 Answers2

3

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.

starfry
  • 9,273
  • 7
  • 66
  • 96
3

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.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Similar to what I had except for the wildcard case. I think those caveats are quite obscure and reasonably ignorable to gain the benefit of not requiring prior knowledge of pass-thru arguments. I didn't appreciate that you could alter the getopts variables. Nice solution. – starfry Oct 18 '16 at 08:57