Why does checking whether a folder exists using -d on an empty string return true?

1. These two tests return true:

# [ -d ] && echo true || echo false
true
# [ -d $SOME_UNSET_VAR ] && echo true || echo false
true

according to POSIX (as explained by @Tim).

2. But this returns false (not true as stated in the question)

# [ -d "" ] && echo true || echo false
false

because test is called with two arguments (although the second one is an empty string).

3. That's why it is good practice to use [[ … ]] instead of test ([ … ]), which most (all?) current shells provide. This construct checks if you supply enough arguments (otherwise throws an error and aborts)

# [[ -d ]] && echo true || echo false
bash: unexpected argument `]]' to conditional unary operator
bash: syntax error near `]]'

or simply behaves like one would expect:

# [[ -d $SOME_UNSET_VAR ]] && echo true || echo false
false

4. And, as pointed out by @Gilles, even more important is to double quote substitutions. So -d "$SOME_UNSET_VAR" expands to -d "" and returns false even with test (equal to case 2). Hence this is also compatible with the Bourne shell sh:

# [ -d "$SOME_UNSET_VAR" ] && echo true || echo false
false

tested with bash 3.00.16(1) and 4.1.5(1)


This question has already been answered on StackOverflow. It says that according to the POSIX standard, test should always return successful if it is called with exactly one non-empty argument (and no other arguments).

This should also be the case with test -e (and in fact it is on my system), so be careful.

Instead use:

[ -d "$ARTIFACTS" ]

test will then be called with two arguments even if the variable is empty and return false in this case.


I solved the problem by using [ -e $ARTIFACTS ] which seems to work

You are wrong. It works because $ARTIFACTS is now set to something.

When a variable isn't set, then saying

[ -d $SOMEVAR ]

or

[ -e $SOMEVAR ]

would both evaluate to true because it implies saying

[ -d ]

and

[ -e ]

respectively. (Saying [ foobar ] would always evaluate to true.)

Saying

set -u

comes in handy in such situations. help set would tell you:

  -u  Treat unset variables as an error when substituting.

See that you set the variable ARTIFACTS and you were checking for ARTIFCATS. Probably mistyping?

Anyway, -d as well as -e would produce same results on unset variables.

Hence use double quotes and it will help you.

ARTIFACTS="/SOME/PATH"
[ -d "$ARTIFACTS" ] && rm -rf -- "$ARTIFACTS/"*

NOTE: If your "/SOME/PATH" has any folder with space, the script you mentioned will break with "binary operator expected" error.

Example:

ARTIFACTS="/home backup/"

1) [ -d $ARTIFACTS ] && rm -rf $ARTIFACTS/*
bash: [: /home: binary operator expected

2) [ -d "$ARTIFACTS" ] && rm -rf "$ARTIFACTS"/*

will do fine. Don't forget to put quotes in the rm invocation as well (rm -rf $ARTIFACTS would cheerfully remove /home then complain about backup/* not existing).

Also, including -L check will make sure that it is a directory and not just a symbolic link to a directory.

So, basically,

[ -d "$ARTIFACTS" && ! -L "$ARTIFACTS" ] && rm -rf -- "$ARTIFACTS"/*