How can you double quote a shell variable in a command line only if the variable is set?

37 Views Asked by At

I need to execute a bash command where, only if a variable is set, I want to include that variable on the command line. The trick is that the variable could have a space in it, and the variable must be passed as a single argument.

The following command will not do the correct thing if foo is unset (but will if it is set), it will pass the command bar an empty argument since $FOO is not set but is double quoted.

FOO=""
bar "$FOO"

The following command fixes the flaw of $FOO being unset, but adds the flaw of passing two arguments if $FOO has a space in it.

FOO="one two"
bar $FOO

I tried using bar ${FOO+"}$FOO${FOO+"} but while it stuck quotes on there, it still passed two arguments. I imagine that if using eval with sufficiently convoluted quoting of all of the other arguments (not listed here) that would be impossible to get right without a lot of testing, the problem could also be solved.

Obviously you could say if [ "$Z" ]; then bar "$Z"; else bar; fi but this does not scale.

What is the best way to accomplish this?

2

There are 2 best solutions below

1
that other guy On BEST ANSWER

Use ${var:+"$var"}

This works because the alternate value respects quotes:

unset var
set -- ${var:+"$var"}
echo "Unset: $# args"

var=""
set -- ${var:+"$var"}
echo "Blank: $# args"

var="foo bar"
set -- ${var:+"$var"}
echo "Multiple words: $# args"

outputs

Unset: 0 args
Blank: 0 args
Multiple words: 1 args

(To pass empty strings but not unset values, use ${var+"$var"} without the colon)

0
Seth Robertson On

The magic shell array quoting syntax is the best answer I could think of, since in the general case no-one knows how many elements exist and they might still need to be double-quoted.

For simple programs, you might have something like bar "$@" which will double quote all arguments to the shell. This is not super convenient for my use case, since I'd have to jump into a function and lose access to the real command arguments.

So for this use case, use a temporary shell array:

Z=("one two")
bar "${Z[@]}"

You can see this in action with:

for Z in "foo bar" ""; do
 echo "Running case with Z=<$Z>"
 if [ "$Z" ]; then Z=("$Z"); else Z=(); fi
 for f in 1 "${Z[@]}" 2; do echo $f; done
done

Which will produce the (desired/correct) output:

Running case with Z=<foo bar>
1
foo bar
2
Running case with Z=<>
1
2

(incorrect outputs would have foo and bar on separate lines, or an empty lines between 1 and 2).