Bash – getopts Gets No Arguments When Called Inside While Loop

argumentsbashgetoptsshell-script

I am trying to call a function in a while loop passing some arguments. However, getopts can only get the arguments for the first call.

Here's a minimal example:

function add_all_external_services() {
  env | sed -n "s/^EXTERNAL_SERVICE_OPTIONS_\(.*\)$/\1/p" > options

  while read -r line
  do
    key="${line%%=*}"
    opt="${line#*=}"

    if [[ -n "$key" && -n "$opt" ]]; then
      echo "Adding external service \"$key\" with options: \"$opt\""
      add_external_service $opt
    else
      echo "Missing one or more variables:
  - Key: \"$key\"
  - Options: \"$opt\"
"
    fi
  done < options

  rm options
}

function add_external_service() {
  local local_service_name=""
  local external_service_name=""
  local external_service_namespace=""
  local service_url=""

  echo "    Options: $@"
  while getopts l:s:n:-: OPT; do
    if [[ "$OPT" = "-" ]]; then   # long option: reformulate OPT and OPTARG
      OPT="${OPTARG%%=*}"         # extract long option name
      OPTARG="${OPTARG#$OPT}"     # extract long option argument (may be empty)
      OPTARG="${OPTARG#=}"        # if long option argument, remove assigning `=`
    fi
    case "$OPT" in
      l | local-service-name)           needs_arg; local_service_name="$OPTARG" ;;
      s | external-service-name)        needs_arg; external_service_name="$OPTARG" ;;
      n | external-service-namespace)   needs_arg; external_service_namespace="$OPTARG" ;;
      external-name)                    needs_arg; service_url="$OPTARG" ;;
      ??* )                             die "Illegal option --$OPT" ;;  # bad long option
      \? )                              exit 2 ;;  # bad short option (error reported via getopts)
    esac
  done

  echo "      - local $local_service_name"
  echo "      - name $external_service_name"
  echo "      - namespace $external_service_namespace"
  echo "      - url $service_url"
}

Then, when calling:

export EXTERNAL_SERVICE_OPTIONS_A="-l local_a -s rasa -n botinstance-12424526-review-feature-ce-swdjtf"
export EXTERNAL_SERVICE_OPTIONS_B="-l local_b -s review-feature-ce-swdjtf -n botinstance-12424526-review-feature-ce-swdjtf"

ventury-deploy add_all_external_services

I get this:

Adding external service "B" with options: "-l local_b -s name_b -n namespace_b"
    Options: -l local_b -s name_b -n namespace_b
      - local local_b
      - name name_b
      - namespace namespace_b
      - url 
Adding external service "A" with options: "-l local_a -s name_a -n namespace_a"
    Options: -l local_a -s name_a -n namespace_a
      - local 
      - name 
      - namespace 
      - url 

I got the getopts part from here and it works fine whenever I call functions outside loops.

After reading this question I tried adding a & after calling the function inside the loop, and it works… all the args are read by getopts. I can't understand why running the commands in the background would make it work.

If I echo $@ right before the getopts, I can see that all arguments are passed correctly, if I call the second function manually, once for every env variable, it also works correctly.

So, how is running these commands in the background different? I mean, what's different for getopts? Also, why echo $@ can see the arguments while getopts can't?

Best Answer

This is because you are not resetting OPTIND. According to the manual:

Each time it is invoked, getopts places the next option in the shell variable name, initializing name if it does not exist, and the index of the next argument to be processed into the variable OPTIND. OPTIND is initialized to 1 each time the shell or a shell script is invoked.

So OPTIND is used to keep track of the next argument to process and its automatically set to 1 when your script starts but NOT reset when your function ends.

To fix your script just add OPTIND=1 to the start of your function:

function add_external_service() {
  OPTIND=1
  local local_service_name=""
  local external_service_name=""
  local external_service_namespace=""
  local service_url=""

  echo "    Options: $@"
  while getopts l:s:n:-: OPT; do
    if [[ "$OPT" = "-" ]]; then   # long option: reformulate OPT and OPTARG
      OPT="${OPTARG%%=*}"         # extract long option name
      OPTARG="${OPTARG#$OPT}"     # extract long option argument (may be empty)
      OPTARG="${OPTARG#=}"        # if long option argument, remove assigning `=`
    fi
    case "$OPT" in
      l | local-service-name)           needs_arg; local_service_name="$OPTARG" ;;
      s | external-service-name)        needs_arg; external_service_name="$OPTARG" ;;
      n | external-service-namespace)   needs_arg; external_service_namespace="$OPTARG" ;;
      external-name)                    needs_arg; service_url="$OPTARG" ;;
      ??* )                             die "Illegal option --$OPT" ;;  # bad long option
      \? )                              exit 2 ;;  # bad short option (error reported via getopts)
    esac
  done

  echo "      - local $local_service_name"
  echo "      - name $external_service_name"
  echo "      - namespace $external_service_namespace"
  echo "      - url $service_url"
}
Related Question