Bash quoting and whitespace
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.