Search code examples
bashgetopts

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.


Solution

  • 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.