21

I'd like to make sure that at a certain point of a script, after sourceing a configuration file, several variables are set and, if they are not, to stop execution, telling the user about the missing variable. I have tried

for var in $one $two $three ; do
    ...

but if for example $two is not set, the loop is never executed for $two. The next thing I tried was

for var in one two three ; do
    if [ -n ${!var} ] ; then
        echo "$var is set to ${!var}"
    else
        echo "$var is not set"
    fi
done

But if two is not set, I still get "two is set to" instead of "two is not set".

How can I make sure that all required variables are set?

Update/Solution: I know that there is a difference between "set" and "set, but empty". I now use (thanks to https://stackoverflow.com/a/16753536/3456281 and the answers to this question) the following:

if [ -n "${!var:-}" ] ; then

so, if var is set but empty, it is still considered invalid.

Jasper
  • 3,628
  • 3
    You can also add set -u to the beginning of your script to terminate it immediately when an unset variable is used. – n.st Mar 25 '14 at 16:44

9 Answers9

9

Quoting error.

if [ -n "${!var}" ] ; then

For the future: Setting

set -x

before running the code would have shown you the problem. Instead of adding that to the code you can call your script with

bash -vx ./my/script.sh
Hauke Laging
  • 90,279
  • This works, but what is happening with/whithout quotes? Is my general approach correct the first place? – Jasper Mar 25 '14 at 16:11
  • Yeah, that's precognitive: I answered the question before it was asked... 8-) – Hauke Laging Mar 25 '14 at 16:13
  • 4
    @Jasper You should always quote variables. That does not cost more time than thinking about whether this is necessary every time. But it avoids the errors. – Hauke Laging Mar 25 '14 at 16:18
  • On the contrary, quoting only protects against expansion. If the expansion is what youre interested in, quoting is certainly not the way to go. For instance: cs=673,290,765, ; set -- $(IFS=, ; echo $cs) ; echo $# ; output : 3 – mikeserv Mar 26 '14 at 08:15
  • 2
    @mikeserv Only single quotes protect against expansion what I obviously do not suggest. Double quotes protect against word splitting. I have not denied that there may be cases where quoting must be omitted. But that is not an argument against my general rule. What kind of people is going to use something like set -- $(IFS=, ; echo $cs)? The kind of people who have to ask here why if [ -n ${!var} ] ; then doesn't work? Probably not. – Hauke Laging Mar 26 '14 at 13:17
  • @HaukeLaging Point taken. But I still believe we should refrain from oversimplifying if we can help it. The worst that can happen is people get frustrated and quit writing shell scripts, in which case there are fewer people writing them that dont understand how. Best case is more people learn more correctly what their scripts do and why, in which case we can all benefit. – mikeserv Mar 26 '14 at 13:32
5

If you want the program stopped:

N= 
${one?var 1 is unset} 
${two:?var 2 is unset or null}
${three:+${N:?var 3 is set and not null}}

That'll do the trick. Each of the messages following the question mark is printed to stderr and the parent shell dies. Well, OK, so not each message - only one - just the first one that fails prints a message cause the shell dies. I like to use these tests like this:

( for v in "$one" "$two" "$three" ; do
    i=$((i+1)) ; : ${v:?var $i is unset or null...} 
done ) || _handle_it

I had a lot more to say about this here.

mikeserv
  • 58,310
4

Only thing you need are quotes in your test:

for var in one two three ; do
    if [ -n "${!var}" ] ; then
        echo "$var is set to ${!var}"
    else
        echo "$var is not set"
    fi
done

Works for me.

TNW
  • 2,110
4

A solution that is maximally friendly tests all the requirements, and reports them together, rather than failing at the first one and requiring back-and-forth to get things right:

#!/bin/bash

required_vars=(one two three)

missing_vars=()
for i in "${required_vars[@]}"
do
    test -n "${!i:+y}" || missing_vars+=("$i")
done
if [ ${#missing_vars[@]} -ne 0 ]
then
    echo "The following variables are not set, but should be:" >&2
    printf ' %q\n' "${missing_vars[@]}" >&2
    exit 1
fi

I use an array variable to track which variables haven't been set, and use the result to compose a user-facing message.

Notes:

  • I quoted ${required_vars[@]} in the for loop mainly out of habit - I wouldn't advise including any shell metacharacters in your variable names!
  • I didn't quote ${#missing_vars[@]}, because that's always an integer, even if you are perverse enough to disregard the preceding advice.
  • I used %q when printing; %s would normally be sufficient.
  • Error output always goes to the error stream with >&2, so it doesn't get piped to downstream commands
  • Silence is golden - don't print progress information or debugging info unless specifically asked. That makes errors more obvious.
Toby Speight
  • 8,678
3

You can add

set -u

to the beginning of your script to make it terminate when it tries to use an unset variable.

A script like

#!/bin/sh
set -u
echo $foo

will result in

script.sh: 3: script.sh: foo: parameter not set

If you're using bash instead, the error will look like this:

script.sh: line 3: foo: unbound variable

n.st
  • 8,128
  • And in your opinion this makes which sense for run time checks? – Hauke Laging Mar 25 '14 at 18:39
  • 2
    @HaukeLaging I don't quite follow you — set -u prevents exactly the kind of error the OP is trying to avoid and (in contrary to all other solutions) isn't limited to a specific set of variables. It is in fact a useful precaution for almost all shell scripts to have them fail safely instead of doing unexpected things when a variable is unset. This article is a handy reference on the topic. – n.st Mar 25 '14 at 18:55
  • @n.st I disagree - null can be just as useful a value as not null if you plan for it. – mikeserv Mar 26 '14 at 08:02
  • 1
    @n.st This doesn't make any sense for the OP as he does not want a protection against accessing unset variables (BTW: a set but empty variable would cause the same error but nor react to your "protection"). He wants a run time check whether a variable is unset / empty. Your suggestion may be useful as a general development help but does not solve the OP's problem. – Hauke Laging Mar 26 '14 at 13:23
  • set -u could be used as well I think, but it is not as flexible as proper parameter expansion (see my updated question). And with set -u I'd have to make a dummy-access to all target variables if I want to check their presence in one place. – Jasper Mar 27 '14 at 11:58
1

I think if you mean not set, so the variable must never be initialized. If you use [ -n "${!var}" ], so the empty variable like two="" will be failed, while it is set. You can try this:

one=1
three=3

for var in one two three; do
  declare -p $var > /dev/null 2>&1 \
  && printf '%s is set to %s\n' "$var" "${!var}" \
  || printf '%s is not set\n' "$var"
done
cuonglm
  • 153,898
1

bash 4.2 lets you test if a variable is set with the -v operator; an unset variable and a variable set to the empty string are two different conditions:

$ unset foo
$ [[ -v foo ]] && echo foo is set
$ [[ -z "$foo" ]] && echo foo is empty
foo is empty
$ foo=
$ [[ -v foo ]] && echo foo is set
foo is set
$ [[ -z "$foo" ]] && echo foo is empty
foo is empty
chepner
  • 7,501
  • I was asking about "multiple" variables, so I was looking for something like indirection... – Jasper Mar 26 '14 at 13:26
  • You don't need indirection, since -v takes the name of a variable, so for var in one two three; [[ -v $var ]] && ...; done would check if each of one, two, and three are set in sequence. – chepner Mar 26 '14 at 13:29
0

A concise (though slightly hacky) solution, to handle the case where it's OK for the sourced script to set the variables to a null value, is to initialise the variables to some special value before sourcing the script, and then check for that value afterwards, e.g.

one=#UNSET#
two=#UNSET#
three=#UNSET#

. set_vars_script

if [[ $one$two$three == *#UNSET#* ]] ; then
  echo "One or more essential variables are unset" >&2
  exit 1
fi
  • 1
    another nice solution, but I'd like to give a more specific error message then "one or more variables unset"... – Jasper Mar 28 '14 at 09:45
0

I've extended the @mikeserv's answer.

This version takes a list of variables to check the presence of, printing the name of the missing variable and triggering a call to usage() on error.

REQD_VALUES=("VARIABLE" "FOO" "BAR" "OTHER_VARIABLE")
( i=0; for var_name in ${REQD_VALUES[@]}; do
    VALUE=${!var_name} ;
    i=$((i+1)) ; : ${VALUE:?$var_name is missing}
done ) || usage

Example output when a value is missing:

./myscript.sh: line 42: VALUE: OTHER_VARIABLE is missing

Note that you can change the variable called VALUE to an alternative name that suits your desired output better.

Ryan
  • 1