${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.
zsh
as well. You can shorten it (inbash
andzsh
) toif [ -n "$VAR" ]; then
. I haven't seen the first line yet. – pfnuesel Sep 03 '23 at 18:27