Conditional expression with exclamation mark works only with double brackets - bash

I have a script which installs a tool, if it's not already on my system:
#!/bin/bash
ASCIIDOCTOR_BIN=`command -v asciidoctor`
if ! [ -x $ASCIIDOCTOR_BIN ]; then ......
The problem is, that it doesn't seem to be working: the conditional expression always evaluates to false.
Even with
#!/bin/bash
ASCIIDOCTOR_BIN=`command -v asciidoctor`
if ! [ false ]; then ......
the condition turns out to be just false.
As I was playing around with it I found out that if I replace the brackets with double brackets, it works:
#!/bin/bash
ASCIIDOCTOR_BIN=`command -v asciidoctor`
if ! [[ -x $ASCIIDOCTOR_BIN ]]; then ......
Can any1 explain why is that? I was told to avoid double brackets and curious, if there are other ways to make this script work.
I am working on Ubuntu 20.04.

You problem is that you are missing quotes, and don't understand how parameter expansion works:
Consider the following, assuming you enter a bash prompt:
$ a=""
$ b="helloWorld"
$ bash -c 'for arg; do echo "got arg: $arg"; done' _ $a $b
got arg: helloWorld
You would assume that bash is called with the positional parameters:
$0 = _
$1 = "" # empty string
$2 = "helloWorld"
But in reality the parameter $a expands to nothing resulting in bash being called with:
$0 = _
$1 = "helloWorld"
Applying our knowledge to your example you will notice that if $ASCIIDOCTOR_BIN is empty, which it is when the command doesn't exists, the if statement will become:
if [ -x ]; then ...
Which will test if the string -x is empty, which it clearly isn't.
The solution to this is to always quote parameter expansions:
$ a=""
$ b="helloWorld"
$ bash -c 'for arg; do echo "got arg: $arg"; done' _ "$a" "$b"
got arg:
got arg: helloWorld
One thing you should know about [ is that it isn't part of the language syntax but rather a command, usually implemented in the shell.
Same goes for true and false, these are also command, which means [ false ] is simply testing if the string false is empty, which it clearly isn't.
So parameter expansion etc applies to all it's arguments.
A note on [[. [[ is a special bash syntax where in parameter expansion works differently, and therefore you code works.
You can find a nice writeup on how parameter expansion works in bash: https://mywiki.wooledge.org/BashGuide/Parameters

Related

Why do I need to use quotes for -n but not for -z?

My understanding is that -n is the opposite of -z.
Testing -z...
#! /bin/bash
if [ -z $1 ]; then
echo bar
fi
$ ./test1.sh
bar
$ ./test1.sh foo
Works as expected!
Testing -n...
#! /bin/bash
if [ -n $1 ]; then
echo bar
fi
$ ./test1.sh
bar
$ ./test1.sh foo
bar
Does not work as expected unless I use quotes... Why?
Trying to figure out when to use quotes and when I can safely omit them.
Testing -n (with quotes)...
#! /bin/bash
if [ -n $1 ]; then
echo bar
fi
$ ./test1.sh
$ ./test1.sh foo
bar
Works as expected!
Does not work as expected unless I use quotes... Why?
From man test (and see also posix test):
-n STRING
the length of STRING is nonzero
STRING equivalent to -n STRING
-z STRING
the length of STRING is zero
When $1 is empty, then [ -n $1 ] executes [ -n ]. [ STRING ] is equivalent to [ -n STRING ], so [ -n ] is equivalent to [ -n -n ], and because -n is nonzero string, [ succeeds.
Check your scripts with http://shellcheck.net
Unquoted variable expansion undergo word splitting and filename expansion!
Research Difference between single and double quotes in Bash and When to wrap quotes around a shell variable? and https://mywiki.wooledge.org/Quotes
So I should ... always use double quotes?
As a rule of thumb you should always wrap expansions in double quotes.
So I should ... use double square brackets ... ?
The [[ is a bash extension not available in other shells, and from [[ documentation we know that: Word splitting and filename expansion are not performed on the words between the [[ and ]], so that's why there is no need to (but it does no harm) to double quote expansions between [[. [[ is specially handled by the shell, it's a special builtin with special syntax.
When working in bash-specific script that will never be ported, you may prefer [[ over [ because it should be tiny bit faster faster.

BASH - never execute unless environment variable is defined and certain value [duplicate]

I've got a few Unix shell scripts where I need to check that certain environment variables are set before I start doing stuff, so I do this sort of thing:
if [ -z "$STATE" ]; then
echo "Need to set STATE"
exit 1
fi
if [ -z "$DEST" ]; then
echo "Need to set DEST"
exit 1
fi
which is a lot of typing. Is there a more elegant idiom for checking that a set of environment variables is set?
EDIT: I should mention that these variables have no meaningful default value - the script should error out if any are unset.
Parameter Expansion
The obvious answer is to use one of the special forms of parameter expansion:
: ${STATE?"Need to set STATE"}
: ${DEST:?"Need to set DEST non-empty"}
Or, better (see section on 'Position of double quotes' below):
: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"
The first variant (using just ?) requires STATE to be set, but STATE="" (an empty string) is OK — not exactly what you want, but the alternative and older notation.
The second variant (using :?) requires DEST to be set and non-empty.
If you supply no message, the shell provides a default message.
The ${var?} construct is portable back to Version 7 UNIX and the Bourne Shell (1978 or thereabouts). The ${var:?} construct is slightly more recent: I think it was in System III UNIX circa 1981, but it may have been in PWB UNIX before that. It is therefore in the Korn Shell, and in the POSIX shells, including specifically Bash.
It is usually documented in the shell's man page in a section called Parameter Expansion. For example, the bash manual says:
${parameter:?word}
Display Error if Null or Unset. If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.
The Colon Command
I should probably add that the colon command simply has its arguments evaluated and then succeeds. It is the original shell comment notation (before '#' to end of line). For a long time, Bourne shell scripts had a colon as the first character. The C Shell would read a script and use the first character to determine whether it was for the C Shell (a '#' hash) or the Bourne shell (a ':' colon). Then the kernel got in on the act and added support for '#!/path/to/program' and the Bourne shell got '#' comments, and the colon convention went by the wayside. But if you come across a script that starts with a colon, now you will know why.
Position of double quotes
blong asked in a comment:
Any thoughts on this discussion? https://github.com/koalaman/shellcheck/issues/380#issuecomment-145872749
The gist of the discussion is:
… However, when I shellcheck it (with version 0.4.1), I get this message:
In script.sh line 13:
: ${FOO:?"The environment variable 'FOO' must be set and non-empty"}
^-- SC2086: Double quote to prevent globbing and word splitting.
Any advice on what I should do in this case?
The short answer is "do as shellcheck suggests":
: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"
To illustrate why, study the following. Note that the : command doesn't echo its arguments (but the shell does evaluate the arguments). We want to see the arguments, so the code below uses printf "%s\n" in place of :.
$ mkdir junk
$ cd junk
$ > abc
$ > def
$ > ghi
$
$ x="*"
$ printf "%s\n" ${x:?You must set x} # Careless; not recommended
abc
def
ghi
$ unset x
$ printf "%s\n" ${x:?You must set x} # Careless; not recommended
bash: x: You must set x
$ printf "%s\n" "${x:?You must set x}" # Careful: should be used
bash: x: You must set x
$ x="*"
$ printf "%s\n" "${x:?You must set x}" # Careful: should be used
*
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
abc
def
ghi
$ x=
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
bash: x: You must set x
$ unset x
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
bash: x: You must set x
$
Note how the value in $x is expanded to first * and then a list of file names when the overall expression is not in double quotes. This is what shellcheck is recommending should be fixed. I have not verified that it doesn't object to the form where the expression is enclosed in double quotes, but it is a reasonable assumption that it would be OK.
Try this:
[ -z "$STATE" ] && echo "Need to set STATE" && exit 1;
Your question is dependent on the shell that you are using.
Bourne shell leaves very little in the way of what you're after.
BUT...
It does work, just about everywhere.
Just try and stay away from csh. It was good for the bells and whistles it added, compared the Bourne shell, but it is really creaking now. If you don't believe me, just try and separate out STDERR in csh! (-:
There are two possibilities here. The example above, namely using:
${MyVariable:=SomeDefault}
for the first time you need to refer to $MyVariable. This takes the env. var MyVariable and, if it is currently not set, assigns the value of SomeDefault to the variable for later use.
You also have the possibility of:
${MyVariable:-SomeDefault}
which just substitutes SomeDefault for the variable where you are using this construct. It doesn't assign the value SomeDefault to the variable, and the value of MyVariable will still be null after this statement is encountered.
Surely the simplest approach is to add the -u switch to the shebang (the line at the top of your script), assuming you’re using bash:
#!/bin/sh -u
This will cause the script to exit if any unbound variables lurk within.
${MyVariable:=SomeDefault}
If MyVariable is set and not null, it will reset the variable value (= nothing happens).
Else, MyVariable is set to SomeDefault.
The above will attempt to execute ${MyVariable}, so if you just want to set the variable do:
MyVariable=${MyVariable:=SomeDefault}
In my opinion the simplest and most compatible check for #!/bin/sh is:
if [ "$MYVAR" = "" ]
then
echo "Does not exist"
else
echo "Exists"
fi
Again, this is for /bin/sh and is compatible also on old Solaris systems.
bash 4.2 introduced the -v operator which tests if a name is set to any value, even the empty string.
$ unset a
$ b=
$ c=
$ [[ -v a ]] && echo "a is set"
$ [[ -v b ]] && echo "b is set"
b is set
$ [[ -v c ]] && echo "c is set"
c is set
I always used:
if [ "x$STATE" == "x" ]; then echo "Need to set State"; exit 1; fi
Not that much more concise, I'm afraid.
Under CSH you have $?STATE.
For future people like me, I wanted to go a step forward and parameterize the var name, so I can loop over a variable sized list of variable names:
#!/bin/bash
declare -a vars=(NAME GITLAB_URL GITLAB_TOKEN)
for var_name in "${vars[#]}"
do
if [ -z "$(eval "echo \$$var_name")" ]; then
echo "Missing environment variable $var_name"
exit 1
fi
done
We can write a nice assertion to check a bunch of variables all at once:
#
# assert if variables are set (to a non-empty string)
# if any variable is not set, exit 1 (when -f option is set) or return 1 otherwise
#
# Usage: assert_var_not_null [-f] variable ...
#
function assert_var_not_null() {
local fatal var num_null=0
[[ "$1" = "-f" ]] && { shift; fatal=1; }
for var in "$#"; do
[[ -z "${!var}" ]] &&
printf '%s\n' "Variable '$var' not set" >&2 &&
((num_null++))
done
if ((num_null > 0)); then
[[ "$fatal" ]] && exit 1
return 1
fi
return 0
}
Sample invocation:
one=1 two=2
assert_var_not_null one two
echo test 1: return_code=$?
assert_var_not_null one two three
echo test 2: return_code=$?
assert_var_not_null -f one two three
echo test 3: return_code=$? # this code shouldn't execute
Output:
test 1: return_code=0
Variable 'three' not set
test 2: return_code=1
Variable 'three' not set
More such assertions here: https://github.com/codeforester/base/blob/master/lib/assertions.sh
This can be a way too:
if (set -u; : $HOME) 2> /dev/null
...
...
http://unstableme.blogspot.com/2007/02/checks-whether-envvar-is-set-or-not.html
None of the above solutions worked for my purposes, in part because I checking the environment for an open-ended list of variables that need to be set before starting a lengthy process. I ended up with this:
mapfile -t arr < variables.txt
EXITCODE=0
for i in "${arr[#]}"
do
ISSET=$(env | grep ^${i}= | wc -l)
if [ "${ISSET}" = "0" ];
then
EXITCODE=-1
echo "ENV variable $i is required."
fi
done
exit ${EXITCODE}
Rather than using external shell scripts I tend to load in functions in my login shell. I use something like this as a helper function to check for environment variables rather than any set variable:
is_this_an_env_variable ()
local var="$1"
if env |grep -q "^$var"; then
return 0
else
return 1
fi
}
The $? syntax is pretty neat:
if [ $?BLAH == 1 ]; then
echo "Exists";
else
echo "Does not exist";
fi

Comparing variables in shell scripts

I have got a project that involves shell scripts and comparing values/variables within them. I have looked here and elsewhere on comparing variables and I have tried all the various examples given but I am running into something that is not as advertised. OS is Solaris10
I have created the following script as a learning experience-
#!/bin/ksh
stest()
{
if $X = $Y
then echo they're the same
else echo they're notthe same
fi
}
X=a
Y=a
stest
echo completed
I keep getting some variation of the following-
using shell sh or ksh-
#./test.sh
./test.sh[2]: a: not found
completed
using shell bash-
#./test.sh
./test.sh: line 5: a: command not found
completed
I have tried enclosing the if $X = $Y line in brackets and double brackets and I get back
[a: not found
or
[[a: not found
If I change the variables X and Y to the numeral "1" I get the same thing-
./test.sh[2]: 1: not found
I have tried enclosing things in single quotes, double quotes & backwards quotes.
Any help is appreciated.
After if, you need a shell command, like anywhere else. $X = $Y is parsed as a shell command, meaning $X is interpreted as a command name (provided that the value of the variable is a single word).
You can use the [ command (also available as test) or the [[ … ]] special syntax to compare two variables. Note that you need spaces on the inside of the brackets: the brackets are a separate token in the shell syntax.
if [ "$X" = "$Y" ]; then …
or
if [[ "$X" = "$Y" ]]; then …
[ … ] works in any shell, [[ … ]] only in ksh, bash and zsh.
Note that you need double quotes around the variables¹. If you leave off the quotes, then the variable is split into multiple words and each word is interpreted as a wildcard pattern. This doesn't happen inside [[ … ]], but the right-hand side of = is interpreted as a wildcard pattern there too. Always put double quotes around variable substitutions (unless you want the value of the variable to be used as a list of filename matching patterns, rather than as a string).
¹ Except on $X the [[ … ]] syntax.
This KornShell (ksh) script should work:
soExample.ksh
#!/bin/ksh
#Initialize Variables
X="a"
Y="a"
#Function to create File with Input
#Params: 1}
stest(){
if [ "${X}" == "${Y}" ]; then
echo "they're the same"
else
echo "they're not the same"
fi
}
#-----------
#---Main----
#-----------
echo "Starting: ${PWD}/${0} with Input Parameters: {1: ${1} {2: ${2} {3: ${3}"
stest #function call#
echo "completed"
echo "Exiting: ${PWD}/${0}"
Output :
user#foo:/tmp $ ksh soExample.ksh
Starting: /tmp/soExample.ksh with Input Parameters: {1: {2: {3:
they're not the same
completed
Exiting: /tmp/soExample.ksh
ksh version:
user#foo:/tmp $ echo $KSH_VERSION
#(#)MIRBSD KSH R48 2013/08/16

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

What's a concise way to check that environment variables are set in a Unix shell script?

I've got a few Unix shell scripts where I need to check that certain environment variables are set before I start doing stuff, so I do this sort of thing:
if [ -z "$STATE" ]; then
echo "Need to set STATE"
exit 1
fi
if [ -z "$DEST" ]; then
echo "Need to set DEST"
exit 1
fi
which is a lot of typing. Is there a more elegant idiom for checking that a set of environment variables is set?
EDIT: I should mention that these variables have no meaningful default value - the script should error out if any are unset.
Parameter Expansion
The obvious answer is to use one of the special forms of parameter expansion:
: ${STATE?"Need to set STATE"}
: ${DEST:?"Need to set DEST non-empty"}
Or, better (see section on 'Position of double quotes' below):
: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"
The first variant (using just ?) requires STATE to be set, but STATE="" (an empty string) is OK — not exactly what you want, but the alternative and older notation.
The second variant (using :?) requires DEST to be set and non-empty.
If you supply no message, the shell provides a default message.
The ${var?} construct is portable back to Version 7 UNIX and the Bourne Shell (1978 or thereabouts). The ${var:?} construct is slightly more recent: I think it was in System III UNIX circa 1981, but it may have been in PWB UNIX before that. It is therefore in the Korn Shell, and in the POSIX shells, including specifically Bash.
It is usually documented in the shell's man page in a section called Parameter Expansion. For example, the bash manual says:
${parameter:?word}
Display Error if Null or Unset. If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted.
The Colon Command
I should probably add that the colon command simply has its arguments evaluated and then succeeds. It is the original shell comment notation (before '#' to end of line). For a long time, Bourne shell scripts had a colon as the first character. The C Shell would read a script and use the first character to determine whether it was for the C Shell (a '#' hash) or the Bourne shell (a ':' colon). Then the kernel got in on the act and added support for '#!/path/to/program' and the Bourne shell got '#' comments, and the colon convention went by the wayside. But if you come across a script that starts with a colon, now you will know why.
Position of double quotes
blong asked in a comment:
Any thoughts on this discussion? https://github.com/koalaman/shellcheck/issues/380#issuecomment-145872749
The gist of the discussion is:
… However, when I shellcheck it (with version 0.4.1), I get this message:
In script.sh line 13:
: ${FOO:?"The environment variable 'FOO' must be set and non-empty"}
^-- SC2086: Double quote to prevent globbing and word splitting.
Any advice on what I should do in this case?
The short answer is "do as shellcheck suggests":
: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"
To illustrate why, study the following. Note that the : command doesn't echo its arguments (but the shell does evaluate the arguments). We want to see the arguments, so the code below uses printf "%s\n" in place of :.
$ mkdir junk
$ cd junk
$ > abc
$ > def
$ > ghi
$
$ x="*"
$ printf "%s\n" ${x:?You must set x} # Careless; not recommended
abc
def
ghi
$ unset x
$ printf "%s\n" ${x:?You must set x} # Careless; not recommended
bash: x: You must set x
$ printf "%s\n" "${x:?You must set x}" # Careful: should be used
bash: x: You must set x
$ x="*"
$ printf "%s\n" "${x:?You must set x}" # Careful: should be used
*
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
abc
def
ghi
$ x=
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
bash: x: You must set x
$ unset x
$ printf "%s\n" ${x:?"You must set x"} # Not quite careful enough
bash: x: You must set x
$
Note how the value in $x is expanded to first * and then a list of file names when the overall expression is not in double quotes. This is what shellcheck is recommending should be fixed. I have not verified that it doesn't object to the form where the expression is enclosed in double quotes, but it is a reasonable assumption that it would be OK.
Try this:
[ -z "$STATE" ] && echo "Need to set STATE" && exit 1;
Your question is dependent on the shell that you are using.
Bourne shell leaves very little in the way of what you're after.
BUT...
It does work, just about everywhere.
Just try and stay away from csh. It was good for the bells and whistles it added, compared the Bourne shell, but it is really creaking now. If you don't believe me, just try and separate out STDERR in csh! (-:
There are two possibilities here. The example above, namely using:
${MyVariable:=SomeDefault}
for the first time you need to refer to $MyVariable. This takes the env. var MyVariable and, if it is currently not set, assigns the value of SomeDefault to the variable for later use.
You also have the possibility of:
${MyVariable:-SomeDefault}
which just substitutes SomeDefault for the variable where you are using this construct. It doesn't assign the value SomeDefault to the variable, and the value of MyVariable will still be null after this statement is encountered.
Surely the simplest approach is to add the -u switch to the shebang (the line at the top of your script), assuming you’re using bash:
#!/bin/sh -u
This will cause the script to exit if any unbound variables lurk within.
${MyVariable:=SomeDefault}
If MyVariable is set and not null, it will reset the variable value (= nothing happens).
Else, MyVariable is set to SomeDefault.
The above will attempt to execute ${MyVariable}, so if you just want to set the variable do:
MyVariable=${MyVariable:=SomeDefault}
In my opinion the simplest and most compatible check for #!/bin/sh is:
if [ "$MYVAR" = "" ]
then
echo "Does not exist"
else
echo "Exists"
fi
Again, this is for /bin/sh and is compatible also on old Solaris systems.
bash 4.2 introduced the -v operator which tests if a name is set to any value, even the empty string.
$ unset a
$ b=
$ c=
$ [[ -v a ]] && echo "a is set"
$ [[ -v b ]] && echo "b is set"
b is set
$ [[ -v c ]] && echo "c is set"
c is set
I always used:
if [ "x$STATE" == "x" ]; then echo "Need to set State"; exit 1; fi
Not that much more concise, I'm afraid.
Under CSH you have $?STATE.
For future people like me, I wanted to go a step forward and parameterize the var name, so I can loop over a variable sized list of variable names:
#!/bin/bash
declare -a vars=(NAME GITLAB_URL GITLAB_TOKEN)
for var_name in "${vars[#]}"
do
if [ -z "$(eval "echo \$$var_name")" ]; then
echo "Missing environment variable $var_name"
exit 1
fi
done
We can write a nice assertion to check a bunch of variables all at once:
#
# assert if variables are set (to a non-empty string)
# if any variable is not set, exit 1 (when -f option is set) or return 1 otherwise
#
# Usage: assert_var_not_null [-f] variable ...
#
function assert_var_not_null() {
local fatal var num_null=0
[[ "$1" = "-f" ]] && { shift; fatal=1; }
for var in "$#"; do
[[ -z "${!var}" ]] &&
printf '%s\n' "Variable '$var' not set" >&2 &&
((num_null++))
done
if ((num_null > 0)); then
[[ "$fatal" ]] && exit 1
return 1
fi
return 0
}
Sample invocation:
one=1 two=2
assert_var_not_null one two
echo test 1: return_code=$?
assert_var_not_null one two three
echo test 2: return_code=$?
assert_var_not_null -f one two three
echo test 3: return_code=$? # this code shouldn't execute
Output:
test 1: return_code=0
Variable 'three' not set
test 2: return_code=1
Variable 'three' not set
More such assertions here: https://github.com/codeforester/base/blob/master/lib/assertions.sh
This can be a way too:
if (set -u; : $HOME) 2> /dev/null
...
...
http://unstableme.blogspot.com/2007/02/checks-whether-envvar-is-set-or-not.html
None of the above solutions worked for my purposes, in part because I checking the environment for an open-ended list of variables that need to be set before starting a lengthy process. I ended up with this:
mapfile -t arr < variables.txt
EXITCODE=0
for i in "${arr[#]}"
do
ISSET=$(env | grep ^${i}= | wc -l)
if [ "${ISSET}" = "0" ];
then
EXITCODE=-1
echo "ENV variable $i is required."
fi
done
exit ${EXITCODE}
Rather than using external shell scripts I tend to load in functions in my login shell. I use something like this as a helper function to check for environment variables rather than any set variable:
is_this_an_env_variable ()
local var="$1"
if env |grep -q "^$var"; then
return 0
else
return 1
fi
}
The $? syntax is pretty neat:
if [ $?BLAH == 1 ]; then
echo "Exists";
else
echo "Does not exist";
fi

Resources