Test for a Bash variable being unset, using a function
Asked Answered
K

7

28

A simple Bash variable test goes:

${varName:?    "${varName} is not defined"}

I'd like to reuse this, by putting it in a function. How can I do it?

The following fails

#
# Test a variable exists
tvar(){
 val=${1:?    "${1}    must be defined, preferably in $basedir"}
 if [ -z ${val}  ]
     then
     echo Zero length value
 else
     echo ${1} exists, value ${1}
 fi
}

I.e., I need to exit if the test fails.

Koziara answered 17/5, 2009 at 11:18 Comment(1)
Your title says "unset", but your example tests for "either unset or set to the empty string". Which case do you care about?Merril
P
47

Thanks to lhunath's answer, I was led to a part of the Bash man page that I've overlooked hundreds of times:

When not performing substring expansion, bash tests for a parameter that is unset or null; omitting the colon results in a test only for a parameter that is unset.

This prompted me to create the following truth table:

Unset Set, but null Set and not null Meaning
${var-_} T F T Not null or not set
${var:-_} T T T Always true, use for subst.
$var F F T 'var' is set and not null
${!var[@]} F T T 'var' is set

This table introduces the specification in the last row. The Bash man page says "If name is not an array, expands to 0 if name is set and null otherwise." For purposes of this truth table, it behaves the same even if it's an array.

Pitchford answered 24/9, 2009 at 2:4 Comment(8)
Very cool, but do you know why this fails when var is an array: test "${!var[@]}" && echo 'T' || echo 'F', error message binary operator expectedEccrinology
@l0b0: It gives me a "T" or an "F" depending on whether var is set or unset. What shell (and its version)? (This only works in Bash.) What is the contents of ${var[@]} (output of declare -p var)?Pitchford
Bash 4.1.5(1)-release (i486-pc-linux-gnu), declare -a var='([0]="var1" [1]="var2" [2]="var3")'. See gistEccrinology
@l0b0: Oh, sorry, test (aka [) doesn't support the same features as [[. Try [[ ${!var[@]}" ]] && echo 'T' || echo 'F'. See also this and this.Pitchford
@Dennis: That doesn't fail, but produces true even for unset (see gist)Eccrinology
@l0b0: Sorry, there was a stray quote. Try this: [[ ${!var[@]} ]] && echo 'T' || echo 'F'. It should give "T" for any value or null and "F" for unset, exactly as shown in the table. An empty array () will give a "F", by the way. There was no link in your last comment.Pitchford
Arrays in general seem to cause issues with your truth table. Try out declare -A test1=();test1["key1"]="value1", declare -A test2=(), declare -A test3=, then repeat those three for declare -a. Each one of them will do something strange to your truth table. To determine if something is "unset" I ended up using declare -p var. It will stderr if it isn't set and the return value, $?, will be 1. If you don't like the stderr and stdout you can just redirect to /dev/nullDhole
The truth table doesn't apply to whole arrays, but it can be applied to array elements, for example: [[ ${arr[3]-_} ]]. If you want information about a whole array, you can use ${#arr[@]} to determine if it has any elements (and how many) elements=${#arr[@]} or the last test in the table to see if it's unset [[ ${!arr[@]} ]] && echo set || echo unsetPitchford
V
19

You're looking for indirection.

assertNotEmpty() {
    : "${!1:? "$1 is empty, aborting."}"
}

That causes the script to abort with an error message if you do something like this:

$ foo=""
$ assertNotEmpty foo
bash: !1:  foo is empty, aborting.

If you just want to test whether foo is empty, instead of aborting the script, use this instead of a function:

[[ $foo ]]

For example:

until read -p "What is your name? " name && [[ $name ]]; do
    echo "You didn't enter your name.  Please, try again." >&2
done

Also, note that there is a very important difference between an empty and an unset parameter. You should take care not to confuse these terms! An empty parameter is one that is set, but just set to an empty string. An unset parameter is one that doesn't exist at all.

The previous examples all test for empty parameters. If you want to test for unset parameters and consider all set parameters OK, whether they're empty or not, use this:

[[ ! $foo && ${foo-_} ]]

Use it in a function like this:

assertIsSet() {
    [[ ! ${!1} && ${!1-_} ]] && {
        echo "$1 is not set, aborting." >&2
        exit 1
    }
}

Which only aborts the script when the parameter name you pass denotes a parameter that isn't set:

$ ( foo="blah"; assertIsSet foo; echo "Still running." )
Still running.

$ ( foo=""; assertIsSet foo; echo "Still running." )
Still running.

$ ( unset foo; assertIsSet foo; echo "Still running." )
foo is not set, aborting.
Volkman answered 17/5, 2009 at 12:36 Comment(2)
That's the one I want. Thanks. I should have said unset. so the function I want is assertIsSet() { [[ ! ${!1} && ${!1-_} ]] && { echo "$1 is not set, aborting." >&2 exit 1 } } Thanks.Koziara
For newer bash, the test [[ ! -v var ]] will be true only when the var is unset. Looks easier than the double test in assertIsSet. :)Gentleness
S
7

You want to use [ -z ${parameter+word} ]

Some part of man bash:

Parameter Expansion
    ...
    In  each  of  the cases below, word is subject to tilde expansion, parameter expansion, command substitution, and
    arithmetic expansion.  When not performing substring expansion, bash tests for a parameter that is unset or null;
    omitting the colon results in a test only for a parameter that is unset.
    ...
    ${parameter:+word}
           Use Alternate Value.  If parameter is null or unset, nothing is substituted, otherwise  the  expansion  of
           word is substituted.
    ...

in other words:

    ${parameter+word}
           Use Alternate Value.  If parameter is unset, nothing is substituted, otherwise  the  expansion  of
           word is substituted.

some examples:

$ set | grep FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
it is unset
$ declare FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ FOOBAR=
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ FOOBAR=1
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ unset FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
it is unset
$
Slier answered 26/11, 2012 at 19:35 Comment(1)
what does "set | grep FOOBAR" do?Discrown
P
3

This function tests for variables that are currently set. The variable may even be an array. Note that in Bash: 0 == TRUE, 1 == FALSE.

function var.defined {
    eval '[[ ${!'$1'[@]} ]]'
}

# Typical usage of var.defined {}

declare you="Your Name Here" ref='you';

read -p "What's your name: " you;

if var.defined you; then   # Simple demo using literal text

    echo "BASH recognizes $you";
    echo "BASH also knows a reference to $ref as ${!ref}, by indirection.";

fi

unset you # Have just been killed by a master :D

if ! var.defined $ref; then    # Standard demo using an expanded literal value

    echo "BASH doesn't know $ref any longer";

fi

read -s -N 1 -p "Press any key to continue...";
echo "";

So to be clear here, the function tests literal text. Every time a command is called in Bash, variables are generally 'swapped-out' or 'substituted' with the underlying value unless:

  • $varRef ($) is escaped: $varRef
  • $varRef is single quoted '$varRef'
Phrasal answered 15/5, 2011 at 2:9 Comment(1)
The command eval is a very risky command, specially when you are expanding a var set by the user (name). Better to use: [[ -v name ]], internal to bash, much safer to use. :)Gentleness
H
1

I.e., I need to exit if the test fails.

The code:

${varName:?    "${varName} is not defined"}

will return a nonzero exit code when there is not a variable named "varName". The exit code of the last command is saved in $?.

About your code:

val=${1:?    "${1}    must be defined, preferably in $basedir"}

Maybe it is not doing what you need. In the case that $1 is not defined, the "${1}" will be substituted with nothing. Probably you want use the single quotes that literally writes ${1} without substitution.

val=${1:?    '${1}    must be defined, preferably in $basedir'
Housemaid answered 17/5, 2009 at 12:2 Comment(2)
I want to encapsulate the test in a function, to allow me to call it with defined SomeVariableName so that I can re-use it.Koziara
My finished function, tests for both unset and empty (belt and braces) assertIsSet2() { if [[ ! ${!1} && ${!1-_} ]] then echo "$1 is not set, aborting." >&2 exit 1 #else # echo ${1} is set, value [${1}] fi : "${!1:? "$1 is empty, aborting."}" } Thanks for the input. I'm back on track!Koziara
V
1

I am unsure if this is exactly what you want, but a handy trick I use when writing a new and complex script is to use "set -o":

set -o # Will make the script bomb out when it finds an unset variable

For example,

$ grep '$1' chex.sh
case "$1" in

$ ./chex.sh
./chex.sh: line 111: $1: unbound variable

$ ./chex.sh foo
incorrect/no options passed.. exiting
Vassalize answered 6/11, 2010 at 0:54 Comment(2)
Do you mean set -u? set -o is for setting options, like set -o pipefail (which you should also use).Mealworm
'set -u' is the same as 'set -o nounset', which was probably intended above.Gracchus
C
0
if set | grep -q '^VARIABLE='
then
    echo VARIABLE is set
fi
Churchgoer answered 1/6, 2014 at 7:40 Comment(1)
Forking and using an external command just to check if a variable set is very expensive.Madid

© 2022 - 2024 — McMap. All rights reserved.