Using unset vs. setting a variable to empty - bash

I'm currently writing a bash testing framework, where in a test function, both standard bash tests ([[) as well as predefined matchers can be used. Matchers are wrappers to '[[' and besides returning a return code, set some meaningful message saying what was expected.
Example:
string_equals() {
if [[ ! $1 = $2 ]]; then
error_message="Expected '$1' to be '$2'."
return 1
fi
}
So, when a matcher is used, and it fails, only then an error_message is set.
Now, at some point later, I test whether the tests succeeded. If it succeeded, I print the expectation in green, if it failed in red.
Furthermore, there may be an error_message set, so I test if a message exists, print it, and then unset it (because the following test may not set an error_message):
if [[ $error_message ]]; then
printf '%s\n' "$error_message"
unset -v error_message
fi
Now my question is, if it is better to unset the variable, or to just set it to '', like
error_message=''
Which one is better? Does it actually make a difference? Or maybe should I have an additional flag indicating that the message was set?

Mostly you don't see a difference, unless you are using set -u:
/home/user1> var=""
/home/user1> echo $var
/home/user1> set -u
/home/user1> echo $var
/home/user1> unset var
/home/user1> echo $var
-bash: var: unbound variable
So really, it depends on how you are going to test the variable.
I will add that my preferred way of testing if it is set is:
[[ -n $var ]] # True if the length of $var is non-zero
or
[[ -z $var ]] # True if zero length

As has been said, using unset is different with arrays as well
$ foo=(4 5 6)
$ foo[2]=
$ echo ${#foo[*]}
3
$ unset foo[2]
$ echo ${#foo[*]}
2

So, by unset'ting the array index 2, you essentially remove that element in the array and decrement the array size (?).
I made my own test..
foo=(5 6 8)
echo ${#foo[*]}
unset foo
echo ${#foo[*]}
Which results in..
3
0
So just to clarify that unset'ting the entire array will in fact remove it entirely.

Based on the comments above, here is a simple test:
isunset() { [[ "${!1}" != 'x' ]] && [[ "${!1-x}" == 'x' ]] && echo 1; }
isset() { [ -z "$(isunset "$1")" ] && echo 1; }
Example:
$ unset foo; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's unset
$ foo= ; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's set
$ foo=bar ; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's set

Related

Comparison between 3 values

In a bash shell I have to determine if three values are equal to each other
if [val1 == val2 == val3]; then
do something
else
do other
fi
But it doesn't seem to work (too many arguments)
Where am I doing wrong?
Thanks
Massimo
You just need to make two comparisons.
if [ "$val1" = "$val2" ] && [ "$val2" = "$val3" ]; then
do something
else
do other
fi
This takes advantage of the fact that all equivalence relations are transitive: if val1 = val2 and val2 = val3, then val1 = val3 must be true as well.
Just because we can do it more complicated:
declare -A a=( [a$var1]= [a$var2]= [a$var3]= )
if [[ "${#a[#]}" == 1 ]]; then echo equal; else echo not equal; fi
unset a
The idea is to create an associative array a with key-value pairs. In the above, the value is always an empty string, so we only care about the keys. The keys in the above are a$var1, a$var2 and a$var3. If any of these keys are identical, then it is as if we defined only a single one. I.e.
declare -A a ( [aa]= [ab]= [aa]= )
is equivalent to
declare -A a ( [aa]= [ab]= )
The value ${#a[#]} returns the total number of entries in the array. So if that is equal to one, it implies that all values are identical.
We prefix the keys with a character (here a) to ensure that the variables can be anything, including empty strings, # and *. Thanks to Ivan for pointing this out.
Let's agree again on 1 law, if a=b AND b=c then it will be a=b=c. Considering that try changing your condition to (BTW IMHO I don't think that bash allows a==b==c type if conditions though Python does if I am correct here):
if [[ "$val1" -eq "$val2" && "$val2" -eq "$val3" ]]
then
do something
else
do other
fi
Example run:
Lets say we have following script with 3 of the variable values:
$ cat script.bash
#!/bin/bash
val1="1"
val2="1"
val3="1"
if [[ "$val1" -eq "$val2" && "$val2" -eq "$val3" ]]
then
echo "do something"
else
echo "do other"
fi
Now when we run it as:
$ ./script.bash
do something
If all values are digits this may work(not)
$ val1=1 val2=1 val3=1
$ ((val1==val2==val3)) && echo ok || echo fail
ok
$ val1=1 val2=2 val3=1
$ ((val1==val2==val3)) && echo ok || echo fail
fail
Then like this
$ val1=3 val2=3 val3=3
$ [[ "$val1 $val1" == "$val2 $val3" ]] && echo ok || echo fail
ok
$ val1=3 val2=3 val3=a2
$ [[ "$val1 $val1" == "$val2 $val3" ]] && echo ok || echo fail
fail
$ val1=asdlkjm2354l val2=$val1 val3=$val1
$ [[ "$val1 $val1" == "$val2 $val3" ]] && echo ok || echo fail
ok
$ val1=asdlkjm2354l val2=$val1 val3='sdf;lkjsdfs235lk;4j25'
$ [[ "$val1 $val1" == "$val2 $val3" ]] && echo ok || echo fail
fail
With (()) also works
$ val1=3 val2=3 val3=3
$ (($val1$val1==$val2$val3)) && echo ok || echo fail
ok
$ val1=33 val2=3 val3=33
$ (($val1$val1==$val2$val3)) && echo ok || echo fail
fail

Bash substitution: log when variable is not set

I use bash substitutions to give neat one-line validation for input, e.g.:
#!/bin/bash
export PARAM1=${1?Error, please pass a value as the first argument"}
# do something...
In some cases though, I want to only log a message when something is unset and then continue as normal. Is this possible at all?
Maybe something along the lines of
test -n "$1" && export PARAM1="$1" || log "\$1 is empty!"
should do; here the test clause returns true if and only if $1 is non-empty.
For regular parameters (in bash 4 or later), you can use the -v operator to check if a parameter (or array element, as of version 4.3) is set:
[[ -v foo ]] || echo "foo not set"
bar=(1 2 3)
[[ -v bar[0] ]] || echo "bar[0] not set"
[[ -v bar[8] ]] || echo "bar[8] not set"
Unfortunately, -v does not work with the positional parameters, but you can use $# instead (since you can't set, say, $3 without setting $1).
(( $# >= 3 )) || echo "third argument not set"
Before -v became available, you would need to compare two default-value expansions to see if a parameter was unset.
[[ -z $foo && ${foo:-bar} == ${foo-bar} ]] && echo "foo is unset, not just empty"
There's nothing special about bar; it's just an arbitrary non-empty string.

Bash Boolean testing

I am attempting to run a block of code if one flag is set to true and the other is set to false. ie
var1=true
var2=false
if [[ $var1 && ! $var2 ]]; then var2="something"; fi
Since that did not evaluate the way that I expected I wrote several other test cases and I am having a hard time understanding how they are being evaluated.
aa=true
bb=false
cc="python"
if [[ "$aa" ]]; then echo "Test0" ; fi
if [[ "$bb" ]]; then echo "Test0.1" ; fi
if [[ !"$aa" ]]; then echo "Test0.2" ; fi
if [[ ! "$aa" ]]; then echo "Test0.3" ; fi
if [[ "$aa" && ! "$bb" ]]; then echo "Test1" ; fi
if [[ "$aa" && ! "$aa" ]]; then echo "Test2" ; fi
if [[ "$aa" ]] && ! [[ "$bb" ]]; then echo "test3" ; fi
if [[ "$aa" ]] && ! [[ "$cc" ]]; then echo "test4" ; fi
if [[ $aa && ! $bb ]]; then echo "Test5" ; fi
if [[ $aa && ! $aa ]]; then echo "Test6" ; fi
if [[ $aa ]] && ! [[ $bb ]]; then echo "test7" ; fi
if [[ $aa ]] && ! [[ $cc ]]; then echo "test8" ; fi
When I run the preceding codeblock the only output I get is
Test0
Test0.1
Test0.2
however, my expectation is that I would get
Test0
Test1
Test3
Test5
Test7
I have tried to understand the best way to run similar tests, however most examples I have found are set up in the format of
if [[ "$aa" == true ]];
which is not quite what I want to do. So my question is what is the best way to make comparisons like this, and why do several of the test cases that I would expect to pass simply not?
Thank you!
Without any operators, [[ only checks if the variable is empty. If it is, then it is considered false, otherwise it is considered true. The contents of the variables do not matter.
Your understanding of booleans in shell context is incorrect.
var1=true
var2=false
Both the above variables are true since those are non-empty strings.
You could instead make use of arithmetic context:
$ a=1
$ b=0
$ ((a==1 && b==0)) && echo y
y
$ ((a==0 && b==0)) && echo y
$
$ ((a && !(b))) && echo y; # This seems to be analogous to what you were attempting
y
The shell does not have Boolean variables, per se. However, there are commands named true and false whose exit statuses are 0 and 1, respectively, and so can be used similarly to Boolean values.
var1=true
var2=false
if $var1 && ! $var2; then var2="something"; fi
The difference is that instead of testing if var1 is set to a true value, you expand it to the name of a command, which runs and succeeds. Likewise, var2 is expanded to a command name which runs and fails, but because it is prefixed with ! the exit status is inverted to indicate success.
(Note that unlike most programming languages, an exit status of 0 indicates success because while most commands have 1 way to succeed, there are many different ways they could fail, so different non-zero values can be assigned different meanings.)
true and false are evaluated as strings ;)
[[ $var ]] is an equivalent of [[ -n $var ]] that check if $var is empty or not.
Then, no need to quote your variables inside [[. See this reminder.
Finally, here is an explication of the difference between && inside brackets and outside.
The closest you can come seems to be use functions instead of variables because you can use their return status in conditionals.
$ var1() { return 0; }
$ var2() { return 1; } # !0 = failure ~ false
and we can test this way
$ var1 && echo "it's true" || echo "it's false"
it's true
$ var2 && echo "it's true" || echo "it's false"
it's false
or this way
$ if var1; then echo "it's true"; else echo "it's false"; fi
it's true
$ if var2; then echo "it's true"; else echo "it's false"; fi
it's false
Hope this helps.

Escaping in test comparisons

In the following, I would like check if a given variable name is set:
$ set hello
$ echo $1
hello
$ echo $hello
$ [[ -z \$$1 ]] && echo true || echo false
false
Since $hello is unset, I would expect the test to return true. What's wrong here? I would assume I am escaping the dollar incorrectly.
TYIA
You are testing if \$$1 is empty. Since it begins with a $, it is not empty. In fact, \$$1 expands to the string $hello.
You need to tell the shell that you want to treat the value of $1 as the name of a parameter to expand.
With bash: [[ -z ${!1} ]]
With zsh: [[ -z ${(P)1} ]]
With ksh: tmp=$1; typeset -n tmp; [[ -z $tmp ]]
Portably: eval "tmp=\$$1"; [ -z "$tmp" ]
(Note that these will treat unset and empty identically, which is usually the right thing.)

How to find whether or not a variable is empty in Bash

How can I check if a variable is empty in Bash?
In Bash at least the following command tests if $var is empty:
if [[ -z "$var" ]]; then
# $var is empty, do what you want
fi
The command man test is your friend.
Presuming Bash:
var=""
if [ -n "$var" ]; then
echo "not empty"
else
echo "empty"
fi
I have also seen
if [ "x$variable" = "x" ]; then ...
which is obviously very robust and shell independent.
Also, there is a difference between "empty" and "unset". See How to tell if a string is not defined in a Bash shell script.
if [ ${foo:+1} ]
then
echo "yes"
fi
prints yes if the variable is set. ${foo:+1} will return 1 when the variable is set, otherwise it will return empty string.
[ "$variable" ] || echo empty
: ${variable="value_to_set_if_unset"}
if [[ "$variable" == "" ]] ...
The question asks how to check if a variable is an empty string and the best answers are already given for that.
But I landed here after a period passed programming in PHP, and I was actually searching for a check like the empty function in PHP working in a Bash shell.
After reading the answers I realized I was not thinking properly in Bash, but anyhow in that moment a function like empty in PHP would have been soooo handy in my Bash code.
As I think this can happen to others, I decided to convert the PHP empty function in Bash.
According to the PHP manual:
a variable is considered empty if it doesn't exist or if its value is one of the following:
"" (an empty string)
0 (0 as an integer)
0.0 (0 as a float)
"0" (0 as a string)
an empty array
a variable declared, but without a value
Of course the null and false cases cannot be converted in bash, so they are omitted.
function empty
{
local var="$1"
# Return true if:
# 1. var is a null string ("" as empty string)
# 2. a non set variable is passed
# 3. a declared variable or array but without a value is passed
# 4. an empty array is passed
if test -z "$var"
then
[[ $( echo "1" ) ]]
return
# Return true if var is zero (0 as an integer or "0" as a string)
elif [ "$var" == 0 2> /dev/null ]
then
[[ $( echo "1" ) ]]
return
# Return true if var is 0.0 (0 as a float)
elif [ "$var" == 0.0 2> /dev/null ]
then
[[ $( echo "1" ) ]]
return
fi
[[ $( echo "" ) ]]
}
Example of usage:
if empty "${var}"
then
echo "empty"
else
echo "not empty"
fi
Demo:
The following snippet:
#!/bin/bash
vars=(
""
0
0.0
"0"
1
"string"
" "
)
for (( i=0; i<${#vars[#]}; i++ ))
do
var="${vars[$i]}"
if empty "${var}"
then
what="empty"
else
what="not empty"
fi
echo "VAR \"$var\" is $what"
done
exit
outputs:
VAR "" is empty
VAR "0" is empty
VAR "0.0" is empty
VAR "0" is empty
VAR "1" is not empty
VAR "string" is not empty
VAR " " is not empty
Having said that in a Bash logic the checks on zero in this function can cause side problems imho, anyone using this function should evaluate this risk and maybe decide to cut those checks off leaving only the first one.
This will return true if a variable is unset or set to the empty string ("").
if [ -z "$MyVar" ]
then
echo "The variable MyVar has nothing in it."
elif ! [ -z "$MyVar" ]
then
echo "The variable MyVar has something in it."
fi
You may want to distinguish between unset variables and variables that are set and empty:
is_empty() {
local var_name="$1"
local var_value="${!var_name}"
if [[ -v "$var_name" ]]; then
if [[ -n "$var_value" ]]; then
echo "set and non-empty"
else
echo "set and empty"
fi
else
echo "unset"
fi
}
str="foo"
empty=""
is_empty str
is_empty empty
is_empty none
Result:
set and non-empty
set and empty
unset
BTW, I recommend using set -u which will cause an error when reading unset variables, this can save you from disasters such as
rm -rf $dir
You can read about this and other best practices for a "strict mode" here.
To check if variable v is not set
if [ "$v" == "" ]; then
echo "v not set"
fi

Resources