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 ?

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
  • 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?](https://unix.stackexchange.com/questions/7655/how-does-bash-interpret-the-equal-operator-with-no-surrounding-spaces-in-a-condi) – 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

2 Answers2

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:

ilkkachu
  • 133,243
  • 15
  • 236
  • 397
  • Why isn't the solution to also always prefer `[[` over `[` (unless `sh` compatibility is required)? – jamesdlin Jun 28 '20 at 06: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 08: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 08: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
  • @StéphaneChazelas, though dash, busybox and zsh seem to also accept `cd -P ''` as a no-op without an error. – ilkkachu Jun 28 '20 at 17:20
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.

John Moon
  • 993
  • 5
  • 9
  • 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
  • @steeldriver, thanks for pointing that out! I was originally looking in the docs for situations in which no argument is passed to `-d`, but must not have found it because it wasn't interpreting it as an operator (due to the number of arguments). – John Moon Jun 27 '20 at 20:52
  • 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: https://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 08:19
  • @larkey ah yes that's a much better reference – steeldriver Jun 28 '20 at 11:49
  • 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