A few tricks with bash variables

Variables are a bit tricky on bash, the most common problem is when “expanding” them because if unquoted, they may cause unforseen effects.

This occurs because after the expansion bash performs “word splitting” but there’s some exceptions.

Here’s an example of word splitting:

$ var='foo bar' $ for element in $var; do > echo $element > done foo bar

The variable got expanded into two separate elements, to tell bash that the whole variable is a single element we must quote it:

$ var='foo bar' $ for element in "$var"; do > echo $element > done foo bar

People sometimes confuse this with the expansion using braces which is used for a different purpose and will not prevent word splitting:

$ var='foo bar' $ for element in ${var}; do > echo $element > done foo bar

You’ll still need to quote that, this notation is used when the variable sits next to some characters that might be interpreted as part of its name, like ${var}iable and also in some other special expansions.

As you can see echo doesn’t care much about the spaces in the var, the [[ is another exception but [ is not:

$ var='foo bar' $ [[ $var = 'foo bar' ]] && echo true true $ [ $var = 'foo bar' ] && echo true bash: [: too many arguments $ [ "$var" = 'foo bar' ] && echo true true

In general commands that already expect one element don’t perform word splitting, such as variable assignment and the case sentence.

$ var='foo bar' $ var2=$var $ echo $var2 foo bar $ case $var in > 'foo bar') > echo ok > ;; > *) > echo bad > ;; > esac ok

You can use word splitting to your advantage, the first example with for is one possible usage, it can also be used to create arrays:

$ var='foo bar' $ arr=($var) $ echo ${arr[0]} foo $ echo ${arr[1]} bar $ arr2=("$var") $ echo ${arr2[0]} foo bar

The read builtin also uses word splitting to capture several vars at once:

$ read a b <<<'foo bar' $ echo $a foo $ echo $b bar

But the most interesting part is when you start playing with the $IFS variable, that variable is used by bash to determine which characters are used for word splitting.

Its default value is <space><tab><newline> but it can be changed:

$ var='foo bar,baz' $ IFS=, $ arr=($var) $ echo ${arr[0]} foo bar $ echo ${arr[1]} baz

Careful because some operations use all the characters while others only use the first one. In the case of word splitting all the charactes are used:

$ var='foo bar,baz' $ IFS=' ,' $ arr=($var) $ echo ${arr[0]} foo $ echo ${arr[1]} bar $ echo ${arr[2]} baz

It’s not advised to leave the IFS changed since it can alter other behaviors so you should restore it to its default value after usage, you can also create a subshell so the changes do not propagate upwards:

$ var='foo bar,baz' $ ( > IFS=, > arr=($var) > echo "${arr[0]}" > echo "${arr[1]}" > ) foo bar baz $ arr=($var) $ echo "${arr[0]}" foo $ echo "${arr[1]}" bar,baz

Or you can also use this 1 weird trick (tee hee) which allows you to modify the value of a variable just for one command:

$ IFS=, read a b <<<'foo bar,baz' $ echo $a foo bar $ echo $b baz $ read a b <<<'foo bar,baz' $ echo $a foo $ echo $b bar,baz

Notice this trick only works with subprocesses, this won’t get the desired effect:

$ HOME=lala echo $HOME /home/samus

But this will:

$ HOME=lala bash -c 'echo $HOME' lala $ echo $HOME /home/samus

We need to use single quotes there otherwise the var will be expanded by the parent shell with its current value instead.

It’s worth mentioning that this notation achieves a similar effect as with export

$ foo=bar $ bash -c 'echo "without export foo is: $foo"' without export foo is: $ export foo $ bash -c 'echo "with export foo is: $foo"' with export foo is: bar

You can use this with several vars at once but if you don’t issue a command the trick loses its magic and becomes normal assignment:

$ HOME=lala foo=bar bash -c 'echo $HOME; echo $foo' lala bar $ echo $HOME; echo $foo /home/samus $ HOME=lala foo=bar $ echo $HOME; echo $foo lala bar

The road to hell is paved with edge cases.