'Posix shell: distinguish between empty and not existing variable

In pure /bin/sh how can I distinguish between an empty variable, an unset variable and a not existing (not defined) variable.

Here are the case:

# Case 1: not existing
echo "${foo}"

# Case 2: unset
foo=
echo "${foo}"

# Case 3: Empty
foo=""
echo "${foo}"

Now I would like to check for each of those three cases. If case 2 and case 3 are actually the same, then I must at least be able to distinguish between them and case 1.

Any idea?

UPDATE Solved thanks to Matteo

Here is how the code looks like:

#foo  <-- not defined
bar1=
bar2=""
bar3="a"

if ! set | grep '^foo=' >/dev/null 2>&1; then
    echo "foo does not exist"
elif [ -z "${foo}" ]; then
    echo "foo is empty"
else
    echo "foo has a value"
fi

if ! set | grep '^bar1=' >/dev/null 2>&1; then
    echo "bar1 does not exist"
elif [ -z "${bar1}" ]; then
    echo "bar1 is empty"
else
    echo "bar1 has a value"
fi

if ! set | grep '^bar2=' >/dev/null 2>&1; then
    echo "bar2 does not exist"
elif [ -z "${bar2}" ]; then
    echo "bar2 is empty"
else
    echo "bar2 has a value"
fi


if ! set | grep '^bar3=' >/dev/null 2>&1; then
    echo "bar3 does not exist"
elif [ -z "${bar3}" ]; then
    echo "bar3 is empty"
else
    echo "bar3 has a value"
fi

And the results:

foo does not exist
bar1 is empty
bar2 is empty
bar3 has a value


Solution 1:[1]

You can use set

If no options or arguments are specified, set shall write the names and values of all shell variables in the collation sequence of the current locale. Each name shall start on a separate line, using the format:

You can list all the variables (set) and grep for the variable name you want to check

set | grep '^foo='

Solution 2:[2]

I dont know about sh, but in bash and dash you can do echo ${TEST:?Error} for case 1 vs. case 2/3. And from quick glance at wikibooks, it seems like it should work for bourne shell too.

You can use it like this in bash and dash (use $? to get the error code)

echo ${TEST:?"Error"}
bash: TEST: Error
[lf@dell:~/tmp/soTest] echo $?
1
[lf@dell:~/tmp/soTest] TEST2="ok"
[lf@dell:~/tmp/soTest] echo ${TEST2:?"Error"}
ok
[lf@dell:~/tmp/soTest] echo $?
0
[lf@dell:~/tmp/soTest] dash
$ echo ${TEST3:?"Error"}       
dash: 1: TEST3: Error
$ TEST3=ok
$ echo ${TEST3:?"Error"}
ok

Solution 3:[3]

You can use ${var?} syntax to throw an error if var is unset and ${var:?} to throw an error if var is unset or empty. For a concrete example:

$ unset foo
$ test -z "${foo?unset}" && echo foo is empty || echo foo is set to $foo
-bash: foo: unset
$ foo=
$ test -z "${foo?unset}" && echo foo is empty || echo foo is set to $foo
foo is empty
$ foo=bar
$ test -z "${foo?unset}" && echo foo is empty || echo foo is set to $foo
foo is set to bar

Solution 4:[4]

I don't see a correct answer here yet that is POSIX compliant. First let me reiterate William Pursell's assertion that the code foo= is indeed setting the variable foo to an empty value the same as foo="". For foo to be unset, it must either never be set or unset with unset foo.

Matteo's answer is correct, but there are caveats. When you run set in bash and posix mode is disabled, it also prints all of the defined functions as well. This can be suppressed like this:

isvar() (
    [ -n "$BASH" ] && set -o posix
    set | grep -q "^$1="
)

By writing it as a sub-shell function, we don't need to worry about what the state of bash's posix setting after we're done.

However, you can still get false-positives from variables whose values contain carriage returns, so understand that this is not 100% foolproof.

$ a="
> b=2
> "
$ set | grep ^b=
b=2

So for maximum correctness you can exploit bash's -v test, when available.

isvar() {
    if [ -n "$BASH" ]; then
        [ -v "$1" ]
    else
        set | grep -q "^$1="
    fi
}

Perhaps somebody has a library somewhere that supports other shells' extensions as well. Essentially, this is a weakness in the POSIX specification and it just hasn't been seen as warranting amendment.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Matteo
Solution 2
Solution 3 William Pursell
Solution 4