6

Say I have the following code:

# Check if the color prompt is enabled and supported on this system
if [ -n "$force_color_prompt" ] && [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then
    GREEN="\033[1;32m"
    DIM="\033[2m"
    RESET="\033[00m"
fi

echo -e "Oh, ${GREEN}green${RESET} world, ${DIM}don't desert me now...${RESET}"

If color support is enabled, it will echo out a pretty, colored line. If color support isn't enabled, values like ${GREEN} will not have been set, and the text will print out in the usual white with black background.

The code relies on the fact that variables that aren't set will simply evaluate to an empty string (which in my tests, they do). Will this cause bugs or problems on some systems, or will all non-existent variables always evaluate to an empty string? Is there any reason I shouldn't rely on this mechanic?

  • I would recommend to do something like this. serverfault.com/a/24063 – Ramesh Jun 23 '14 at 15:00
  • @Ramesh So, would the resulting code end up looking something like ${GREEN:-}? – IQAndreas Jun 23 '14 at 15:07
  • I have not tried that option. But, as per the suggestion in that answer, if your variable is empty, it will get set to something like empty and if it is not empty, it will get set to something else. – Ramesh Jun 23 '14 at 15:09
  • @Ramesh That will not help in this situation because the whole point of this question is that he is trying to avoid writing the assignment statement twice for the same variable. If he didn't care about that he could simple add GREEN="" DIM="" RESET="" before the if statement. – krowe Jun 23 '14 at 17:20
  • I hope you do not export the var's and call another script with GREEN defined different ("#FF0000"). – Walter A Jun 29 '14 at 21:41
7

Non-existent variables will always evaluate to an empty string when expanded as $FOO or (equivalently) ${FOO}, and there's no harm in depending on that, except in one particular case:

If someone has called set -u in the current shell before you try to use that variable, they've enabled this setting:

              -u      Treat unset variables as an error when performing param-
                      eter  expansion.   If expansion is attempted on an unset
                      variable, the shell prints an error message, and, if not
                      interactive, exits with a non-zero status.

This means that if you're writing a function that's designed to be sourced into a script that someone else is in control of, you may need to be paranoid about using unset variables - otherwise, if they used set -u prior to calling your function, their script would exit with an error message the first time you tried to expand an unset variable.

If you're writing your own script, there's no harm in counting on unset variables expanding to the empty string.

EDIT - Also, just a thought - since you're making the whole thing conditional on whether the terminfo color capabilities are available for your terminal, why not actually use terminfo to generate the sequences, rather than hardcoding the vt100 values? Something like:

if [ -n "$force_color_prompt" ] && type tput &>/dev/null; then
    GREEN="$(tput setaf 2)$(tput bold)"
    DIM="$(tput dim)"
    RESET="$(tput sgr0)"
fi

This may gain you some portability across other terminals (though, admittedly, the number of terminals that don't use the codes you showed is small and shrinking). It may also lose some portability, as some capabilities may not exist on some platforms depending on how correct the terminfo definitions are. YMMV.

  • In my own convenience (bash)-scripts I do, for example, mplayer "${MPEXTRAOPTS[@]}" "$CALCULATED_FILENAME" and depend on that the "${MPEXTRAOPTS[@]}" expand to nothing (not even an empty argument) if MPEXTRAOPTS is unset. Then if I need some extra arguments to be passed, I set the array, otherwise I just let it be unset. Since scalar variables are auto-promoted (can be used as) arrays with only element [0] set, if I need just one extra argument, I would just set the variable in a scalar fashion, like MPEXTRAARGS=-fs. – Johan E Jun 23 '14 at 17:03
  • Nonexistent variables do not always evaluate to an empty string. For instance: unset unset ; echo "${unset-not an empty string}" – mikeserv Jun 24 '14 at 2:58
  • 1
    @mikeserv, I didn't mean to imply that unset variables and empty variables were equivalent, or that using different types of parameter expansion couldn't change the answer - I was merely answering the question as posed, where all parameter expansions where of the basic ${VAR} sort. The question was "is there any harm..." and the correct answer is "not generally, and sometimes it's even helpful, though there are special cases where someone sets a flag saying that unset variables cause errors". – godlygeek Jun 24 '14 at 11:11
  • ok. maybe I was a little hasty. But is there anything you might edit? Not because you need to really, but I'd like to reverse my vote and can't do so unless it is edited... – mikeserv Jun 24 '14 at 11:23
  • @mikeserv, Sure - reworded the first clause to "Non-existent variables will always evaluate to an empty string when expanded as $FOO or (equivalently) ${FOO}", to address your concern about different flavors of parameter expansion. – godlygeek Jun 24 '14 at 11:26
1

One of the most unique features of the POSIX-compatible shell scripting language is parameter-expansion. It can be used in a variety of ways to accomplish tasks that are not commonly associated with variable values. In the shell a variable has the potential to be more than just a value - it can be an actionable item. It has the potential to test itself. And this comes explicitly - without any need of setting shell options.

For instance, your code could look like:

N= ERR='error encountered - exiting' 
: ${force_color_prompt?"$ERR"}
/usr/bin/tput setaf >/dev/null 2>&1 || ${N:?"$ERR"}
: "${GREEN:=\033[1;32m}" "${DIM:=\033[2m}" "${RESET:=\033[00m}"
printf %b\\n \
    "Oh, ${GREEN}green${RESET} world, ${DIM}don't desert me now...${RESET}"

The $N variable is explicitly set to the null string, and so when it is evaluated with the ${N:?} form of parameter expansion its parent shell is automatically exited, and the statement following ? is evaluated for expansion as well, the results of which are output on stderr. The same goes for $force_color_prompt - if it is not set then the script exits with an error and outputs $ERR to stderr - all automatically.

The $GREEN $RESET and $DIM are set to the values you defined if either they are currently unset or they are set to the '' null string. This enables you to pass their values into the script as environment variables. For instance, if the above snippet were in a script called greenworld.sh and I called it like:

GREEN="$(tput setaf 2)$(tput bold)" greenworld.sh

Then $GREEN would not be reset in the contents of the script and would instead inherit the explicit value I set for it. This makes shell-scripts flexible.

And using tput in that way, as godlygeek has recommended, is a recommendation I, for one, second.

In the shell an unset variable can sometimes be just as useful as a set one. Here's a different example:

set -- * 
while ${1+:} false ; do
    #do stuff until all positionals are shifted away
shift ; done

In that example so long as the first parameter is defined it will expand to the shell's builtin : null, which will consequently render the following false invocation a no-op. But as soon all positional parameters have been shifted away ${1} does not expand in that way, and false is invoked, and the while loop ends. You can do countless variations on this.

-1

In ordinary use, i.e. when you're just expanding variables, an unset variable is treated as empty in all Bourne/POSIX-style shells. The exception is when set -o unset a.k.a. set -u is in effect, in which case the shell will raise an error if you try to access the value of an unset variable (see godlygeek's answer).

There are ways to test whether a variable is unset or empty; for example, the construct ${foo-bar} expands to the value of foo if it is set (even if it's empty) and to bar if foo is unset;
${foo:-bar} (with an extra colon) treats an empty variable as if it were unset. Other similar expansion constructs ${foo+bar}, ${foo?bar}, ${foo=bar} behave similarly. An empty variable will also appear in the output of set and export (if exported), among other differences. You'll only run into these differences if you want to.

However, there is a different reason to always initialize variables: how do you know that the variable is really unset? The caller may have defined a similar variable for its own purposes. If the caller is some other part of the same script, then your use in a function will overwrite the caller's, unless you declare the variable as local (which is only possible in ksh/bash/zsh), so variable name clashes are a problem anyway. But it can also happen that the variable is present in the environment, because someone else picked the same variable name as you. There is a sort of convention to use lowercase names for shell variables and uppercase names for environment variables, but it doesn't solve all clashes and it isn't followed universally.

$ export DIM=1 SUM=42
$ bash
Oh, green world, 1don't desert me now...
  • I'm playing leapfrog with the vote thing because in one paragraph you state specifically how a caller might know if a variable is set, and in the very next state they must always be initialized because you can't know if they're unset. This is not the case - you demonstrate fully portable means of knowing whether they are or not, though maybe you don't know it. POSIX parameter expansion enables a caller to use a concept not widely available in other languages - self-testing variables. – mikeserv Jun 24 '14 at 3:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.