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.

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.

Load Disqus comments