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
  • 138,973

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
  • 138,973
  • 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
  • 1,013
  • 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