Bash quoting and whitespace

18 Apr 2009

A common thing when writing shell scripts is to allow the user to specify options to commands in a variable. Something like the following:

$ OPTS="--some-option --some-other-option"
$ my_command $OPTS

We can set my_command to the following script to see exactly what gets passed:

#!/bin/bash
for t; do
    echo "'$t'"
done

Running the above prints the following output:

'--some-option'
'--some-other-option'

This works fine, until you want to include options with whitespace in them:

$ OPTS="--libs='-L/usr/lib -L/usr/local/lib'"
$ my_command $OPTS
'--libs='-L/usr/lib'
'-L/usr/local/lib''

This output clearly isn’t what we want. We want a single parameter passed with the entire content of $OPTS. The culprit here is Word Splitting. Bash will split the value of $OPTS into individual parameters based on whitespace. One way to get around this is to put $OPTS in double quotes:

$ OPTS="--libs='-L/usr/lib -L/usr/local/lib'"
$ my_command "$OPTS"
'--libs='-L/usr/lib -L/usr/local/lib''

$ OPTS="--libs=-L/usr/lib -L/usr/local/lib"
$ my_command "$OPTS"
'--libs=-L/usr/lib -L/usr/local/lib'

Putting $OPTS in double quotes suppresses word expansion. In the above example, that works as expected. The second command has the single quotes removed as they were passed directly to the command, which isn’t what we wanted. So far, so good. The problem, as you may have spotted by the removal of the single quotes, comes when we want to pass more than one parameter in $OPTS:

$ OPTS="--cflags=O3 --libs=-L/usr/lib -L/usr/local/lib"
$ my_command "$OPTS"
'--cflags=O3 --libs=-L/usr/lib -L/usr/local/lib'

Here, the entire $OPTS variable gets passed as a single parameter, which isn’t what we want. We want --cflags to be passed as one parameter, and --libs (and everything that comes with it) to be passed as another parameter. Adding more quotes, backslash escaped or not, does nothing to help.

The solution? Use bash arrays:

$ OPTS=("--cflags=O3" "--LIBS=-L/usr/lib -L/usr/local/lib")
$ my_command "${OPTS[@]}"
'--cflags=O3'
'--LIBS=-L/usr/lib -L/usr/local/lib'

Perfect. But what about backward compatibility? If you have hundreds of scripts that use a string for $OPTS, how does it work if you change to using arrays? Let’s try it out:

$ OPTS="--some-option --some-other-option"
$ my_command "${OPTS[@]}"
'--some-option --some-other-option'

So it works if your old scripts only have single options, but if multiple scripts are needed, then they will need to be changed to use arrays instead. This however seems to be the best option for passing multiple arguments with whitespace.