8

I have some variables in a bash script that may contain a file name or be unset. Their content should be passed as an additional argument to a program. But this leaves an empty argument when the variable is unset.

$ afile=/dev/null
$ anotherfile=/dev/null
$ unset empty
$ cat "$afile" "$empty" "$anotherfile"
cat: : No such file or directory

Without quotes, it works just fine as the additional argument is simply omitted. But as the variables may contain spaces, they have to be quoted here.

I understand that I could simply wrap the whole line in a test on emptiness.

if [ -z "$empty" ]; then
  cat "$afile" "$anotherfile"
else
  cat "$afile" "$empty" "$anotherfile"
fi

But one test for each variable would lead to a huge and convoluted decision tree.

Is there a more compact solution to this? Can bash made to omit a quoted empty variable?

XZS
  • 2,374
  • 2
  • 19
  • 38

4 Answers4

8

You can use an alternate value parameter expansion (${var+altvalue}) to include the quoted variable IF it's set:

cat ${afile+"$afile"} ${empty+"$empty"} ${anotherfile+"$anotherfile"}

Since the double-quotes are in the alternate value string (not around the entire parameter expression), they only take effect if the variable is set. Note that you can use either + (which uses the alternate value if the variable is set) or :+ (which uses the alternate value if the variable is set AND not empty).

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • I thought about the delicacies of parameter expansion but quoting inside the braces did not come to my mind. Very elegant, indeed, and thus exactly what I was looking for. – XZS Jul 18 '15 at 16:09
  • 1
    brilliant! And [posix](https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_06_02) – user1593842 Apr 02 '22 at 20:02
2

A pure bash solution is possible using arrays. While "$empty" will evaluate to an empty argument, "${empty[@]}" will expand to all the array fields, quoted, which are, in this case, none.

$ afile=(/dev/null)
$ unset empty
$ alsoempty=()
$ cat "${afile[@]}" "${empty[@]}" "${alsoempty[@]}"

In situations where arrays are not an option, refer to pasaba por aqui's more versatile answer.

XZS
  • 2,374
  • 2
  • 19
  • 38
1

Try with:

printf "%s\n%s\n%s\n" "$afile" "$empty" "$anotherfile" | egrep -v '^$' | tr '\n' '\0' | xargs -0 cat
pasaba por aqui
  • 3,446
  • 16
  • 40
  • Unfortunately, your line just leaves me with the same error. Just employing xargs does not yet remove empty arguments. Inserting `sed 's/\x0\x0/\x0/g;s/\x0*$//;s/^\x0*//' |` before xargs can do this. With it added, I would accept this as the right answer. – XZS Jul 18 '15 at 11:49
  • Also, the `tr` part can be removed. `printf` can print nulls directly by replacing `\n` in the format string with `\0`. – XZS Jul 18 '15 at 11:50
  • @XZS: updated using "grep" instead of "xargs -r". About your solution using sed, I suggest you post it as independent answer (stackoverflow promotes the answers no matter if the author is the author of the question). – pasaba por aqui Jul 18 '15 at 12:06
  • I actually prefer your `grep` way, as it is more compact. Still, it could be condensed even more by writing nulls directly saving the `tr` step. `grep` can handle null delimiters with the `-zZ` options. – XZS Jul 18 '15 at 13:15
1

In the case of a command like cat where you could replace an empty argument with an empty file, you can use the standard shell default replacement syntax:

cat "${file1:-/dev/null}" "${file2:-/dev/null}" "${file3:-/dev/null}"

Alternatively, you could create a concatenated output stream from the arguments which exist, either by piping (as shown below) or through process substitution:

{ [[ -n "$file1" ]] && cat "$file1";
  [[ -n "$file2" ]] && cat "$file2";
  [[ -n "$file3" ]] && cat "$file3"; } | awk ...

This could be simplified with a utility function:

cat_if_named() { [[ -n "$1" ]] && cat "$1"; }

In the particular case of cat to build up a new file, you could just do a series of appends:

# Start by emptying or creating the output file.
. > output_file
cat_if_named "$file1" >> output_file 
cat_if_named "$file2" >> output_file 
cat_if_named "$file3" >> output_file 

If you need to retain the individual arguments -- for example, if you want to pass the list to grep, which will print the filename along with the matches -- you could build up an array of arguments, choosing only the arguments which exist:

args=()
[[ -n "$file1" ]] && args+=("$file1")
[[ -n "$file2" ]] && args+=("$file2")
[[ -n "$file3" ]] && args+=("$file3")

With bash 4.3 or better, you can use a nameref to make a utility function to do the above, which is almost certainly the most compact and general solution to the problem:

non_empty() {
  declare -n _args="$1"
  _args=()
  shift
  for arg; do [[ -n "$arg" ]] && _args+=("$arg"); done
}

eg:

non_empty my_args "$file1" "$file2" "$file3"
grep "$pattern" "${my_args[@]}"
rici
  • 234,347
  • 28
  • 237
  • 341