Pages: Welcome | Projects

Conditional expansion of commands parameters

2019/3/18
Tags: [ shell ] [ Today I learned ]

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]