omit passing an empty quoted argument
Asked Answered
N

4

9

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?

Nunnally answered 18/7, 2015 at 9:0 Comment(0)
M
11

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).

Macaco answered 18/7, 2015 at 15:4 Comment(2)
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.Nunnally
brilliant! And posixMothering
N
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.

Nunnally answered 18/7, 2015 at 13:16 Comment(0)
D
1

Try with:

printf "%s\n%s\n%s\n" "$afile" "$empty" "$anotherfile" | egrep -v '^$' | tr '\n' '\0' | xargs -0 cat
Disused answered 18/7, 2015 at 10:34 Comment(4)
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.Nunnally
Also, the tr part can be removed. printf can print nulls directly by replacing \n in the format string with \0.Nunnally
@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).Disused
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.Nunnally
B
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[@]}"
Binate answered 18/7, 2015 at 14:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.