Why bash & ldquo; test -n & rdquo; command gives the wrong result for the $ @ (dollar at) position parameter while & ldquo; test -z & rdquo; works?

advertisements

Using [email protected] inside a bash test expression gives odd results.

Here's a minimal test script to reproduce the issue (among the alternative formulations for the same test which do pass without error):

#! /bin/bash
# file path: ./bash_weirdness.sh

echo "args: '[email protected]'"

echo "--- empty ---"

if test "" ; then echo      "1.A1.TEST      - not empty?"; fi
if test -z "" ; then echo   "1.A2.EMPTY     - empty?"; fi
if test -n "" ; then echo   "1.A3.NE        - not empty?"; fi
if ! test "" ; then echo    "1.B1.NOT.TEST  - empty?"; fi
if ! test -z "" ; then echo "1.B2.NOT.EMPTY - not empty?"; fi
if ! test -n "" ; then echo "1.B3.NOT.NE    - empty?"; fi

echo "--- space ---"

if test " " ; then echo      "2.A1.TEST      - not empty?"; fi
if test -z " " ; then echo   "2.A2.EMPTY     - empty?"; fi
if test -n " " ; then echo   "2.A3.NE        - not empty?"; fi
if ! test " " ; then echo    "2.B1.NOT.TEST  - empty?"; fi
if ! test -z " " ; then echo "2.B2.NOT.EMPTY - not empty?"; fi
if ! test -n " " ; then echo "2.B3.NOT.NE    - empty?"; fi

echo "--- \[email protected] ---"

if test "[email protected]" ; then echo      "3.A1.TEST      - not empty?"; fi
if test -z "[email protected]" ; then echo   "3.A2.EMPTY     - empty?"; fi
if test -n "[email protected]" ; then echo   "3.A3.NE        - not empty?"; fi
if ! test "[email protected]" ; then echo    "3.B1.NOT.TEST  - empty?"; fi
if ! test -z "[email protected]" ; then echo "3.B2.NOT.EMPTY - not empty?"; fi
if ! test -n "[email protected]" ; then echo "3.B3.NOT.NE    - empty?"; fi

echo "--- \$* ---"

if test "$*" ; then echo      "4.A1.TEST      - not empty?"; fi
if test -z "$*" ; then echo   "4.A2.EMPTY     - empty?"; fi
if test -n "$*" ; then echo   "4.A3.NE        - not empty?"; fi
if ! test "$*" ; then echo    "4.B1.NOT.TEST  - empty?"; fi
if ! test -z "$*" ; then echo "4.B2.NOT.EMPTY - not empty?"; fi
if ! test -n "$*" ; then echo "4.B3.NOT.NE    - empty?"; fi

The first two sections ('empty' and 'space') are in there to verify that test, test -n and test -z work fine; that's how bad the confusion is.

The problem occurs when [email protected] represents 'no arguments', i.e. when $# would be zero(0).

Three test runs show what's wrong: notice the '3.A3.NE - not empty?' report in the third run:

$ ./bash_weirdness.sh x
->
args: 'x'
--- empty ---
1.A2.EMPTY     - empty?
1.B1.NOT.TEST  - empty?
1.B3.NOT.NE    - empty?
--- space ---
2.A1.TEST      - not empty?
2.A3.NE        - not empty?
2.B2.NOT.EMPTY - not empty?
--- [email protected] ---
3.A1.TEST      - not empty?
3.A3.NE        - not empty?
3.B2.NOT.EMPTY - not empty?
--- $* ---
4.A1.TEST      - not empty?
4.A3.NE        - not empty?
4.B2.NOT.EMPTY - not empty?

Which is fine.

$ ./bash_weirdness.sh ""
->
args: ''
--- empty ---
1.A2.EMPTY     - empty?
1.B1.NOT.TEST  - empty?
1.B3.NOT.NE    - empty?
--- space ---
2.A1.TEST      - not empty?
2.A3.NE        - not empty?
2.B2.NOT.EMPTY - not empty?
--- [email protected] ---
3.A2.EMPTY     - empty?
3.B1.NOT.TEST  - empty?
3.B3.NOT.NE    - empty?
--- $* ---
4.A2.EMPTY     - empty?
4.B1.NOT.TEST  - empty?
4.B3.NOT.NE    - empty?

Which is fine too.

$ ./bash_weirdness.sh
->
args: ''
--- empty ---
1.A2.EMPTY     - empty?
1.B1.NOT.TEST  - empty?
1.B3.NOT.NE    - empty?
--- space ---
2.A1.TEST      - not empty?
2.A3.NE        - not empty?
2.B2.NOT.EMPTY - not empty?
--- [email protected] ---
3.A2.EMPTY     - empty?
3.A3.NE        - not empty?
3.B1.NOT.TEST  - empty?
--- $* ---
4.A2.EMPTY     - empty?
4.B1.NOT.TEST  - empty?
4.B3.NOT.NE    - empty?

Which is not fine at all: notice the incorrect '3.A3.NE' result.


"[email protected]" and test don't mix

test -n "[email protected]"

This test isn't properly written. The -n test expects a single argument and tells you if that argument is an empty string or not. "[email protected]" will expand into one word, multiple words, or even no words at all. It is therefore inappropriate to write test -n "[email protected]".

If you want to check that arguments were passed, use one of these:

test $# -gt 0
[[ $# -gt 0 ]]
(($# > 0))
(($#))

If you want to check that arguments were passed and also are non-empty, then use "$*" instead. $* always expands to a single string, so it's compatible with test -n.

test -n "$*"
[[ -n $* ]]

The confusing case of test -n

$ test -n; echo $?
0

So why the heck does this test pass? Well, if you don't pass an argument to -n then bash doesn't see the -n as an operator. Instead, it understands the test as a form of

test STRING

If you write test STRING without an operator, it's implicitly equivalent to test -n STRING. test STRING is a shorthand way of testing if STRING is non-empty.

In other words, test -n is equivalent to

test -n '-n'

And this test passes since '-n' is a non-empty string.

"[email protected]" is awesome

By the way, the way "[email protected]" behaves is in fact a great thing. "[email protected]" is the best way to access the full argument list while handling whitespace correctly and avoiding issues with word splitting and globbing. When in doubt, use "[email protected]".

And if you want to store "[email protected]" in a variable, use an array.

args=("[email protected]")

for arg in "${args[@]}"; do
    echo "$arg"
done

That will preserve the multi-wordedness of "[email protected]".