tl;dr - In a little Eureka moment, I realized the reason (or one of the
reasons?) why we need the ${parameter:+word}
expansion with shell
scripting.
Various Unix shells provide a nice set of modifiers to the regular variable expansion. They are defined by the POSIX standard: check it out here.
Quoting directly from the standard we have:
${parameter:-word}
Use Default Values. If parameter is unset or null, the expansion of
word shall be substituted; otherwise, the value of parameter shall
be substituted.
${parameter:=word}
Assign Default Values. If parameter is unset or null, the expansion
of word shall be assigned to parameter. In all cases, the final
value of parameter shall be substituted. Only variables, not
positional parameters or special parameters, can be assigned in this
way.
${parameter:?[word]}
Indicate Error if Null or Unset. If parameter is unset or null, the
expansion of word (or a message indicating it is unset if word is
omitted) shall be written to standard error and the shell exits with
a non-zero exit status. Otherwise, the value of parameter shall be
substituted. An interactive shell need not exit.
${parameter:+word}
Use Alternative Value. If parameter is unset or null, null shall be
substituted; otherwise, the expansion of word shall be substituted.
While the first three of them are straightforward, the last one is somewhat weird: why should I decide to substitute a value only if it exists?
Let's consider this simple scenario: we are writing a wrapper in /bin/sh
for the command foo
, which has the following synopsis:
foo [-x <argument1>] <argument2>
Our wrapper is supposed to do a bit of preliminary work and then invoke
foo
with or without the -x <argument>
part depending on the user
input. No other command line option should be passed to foo
.
We can easily read the option -x
together with our wrapper's options by
mean of getopts
:
while getopts "y:z:x:" opt; do
case "$opt" in
x)
# we want to pass -x and its parameter
pass_x="$OPTARG"
;;
y)
# ... option for the wrapper
;;
z)
# ... option for the wrapper
;;
esac
done
But now we have to take a conditional: if pass_x
is defined we invoke
foo -x "$pass_x" --other --args
, otherwise we need to invoke foo
--other --args
without the -x '$pass_x"
.
A trivial implementation would be
#!/bin/sh
# while getops ... etc
if [ "$pass_x" ]; then
foo -x "$pass_x" --other --args
else
foo --other --args
fi
But this forces us to have separate invocations, and if we have a
long set of arguments (instead of just --other --args
) we have quite an
ugly script, with duplicated code.
Another shot at it can be
#!/bin/sh
# while getops ... etc
foo_args=""
if [ "$pass_x" ]; then
foo_args="-x $pass_x"
fi
foo $foo_args --other --args
This works a bit better in terms of code duplication, but has an obvious
white-space flaw. Just in case you don't see it, replace foo
with printf
'[%s]\n'
like this:
#!/bin/sh
# while getops ... etc
foo_args=""
if [ "$pass_x" ]; then
foo_args="-x $pass_x"
fi
printf '[%s]\n' $foo_args --other --args
If we execute this script, the output will be
[--other]
[--args]
If we assign pass_x=hello-world
we get
[-x]
[hello-world]
[--other]
[--args]
But if we assign pass_x="hello world"
things start to break.
[-x]
[hello]
[world]
[--other]
[--args]
And that's where the ${parameter:+word}
expansion starts to get in
handy. Consider the following:
printf '[%s]\n' ${pass_x:+-x "${pass_x}"} --other --args
And note how, besides being short and sweet, it also does the Right Thing™:
[-x]
[hello world]
[--other]
[--args]