22

I would expect

find . -delete

to delete the current directory, but it doesn't. Why not?

  • 3
    Most likely because removing the current working directory would not be a good idea. – Alexej Magura Nov 18 '16 at 14:27
  • Agreed--I like the default behavior, but it is not consistent with, e.g., find . -print. – mbroshi Nov 18 '16 at 15:35
  • @AlexejMagura although I sympathize, I do not see why removing the current directory should be any different than removing an open file. The object will stay alive until a reference to it exists, and then garbage collected afterward. You can do cd ..; rm -r dir with another shell with quite clear semantics... – Rmano Nov 18 '16 at 16:22
  • @Rmano this is true: it's just something I wouldn't do on principle: just go up a directory and then delete the current directory. I'm not entirely sure why it's such a big deal--though I have had some misfortunes with the current directory no longer existing, such as relative paths no longer working, but you can always get out by using an absolute path--but some part of me just says that it isn't a good idea in general. – Alexej Magura Nov 18 '16 at 16:24
29

The members of findutils aware of it, it's for compatible with *BSD:

One of the reasons that we skip deletion of "." is for compatibility with *BSD, where this action originated.

The NEWS in findutils source code shows that they decided to keep the behavior:

#20802: If -delete fails, find's exit status will now be non-zero. However, find still skips trying to delete ".".

[UPDATE]

Since this question become one of the hot topic, so i dive into FreeBSD source code and come out a more convincing reason.

Let's see the find utility source code of FreeBSD:

int
f_delete(PLAN *plan __unused, FTSENT *entry)
{
    /* ignore these from fts */
    if (strcmp(entry->fts_accpath, ".") == 0 ||
        strcmp(entry->fts_accpath, "..") == 0)
        return 1;
...
    /* rmdir directories, unlink everything else */
    if (S_ISDIR(entry->fts_statp->st_mode)) {
        if (rmdir(entry->fts_accpath) < 0 && errno != ENOTEMPTY)
            warn("-delete: rmdir(%s)", entry->fts_path);
    } else {
        if (unlink(entry->fts_accpath) < 0)
            warn("-delete: unlink(%s)", entry->fts_path);
    }
...

As you can see, if it doesn't filter out dot and dot-dot, then it will reach rmdir() C function defined by POSIX's unistd.h.

Do a simple test, rmdir with dot/dot-dot argument will return -1:

printf("%d\n", rmdir(".."));

Let's take a look how POSIX describe rmdir:

If the path argument refers to a path whose final component is either dot or dot-dot, rmdir() shall fail.

No reason was given why shall fail.

I found rename explain some reason:

Renaming dot or dot-dot is prohibited in order to prevent cyclical file system paths.

Cyclical file system paths ?

I look over The C Programming Language (2nd Edition) and search for directory topic, surprisingly i found the code is similar:

if(strcmp(dp->name,".") == 0 || strcmp(dp->name,"..") == 0)
    continue;

And the comment !

Each directory always contains entries for itself, called ".", and its parent, ".."; these must be skipped, or the program will loop forever.

"loop forever", this is same like how rename describe it as "cyclical file system paths" above.

I slightly modify the code and to make it run in Kali Linux based on this answer:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h> 
#include <dirent.h>
#include <unistd.h>

void fsize(char *);
void dirwalk(char *, void (*fcn)(char *));

int
main(int argc, char **argv) {
    if (argc == 1)
        fsize(".");
    else
        while (--argc > 0) {
            printf("start\n");
            fsize(*++argv);
        }
    return 0;
}

void fsize(char *name) {
    struct stat stbuf;
    if (stat(name, &stbuf) == -1 )  {
        fprintf(stderr, "fsize: can't access %s\n", name);
        return;
    }
    if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
        dirwalk(name, fsize);
    printf("%81d %s\n", stbuf.st_size, name);
}

#define MAX_PATH 1024
void dirwalk(char *dir, void (*fcn)(char *))
{
    char name[MAX_PATH];
    struct dirent *dp;

    DIR *dfd;

    if ((dfd = opendir(dir)) == NULL) {
            fprintf(stderr, "dirwalk: can't open %s\n", dir);
            return;
    }

    while ((dp = readdir(dfd)) != NULL) {
            sleep(1);
            printf("d_name: S%sG\n", dp->d_name);
            if (strcmp(dp->d_name, ".") == 0
                            || strcmp(dp->d_name, "..") == 0) {
                    printf("hole dot\n");
                    continue;
                    }
            if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name)) {
                    printf("mocha\n");
                    fprintf(stderr, "dirwalk: name %s/%s too long\n",
                                    dir, dp->d_name);
                    }
            else {
                    printf("ice\n");
                    (*fcn)(dp->d_name);
            }
    }
    closedir(dfd);
}

Let's see:

xb@dnxb:/test/dot$ ls -la
total 8
drwxr-xr-x 2 xiaobai xiaobai 4096 Nov 20 04:14 .
drwxr-xr-x 3 xiaobai xiaobai 4096 Nov 20 04:14 ..
xb@dnxb:/test/dot$ 
xb@dnxb:/test/dot$ cc /tmp/kr/fsize.c -o /tmp/kr/a.out 
xb@dnxb:/test/dot$ /tmp/kr/a.out .                     
start
d_name: S..G
hole dot
d_name: S.G
hole dot
                                                                             4096 .
xb@dnxb:/test/dot$ 

It work correctly, now what if I comment out the continue instruction:

xb@dnxb:/test/dot$ cc /tmp/kr/fsize.c -o /tmp/kr/a.out 
xb@dnxb:/test/dot$ /tmp/kr/a.out .
start
d_name: S..G
hole dot
ice
d_name: S..G
hole dot
ice
d_name: S..G
hole dot
ice
^C
xb@dnxb:/test/dot$

As you can see, I have to use Ctrl+C to kill this infinitely loop program.

The '..' directory read its first entry '..' and loop forever.

Conclusion:

  1. GNU findutils try to compatible with find utility in *BSD.

  2. find utility in *BSD internally use rmdir POSIX-compliant C function which dot/dot-dot is not allow.

  3. The reason of rmdir do not allow dot/dot-dot is prevent cyclical file system paths.

  4. The C Programming Language written by K&R shows the example of how dot/dot-dot will lead to forever loop program.

| improve this answer | |
16

Because your find command returns . as result. From the info page of rm:

Any attempt to remove a file whose last file name component is ‘.’ or ‘..’ is rejected without any prompting, as mandated by POSIX.

So, it looks like find just sticks to POSIX rules in this case.

| improve this answer | |
  • 2
    As it should: POSIX is king, plus removing the current directory could cause some very big problems depending on the parent application and what not. Like what if the current directory were /var/log and you ran that as root, thinking that it'd remove all the subdirs and it removed the current directory as well? – Alexej Magura Nov 18 '16 at 15:39
  • 1
    That's a good theory, but the man page for find says: "If the removal failed, an error message is issued." Why is no error printed? – mbroshi Nov 18 '16 at 15:42
  • 1
    @AlexejMagura Removing the current directory works fine in general: mkdir foo && cd foo && rmdir $(pwd). It's removing . (or ..) that doesn't work. – Tavian Barnes Nov 24 '16 at 21:27
4

The rmdir system call fails with EINVAL if the last component of its argument path is ".". It's documented at http://pubs.opengroup.org/onlinepubs/009695399/functions/rmdir.html and the rationale for the behavior is:

The meaning of deleting pathname /dot is unclear, because the name of the file (directory) in the parent directory to be removed is not clear, particularly in the presence of multiple links to a directory.

| improve this answer | |
2

Calling rmdir(".") as a system call didn't work when I tried it, so no higher level tool can succeed.

You must delete the directory through its real name not its . alias.

| improve this answer | |
1

While 林果皞 and Thomas already gave good answers on this, I feel that their answers forgot to explain why this behaviour was implemented in the first place.

In your find . -delete example deleting the current directory sounds pretty logical and sane. But consider:

$ find . -name marti\*
./martin
./martin.jpg
[..]

Does deleting . still sound logical and sane to you?

Deleting a non-empty directory is an error – so you're unlikely to lose data with this with find (although you could with rm -r) – but your shell will have its current working directory set to a directory that no longer exist, leading to some confusing and surprising behaviour:

$ pwd
/home/martin/test
$ rm -r ../test 
$ touch foo
touch: cannot touch 'foo': No such file or directory

Not deleting the current directory is simply good interface design and complies with the principle of least surprise.

| improve this answer | |

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.