Bash Quoting
Quoting things in bash is quite simple... until it isn't. I've written scripts where I'd swear 90% of the effort was getting bash to quote things correctly.
The basics of quoting are simple, use single or double quotes when a value contains spaces:
a="hello world"
a='hello world'
But single and double quotes are different. Single quotes don't support any kind of variable substitution or escaping of special characters inside the quoted string. Double quotes, on the other hand, support both:
a="hello \"there\" world"
a='hello "there" world'
a='hello \'there\' world' # causes an error
a="hello 'there' world"
b="there"
a='hello \"$b\" world' # a is >>hello \"$b\" world
a="hello \"$b\" world" # a is >>hello "there" world
One way around the problem that single quotes don't support variable substitution is to quote the individual literal parts of the string and then put the variable substitutions between them:
b='"there"'
a='"hello" '$b' "world"' # a is: >>"hello" "there" "world"<<
Note that "$b" is not actually inside a quoted part of the string. Since there's no space between the two literal parts of the string and the "$b", bash puts the final result into a single string and then assigns it to a.
One place where quoting problems often occur is when you're dealing with files that have spaces in their names. For example, if we have a file named "file with spaces" and we run the following script:
#/bin/bash
for i in $(find .)
do
echo $i
done
We don't get what we want:
$ bash t1.sh
.
./file
with
spaces
./t2.sh
./t1.sh
...
One solution is to put the file names into a bash array when the Internal Field Separator is set to just a newline. Then we use the value "${array[@]}" as the list for the for loop.
#/bin/bash
newline='
'
OIFS=$IFS
IFS=$newline
files=($(find .))
IFS=$OIFS
for i in "${files[@]}"
do
echo $i
done
This produces the correct output:
$ bash t2.sh
.
./file with spaces
./t2.sh
./t1.sh
...
The difference between "${array[*]}" and "${array[@]}" is analagous to the difference between "$*" and "$@", namely that in the later case the result is that each word becomes separately quoted rather than the entire thing being quoted as a single string.
Another option, which avoids special quoting is to just set the Internal Field Separator variable before the for loop runs and reset it as the first statement in the loop body:
#/bin/bash
newline='
'
OIFS=$IFS
IFS=$newline
for i in $(find .)
do
IFS=$OIFS
echo $i
done
Another quoting problem is when you have a string which you'd like to break up into separate words but you'd also like to honor any quoted substrings in the string. Consider the string:
a="hello \"there big\" world"
and suppose you'd like to break that into its three parts:
- hello
- there big
- world
If you run this:
#!/bin/bash
a="hello \"there big\" world"
for i in $a
do
echo $i
done
It produces this:
$ bash t4.sh
hello
"there
big"
world
The trick here is to use a special form of the set command to reset $* (and therefore, $1, $2, etc). My first version is always like this:
#!/bin/bash
a="hello \"there big\" world"
set -- $a
for i in "$@"
do
echo $i
done
And of course it doesn't work:
$ bash t5.sh
hello
"there
big"
world
Then I usually remember you have to eval the set statement:
#!/bin/bash
a="hello \"there big\" world"
eval set -- $a
for i in "$@"
do
echo $i
done
At which point it works:
$ bash t6.sh
hello
there big
world
at this point I'm usually tired of quoting things.