135

The following code exits with a unbound variable error. How can I fix this, while still using the set -o nounset option?

#!/bin/bash

set -o nounset

if [ ! -z ${WHATEVER} ];
 then echo "yo"
fi

echo "whatever"
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
vinodkone
  • 2,731
  • 4
  • 22
  • 21

7 Answers7

134
#!/bin/bash

set -o nounset


VALUE=${WHATEVER:-}

if [ ! -z ${VALUE} ];
 then echo "yo"
fi

echo "whatever"

In this case, VALUE ends up being an empty string if WHATEVER is not set. We're using the {parameter:-word} expansion, which you can look up in man bash under "Parameter Expansion".

Flimm
  • 136,138
  • 45
  • 251
  • 267
Angelom
  • 2,413
  • 1
  • 15
  • 8
  • 23
    just replace if [ ! -z ${VALUE} ]; with if [ ! -z ${WHATEVER:-} ]; – Angelom Oct 20 '11 at 07:00
  • 21
    `:-` checks whether the variable is unset *or* empty. If you want to check *only* whether it's unset, use `-`: `VALUE=${WHATEVER-}`. Also, a more readable way to check whether a variable is empty: `if [ "${WHATEVER+defined}" = defined ]; then echo defined; else echo undefined; fi` – l0b0 Oct 24 '11 at 11:04
  • 1
    Also, this won't work if `$WHATEVER` contains only whitespace - See my answer. – l0b0 Mar 22 '12 at 15:07
  • 17
    Is there a reason for using "`! -z`" instead of just "`-n`" ? – Jonathan Hartley Aug 08 '19 at 20:55
39

You need to quote the variables if you want to get the result you expect:

check() {
    if [ -n "${WHATEVER-}" ]
    then
        echo 'not empty'
    elif [ "${WHATEVER+defined}" = defined ]
    then
        echo 'empty but defined'
    else
        echo 'unset'
    fi
}

Test:

$ unset WHATEVER
$ check
unset
$ WHATEVER=
$ check
empty but defined
$ WHATEVER='   '
$ check
not empty
l0b0
  • 55,365
  • 30
  • 138
  • 223
  • I tried this and I'm surprised this works... Everything is correct except according to **`"info bash"`**, **`"${WHATEVER-}"`** should have a `":"` (colon) before the `"-"` (dash) like: **`"${WHATEVER:-}"`**, and **`"${WHATEVER+defined}"`** should have a colon before the `"+"` (plus) like: **`"${WHATEVER:+defined}"`**. For me, it works either way, with or without the colon. On some versions of 'nix it probably won't work without including the colon, so it should probably be added. – Kevin Fegan Jan 07 '14 at 20:58
  • 10
    Nope, `-`, `+`, `:+`, and `:-` are all supported. The former detect whether the variable is *set*, and the latter detect whether it is *set **or** empty*. From `man bash`: "Omitting the colon results in a test only for a parameter that is unset." – l0b0 Jan 07 '14 at 21:00
  • 2
    Nevermind =). You are correct. I don't know how I missed that. – Kevin Fegan Jan 07 '14 at 21:11
  • From the [docs](https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion): _Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence._ – Asclepius Apr 03 '14 at 00:25
12

Use a oneliner:

[ -z "${VAR:-}" ] && echo "VAR is not set or is empty" || echo "VAR is set to $VAR"

-z checks both for empty or unset variable

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NublaII
  • 169
  • 1
  • 5
  • 2
    No, `-z` only checks if the next parameter is empty. `-z` is is just an argument of the `[` command. Variable expansion happens before `[ -z` can do anything. – dolmen Mar 30 '15 at 21:39
  • 1
    This seems like the correct solution, in that it does not generate an error if $VAR is not set. @dolmen can you provide an example of when it would not work? – Chris Stryczynski Nov 22 '17 at 14:29
  • @dolmen having read various bash resources about param expansion and finding the other answers over-complicated , i see nothing wrong with this one. so your “clarification”, while technically correct, seems rather pointless in practice, unless you need to differentiate unset vs empty. I tested unset, empty and non-empty, (bash 4) and it pretty much did what’s advertised each time. – JL Peyret Oct 11 '19 at 01:40
12

Assumptions:

$ echo $SHELL

/bin/bash

$ /bin/bash --version | head -1

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

$ set -o nounset

If you want a non-interactive script to print an error and exit if a variable is null or not set:

$ [[ "${HOME:?}" ]]

$ [[ "${IAMUNBOUND:?}" ]]

bash: IAMUNBOUND: parameter null or not set

$ IAMNULL=""
$ [[ "${IAMNULL:?}" ]]

bash: IAMNULL: parameter null or not set

If you don't want the script to exit:

$ [[ "${HOME:-}" ]] || echo "Parameter null or not set."

$ [[ "${IAMUNBOUND:-}" ]] || echo "Parameter null or not set."

Parameter null or not set.

$ IAMNULL=""
$ [[ "${IAMUNNULL:-}" ]] || echo "Parameter null or not set."

Parameter null or not set.

You can even use [ and ] instead of [[ and ]] above, but the latter is preferable in Bash.

Note what the colon does above. From the documentation:

Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.

There is apparently no need for -n or -z.

In summary, I may typically just use [[ "${VAR:?}" ]]. Per the examples, this prints an error and exits if a variable is null or not set.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Asclepius
  • 57,944
  • 17
  • 167
  • 143
6

You can use

if [[ ${WHATEVER:+$WHATEVER} ]]; then

but

if [[ "${WHATEVER:+isset}" == "isset" ]]; then

might be more readable.

Aleš Friedl
  • 61
  • 1
  • 2
  • String comparisons should use the standard (POSIX) `=` operator, not `==` to aid in portability, and `[` instead of `[[` if possible. – Jens Oct 26 '12 at 09:30
  • 3
    @Jens The question is specific to bash and includes `set -o nounset` which is specific to bash. If you put a `#!/bin/bash` at the top of your script, it is actually best to use bash's enhancements. – Bruno Bronosky Jan 25 '17 at 14:49
1

While this isn't exactly the use case asked for, I've found that if you want to use nounset (or -u) the default behavior is the one you want: to exit nonzero with a descriptive message.

If all you want is to echo something else when exiting, or do some cleanup, you can use a trap.

The :- operator is probably what you want otherwise.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Max Bileschi
  • 2,103
  • 2
  • 21
  • 19
1

To me, most of the answers are at best confusing, not including a test matrix. They also often do not address the scenario where variable contains the defaulting value.

The solution from l0b0 is the only readable, testable (and correct in respect to the actual question IMO), but it is unclear if inverting/reordering the tests to simplify the logic produces correct result. I minified hirs solution

The (already minified) contrast solution from Aleš, exposes the difference of a variable being declared but undefined. The one or the other might fit your scenario.

#!/bin/bash -eu

check1() {
    if [[ -n "${WHATEVER-}" ]]; then
        echo 'something else: not empty'
    elif [[ "${WHATEVER+defined}" = defined ]]; then
        echo 'something else: declared but undefined'
    else
        echo 'unset'
    fi
}

check2() {
    if [[ "${WHATEVER+defined}" != "defined" ]]; then
        echo 'unset'
    else
        echo "something else"
    fi
}

check3() {
    if [[ "${WHATEVER-defined}" = defined ]]; then
        echo 'unset'
    else
        echo 'something else'
    fi
}

check4() {
    if [[ ! ${WHATEVER+$WHATEVER} ]]; then
        echo 'unset'
    else
        echo 'something else'
    fi
}

echo
echo "check1 from l0b0"

unset WHATEVER
check1
WHATEVER=
check1
WHATEVER='   '
check1
WHATEVER='defined'
check1

echo
echo "check2 prove simplification keeps semantics"

unset WHATEVER
check2
WHATEVER=
check2
WHATEVER='   '
check2
WHATEVER='defined'
check2

echo
echo "check3 other promising operator?"

unset WHATEVER
check3
WHATEVER=
check3
WHATEVER='   '
check3
WHATEVER='defined'
check3

echo
echo "check4 other interesting suggestion, from aleš"

unset WHATEVER
check4
WHATEVER=
check4
WHATEVER='   '
check4
WHATEVER='defined'
check4
  • Check1 and Check2 behave identically
  • Check3 is plainly wrong
  • Check4: correct, depending on what you consider a declared/defined variable.
check1 from l0b0
unset
something else: declared but undefined
something else: not empty
something else: not empty

check2 prove simplification keeps semantics
unset
something else
something else
something else

check3 other promising operator?
unset
something else
something else
unset

check4 other interesting suggestion, from aleš
unset
unset
something else
something else