zsh -z test meaning of "+x"
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.
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
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.
${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}
IfNAME
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.