How to tell if a string is not defined in a Bash shell script - bash

If I want to check for the null string I would do
[ -z $mystr ]
but what if I want to check whether the variable has been defined at all? Or is there no distinction in Bash scripting?

I think the answer you are after is implied (if not stated) by Vinko's answer, though it is not spelled out simply. To distinguish whether VAR is set but empty or not set, you can use:
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
if [ -z "$VAR" ] && [ "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
You probably can combine the two tests on the second line into one with:
if [ -z "$VAR" -a "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
However, if you read the documentation for Autoconf, you'll find that they do not recommend combining terms with '-a' and do recommend using separate simple tests combined with &&. I've not encountered a system where there is a problem; that doesn't mean they didn't used to exist (but they are probably extremely rare these days, even if they weren't as rare in the distant past).
You can find the details of these, and other related shell parameter expansions, the test or [ command and conditional expressions in the Bash manual.
I was recently asked by email about this answer with the question:
You use two tests, and I understand the second one well, but not the first one. More precisely I don't understand the need for variable expansion
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
Wouldn't this accomplish the same?
if [ -z "${VAR}" ]; then echo "VAR is not set at all"; fi
Fair question - the answer is 'No, your simpler alternative does not do the same thing'.
Suppose I write this before your test:
VAR=
Your test will say "VAR is not set at all", but mine will say (by implication because it echoes nothing) "VAR is set but its value might be empty". Try this script:
(
unset VAR
if [ -z "${VAR+xxx}" ]; then echo "JL:1 VAR is not set at all"; fi
if [ -z "${VAR}" ]; then echo "MP:1 VAR is not set at all"; fi
VAR=
if [ -z "${VAR+xxx}" ]; then echo "JL:2 VAR is not set at all"; fi
if [ -z "${VAR}" ]; then echo "MP:2 VAR is not set at all"; fi
)
The output is:
JL:1 VAR is not set at all
MP:1 VAR is not set at all
MP:2 VAR is not set at all
In the second pair of tests, the variable is set, but it is set to the empty value. This is the distinction that the ${VAR=value} and ${VAR:=value} notations make. Ditto for ${VAR-value} and ${VAR:-value}, and ${VAR+value} and ${VAR:+value}, and so on.
As Gili points out in his answer, if you run bash with the set -o nounset option, then the basic answer above fails with unbound variable. It is easily remedied:
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
if [ -z "${VAR-}" ] && [ "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
Or you could cancel the set -o nounset option with set +u (set -u being equivalent to set -o nounset).

~> if [ -z $FOO ]; then echo "EMPTY"; fi
EMPTY
~> FOO=""
~> if [ -z $FOO ]; then echo "EMPTY"; fi
EMPTY
~> FOO="a"
~> if [ -z $FOO ]; then echo "EMPTY"; fi
~>
-z works for undefined variables too. To distinguish between an undefined and a defined you'd use the things listed here or, with clearer explanations, here.
Cleanest way is using expansion like in these examples. To get all your options check the Parameter Expansion section of the manual.
Alternate word:
~$ unset FOO
~$ if test ${FOO+defined}; then echo "DEFINED"; fi
~$ FOO=""
~$ if test ${FOO+defined}; then echo "DEFINED"; fi
DEFINED
Default value:
~$ FOO=""
~$ if test "${FOO-default value}" ; then echo "UNDEFINED"; fi
~$ unset FOO
~$ if test "${FOO-default value}" ; then echo "UNDEFINED"; fi
UNDEFINED
Of course you'd use one of these differently, putting the value you want instead of 'default value' and using the expansion directly, if appropriate.

Advanced Bash scripting guide, 10.2. Parameter Substitution:
${var+blahblah}: if var is defined, 'blahblah' is substituted for the
expression, else null is substituted
${var-blahblah}: if var is defined, it is itself substituted, else
'blahblah' is substituted
${var?blahblah}: if var is defined, it is substituted, else the
function exists with 'blahblah' as an error message.
To base your program logic on whether the variable $mystr is defined or not, you can do the following:
isdefined=0
${mystr+ export isdefined=1}
Now, if isdefined=0 then the variable was undefined, and if isdefined=1 the variable was defined.
This way of checking variables is better than the previous answers, because it is more elegant, readable, and if your Bash shell was configured to error on the use of undefined variables (set -u), the script will terminate prematurely.
Other useful stuff:
To have a default value of 7 assigned to $mystr if it was undefined, and leave it intact otherwise:
mystr=${mystr- 7}
To print an error message and exit the function if the variable is undefined:
: ${mystr? not defined}
Beware here that I used ':' so as not to have the contents of $mystr executed as a command in case it is defined.

A summary of tests.
[ -n "$var" ] && echo "var is set and not empty"
[ -z "$var" ] && echo "var is unset or empty"
[ "${var+x}" = "x" ] && echo "var is set" # may or may not be empty
[ -n "${var+x}" ] && echo "var is set" # may or may not be empty
[ -z "${var+x}" ] && echo "var is unset"
[ -z "${var-x}" ] && echo "var is set and empty"

The explicit way to check for a variable being defined would be:
[ -v mystr ]

Test if a variable is set in bash when using "set -o nounset" contains a better answer (one that is more readable and works with set -o nounset enabled). It works roughly like this:
if [ -n "${VAR-}" ]; then
echo "VAR is set and is not empty"
elif [ "${VAR+DEFINED_BUT_EMPTY}" = "DEFINED_BUT_EMPTY" ]; then
echo "VAR is set, but empty"
else
echo "VAR is not set"
fi

Another option: the "list array indices" expansion:
$ unset foo
$ foo=
$ echo ${!foo[*]}
0
$ foo=bar
$ echo ${!foo[*]}
0
$ foo=(bar baz)
$ echo ${!foo[*]}
0 1
The only time this expands to the empty string is when foo is unset, so you can check it with the string conditional:
$ unset foo
$ [[ ${!foo[*]} ]]; echo $?
1
$ foo=
$ [[ ${!foo[*]} ]]; echo $?
0
$ foo=bar
$ [[ ${!foo[*]} ]]; echo $?
0
$ foo=(bar baz)
$ [[ ${!foo[*]} ]]; echo $?
0
should be available in any Bash version 3.0 or greater.

The Bash Reference Manual is an authoritative source of information about Bash.
Here's an example of testing a variable to see if it exists:
if [ -z "$PS1" ]; then
echo This shell is not interactive
else
echo This shell is interactive
fi
(From section 6.3.2.)
Note that the whitespace after the open [ and before the ] is not optional.
Tips for Vim users
I had a script that had several declarations as follows:
export VARIABLE_NAME="$SOME_OTHER_VARIABLE/path-part"
But I wanted them to defer to any existing values. So I rewrote them to look like this:
if [ -z "$VARIABLE_NAME" ]; then
export VARIABLE_NAME="$SOME_OTHER_VARIABLE/path-part"
fi
I was able to automate this in Vim using a quick regex:
s/\vexport ([A-Z_]+)\=("[^"]+")\n/if [ -z "$\1" ]; then\r export \1=\2\rfi\r/gc
This can be applied by selecting the relevant lines visually, then typing :. The command bar pre-populates with :'<,'>. Paste the above command and hit Enter.
It was tested on this version of Vim:
VIM - Vi IMproved 7.3 (2010 Aug 15, compiled Aug 22 2015 15:38:58)
Compiled by root#apple.com
Windows users may want different line endings.

Not to shed this bike even further, but wanted to add
shopt -s -o nounset
is something you could add to the top of a script, which will error if variables aren't declared anywhere in the script.
The message you'd see is unbound variable, but as others mention, it won't catch an empty string or null value.
To make sure any individual value isn't empty, we can test a variable as it's expanded with ${mystr:?}, also known as dollar sign expansion, which would error with parameter null or not set.

Here is what I think is a much clearer way to check if a variable is defined:
var_defined() {
local var_name=$1
set | grep "^${var_name}=" 1>/dev/null
return $?
}
Use it as follows:
if var_defined foo; then
echo "foo is defined"
else
echo "foo is not defined"
fi

A shorter version to test an undefined variable can simply be:
test -z ${mystr} && echo "mystr is not defined"

Call set without any arguments... it outputs all the defined variables.
The last ones on the list would be the ones defined in your script.
So you could pipe its output to something that could figure out what things are defined and what’s not.

Related

Redundant use of :- in Bash?

I have this code inside a function:
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
What is the role of :- here? Isn't it redundant? To be specific, could we not write the if statement this way?
if [ "${!var}" ] && [ "${!fileVar}" ]; then
How does it help to have an empty "word" on the right side of :-?
Figured it out. The :- construct in indirect parameter expansion prevents the script from failing when run with set -u.
Here is an example:
set -u
var=x
[[ ${!var:-} ]] && echo This works
[[ ${!var} ]] && echo This should fail
echo "This should print only when run without 'set -u'"
which gives this output:
line 6: !var: unbound variable
If the same statements are run without set -u, we get:
This should print only when run without 'set -u'
However, this trick wouldn't work in case we are using direct parameter expansion. So,
set -u
[[ ${var:-} ]] && echo Does this work
still throws the error:
tt.sh: line 6: var: unbound variable

Bash function to check for unset and blank variables

I'm writing a bash function to check for blank or unset variables.
function exit_if_var_does_not_exist {
TMP_NAME=$1
TMP_VALUE=`echo $TMP_NAME`
if [ -z ${TMP_VALUE+x} ]; then
echo "Variable [${TMP_NAME}] is unset. Exiting."
exit 1
else
if [ ${TMP_VALUE} = "" ]; then
echo "Variable [${TMP_NAME}] is set to ''. Exiting."
exit 1
fi
fi
echo "Variable [${TMP_NAME}] is set to ${TMP_VALUE}"
}
VAR=Test
exit_if_var_does_not_exist VAR
BLANK=
exit_if_var_does_not_exist BLANK
This does not give me the expected output in TMP_VALUE. Can someone please help me with what I'm missing here?
-bash-3.2$ ./x.sh
Variable [VAR] is set to VAR
Variable [BLANK] is set to BLANK
The problem with the empty test block is that at no point in this snippet do you ever actually get the value of the originally named variable.
When you use TMP_NAME=$1 you assign the name of the input variable to TMP_NAME and then
TMP_VALUE=`echo $TMP_NAME`
just assigns that name to TMP_VALUE. Effectively you just ran TMP_VALUE=$1.
So you aren't testing for whether the originally named variable has contents anywhere here.
To do that you need to use indirect expansion.
Namely TMP_VALUE=${!TMP_NAME} or TMP_VALUE=${!1}.
Side comment your "unset" test block at the top can never trigger.
TMP_VALUE can never be unset because you assign to it. Even TMP_VALUE= marks the variable as "set". So that bit is useless. Though, and thank David C. Rankin for trying long enough to make me think of this, you can use an indirect expansion trick to make this work.
[ -z "${!TMP_NAME+x}" ] will return true for any set variable named in TMP_NAME and false for an unset variable.
That all said if what you want to do is error if a variable is unset or blank the shell has you covered. Just use
: "${VAR:?Error VAR is unset or blank.}" || exit 1
Finally, as David C. Rankin points out inside [ you need to quote expansions that might disappear when they change the meaning of tests (you see this in the -z test above) as well as here.
So [ ${TMP_VALUE} = "" ] needs to be [ "${TMP_VALUE}" = "" ] because is TMP_VALUE is empty the first version would become a syntax error because [ = "" ] isn't a valid invocation of test/[.
I'm not sure this will work for all cases, but try:
#!/bin/bash
function exit_if_var_does_not_exist {
local TMP_NAME=$1
local TMP_VALUE="${!TMP_NAME}"
[ -z "${!TMP_NAME}" ] && {
echo "Variable [${TMP_NAME}] is unset. Exiting."
return 1
}
[ "$TMP_VALUE" = "" ] && {
echo "Variable [${TMP_NAME}] is set to ''. Exiting."
return 1
}
echo "Variable [${TMP_NAME}] is set to ${TMP_VALUE}"
}
VAR=Test
exit_if_var_does_not_exist VAR
# BLANK=
exit_if_var_does_not_exist BLANK
Output
$ bash tstempty.sh
Variable [VAR] is set to Test
Variable [BLANK] is unset. Exiting.
Improved Indirection Soup
After a bit of discussion in the comments and a suggestion, I think I have a version that works more consistently. Again, I qualify this with I am not sure it will work in all situations, but for the testing it seems to:
#!/bin/bash
function exit_if_var_does_not_exist {
local TMP_NAME=$1
local TMP_VALUE="${!TMP_NAME}"
if ! declare -p $1 >/dev/null 2>&1 ; then
[ -z "${!TMP_NAME}" ] && {
echo "Variable [${TMP_NAME}] is unset. Exiting."
return 1
}
elif [ "$TMP_VALUE" = "" ]; then
echo "Variable [${TMP_NAME}] is set to ''. Exiting."
return 1
fi
echo "Variable [${TMP_NAME}] is set to ${TMP_VALUE}"
}
VAR=Test
exit_if_var_does_not_exist VAR
# BLANK=
exit_if_var_does_not_exist BLANK
EMPTY=""
exit_if_var_does_not_exist EMPTY
Output
$ bash tstempty.sh
Variable [VAR] is set to Test
Variable [BLANK] is unset. Exiting.
Variable [EMPTY] is set to ''. Exiting.
Another way:
exit_if_var_does_not_exist() {
if ! (set -u; : "${!1}") >/dev/null 2>&1; then
printf 'Variable [%s] is unset. Exiting.\n' "$1"
exit 1
elif [ -z "${!1}" ]; then
printf "Variable [%s] is set to ''. Exiting.\n" "$1"
exit 1
fi
printf 'Variable [%s] is set to %s\n' "$1" "${!1}"
}
If you don't need to separate the two error messages:
exit_if_var_does_not_exist() {
: "${!1:?$1 is unset or blank}"
printf 'Variable [%s] is set to %s\n' "$1" "${!1}"
}

How to echo (print) the value of a variable in Bash?

I am trying to write a little script, and I can not figure out how to choose the variable to be echo'ed (echo $'TEST'_"$response") dynamically depending on the user's input:
#!/usr/bin/env sh
response=response
TEST_1="Hi from 1!"
TEST_2="Hi from 2!"
while [ $response ]; do
read -p "Enter a choice between 1 - 2, or 'bye': " response
if [ $response = 'bye' ]; then
echo "See You !"; exit
elif [ $response -ge 1 ] && [ $response -le 2 ]; then
echo $'TEST'_"$response"
else
echo "Input is not a valid value."
fi
done
The desired output would be the value of one of the variables declared at the beginning of my script ("Hi from 1!" or "Hi from 2!"). Instead my script simple outputs the name of the variable as a string "TEST_1" or "TEST_2". I do not simply want to hardcode the variable that will be printed like:
if [ $response -ge 1 ]; then
echo $TEST_1
fi
since it is not scalable. Using backticks like
echo `$'TEST'_"$response"`
doesn't help either since bash will expect to run the result "TEST_1" or "TEST_2" as a command.
Any hint will be greatly appreciated.
You need indirect expansion, to be used with ${!var}:
$ TEST1="hello"
$ TEST2="bye"
$ v=1
$ var="TEST$v" #prepare the variable name of variable
$ echo ${!var} #interpret it
hello
$ v=2
$ var="TEST$v" #the same with v=2
$ echo ${!var}
bye
That is, you need to use a variable name of a variable and this is done with the indirect expansion: you use a variable with the name of the variable and then you evaluate it with the ${!var} syntax.
In your case, use:
myvar="TEST$response"
echo "${!myvar}"
Always use quotes, such as "$string", for anything other than numbers. For numbers, just keep it normal (i.e. $number).

Improve script output

I have written a function which checks whether one or more variables passed as arguments are empty (got the idea from here)
Here is what the script looks like:
#!/bin/bash
is_variable_defined?() {
for VAR in "$#"; do
: "${!VAR? "ERROR: $VAR is undefined."}"
done
}
TEST='a'
is_variable_defined? TEST TEST2
And here is the output:
/path/to/script.sh: line 4: !VAR: ERROR: TEST2 is undefined.
However what I would like to output is:
TEST2 is undefined
I have tried tweaking the : "${!VAR? "ERROR: $VAR is undefined."}" line but whatever I do it breaks.
Does anybody know what to modify in the script to get the output I want?
Are you seeing if the variable is undefined or simply a null string?
Null Test
is_variable_defined() {
for var in $#
do
if [ -z ${!var} ] # Or if [[ ${!var} ]]
then
echo "Var $var has no value"
fi
done
}
Undefined Test
is_variable_defined() {
for var in $#
do
if ! set | grep -q "^$var="
then
echo "Var $var is not set"
fi
done
The last is looking at the output of the set command to see if the variable is listed there. The -q will make grep quiet. And you look at the exit status to see if it found the string. If your grep doesn't support the -q parameter, you can try grep "^$var=" 2> /dev/null.
Using the [[ $foo ]] syntax for testing won't necessarily work. The below will print $foo is not set even though it was set to an empty string:
foo=""
if [[ $foo ]]
then
echo "\$foo = '$foo'"
else
echo "\$foo is not set"
fi

Test for a Bash variable being unset, using a function

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.
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.
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.
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
$
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'
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'
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
if set | grep -q '^VARIABLE='
then
echo VARIABLE is set
fi

Resources