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.
test command actually provides three forms of (non-)emptiness checking:
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 $?
$ test x ; echo $?
$ test -r ; echo $?
test: argument expected
$ test '' = '' ; echo $?
$ test x = '' ; echo $?
$ test x = x ; echo $?
$ test -r = -r ; echo $?
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.)
-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:
foo is non-empty, with two arguments:
-z, and the contents of
foo; this then returns failure, as expected.
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
Either way, the command returns success – as expected.
-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