147

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"

8 Answers 8

143
#!/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".

Sign up to request clarification or add additional context in comments.

6 Comments

just replace if [ ! -z ${VALUE} ]; with if [ ! -z ${WHATEVER:-} ];
:- 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
Also, this won't work if $WHATEVER contains only whitespace - See my answer.
Is there a reason for using "! -z" instead of just "-n" ?
@JonathanHartley easier to read out: "If not zero, do blah", vs "If Enn??nnNnnnnN??..."
|
46

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

4 Comments

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.
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."
Nevermind =). You are correct. I don't know how I missed that.
From the docs: 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.
14

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.

Comments

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

3 Comments

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.
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?
@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.
7

You can use

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

but

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

might be more readable.

2 Comments

String comparisons should use the standard (POSIX) = operator, not == to aid in portability, and [ instead of [[ if possible.
@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.
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.

Comments

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

Comments

0

This is my contribution bc once I began working with arrays/associative arrays, I needed an easy way to get the answer. My issue was: with 0 element arrays, this would return an error:

if [ ${#my_array[@]} -eq 0 ]; then #this will error if nounset is enabled!!
    echo "return error due to empty array"
    return 1
fi
var_stat() {
    #Determines if a variable (or array) is unset, blank (or no keys) or populated (has key(s))
    #Input: single parameter to be text of a variable name or an array or an associative array
    #stdout depends on whether the input represents a variable (or array) that is:
        # -1: unset
        #  0: set and blank (or set with 0 keys)
        #  1: set and not blank (or set and has key(s))
    local input_var_name="${1:-"__NA"}"
    if [[ "${input_var_name:-"__NA"}" = "__NA" ]]; then
        echo -1
        return 0
    fi
    #evaluate results of declare -p
    case "$( declare -p ${input_var_name} 2>/dev/null || echo "__NA" )" in
        #if begins with __NA then above command failed and variable is unset
        __NA*)
            echo -1
            ;;
        *\))
            #if ending with ) (escaped as '\)') then it is an array and populated
            echo 1
            ;;
        declare\ -[aA]*)
            #if it begins with declare -a or declare -A it is an empty array (bc it failed the prior test)
            echo 0
            ;;
        #otherwise, it is a regular variable; z test is safe
        *)
            if [ -z "${!input_var_name}" ]; then  #"${!input_var_name}" is indirect variable usage
                #it is empty
                echo 0
            else
                #it is not empty
                echo 1
            fi
    esac
}

Unit Test:

#Usage - tested with both errexit/nounset on/off
#first, define a bunch of variables, some blank, some keyless, etc.
    mysettext="settext"
    myblanktext=""
    declare -A myemptyAA
    declare -a myemptya
    declare -A myhaskeyAA
        myhaskeyAA[oneElement]="associative array with one key"
    declare -a myhaskeya
        myhaskeya+=("array with one value")
    declare -i myemptyinteger
    declare -i mysetinteger
        mysetinteger=3

#regular array to store the variable names of our tests
unset var_stat_test_array
declare -a var_stat_test_array
    var_stat_test_array+=('mysettext')
    var_stat_test_array+=('myblanktext')
    var_stat_test_array+=('myemptyAA')
    var_stat_test_array+=('myemptya')
    var_stat_test_array+=('myhaskeyAA')
    var_stat_test_array+=('myhaskeya')
    var_stat_test_array+=('myemptyinteger')
    var_stat_test_array+=('mysetinteger')
    var_stat_test_array+=('myUNSET') #this is not set per above

#cycle through the test array, run declare -p then the var_stat function etc.
    for eachElement in "${var_stat_test_array[@]}"; do
        declare -p ${eachElement} || true #guarantee we succeed even if unset
        var_stat "${eachElement}" | sed "s/^/    var_stat returned:/"
        # echo "    $? varstat outcome" #uncomment to verify 100% successes
        echo "" #just formatting output
    done

Output:

declare -- mysettext="settext"
    var_stat returned:1

declare -- myblanktext=""
    var_stat returned:0

declare -A myemptyAA
    var_stat returned:0

declare -a myemptya
    var_stat returned:0

declare -A myhaskeyAA=([oneElement]="associative array with one key" )
    var_stat returned:1

declare -a myhaskeya=([0]="array with one value")
    var_stat returned:1

declare -i myemptyinteger
    var_stat returned:0

declare -i mysetinteger="3"
    var_stat returned:1

-bash: declare: myUNSET: not found
    var_stat returned:-1

Practical examples:

if [[ $( var_stat my_unset_var ) -eq -1 ]]; then
    echo 'exit with error if this is a required variable; else safe to succeed'
    return 1
fi

declare -a my_empty_array  #or declare -A my_empty_array 
if [[ $( var_stat my_empty_array )  -le 0 ]]; then
    echo 'exit with error if an array (associative or not) must have at least one key/element; else safe to succeed'
    return 1
fi

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.