58

Say I have a script doing:

some-command "$var1" "$var2" ...

And, in the event that var1 is empty, I'd rather that it be replaced with nothing instead of the empty string, so that the command executed is:

some-command "$var2" ...

and not:

some-command '' "$var2" ...

Is there a simpler way than testing the variable and conditionally including it?

if [ -n "$1" ]; then
    some-command "$var1" "$var2" ...
    # or some variant using arrays to build the command
    # args+=("$var1")
else
    some-command "$var2" ...
fi

Is there a parameter substitution than can expand to nothing in bash, zsh, or the like? I might still want to use globbing in the rest of the arguments, so disabling that and unquoting the variable is not an option.

Michael Homer
  • 76,565
muru
  • 72,889
  • I knew I had seen and probably used this before, but it proved difficult to search. Now that Michael showed the syntax, I remembered where I'd first seen it quickly enough: https://unix.stackexchange.com/a/269549/70524, https://unix.stackexchange.com/q/68484/70524 – muru Jan 10 '18 at 07:25
  • If you know it is some kind of parameter substitution, why didn't you look into the parameter expansion section of the man page? (-; – Philippos Jan 10 '18 at 08:29
  • 1
    @Philippos I didn't know what it was at the time, only that I'd seen or used it before. Known knowns and unknown knowns. :( – muru Jan 10 '18 at 08:33
  • 1
    extra cookie points for mentioning using an array to hold the arguments in the question itself. – ilkkachu Jan 10 '18 at 15:12
  • I had the exact same question today. Luckily it was already asked and a great answer below. – WinEunuuchs2Unix Jun 20 '20 at 18:47

3 Answers3

65

Posix compliant shells and Bash have ${parameter:+word}:

If parameter is unset or null, null shall be substituted; otherwise, the expansion of word (or an empty string if word is omitted) shall be substituted.

So you can just do:

${var1:+"$var1"}

and have var1 be checked, and "$var1" be used if it's set and non-empty (with the ordinary double-quoting rules). Otherwise it expands to nothing. Note that only the inner part is quoted here, not the whole thing.

The same also works in zsh. You have to repeat the variable, so it's not ideal, but it works out exactly as you wanted.

If you want a set-but-empty variable to expand to an empty argument, use ${var1+"$var1"} instead.

ilkkachu
  • 138,973
Michael Homer
  • 76,565
  • 1
    Good enough for me. So, in this, the whole thing is not quoted, only the word part is. – muru Jan 10 '18 at 06:57
  • 1
    As the question asked for "bash, zsh, or the like" (and for the Q&A archive), I edited to reflect that this is a posix feature. Even, if you add a /bash tag to the question after my comment. (-; – Philippos Jan 10 '18 at 08:25
  • 2
    I took the liberty of editing in the difference between :+ and +. – ilkkachu Jan 10 '18 at 15:16
  • Thanks much for a POSIX-compliant solution! I try to use sh/dash for potential chokepoint scripts, so I always appreciate it when someone shows how a thing is possible without resorting to Bash. – JamesTheAwesomeDude May 29 '18 at 20:19
  • I was looking for the opposite effect (Expand to something else if it starts as null). It turns out this logic can be inverted by changing the + to a - as in: ${empty_var:-replacement} – Alex Jansen Mar 22 '19 at 06:40
  • This still replaces it with empty space...and sometimes you don't even want a space there, like in filenames, etc – Freedo Mar 17 '20 at 05:11
  • @Freedo No, it doesn't... and "empty space" and "a space" are not interchangeable. – Michael Homer Mar 17 '20 at 05:13
  • Trying to create a filename with 2 variables $title and $year. Sometimes ỳear is empty, and when it's not it has a space... like 2019 and this syntax still creates a file where year has been replaced by a space.... $title${year:+"$year"}. I'd expect the filename to end with $title if year is empty...but it has a space after the title. Like $title – Freedo Mar 17 '20 at 05:17
  • 1
    @Freedo It sounds like you want to ask a new question, but try to be concrete in what you're observing and expecting. I would not be surprised if a variable containing a space gives you back a space when you ask for its value, if that's what you're experiencing. – Michael Homer Mar 17 '20 at 05:19
  • I only add a space to the variable if it's not empty, so this is not what happening... if [ "$year" == "null" ]; then year=""; else year=" ($year)"; fi and I can see the variable is empty, still it's replaced by a space – Freedo Mar 17 '20 at 05:22
  • @Freedo Ask a new question with your code and observations, where people can see it to answer rather than in the comments here. – Michael Homer Mar 17 '20 at 05:30
10

That's what zsh does by default when you omit the quotes:

some-command $var1 $var2

Actually, the only reason why you still need quotes in zsh around parameter expansion is to avoid that behaviour (the empty removal) as zsh doesn't have the other problems that affect other shells when you don't quote parameter expansions (the implicit split+glob).

You can do the same with other POSIX-like shells if you disable split and glob:

(IFS=; set -o noglob; some-command $var1 $var2)

Now, I'd argue that if your variable can have either 0 or 1 value, it should be an array and not a scalar variable and use:

some-command "${var1[@]}" "${var2[@]}"

And use var1=(value) when var1 is to contain one value, var1=('') when it's to contain one empty value, and var1=() when it's to contain no value.

-1

I ran into this using rsync in a bash script that started the command with or without a -n to toggle dry runs. It turns out that rsync and a number of gnu commands take '' as a valid first argument and act differently than if it were not there.

This took quite awhile to debug as null parameters are almost completely invisible.

Someone on the rsync list showed me a way to avoid this problem while also greatly simplifying my coding. If I understand it correctly, this is a variation on @Stéphane Chazelas's last suggestion.

Build your command arguments in a number of separate variables. These can be set in any order or logic that suits the problem.

Then, at the end, use the variables to construct an array with everything in its proper place and use that as arguments to the actual command.

This way the command is only issued at one place in the code instead of being repeated for each variation of the arguments.

Any variable that is empty just disappears using this method.

I know using eval is highly frowned upon. I don't remember all the details, but I seemed to need it to get things to work this way - something to do with handling parameters with embedded white space.

Example:

dry_run=''
if [[ it is a test run ]]
then
  dry_run='-n'
fi
...
rsync_options=(
  ${dry_run}
  -avushi
  ${delete}
  ${excludes}
  --stats
  --progress
)
...
eval rsync "${rsync_options[@]}" ...
Joe
  • 1,368
  • That's a worse way of doing what I described in the question (building an array of arguments conditionally). By leaving the variables unquoted, who knows what problems you're leaving yourself open to. – muru Jan 13 '18 at 15:35
  • @muru You're right, but I still need something like this. I don't quite see how to fix it using the techniques from the other answers. It would be great to see a code fragment done the right way that puts it all together. I'm going to play with Stephane's last option later as that might do what I want. – Joe Jan 14 '18 at 10:20
  • Like https://paste.ubuntu.com/26383847/? – muru Jan 14 '18 at 10:31
  • Just got back to this. Thanks for the example. It makes sense. I'm going to work with it. – Joe Mar 09 '18 at 05:11
  • 1
    I have a similar use case here, but you could do something like this: if [ "$dry" == "true" ]; then dry="-n"; fi...so if variable dry is true, you make it be -n, and then you build your rsync command as usual and have it like this: rsync ${dry1:+"$dry1"}... if it's null nothing will happen, otherwise it will become -n in your command – Freedo Apr 22 '19 at 02:57
  • 1
    You're already using arrays, using eval makes no sense and defats the purpose of using arrays.. To build an array based on other arrays rsync_options=( "${dry_run[@]}" "${other_options[@]}"...) (and use dry_run=( ) / dry_run=( -n )... – Stéphane Chazelas May 10 '23 at 11:57
  • @StéphaneChazelas That's quite a nice construct. I'll have to digest it a bit, but I think I understand how it works. – Joe Aug 06 '23 at 12:19