quote only if variable is non-empty

I have an ancient bash script that I'd like to improve. This script used many variables and in some cases some of the variables are referenced but empty or unset. This is generally not a problem here because the script has been designed like this from the beginning.

However, I have started to use shellcheck which complains about missing quotes a lot. So I have added quotes and came across an inconvience for which I do not have a nice solution.

There are many lines that look like this:

cmd $param1 $param2 $param3 $param4

If one of the params is empty or unset it is still working and the number of params that cmd gets is reduced. But if I start adding quotes

cmd "$param1" "$param2" "$param3" "$param4"

cmd will always get 4 params, no matter if any of them is empty.

Why it is like that is perfectly clear. The workaround is also clear (checking for emptyness before using the param) but it is ugly and requires a lot of additional code.

So I am looking for clever solution that either a) omits a param (including the quotes!) if it is empty b) adds the quoting only for non-empty variables


Solution 1:

What shell are you using for this script? I assume it's /bin/sh

The only array-like entity you have in a POSIX shell is the positional parameters. You could do this:

set --  # clear the positional parameters
for param in "$param1" "$param2" "$param3" "$param4"; do
    if [ -n "$param" ]; then
        # append this param to the positional params
        set -- "$@" "$param"
    fi
done
# invoke the command with the positional params, properly quoted
cmd "$@"

If you wanted to get cleverer about it, wrap the invocation of cmd in a function:

invoke_cmd() {
  n="$#"  # remember the original number of params
  # append to the args list only if non-empty                    
  for arg; do [ -n "$arg" ] && set -- "$@" "$arg"; done
  # discard the original params (including the empty ones)
  shift "$n"
  # and invoke the cmd
  cmd "$@"
}
invoke_cmd "$param1" "$param2" "$param3" "$param4"

For actual bash, simplify

invoke_cmd() {
  local args=()
  for arg; do [[ "$arg" ]] && args+=("$arg"); done
  cmd "${args[@]}"
}
invoke_cmd "$param1" "$param2" "$param3" "$param4"

Solution 2:

Another option (besides what @glennjackman described) is to use conditional expansion with :+ to include a (properly quoted) variable only if it's set and nonempty:

cmd ${param1:+"$param1"} ${param2:+"$param2"} ${param3:+"$param3"} ${param4:+"$param4"}

(Note: conditional expansion is in the posix spec, in section 2.6.2; but I'm not entirely sure all posix shells will properly respect the double-quotes in the alternate value.)

If you're willing to make more extensive changes to the script, and restrict it to bash (i.e. if you're using a #!/bin/bash or #!/usr/bin/env bash shebang), it's generally cleaner to accumulate parameters in an array:

cmd_parameters=() # start with an empty array
if [ something ]; then
    cmd_parameters+=("$param1") # Add an element -- note that the () are critical here!
fi
if [ something_else ]; then
    cmd_parameters+=("$param2")
fi
...etc
cmd "${cmd_parameters[@]}"

Note that it's entirely possible to mix these approaches, including using the :+ trick to conditionally add an array element:

cmd_parameters+=(${param3:+"$param3"})

and/or mixing array and conditional parameters to the same command:

cmd "${cmd_parameters[@]}" ${param4:+"$param4"}

Also, playing on @glennjackman's suggestion of a wrapper function, it'd be possible to write a generic wrapper function:

conditional_args() {
    # invoke a command ($1) with any empty arguments omitted
    args=()
    for arg; do
        args+=(${arg:+"$arg"})
    done
    "${args[@]}" # Note that the first "arg" is actually the command itself
}

conditional_args cmd "$param1" "$param2" "$param3" "$param4"