Hi,

I created a patch adding new option to rm (-d/--dir) which allows to
remove empty directories.
You may have seen this issue being discussed in comments on Rob Pike's Google+
(https://plus.google.com/101960720994009339267/posts/3WDmR2RTTrv).

Removing empty directories by rm was default behaviour in the Research
Unix and Plan 9.
I think it's safer to add a flag and let people opt in with alias rm='rm --dir'.

It was raised in the Google+ discussion that -d flag has been added to
BSD code base in 1990.
I checked the man pages of FreeBSD, NetBSD, OpenBSD and Mac OS X and
the -d option is there.

I attach output of `git format-patch --stdout -1`, you can also
get the changes from my github (https://github.com/goj/coreutils,
rm-d branch).

This is my first patch to the coreutils, so please review it carefully.

Best regards,

-- 
Krzysztof Goj
From fa48dbf2f11c9ba7dd55e22a420c6b08d54aa706 Mon Sep 17 00:00:00 2001
From: Krzysztof Goj <[email protected]>
Date: Sun, 22 Jan 2012 01:39:59 +0100
Subject: [PATCH] rm: new option (-d) to remove empty directories

Adds new option to rm (-d/--dir), which allows removal of
empty directories, while still safely disallowing removal
of non-empty ones.

* src/remove.c (rm_fts): allow removal of empty dir if the option is set
* src/remove.h (rm_options): new option - remove_empty_directories
* src/rm.c (long_opts, usage, main): usage && option parsing
* tests/Makefile.am: added new test cases (d-1, d-2)
* tests/rm/d-1: new test case - successfully delete empty dir
* tests/rm/d-2: new test case - refuse to delete nonempty dir
---
 src/remove.c      |   18 ++++++++++++------
 src/remove.h      |    3 +++
 src/rm.c          |    9 ++++++++-
 tests/Makefile.am |    2 ++
 tests/rm/d-1      |   42 ++++++++++++++++++++++++++++++++++++++++++
 tests/rm/d-2      |   34 ++++++++++++++++++++++++++++++++++
 6 files changed, 101 insertions(+), 7 deletions(-)
 create mode 100755 tests/rm/d-1
 create mode 100755 tests/rm/d-2

diff --git a/src/remove.c b/src/remove.c
index 60d9110..acd4164 100644
--- a/src/remove.c
+++ b/src/remove.c
@@ -460,12 +460,18 @@ rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x)
     case FTS_D:			/* preorder directory */
       if (! x->recursive)
         {
-          /* This is the first (pre-order) encounter with a directory.
-             Not recursive, so arrange to skip contents.  */
-          error (0, EISDIR, _("cannot remove %s"), quote (ent->fts_path));
-          mark_ancestor_dirs (ent);
-          fts_skip_tree (fts, ent);
-          return RM_ERROR;
+          if (! x->remove_empty_directories
+              || !is_empty_dir (fts->fts_cwd_fd, ent->fts_accpath))
+            {
+              /* This is the first (pre-order) encounter with a directory
+               * which we can not delete.
+                 Not recursive, so arrange to skip contents.  */
+              int err = x->remove_empty_directories ? ENOTEMPTY : EISDIR;
+              error (0, err, _("cannot remove %s"), quote (ent->fts_path));
+              mark_ancestor_dirs (ent);
+              fts_skip_tree (fts, ent);
+              return RM_ERROR;
+            }
         }
 
       /* Perform checks that can apply only for command-line arguments.  */
diff --git a/src/remove.h b/src/remove.h
index c4c59a2..53c6c12 100644
--- a/src/remove.h
+++ b/src/remove.h
@@ -49,6 +49,9 @@ struct rm_options
   /* If true, recursively remove directories.  */
   bool recursive;
 
+  /* If true, remove empty directories. */
+  bool remove_empty_directories;
+
   /* Pointer to the device and inode numbers of '/', when --recursive
      and preserving '/'.  Otherwise NULL.  */
   struct dev_ino *root_dev_ino;
diff --git a/src/rm.c b/src/rm.c
index 6eeeafc..e09d884 100644
--- a/src/rm.c
+++ b/src/rm.c
@@ -77,6 +77,7 @@ static struct option const long_opts[] =
   {"-presume-input-tty", no_argument, NULL, PRESUME_INPUT_TTY_OPTION},
 
   {"recursive", no_argument, NULL, 'r'},
+  {"dir", no_argument, NULL, 'd'},
   {"verbose", no_argument, NULL, 'v'},
   {GETOPT_HELP_OPTION_DECL},
   {GETOPT_VERSION_OPTION_DECL},
@@ -154,6 +155,7 @@ Remove (unlink) the FILE(s).\n\
       --no-preserve-root  do not treat '/' specially\n\
       --preserve-root   do not remove '/' (default)\n\
   -r, -R, --recursive   remove directories and their contents recursively\n\
+  -d, --dir             remove empty directories\n\
   -v, --verbose         explain what is being done\n\
 "), stdout);
       fputs (HELP_OPTION_DESCRIPTION, stdout);
@@ -189,6 +191,7 @@ rm_option_init (struct rm_options *x)
   x->ignore_missing_files = false;
   x->interactive = RMI_SOMETIMES;
   x->one_file_system = false;
+  x->remove_empty_directories = false;
   x->recursive = false;
   x->root_dev_ino = NULL;
   x->stdin_tty = isatty (STDIN_FILENO);
@@ -220,10 +223,14 @@ main (int argc, char **argv)
   /* Try to disable the ability to unlink a directory.  */
   priv_set_remove_linkdir ();
 
-  while ((c = getopt_long (argc, argv, "firvIR", long_opts, NULL)) != -1)
+  while ((c = getopt_long (argc, argv, "dfirvIR", long_opts, NULL)) != -1)
     {
       switch (c)
         {
+        case 'd':
+          x.remove_empty_directories = true;
+          break;
+
         case 'f':
           x.interactive = RMI_NEVER;
           x.ignore_missing_files = true;
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 8b670fc..dcbe2c8 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -122,6 +122,8 @@ TESTS =						\
   rm/r-2					\
   rm/r-3					\
   rm/r-4					\
+  rm/d-1					\
+  rm/d-2					\
   rm/readdir-bug				\
   rm/rm1					\
   touch/empty-file				\
diff --git a/tests/rm/d-1 b/tests/rm/d-1
new file mode 100755
index 0000000..69c5860
--- /dev/null
+++ b/tests/rm/d-1
@@ -0,0 +1,42 @@
+#!/bin/sh
+# Test "rm -r --verbose".
+
+# Copyright (C) 1997-1998, 2000, 2002, 2004, 2006-2012 Free Software
+# Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+test=d-1
+
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+print_ver_ rm
+
+mkdir a || framework_failure_
+> b || framework_failure_
+
+cat <<\EOF > $test.E || framework_failure_
+removed directory: 'a'
+removed 'b'
+EOF
+
+rm --verbose -d a b > $test.O || fail=1
+
+if test -e a -o -e b; then
+fail=1
+fi
+
+# Compare expected and actual output.
+compare $test.E $test.O || fail=1
+
+Exit $fail
diff --git a/tests/rm/d-2 b/tests/rm/d-2
new file mode 100755
index 0000000..97e9250
--- /dev/null
+++ b/tests/rm/d-2
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Ensure that 'rm dir' (i.e., without --recursive) gives a reasonable
+# diagnostic when failing.
+
+# Copyright (C) 2005-2012 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+print_ver_ rm
+
+mkdir d || framework_failure_
+> d/a
+
+
+rm -d d 2> out && fail=1
+cat <<\EOF > exp || fail=1
+rm: cannot remove 'd': Directory not empty
+EOF
+
+compare exp out || fail=1
+
+Exit $fail
-- 
1.7.8.3

Reply via email to