rm: new option --dir (-d) to remove empty directories
authorKrzysztof Goj <krzysztof.goj@gmail.com>
Sun, 22 Jan 2012 00:39:59 +0000 (01:39 +0100)
committerJim Meyering <meyering@redhat.com>
Tue, 14 Aug 2012 16:54:16 +0000 (18:54 +0200)
Add new option to rm (-d/--dir), which allows removal of
empty directories, while still safely disallowing removal
of non-empty ones.

This improves compatibility with Mac OS X and BSD systems,
which honor the -d option.

* src/remove.c (rm_fts): Remove empty directories when requested.
* src/remove.h (rm_options) [remove_empty_directories]: New member.
* src/rm.c (long_opts, usage, main): Update usage and option parsing.
(rm_option_init): Initialize the new member.
* src/mv.c (rm_option_init): Initialize the new member.
* tests/rm/d-1: New test case - successfully delete empty dir.
* tests/rm/d-2: New test case - refuse to delete nonempty dir.
* tests/Makefile.am (TESTS): Add them.

NEWS
doc/coreutils.texi
src/mv.c
src/remove.c
src/remove.h
src/rm.c
tests/Makefile.am
tests/rm/d-1 [new file with mode: 0755]
tests/rm/d-2 [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 46d0a41..012a633 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -2,11 +2,20 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 * Noteworthy changes in release ?.? (????-??-??) [?]
 
+** Bug fixes
+
   df now fails when the list of mounted file systems (/etc/mtab) cannot
   be read, yet the file system type information is needed to process
   certain options like -a, -l, -t and -x.
   [This bug was present in "the beginning".]
 
+** New features
+
+  rm now accepts the --dir (-d) option which makes it remove empty directories.
+  Since removing empty directories is relatively safe, this option can be
+  used as a part of the alias rm='rm --dir'.  This improves compatibility
+  with Mac OS X and BSD systems which also honor the -d option.
+
 
 * Noteworthy changes in release 8.18 (2012-08-12) [stable]
 
index 744b41a..62b31fe 100644 (file)
@@ -8807,6 +8807,13 @@ The program accepts the following options.  Also see @ref{Common options}.
 
 @table @samp
 
+@item -d
+@itemx --dir
+@opindex -d
+@opindex --dir
+@cindex directories, removing
+Remove the listed directories if they are empty.
+
 @item -f
 @itemx --force
 @opindex -f
index ee2f5a1..4f5708e 100644 (file)
--- a/src/mv.c
+++ b/src/mv.c
@@ -73,6 +73,7 @@ static void
 rm_option_init (struct rm_options *x)
 {
   x->ignore_missing_files = false;
+  x->remove_empty_directories = true;
   x->recursive = true;
   x->one_file_system = false;
 
index 5ebd2ce..61ba5f3 100644 (file)
@@ -414,11 +414,15 @@ rm_fts (FTS *fts, FTSENT *ent, struct rm_options const *x)
   switch (ent->fts_info)
     {
     case FTS_D:                        /* preorder directory */
-      if (! x->recursive)
+      if (! x->recursive
+          && !(x->remove_empty_directories
+               && is_empty_dir (fts->fts_cwd_fd, ent->fts_accpath)))
         {
-          /* This is the first (pre-order) encounter with a directory.
+          /* This is the first (pre-order) encounter with a directory
+             that we can not delete.
              Not recursive, so arrange to skip contents.  */
-          error (0, EISDIR, _("cannot remove %s"), quote (ent->fts_path));
+          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;
index 4eab282..f994517 100644 (file)
@@ -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;
index 02809f2..a45594e 100644 (file)
--- 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;
index 273405f..09d2658 100644 (file)
@@ -98,6 +98,8 @@ TESTS =                                               \
   chgrp/basic                                  \
   rm/dangling-symlink                          \
   misc/ls-time                                 \
+  rm/d-1                                       \
+  rm/d-2                                       \
   rm/deep-1                                    \
   rm/deep-2                                    \
   rm/dir-no-w                                  \
diff --git a/tests/rm/d-1 b/tests/rm/d-1
new file mode 100755 (executable)
index 0000000..f35e951
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/sh
+# Test "rm --dir --verbose".
+
+# Copyright (C) 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 a || framework_failure_
+> b || framework_failure_
+
+rm --verbose --dir a b > out || fail=1
+
+cat <<\EOF > exp || framework_failure_
+removed directory: 'a'
+removed 'b'
+EOF
+
+test -e a && fail=1
+test -e b && fail=1
+
+# Compare expected and actual output.
+compare exp out || fail=1
+
+Exit $fail
diff --git a/tests/rm/d-2 b/tests/rm/d-2
new file mode 100755 (executable)
index 0000000..a63cff6
--- /dev/null
@@ -0,0 +1,33 @@
+#!/bin/sh
+# Ensure that 'rm -d dir' (i.e., without --recursive) gives a reasonable
+# diagnostic when failing.
+
+# Copyright (C) 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 || framework_failure_
+
+rm -d d 2> out && fail=1
+printf "%s\n" \
+    "rm: cannot remove 'd': Directory not empty" \
+    > exp || framework_failure_
+
+compare exp out || fail=1
+
+Exit $fail