Note: This answer is to an extent speculative, and I’m not sure I can find sources for some of the claims I make here. So I cannot say for certain that this is the definitive reason. But I find it pretty plausible.
The test
command actually provides three forms of (non-)emptiness checking:
test "$a"
test -n "$a"
vs test -z "$a"
test "$a" != ""
vs test "$a" = ""
In contemporary Unix-like systems, they can be reliably distinguished by counting arguments to the test
command: one argument means the first form was used, two arguments mean it’s the second form, and three arguments mean it’s the third form. This method is recommended by SUS and by POSIX (IEEE 1003.1-2017 vol. 3, ch. 4, pp. 3290–3291). Historically though, different algorithms have been used: some implementations of test
parsed options like any other utility, matching command line arguments against a list of known options, which caused problems if the string to be tested happened to match one of them. The following transcript, for example, comes from Version 7 Unix:
$ test '' ; echo $?
1
$ test x ; echo $?
0
$ test -r ; echo $?
test: argument expected
255
$ test '' = '' ; echo $?
0
$ test x = '' ; echo $?
1
$ test x = x ; echo $?
0
$ test -r = -r ; echo $?
1
Also, not all of these constructs were available on all systems. Some provided -z
, but not -n
, and apparently some even provided neither. Furthermore, there were shells in use in which empty string arguments were not passed to the command executed, so that test "$a" = ""
could run the test
command with just two or one arguments, and trigger a syntax error. For these reasons, in configure
scripts generated by GNU Autoconf, emptiness testing is performed via the command test "x$var" = x
.
Add to that that some shell scripts did not even bother to quote variables: instead of writing "$a"
each time, they would write $a
, so an empty variable would have resulted in an empty string argument even if the shell supported those correctly. (This remains a common shell programming blunder to this day.)
The -z
option may have originated as a workaround for some of these problems:
- Having an explicit operator ensures that tested strings would not be themselves interpreted as testing operators.
- With a buggy shell that doesn’t pass empty arguments correctly,
test -z "$foo"
may result in test
being called:
If foo
is non-empty, with two arguments: -z
, and the contents of foo
; this then returns failure, as expected.
If foo
is empty, with one argument -z
, which the test
command may then interpret as either:
- a check whether an empty list of remaining arguments is empty (which it is), or
- a check whether the
-z
string is non-empty, which just so happens to have the same truth value as the emptiness of "$foo"
.
Either way, the command returns success – as expected.
The -n
command may have been added to similarly disambiguate the first form from a missing argument when the string to be tested matches a known test
operator, e.g. test -n '-t'
vs test -t
.