522

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.

AndrewR
  • 10,759
  • 10
  • 45
  • 56
  • 2
    Many of the answers to this question feel like something you would see on [Code Golf Stack Exchange](https://codegolf.stackexchange.com). – Daniel Schilling Oct 16 '17 at 17:11

14 Answers14

613

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.

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 3
    That's the thing I need. I've been using various versions of Unix since 1987 and I've never seen this syntax - just goes to show... – AndrewR Nov 21 '08 at 03:37
  • How does this work and where is it documented? I'm wondering if it can be modified to check the the variable exists and is set to a specific value. – jhabbott Nov 18 '11 at 11:10
  • 8
    It is documented in the shell manual page, or the Bash manual, usually under the header 'Parameter Expansion'. The standard way to check whether the variable exists and is set to a specific value (say 'abc') is simply: `if [ "$variable" = "abc" ]; then : OK; else : variable is not set correctly; fi`. – Jonathan Leffler Nov 18 '11 at 21:54
  • 1
    How do I use this syntax with a string variable with spaces in it. I get an error saying "This: command not found" when I set the variable I want to check to "This is a test". Any ideas? – user2294382 Nov 25 '13 at 17:41
  • @user2294382: The colon at the start of the line is not an accident -- it is a shell command (that always succeeds, assuming its arguments are evaluated successfully). So: `myVar="This is a test"; : ${myVar:="myVar was not set"}; echo "$myVar"; myVar=""; : ${myVar:="myVar was not set"}; echo "$myVar";`. Does that answer your question? In other contexts (like a `test` or `[` or `[[` command), the safe way is to wrap the variable in quotes: `if [ "${myVar:-'set to empty'}" != "set to empty" ]; then echo "myVar = $myVar"; fi`, etc. – Jonathan Leffler Nov 25 '13 at 17:58
  • Is there a way to make it exit with code 1 if not set using this form? – Andrew De Andrade Apr 15 '14 at 21:52
  • @AndrewDeAndrade: Do you mean 'exit status 1 rather than any other value such as 127', or do you mean 'exit status 1 or other failure status rather than 0 success'? Simple testing with Bash suggests the exit status is 127 (`bash -c 'var1=${VAR39X:?}; echo Hi, $var1; exit 0'; echo $?`). That meets the 'any non-zero status' criterion, but not the 'exit status 1 specifically' criterion. I doubt if there is a way to alter that behaviour. If you need exit status 1 as opposed to any non-zero exit status, can you explain why it matters? (I can guess some possibilities, but I'd rather not guess.) – Jonathan Leffler Apr 15 '14 at 22:18
  • @AndrewDeAndrade: I wouldn't recommend it, but you can play with the `trap` command like this: `bash -c 'trap "echo Trapped; exit 1" 0; var1=${VAR39X:?}; echo "Hi $var1"; trap 0; exit 0'; echo $?`. With the variable unset, it produces 3 lines of output containing: `bash: VAR39X: parameter null or not set`, `Trapped`, `1`. With the variable set and non-empty (`VAR39X=123`), it produces two lines of output: `Hi 123`, `0`. If you script the `trap 0` before you validate your parameters and cancel the trap afterwards, you should be OK if you absolutely need exit status 1. – Jonathan Leffler Apr 15 '14 at 22:24
  • @JonathanLeffler No reason for 1. I'm just used to using 1 as the general error code. If 127 is the more correct code and is what the code above does, then that's good. I just need a non-zero status returned. – Andrew De Andrade Apr 16 '14 at 19:05
  • @AndrewDeAndrade: The `${VAR:?}` notation definitely exits with a non-zero status. – Jonathan Leffler Apr 16 '14 at 20:21
  • 1
    Any thoughts on this discussion? https://github.com/koalaman/shellcheck/issues/380#issuecomment-145872749 – blong Oct 06 '15 at 15:04
  • Is there a way to prevent bash from outputting a message? That way I can just use it silently? – CMCDragonkai Dec 11 '15 at 08:47
  • @CMCDragonkai: What do you want to happen instead of an error message? There are several ways that your question could be answered. Yes: don't use the `${var?}` notation. No: the `${var?}` notation always generates a message. But there will probably be a way to achieve the effect you want. Please clarify what you want to achieve. – Jonathan Leffler Dec 11 '15 at 14:20
  • Shouldn't it be `"${x:?"You must set x"}"`? The outer quotes are used to tell the shell not to interpret the contents are substituting, the inner quotes are to make sure all nasty characters (like `}`) in the value are treated verbatim. – Alfe Sep 05 '17 at 08:56
144

Try this:

[ -z "$STATE" ] && echo "Need to set STATE" && exit 1;
David Schlosnagle
  • 4,263
  • 2
  • 23
  • 16
56

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.

Karusmeister
  • 871
  • 2
  • 12
  • 25
Rob Wells
  • 36,220
  • 13
  • 81
  • 146
54

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.

Jens
  • 69,818
  • 15
  • 125
  • 179
Paul Makkar
  • 557
  • 4
  • 2
40
${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}
bbarker
  • 11,636
  • 9
  • 38
  • 62
Vincent Van Den Berghe
  • 5,425
  • 2
  • 31
  • 40
23

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.

Adriano
  • 487
  • 4
  • 8
  • This 'works', but even old Solaris systems have a `/bin/sh` that support the `${var:?}` notation, etc. – Jonathan Leffler Sep 04 '15 at 23:02
  • The best answer so far. – Ole Aug 23 '16 at 19:44
  • 4
    Being set to the empty string is distinct from not being set at all. This test would print "Does not exist" after both `unset MYVAR` and `MYVAR=`. – chepner Sep 21 '16 at 15:52
  • @chepner, I upvoted your comment for the presumably valuable heads-up, but then I went on testing it (with bash), and couldn't actually set a var to an empty string without it also being unset. How would you do that? – Sz. Jan 02 '18 at 12:33
  • @Sz I'm not sure what you mean. "Unset" means the name has no value at all assigned to it. If `foo` is unset, `"$foo"` still expands to the empty string. – chepner Jan 02 '18 at 13:45
17

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
chepner
  • 497,756
  • 71
  • 530
  • 681
16

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.

Mr.Ree
  • 8,320
  • 27
  • 30
  • 2
    This checks if `STATE` is empty or unset, not just unset. – chepner Sep 21 '16 at 15:57
  • `[ "x$STATE" = x ]` is a hack you only need in really ancient shells. In any modern shell, `[ "$STATE" = "" ]` will work; and in an ancient shell, `==` won't be honored as a string comparison operator at all in the first place -- see [the POSIX standard for `test`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html), which only specifies `=` (one character, not two!) as a string comparison operator. – Charles Duffy Jan 25 '23 at 22:52
10

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
Denis Besic
  • 3,067
  • 3
  • 24
  • 35
ichigolas
  • 7,595
  • 27
  • 50
3

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

codeforester
  • 39,467
  • 16
  • 112
  • 140
2

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}
Gudlaugur Egilsson
  • 2,420
  • 2
  • 24
  • 23
2

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

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Note: the output redir. is to silence an error message when the variable is not set. – Sz. Jan 02 '18 at 12:25
-1

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
 }
nafdef
  • 1
  • 2
  • 1
    This is wrong. `is_this_an_env_variable P` will return success because `PATH` exists. – Eric Mar 16 '18 at 22:42
  • It exists because it's an environment variable. Whether it's been set eqaul nonnull string is another matter. – nafdef Mar 24 '18 at 00:28
-7

The $? syntax is pretty neat:

if [ $?BLAH == 1 ]; then 
    echo "Exists"; 
else 
    echo "Does not exist"; 
fi
Graeme
  • 95
  • 1
  • 5
  • This works, but does anyone here know where I can find docs on that syntax? $? is normally the previous return value. – Danny Staple Dec 23 '10 at 16:35
  • My bad - this does not appear to work. Try it in a simple script, with and without that set. – Danny Staple Dec 23 '10 at 16:38
  • 11
    In Bourne, Korn, Bash and POSIX shells, `$?` is the exit status of the previous command; `$?BLAH` is the string consisting of the exit status of the previous command concatenated with 'BLAH'. This does not test the variable `$BLAH` at all. (There's a rumour it might do more or less what's required in `csh`, but sea-shells are best left on the sea shore.) – Jonathan Leffler Jan 07 '13 at 15:46
  • This is a completely wrong answer as far as Bash is concerned. – codeforester Feb 04 '18 at 00:49