4

Well I've this code

dirname=

if [ -d $dirname ];
 then
 cd $dirname && rm *
fi

as you see I've this empty variable, what I want to know is why when using thing like this empty variable with the single square brackets it removes all the user's home directory files

And if I used the double square brackets it does not remove the user's home directory files

Like this

dirname=

if [[ -d $dirname ]];
 then
 cd $dirname && rm *
fi

I've read the difference syntax when using both Single Square Brackets and Double Square Brackets

May I know why this happens ?

  • 6
    beginner's mistake, no quotes? – alecxs Jun 27 '20 at 19:02
  • 10
    When unquoted $dirname is empty, your test becomes [ -d ], which returns true if -d is a non-null string (i.e. always true). See the related question How does bash interpret the equal operator with no surrounding spaces in a conditional? – steeldriver Jun 27 '20 at 19:03
  • 2
    You don't actually need the test. If "$dirname" (mind the quotes) is not a directory, then cd "$dirname" will fail and rm * will not be executed. – chepner Jun 28 '20 at 19:53
  • 2
    @chepner cd will follow symlinks while [ -d will exclude them, so the above code makes a slight difference (although its most likely not intended by OP) ;) – alecxs Jun 28 '20 at 22:30
  • There's also a race condition: the availability of the directory can also change between testing for it and trying to switch to it. – chepner Jun 28 '20 at 23:14
12

When cd is not passed an argument, it will change to the default directory (in most cases, the user's home directory, $HOME).

The interesting part of this question is that when you pass nothing to the -d operator of the bash test or [ built-in, bash appears to exit with 0 (in my opinion, this is unexpected).

When you set dirname to an empty variable, you're essentially running this:

if [ -d ]; then
    cd && rm *
fi

To me, it's surprising that the code inside this if block is executed, but I can confirm on my machine that it is. As a comment above explains, test is not interpreting the -d as an operator anymore and is simply returning 0 because it's a non-null string. One way to help guard against this kind of behavior is to make sure you quote your variables:

if [ -d "$dirname" ]; then
    cd "$dirname" && rm *
fi

In this case, if $dirname is empty, you'll be passing "" to the -d operator which correctly evaluates a non-zero exit code so the code inside the block is not executed.

  • 8
    It is exactly documented, in man bash for example: "test and [ evaluate conditional expressions using a set of rules based on the number of arguments... 1 argument: The expression is true if and only if the argument is not null." – steeldriver Jun 27 '20 at 19:10
  • ah, read it too quickly, missed the other place where the variable being unquoted changes stuff – ilkkachu Jun 27 '20 at 19:13
  • 1
    @JohnMoon yeah in my experience the bash manual is almost impossible to search unless you already know what you are looking for ;) – steeldriver Jun 27 '20 at 20:54
  • 2
    @steeldriver it's even in POSIX: pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html# I find man bash unwieldy as well, but man -s 1 test is good and the POSIX doc on sh is "simple and coherent" – ljrk Jun 28 '20 at 8:19
  • 2
    You need the -P option to cd if you want cd to interpret the contents of $dirname the same way [ does. You also need some -- for both cd and rm. – Stéphane Chazelas Jun 28 '20 at 16:30
12

The unquoted $dirname is subject to word splitting, and if it's empty, it gets removed, in both [ -d $dirname ] and cd $dirname, leaving just [ -d ] and cd.

In the first one, [ sees only one argument (between [ and ]), and in that case, the test is to see if that argument is a non-empty string. -d is, so the test is true. (This is similar to how [ -z $var ] seems to work to test if $var is empty, but [ -n $var ] doesn't work at all.)

As for the cd, a plain cd will change to the user's home directory, so that's where the rm * happens.


With [[ .. ]], word splitting doesn't happen since [[ is a special shell construct, unlike [ which is just a regular command.

The solution is to double-quote the variable expansions, which you'll want to do in most cases anyway, for various reasons. Like so:

if [ -d "$dirname" ]; then
    cd -- "$dirname" && rm ./*
fi

See:

  • Why isn't the solution to also always prefer [[ over [ (unless sh compatibility is required)? – jamesdlin Jun 28 '20 at 6:40
  • 1
    @jamesdlin Many bashisms have hidden complexities or other weird problems. The problems of sh (and test) exist, but as these tools are so small it's comparatively easy to understand them and take care – at least in my experience. – ljrk Jun 28 '20 at 8:21
  • 1
    @jamesdlinit it works for the test, sure, and I mentioned it originally here. But the quoting issue comes in other places too, like with the cd here. Not that quoting seems to really help there, cd "" just succeeds without doing anything... – ilkkachu Jun 28 '20 at 8:47
  • 1
    Technically, it's subject to split +glob. The split part alone could yield an empty list even on a non-empty variable with the default value of $IFS if the variable contained only spc, tab or nl characters. The glob part could also yield an empty list if the nullglob option was enabled and the variable contained glob operators (*, ?, [, also backslash in bash5, possibly more with extglob). – Stéphane Chazelas Jun 28 '20 at 16:28
  • 1
    You need the -P option to cd (and possibly unset CDPATH) if you want cd to interpret the contents of $dirname the same way [ does (though there'd still be a problem with the - value). – Stéphane Chazelas Jun 28 '20 at 16:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.