existing empty null variable in bash - bash

I am using the code below to determine if a variable in bash exists, if it is empty, or if it has length>0. The code works, but I can't find a good explanation for how if [ -n "${emptyvar+1}" ] can detect if emptyvar is not set. If I remove the +1 then the test fails for "". What is the purpose of the +1 in the test?
#!/bin/bash
emptyvar="a"
if [ -n "${emptyvar+1}" ]
then
echo "emptyvar is defined"
if [[ -z $emptyvar ]]
then
echo "emptyvar is empty";
else
echo "emptyvar is NOT empty";
if [[ -n $emptyvar ]]
then
echo "emptyvar has length > 0";
else
echo "emptyvar has length 0";
fi
fi
else
echo "emptyvar is not defined"
fi

From the bash documentation of Shell Parameter Expansion:
${parameter:+word} If parameter is null or unset, nothing is
substituted, otherwise the expansion of word is substituted.
Omitting the colon (:) makes it test only if the variable is unset, rather than null or unset.
So ${emptyvar+1} tests if $emptyvar is unset. If it is, it expands to the empty string; if not, it expands to 1.

You can also create sets of functions to test a variable passed by its name:
function is_var_set {
[[ -n ${!1+.} ]]
}
function is_var_empty {
[[ -z ${!1} ]]
}
Test:
> A=''
> is_var_set A && echo "A is set." || echo "A is unset."
A is set.
> is_var_empty A && echo "A is empty." || echo "A is not empty."
A is empty.

bash 4.2 added a new test operator, -v, that tests if a variable has been set.
# -v takes the name of the variable, not its values (since you are
# testing if it has a value or not).
if [[ -v emptyvar ]]; then
echo "emptyvar is defined"
if [[ -z $emptyvar ]]
then
echo "emptyvar is empty";
else
echo "emptyvar is NOT empty";
fi
else
echo "emptyvar is not defined"
fi
Note that an empty variable is one whose string has length 0, so your -n test is redundant.

Another way to express it:
$ unset var
$ if [[ -z ${var+x} ]]; then echo unset; elif [[ -z $var ]]; then echo empty; else echo not empty; fi
unset
$ var=
$ if [[ -z ${var+x} ]]; then echo unset; elif [[ -z $var ]]; then echo empty; else echo not empty; fi
empty
$ var=foo
$ if [[ -z ${var+x} ]]; then echo unset; elif [[ -z $var ]]; then echo empty; else echo not empty; fi
not empty
You'll never get "emptyvar has length 0" -- that's what "empty" is

Related

bash: check if two variables both do or do not exist (aka comparing results of comparisons)

I am writing a bash script that sometimes will use environment variables GIT_DIR and GIT_WORK_TREE. The bash script can only operate correctly if either both variables exist or neither exist. In case there's a technical difference, it makes no difference
This works, but there has to be a better way:
if [[ -z "${GIT_DIR}" ]]; then
_GIT_DIR_EXISTS=0
else
_GIT_DIR_EXISTS=1
fi
if [[ -z "${GIT_WORK_TREE}" ]]; then
_GIT_WORK_TREE_EXISTS=0
else
_GIT_WORK_TREE_EXISTS=1
fi
if [[ "${_GIT_DIR_EXISTS}" -ne "${_GIT_WORK_TREE_EXISTS}" ]]; then
echo "GIT_DIR is ${GIT_DIR}"
echo "GIT_WORK_TREE is ${GIT_WORK_TREE}"
echo "Both or none must exist"
exit 1
fi
I tried:
if [[ (-z "${GIT_DIR}") -ne (-z "${GIT_WORK_TREE}") ]]; then
But that gives this error:
bash: syntax error in conditional expression
bash: syntax error near '-ne'
I then resorted to trying semi-random things, with varying errors:
if [[ -z "${GIT_DIR}" -ne -z "${GIT_WORK_TREE}" ]]; then
if [[ [-z "${GIT_DIR}"] -ne [-z "${GIT_WORK_TREE}"] ]]; then
if [[ [[-z "${GIT_DIR}"]] -ne [[-z "${GIT_WORK_TREE}"]] ]]; then
if [[ -z "${GIT_DIR}" ]] ^ [[ -z "${GIT_WORK_TREE}" ]]; then
if { [[ -z "${GIT_DIR}" ]] } -ne { [[ -z "${GIT_WORK_TREE}" ]] }; then
if [[ (( -z "${GIT_DIR}" )) -ne (( -z "${GIT_WORK_TREE}" )) ]]; then
I tried:
if [[ $(test -z "${GIT_DIR}") -ne $(test -z "${GIT_WORK_TREE}") ]]; then
But realized that doesn't work because it's a sub-process, and they'd need to be exported. as Socowl comments, this compares the outputs of the test commands which output nothing, not their exit statuses.
I apologize if this is a duplicate. I've searched here and google for a while, and must not be using the right terminology.
How about this:
if [[ "${GIT_DIR:+set}" != "${GIT_WORK_TREE:+set}" ]]; then
echo "GIT_DIR is '${GIT_DIR}'"
echo "GIT_WORK_TREE is '${GIT_WORK_TREE}'"
echo "Both or none must exist"
exit 1
fi
Explanation: ${var:+value} is a variant of parameter expansion that gives "value" if var is set to a nonempty string, or the empty string if var is unset or empty. So if both vars are unset/empty, it becomes if [[ "" != "" ]]; then, and if they're both set it becomes if [[ "set" != "set" ]]; then etc.
BTW, if you want to test whether the variables are set at all (even if to the empty string), use ${var+value} (note the lack of colon). The bash manual lists the :+ version, but not the + version.

Bash function to check if a given variable is set

As explained in this answer, the "right" way to check if a variable is set in bash looks like this:
if [ -z ${var+x} ]; then
echo "var is unset"
else
echo "var is set to '$var'"
fi
What I'm interested in is how to extract this into a function that can be reused for different variables.
The best I've been able to do so far is:
is_set() {
local test_start='[ ! -z ${'
local test_end='+x} ]'
local tester=$test_start$1$test_end
eval $tester
}
It seems to work, but is there a better way that doesn't resort to calling eval?
In Bash you can use [[ -v var ]]. There's no need for a function or convoluted schemes.
From the manpage:
-v varname
True if the shell variable varname is set (has been assigned a value).
The first 2 command sequences prints ok:
[[ -v PATH ]] && echo ok
var="" ; [[ -v var ]] && echo ok
unset var ; [[ -v var ]] && echo ok
With [[ ... ]], you can simply do this (there is no need for double quotes):
[[ $var ]] && echo "var is set"
[[ $var ]] || echo "var is not set or it holds an empty string"
If you really want a function, then we can write one-liners:
is_empty() { ! [[ $1 ]]; } # check if not set or set to empty string
is_not_empty() { [[ $1 ]]; } # check if set to a non-empty string
var="apple"; is_not_empty "$var" && echo "var is not empty" # shows "var is not empty"
var="" ; is_empty "$var" && echo "var is empty" # shows "var is empty"
unset var ; is_empty "$var" && echo "var is empty" # shows "var is empty"
var="apple"; is_empty "$var" || echo "var is not empty" # shows "var is not empty"
Finally, is_unset and is_set could be implemented to treat $1 as the name of the variable:
is_unset() { [[ -z "${!1+x}" ]]; } # use indirection to inspect variable passed through $1 is unset
is_set() { ! [[ -z "${!1+x}" ]]; } # check if set, even to an empty string
unset var; is_unset var && echo "var is unset" # shows "var is unset"
var="" ; is_set var && echo "var is set" # shows "var is set"
var="OK" ; is_set var && echo "var is set" # shows "var is set"
Related
Test for non-zero length string in Bash: [ -n “$var” ] or [ “$var” ]

If statement with multiple null string comparisons, print error with the ones that fail

Let's say I have the following if statement that checks multiple variables
if [[ -z ${var1} || -z ${var2} || -z ${var3} ]]; then
echo "Error, one or more variables are empty"
exit 2
fi
I'm looking for a way in which I can do the same test, but somehow be able to print the exact variables that are empty, something like this:
if [[ -z ${var1} || -z ${var2} || -z ${var3} ]]; then
echo "Error, the following variables are empty: ${var1} and ${var3}"
exit 2
fi
Of course, I could test them individually:
if [[ -z ${var1} ]]; then
echo "Error, the following variable is empty: ${var1}"
exit 2
fi
if [[ -z ${var2} ]]; then
echo "Error, the following variable is empty: ${var2}"
exit 2
fi
if [[ -z ${var3} ]]; then
echo "Error, the following variable is empty: ${var3}"
exit 2
fi
But this method also has a big flaw: if there are multiple empty variables, the script will exit after the first one, you'll never know that others are also empty.
One possibility, using indirect expansion:
empty=()
for i in var1 var2 var3; do
[[ -z ${!i} ]] && empty+=( "$i" )
done
if ((${#empty[#]})); then
echo "Error, the following variables are empty: ${empty[*]}"
exit 2
fi
The advantage of the loop that checks for variables is that you don't have to duplicate code.
Instead of executing exit 2 immediately, set a flag (say error=1) and then add the following after the third if:
[[ $error ]] && exit 2
You could also use a loop instead of manually repeating all these ifs and echos:
for varname in var1 var2 var3
do
if [[ -z "${!varname}" ]]
then
echo "Error, the following variable is empty: $varname"
error=1
fi
done
[[ $error ]] && exit 2
You could combine your code pieces like this:
if [[ -z ${var1} ]]; then
echo "Error, the following variable is empty: ${var1}"
fi
if [[ -z ${var2} ]]; then
echo "Error, the following variable is empty: ${var2}"
fi
if [[ -z ${var3} ]]; then
echo "Error, the following variable is empty: ${var3}"
fi
if [[ -z ${var1} || -z ${var2} || -z ${var3} ]]; then
exit 2
fi

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.

Bash - why the "0" is not recognized as a number?

I found an interesting Bash script that will test if a variable is numeric/integer. I like it, but I do not understand why the "0" is not recognized as a number? I can not ask the author, hi/shi is an anonymous.
#!/bin/bash
n="$1"
echo "Test numeric '$n' "
if ((n)) 2>/dev/null; then
n=$((n))
echo "Yes: $n"
else
echo "No: $n"
fi
Thank you!
UPDATE - Apr 27, 2012.
This is my final code (short version):
#!/bin/bash
ANSWER=0
DEFAULT=5
INDEX=86
read -p 'Not choosing / Wrong typing is equivalent to default (#5): ' ANSWER;
shopt -s extglob
if [[ $ANSWER == ?(-)+([0-9]) ]]
then ANSWER=$((ANSWER));
else ANSWER=$DEFAULT;
fi
if [ $ANSWER -lt 1 ] || [ $ANSWER -gt $INDEX ]
then ANSWER=$DEFAULT;
fi
It doesn't test if it is a numeric/integer. It tests if n evaluates to true or false, if 0 it is false, else (numeric or other character string) it is true.
use pattern matching to test:
if [[ $n == *[^0-9]* ]]; then echo "not numeric"; else echo numeric; fi
That won't match a negative integer though, and it will falsely match an empty string as numeric. For a more precise pattern, enable the shell's extended globbing:
shopt -s extglob
if [[ $n == ?(-)+([0-9]) ]]; then echo numeric; else echo "not numeric"; fi
And to match a fractional number
[[ $n == #(?(-)+([0-9])?(.*(0-9))|?(-)*([0-9]).+([0-9])) ]]

Resources