2

Bash Strict Mode is defined as follows:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

http://redsymbol.net/articles/unofficial-bash-strict-mode/

Consider the following parsing of positional parameters in a Bash script:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

usage() {
  echo ""
  echo "usage:"
  echo "$0 [options]"
  echo "  -r | --redirect-uri:  redirect uri"
  echo "  -a | --app-role:      app role id"
  echo "  -h | --help:          help"
  echo ""
}

redirect_uri=""
app_role=""

while [ "$1" != "" ]; do
  case $1 in
  -r | --redirect-uri)
    shift
    redirect_uri="$1"
    ;;
  -a | --app-role)
    shift
    app_role="$1"
    ;;
  -h | --help)
    usage
    exit 0
    ;;
  *)
    usage
    exit 1
    ;;
  esac
  shift
done

...

This doesn't work with the following error, e.g.:

$ ./app.sh -r https://example.net:8080/auth/callback -a 53b37b21-2c6e-4731-a5e5-15b98052c686        
./app.sh: line 18: $1: unbound variable

I think the reason is the final check in the while condition, after shift, where $1 is undefined.

How can I terminate parameter parsing in the while statement without causing the script to crash, when using Bash Strict Mode?

Shuzheng
  • 4,023
  • 1
  • 31
  • 71
  • Which not consistently using the long more self described options like `set -o errexit -o nounset -o pipefail`? (btw, there's nothing bash-specific in those). – Stéphane Chazelas Apr 04 '23 at 09:43
  • Aren't those options Bash specific? – Shuzheng Apr 04 '23 at 10:11
  • 1
    While `set -u` can be useful at least as a development helper, I'd be really wary of things like `set -e` esp. in connection with `pipefail`. If you ever use pipelines, I'd wager that _will_ cause your script to mysteriously fail without you having any idea why it happens. – ilkkachu Apr 04 '23 at 12:54

1 Answers1

5

You get the unbound variable error since you are trying to access $1 when it's not defined, which it won't be after the last iteration of the while loop. So, the problematic command is the test that you use with while:

[ "$1" != "" ]

Instead, use something like

[ "${1+set}" = set ]

or

[ -n "${1+set}" ]

The parameter expansion ${parameter+word} will expand to word if parameter is an existing variable (regardless of whether it has a non-empty value or not). The word set was chosen arbitrarily, but in a way that hints at what it's used for.

You could also test on $#, which holds the number of positional parameters:

[ "$#" -ne 0 ]

or

[ "$#" -gt 0 ]

These are arithmetic tests that are true whenever there are positional parameters that still need to be processed and shifted off the list.

Either of these variations avoids bumping against the restrictions imposed by the nounset shell option (set -u).

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • Or `while (( $# ))` in Korn-like shells such as bash. – Stéphane Chazelas Apr 04 '23 at 10:00
  • When does `$#` need quoting? I thought it always expanded to a digit sequence, hence always safe without quotes. – Toby Speight Apr 04 '23 at 10:15
  • @TobySpeight If you can guarantee that everyone reading this answer _never_ set `IFS` to a value containing digits. – Kusalananda Apr 04 '23 at 10:15
  • Ah, that old edge-case! Thanks. :-) – Toby Speight Apr 04 '23 at 10:16
  • 1
    @TobySpeight See also the "What about [ $# -gt 1 ]" section here: [Security implications of forgetting to quote a variable in bash/POSIX shells](https://unix.stackexchange.com/a/171347) – Kusalananda Apr 04 '23 at 10:17
  • @Kusalananda, though note that here, they're explicitly setting `IFS=$'\n\t'` as part of that hyperstrict preamble of theirs... out of the four things there, that's probably the most useful part (IMO anyway, and yes, they could still change it later) – ilkkachu Apr 04 '23 at 12:52
  • @ilkkachu I know they do, but I'm assuming they are not the only ones that will read the answer. I'm also assuming that people that read the answer do not necessarily read, understand, or care about the finer details of the code in the question. – Kusalananda Apr 04 '23 at 13:47