5

I am new to zsh and have been a bash user for years.

In an example zsh script I see a test:

if [ ! -z ${ZSH_MOTD_CUSTOM+x} ]; then

In bash I would expect:

if [ ! -z "$ZSH_MOTD_CUSTOM" ]; then

I don't understand the meaning of +x in the zsh example and if is applicable to bash.

Que
  • 53

3 Answers3

5

${var+string} is the same operator as in the Bourne shell (from the late 70s) and in any POSIX shell (including bash). You can find it described in the zsh documentation in info zsh 'Parameter Expansion':

${NAME+WORD}
${NAME:+WORD}
If NAME is set, or in the second form is non-null, then substitute WORD; otherwise substitute nothing.

It's harder to find in info bash 'Parameter Expansion' but it's the same there. For sh, you can check the POSIX specification (though like in the bash manual, you need to pay attention to the sentence that mentions the effect of omitting the colon in ${parameter:+word}¹).

The main difference with bash and other Bourne-like shells, is that that ${ZSH_MOTD_CUSTOM+x} being unquoted is not subject to split+glob, because in zsh, when you do want IFS-splitting and/or globbing performed upon parameter expansion you have to request it explicitly ($=var for splitting, $~var for globbing (strictly speaking for the contents of $var to be treated as a pattern), $=~var for both, which be the equivalent of $var in other shells).

It's wrong though as unquoted expansions are still subject to empty removal. In that case though, by accident, it's not going to be a problem, and may be why the author chose to write it [ ! -z $expansion ] instead of [ -n $expansion ] which wouldn't work.

If $expansion is empty (in the case of ${ZSH_MOTD_CUSTOM+x} if $ZSH_MOTD_CUSTOM is not set), [ ! -z $expansion ] becomes [ ! -z ] instead of [ ! -z '' ], so it doesn't test whether the expansion is non empty, but whether -z itself is an empty string (which it isn't obviously), so it achieves the right outcome (test whether the variable is set) for the wrong reason.

In bash, that unquoted ${ZSH_MOTD_CUSTOM+x} would have been subject to split+glob so that would be even more wrong, but because it can only expand to either the empty string or a literal x, the problem would have been only if $IFS happened to contain x:

$ bash -xc 'IFS=y; [ ! -z ${HOME+x} ]; echo "$?"'
+ IFS=y
+ '[' '!' -z x ']'
+ echo 0
0
$ bash -xc 'IFS=x; [ ! -z ${HOME+x} ]; echo "$?"'
+ IFS=x
+ '[' '!' -z '' ']'
+ echo 1
1
$ zsh -xc 'IFS=x; [ ! -z ${HOME+x} ]; echo "$?"'
+zsh:1> IFS=x
+zsh:1> [ ! -z x ']'
+zsh:1> echo 0
0

The correct syntax in any POSIX shell would be:

if [ -n "${ZSH_MOTD_CUSTOM+x}" ]

Even works here in the Bourne shell where [ -n "$var" ] fails for values of $var that are things like =, -gt... as the expansion can only be x or the empty string (so not any of the problematic ones in those ancient implementations of [).

Now zsh has more idiomatic ways to check whether a variable is set such as:

if (( $+ZSH_MOTD_CUSTOM ))

Where $+var expands to 1 if $var is set and 0 otherwise.

Or:

if [[ -v ZSH_MOTD_CUSTOM ]]

À la ksh (also found in bash).

In any case, [ ! -z "$ZSH_MOTD_CUSTOM" ], itself a convoluted way to write [ -n "$ZSH_MOTD_CUSTOM" ] does something different: if checks whether the variable is non-empty or not regardless of whether it is set or not. The main difference is that it will return false if $ZSH_MOTD_CUSTOM is set, but to an empty value. Contrary to the ${ZSH_MOTD_CUSTOM+x} variant, it would also cause an error if the variable was unset and the nounset option was enabled.

$ zsh -c 'if [ -n "$foo" ]; then echo non-empty; else echo empty; fi'
empty
$ foo= zsh -o nounset -c 'if [ -n "$foo" ]; then echo non-empty; else echo empty; fi'
empty
$ foo= zsh -o nounset -c 'if [ -n "${foo+x}" ]; then echo set; else echo unset; fi'
set
$ zsh -o nounset -c 'if [ -n "$foo" ]; then echo non-empty; else echo empty; fi'
zsh:1: foo: parameter not set
$ zsh -o nounset -c 'if [ -n "${foo+x}" ]; then echo set; else echo unset; fi'
unset
$ zsh -o nounset -c 'if [ -n "${foo-}" ]; then echo non-empty; else echo empty; fi'
empty

(in the last one, we use ${foo-} which expands to the same thing as $foo but avoids the effect of nounset (aka set -u)).


¹ Note that in the Bourne shell where the feature comes from, initially (in Unix 7th edition from the late 70s) only the versions without colon were supported, the ones with colons came later in SysIII.

3

This isn't due to test, but on zsh variable expansion.

The construct ${foo+bar} will return bar if $foo is set.

So, for example:

zsh% unset foo
zsh% echo ${foo+bar}

zsh% foo= zsh% echo ${foo+bar} bar

3

This is a parameter (variable) expansion syntax that's also found in Bash. Probably in most other Bourne-style shells, too. The only thing special to zsh in your example is the variable being tested.

In the bash man page we see two things. The explanation of this expansion syntax:

  ${parameter:+word}
    Use Alternate Value. If parameter is null or unset, nothing is substituted,
    otherwise the expansion of word is substituted.

And a few paragraphs earlier, at the top of the section of expansion syntax, there is something a lot of people overlook:

  When not performing substring expansion, using the forms documented below
  (e.g., :-),  bash  tests for a parameter that is unset or null.  Omitting
  the colon results in a test only for a parameter that is unset.

That last sentence is the relevant part: Omitting the colon results in a test only for a parameter that is unset. To illustrate, the syntax as given in the man page is:

  ${parameter:+word}

with the colon omitted it is:

  ${parameter+word}

And the difference is that the latter syntax returns the value in word in all cases except when the variable is unset (doesn't exist). So the syntax you saw in your zsh script:

  ${ZSH_MOTD_CUSTOM+x}

Returns x if $ZSH_MOTD_CUSTOM has been defined (no matter what the value is - even an empty string), but returns an empty string if the variable is not defined.

In the end, your script example tests only that the variable exists, without regard to the value it contains. I answered by quoting the Bash man page because you mentioned you have been a bash user and will find the bash descriptions familiar.

It's not a common idiom in the Bourne-style shell scripts I've seen over my (fairly long) career, so a lot of people aren't aware of the :-, :=, :?, and :+ syntax without the :. I learned about them only recently myself.

Sotto Voce
  • 4,131
  • 1
    "the latter syntax returns the value in word only when the variable is unset (doesn't exist)." -- pretty much the opposite actually, doesn't it? – ilkkachu Sep 04 '23 at 06:09
  • it doesn't have anything to do with the quotes (not even the part you formatted as code even though it clearly isn't. If you wanted to highlight it, why not use italics or bold?) It has to do with what + does. Try it: unset foo; echo "${foo+x}" vs. the same with foo=, or foo=abc, or with "${foo-x}" instead. – ilkkachu Sep 04 '23 at 06:29
  • anyway, I would also suggest using quote formatting instead of code blocks when quoting man pages, mostly because having word wrapping helps when the reader's screen isn't wide enough that the whole text fits. It's not like documentation text is that sensitive to having the linebreaks at the exact places. – ilkkachu Sep 04 '23 at 06:34
  • @ikkachu thanks for the suggestion regarding formatting, but I considered it more important in these excerpts from man pages that they look more like man page output - with similar monospace font and indentation. I don't believe anyone will see those particular excerpts and mistakenly believe they're code. – Sotto Voce Sep 04 '23 at 06:39
  • Did you test any of that? Where did I get the idea that ${parameter+word} returns word when the parameter is set? Humor me, try foo=123; echo "${foo+x}" and tell me what it prints? What shell are you running? – ilkkachu Sep 04 '23 at 06:40
  • Did you read the parts you quoted from the man page? Did you notice how the first one has ${parameter:+word} and mentions it substitutes the expansion of "word" if the parameter is set and not null, and how the second one refers to :- instead? What the + and - do are listed in the man page... – ilkkachu Sep 04 '23 at 06:42
  • Note that there's also the online reference manual, with the same content but without the monospace formatting: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html (except for the parts that are actually code, and it also makes the :- within the text at least a bit more visibly marked) – ilkkachu Sep 04 '23 at 06:44
  • 2
    As an aside, I think this area is one where the Bash manual is a bit too confusingly written, mostly about how it almost ignores the colon-less variants. On the other hand, the POSIX text has a nice table of the combinations: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 – ilkkachu Sep 04 '23 at 06:48
  • @ilkkachu, yes there are dozens if not hundreds of Q&A's here which are mostly a consequence of the documentation for ${var-x}/${var+x}/${var=x} being hard to find in the bash manual. – Stéphane Chazelas Sep 04 '23 at 06:56
  • 1
    @ikkachu I tested and saw my error describing how the expansion works. Fixed that error. Thank you. – Sotto Voce Sep 04 '23 at 07:04