]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
merge from rm-fix branch
authorJim Meyering <jim@meyering.net>
Sun, 14 Sep 1997 02:00:48 +0000 (02:00 +0000)
committerJim Meyering <jim@meyering.net>
Sun, 14 Sep 1997 02:00:48 +0000 (02:00 +0000)
src/rm.c

index 4f226426f622f157e4cabe815ea9c7f68090315e..8302ae4d223377e36ddca32cdc10a77dc0205ef2 100644 (file)
--- a/src/rm.c
+++ b/src/rm.c
    along with this program; if not, write to the Free Software Foundation,
    Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
 
-/* Written by Paul Rubin, David MacKenzie, and Richard Stallman.  */
+/* Written by Paul Rubin, David MacKenzie, and Richard Stallman.
+   Reworked to use chdir and hash tables by Jim Meyering.  */
 
 #include <config.h>
 #include <stdio.h>
 #include <getopt.h>
 #include <sys/types.h>
+#include <assert.h>
 
+#include "save-cwd.h"
 #include "system.h"
 #include "error.h"
+#include "obstack.h"
+#include "oa-hash.h"
+
+#define obstack_chunk_alloc malloc
+#define obstack_chunk_free free
 
 #ifdef D_INO_IN_DIRENT
 # define D_INO(dp) ((dp)->d_ino)
 # define D_INO(dp) 1
 #endif
 
-/* An element in a stack of pointers into `pathname'.
-   `pathp' points to where in `pathname' the terminating '\0' goes
-   for this level's directory name.  */
-struct pathstack
+#if !defined (S_ISLNK)
+# define S_ISLNK(Mode) 0
+#endif
+
+#define DOT_OR_DOTDOT(Basename) \
+  (Basename[0] == '.' && (Basename[1] == '\0' \
+                         || (Basename[1] == '.' && Basename[2] == '\0')))
+
+#if defined strdupa
+# define ASSIGN_STRDUPA(DEST, S)               \
+  do { DEST = strdupa(S); } while (0)
+#else
+# define ASSIGN_STRDUPA(DEST, S)               \
+  do                                           \
+    {                                          \
+      size_t len_ = strlen (S) + 1;            \
+      char *tmp_dest_ = alloca (len_);         \
+      memcpy (tmp_dest_, (S), len_);           \
+      DEST = tmp_dest_;                                \
+    }                                          \
+  while (0)
+#endif
+
+enum RM_status
+{
+  /* FIXME: describe and explain ordering: `ok' increasing in seriousness. */
+  RM_OK = 2,
+  RM_USER_DECLINED,
+  RM_ERROR
+};
+
+#define VALID_STATUS(S) \
+  ((S) == RM_OK || (S) == RM_USER_DECLINED || (S) == RM_ERROR)
+
+/* Initial capacity of per-directory hash table of entries that have
+   been processed but not been deleted.  */
+#define HT_INITIAL_CAPACITY 13
+
+/* Initial capacity of the active directory hash table.  This table will
+   be resized only for hierarchies more than about 45 levels deep. */
+#define ACTIVE_DIR_INITIAL_CAPACITY 53
+
+struct File_spec
 {
-  struct pathstack *next;
-  char *pathp;
+  char *filename;
+  unsigned int have_filetype_mode:1;
+  unsigned int have_full_mode:1;
+  mode_t mode;
   ino_t inum;
 };
 
+#ifndef STDC_HEADERS
+void free ();
+char *malloc ();
+#endif
+
 char *base_name ();
+int euidaccess ();
 char *stpcpy ();
+char *stpncpy ();
+void strip_trailing_slashes ();
 char *xmalloc ();
 char *xrealloc ();
-int euidaccess ();
 int yesno ();
-void strip_trailing_slashes ();
 
-static int clear_directory __P ((struct stat *statp));
-static int duplicate_entry __P ((struct pathstack *stack, ino_t inum));
-static int remove_dir __P ((struct stat *statp));
-static int remove_file __P ((struct stat *statp));
-static int rm __P ((void));
-static void usage __P ((int status));
+static enum RM_status rm (struct File_spec *fs, int user_specified_name);
 
 /* Name this program was run with.  */
 char *program_name;
 
-/* Linked list of pathnames of directories in progress in recursive rm.
-   The entries actually contain pointers into `pathname'.
-   `pathstack' is the current deepest level.  */
-static struct pathstack *pathstack = NULL;
-
-/* Path of file now being processed; extended as necessary.  */
-static char *pathname;
-
-/* Number of bytes currently allocated for `pathname';
-   made larger when necessary, but never smaller.  */
-static int pnsize;
-
 /* If nonzero, display the name of each file removed.  */
 static int verbose;
 
@@ -98,6 +136,36 @@ static int show_help;
 /* If nonzero, print the version on standard output and exit.  */
 static int show_version;
 
+/* FIXME: describe */
+static struct obstack dir_stack;
+
+/* Stack of lengths of directory names (including trailing slash)
+   appended to dir_stack.  We have to have a separate stack of lengths
+   (rather than just popping back to previous slash) because the first
+   element pushed onto the dir stack may contain slashes.  */
+static struct obstack len_stack;
+
+/* Set of `active' directories from the current command-line parameter
+   to the level in the hierarchy at which files are being removed.
+   A directory is added to the active set when RM begins removing it
+   (or its entries), and it is removed from the set just after RM has
+   finished processing it.
+
+   This is actually a map (not a set), implemented with a hash table.
+   For each active directory, it maps the directory's inode number to the
+   depth of that directory relative to the root of the tree being deleted.
+   A directory specified on the command line has depth zero.
+   This construct is used to detect directory cycles so that RM can warn
+   about them rather than iterating endlessly.  */
+static struct hash_table *active_dir_map;
+
+/* An entry in the active_dir_map.  */
+struct active_dir_ent
+{
+  ino_t inum;
+  unsigned int depth;
+};
+
 static struct option const long_opts[] =
 {
   {"directory", no_argument, &unlink_dirs, 1},
@@ -110,437 +178,766 @@ static struct option const long_opts[] =
   {NULL, 0, NULL, 0}
 };
 
-int
-main (int argc, char **argv)
+static __inline unsigned int
+current_depth (void)
 {
-  int err = 0;
-  int c;
+  return obstack_object_size (&len_stack) / sizeof (size_t);
+}
 
-  program_name = argv[0];
-  setlocale (LC_ALL, "");
-  bindtextdomain (PACKAGE, LOCALEDIR);
-  textdomain (PACKAGE);
+static void
+print_nth_dir (FILE *stream, unsigned int depth)
+{
+  size_t *length = (size_t *) obstack_base (&len_stack);
+  char *dir_name = (char *) obstack_base (&dir_stack);
+  unsigned int sum = 0;
+  unsigned int i;
 
-  verbose = ignore_missing_files = recursive = interactive
-    = unlink_dirs = 0;
-  pnsize = 256;
-  pathname = xmalloc (pnsize);
+  assert (0 <= depth && depth < current_depth ());
 
-  while ((c = getopt_long (argc, argv, "dfirvR", long_opts, NULL)) != -1)
+  for (i = 0; i <= depth; i++)
     {
-      switch (c)
-       {
-       case 0:         /* Long option.  */
-         break;
-       case 'd':
-         unlink_dirs = 1;
-         break;
-       case 'f':
-         interactive = 0;
-         ignore_missing_files = 1;
-         break;
-       case 'i':
-         interactive = 1;
-         ignore_missing_files = 0;
-         break;
-       case 'r':
-       case 'R':
-         recursive = 1;
-         break;
-       case 'v':
-         verbose = 1;
-         break;
-       default:
-         usage (1);
-       }
+      sum += length[i];
     }
 
-  if (show_version)
+  fwrite (dir_name, 1, sum, stream);
+}
+
+static __inline struct active_dir_ent *
+make_active_dir_ent (ino_t inum, unsigned int depth)
+{
+  struct active_dir_ent *ent;
+  ent = (struct active_dir_ent *) xmalloc (sizeof *ent);
+  ent->inum = inum;
+  ent->depth = depth;
+  return ent;
+}
+
+static unsigned long
+hash_active_dir_ent_1 (void const *x)
+{
+  struct active_dir_ent const *ade = x;
+  return_INTEGER_HASH_1 (ade->inum);
+}
+
+static unsigned long
+hash_active_dir_ent_2 (void const *x)
+{
+  struct active_dir_ent const *ade = x;
+  return_INTEGER_HASH_2 (ade->inum);
+}
+
+static int
+hash_compare_active_dir_ents (void const *x, void const *y)
+{
+  struct active_dir_ent const *a = x;
+  struct active_dir_ent const *b = y;
+  return_INTEGER_COMPARE (a->inum, b->inum);
+}
+
+static unsigned long
+hash_string_1 (void const *x)
+{
+  return_STRING_HASH_1 (x);
+}
+
+static unsigned long
+hash_string_2 (void const *x)
+{
+  return_STRING_HASH_2 (x);
+}
+
+static int
+hash_compare_strings (void const *x, void const *y)
+{
+  return strcmp (x, y);
+}
+
+static void
+usage (int status)
+{
+  if (status != 0)
+    fprintf (stderr, _("Try `%s --help' for more information.\n"),
+            program_name);
+  else
     {
-      printf ("rm (%s) %s\n", GNU_PACKAGE, VERSION);
-      exit (0);
+      printf (_("Usage: %s [OPTION]... FILE...\n"), program_name);
+      printf (_("\
+Remove (unlink) the FILE(s).\n\
+\n\
+  -d, --directory       unlink directory, even if non-empty (super-user only)\n\
+  -f, --force           ignore nonexistent files, never prompt\n\
+  -i, --interactive     prompt before any removal\n\
+  -r, -R, --recursive   remove the contents of directories recursively\n\
+  -v, --verbose         explain what is being done\n\
+      --help            display this help and exit\n\
+      --version         output version information and exit\n\
+"));
+      puts (_("\nReport bugs to <fileutils-bugs@gnu.ai.mit.edu>."));
     }
+  exit (status);
+}
 
-  if (show_help)
-    usage (0);
+static __inline void
+push_dir (const char *dir_name)
+{
+  size_t len;
 
-  if (optind == argc)
+  len = strlen (dir_name);
+
+  /* Append the string onto the stack.  */
+  obstack_grow (&dir_stack, dir_name, len);
+
+  /* Append a trailing slash.  */
+  obstack_1grow (&dir_stack, '/');
+
+  /* Add one for the slash.  */
+  ++len;
+
+  /* Push the length (including slash) onto its stack.  */
+  obstack_grow (&len_stack, &len, sizeof (len));
+}
+
+static __inline void
+pop_dir (void)
+{
+  int n_lengths = obstack_object_size (&len_stack) / sizeof (size_t);
+  size_t *length = (size_t *) obstack_base (&len_stack);
+  size_t top_len;
+
+  assert (n_lengths > 0);
+  top_len = length[n_lengths - 1];
+  assert (top_len >= 2);
+
+  /* Pop off the specified length of pathname.  */
+  assert (obstack_object_size (&dir_stack) >= top_len);
+  obstack_blank (&dir_stack, -top_len);
+
+  /* Pop the length stack, too.  */
+  assert (obstack_object_size (&len_stack) >= sizeof (size_t));
+  obstack_blank (&len_stack, -(sizeof (size_t)));
+}
+
+/* Copy the SRC_LEN bytes of data beginning at SRC into the DST_LEN-byte
+   buffer, DST, so that the last source byte is at the end of the destination
+   buffer.  If SRC_LEN is longer than DST_LEN, then set *TRUNCATED to non-zero.
+   Set *RESULT to point to the beginning of (the portion of) the source data
+   in DST.  Return the number of bytes remaining in the destination buffer.  */
+
+static size_t
+right_justify (char *dst, size_t dst_len, const char *src, size_t src_len,
+              char **result, int *truncated)
+{
+  const char *sp;
+  char *dp;
+
+  if (src_len <= dst_len)
     {
-      if (ignore_missing_files)
-       exit (0);
-      else
-       {
-         error (0, 0, _("too few arguments"));
-         usage (1);
-       }
+      sp = src;
+      dp = dst + (dst_len - src_len);
+      *truncated = 0;
+    }
+  else
+    {
+      sp = src + (src_len - dst_len);
+      dp = dst;
+      src_len = dst_len;
+      *truncated = 1;
     }
 
-  stdin_tty = isatty (STDIN_FILENO);
+  memcpy (dp, sp, src_len);
 
-  for (; optind < argc; optind++)
-    {
-      int len;
+  *result = dp;
+  return dst_len - src_len;
+}
 
-      /* Stripping slashes is harmless for rmdir;
-        if the arg is not a directory, it will fail with ENOTDIR.  */
-      strip_trailing_slashes (argv[optind]);
-      len = strlen (argv[optind]);
-      if (len + 1 > pnsize)
+/* Using the global directory name obstack, create the full path to FILENAME.
+   Return it in sometimes-realloc'd space that should not be freed by the
+   caller.  Realloc as necessary.  If realloc fails, use a static buffer
+   and put as long a suffix in that buffer as possible.  */
+
+static char *
+full_filename (const char *filename)
+{
+  static char *buf = NULL;
+  static size_t n_allocated = 0;
+
+  int dir_len = obstack_object_size (&dir_stack);
+  char *dir_name = (char *) obstack_base (&dir_stack);
+  size_t n_bytes_needed;
+  size_t filename_len;
+
+  filename_len = strlen (filename);
+  n_bytes_needed = dir_len + filename_len + 1;
+
+  if (n_bytes_needed > n_allocated)
+    {
+      /* FIXME: use realloc, not xrealloc.  */
+      /* But be sure realloc accepts NULL first arg.
+        FIXME: replace with rpl_realloc if not.  */
+      /* This funciton can't use xrealloc.  Otherwise, out-of-memory
+        errors involving a file name to be expanded here wouldn't ever
+        be issued.  Use realloc and fall back on using a static buffer
+        if memory is a problem.  */
+      buf = xrealloc (buf, n_bytes_needed);
+      n_allocated = n_bytes_needed;
+
+      if (buf == NULL)
        {
-         free (pathname);
-         pnsize = 2 * (len + 1);
-         pathname = xmalloc (pnsize);
+#define SBUF_SIZE 512
+#define ELLIPSES_PREFIX "[...]"
+         static char static_buf[SBUF_SIZE];
+         int truncated;
+         size_t len;
+         char *p;
+
+         len = right_justify (static_buf, SBUF_SIZE, filename,
+                              filename_len + 1, &p, &truncated);
+         right_justify (static_buf, len, dir_name, dir_len, &p, &truncated);
+         if (truncated)
+           {
+             memcpy (static_buf, ELLIPSES_PREFIX,
+                     sizeof (ELLIPSES_PREFIX) - 1);
+           }
+         return p;
        }
-      strcpy (pathname, argv[optind]);
-      err += rm ();
     }
 
-  exit (err > 0);
+  /* Copy directory part, including trailing slash.  */
+  memcpy (buf, dir_name, dir_len);
+
+  /* Append filename part, including trailing zero byte.  */
+  memcpy (buf + dir_len, filename, filename_len + 1);
+
+  assert (strlen (buf) + 1 == n_bytes_needed);
+
+  return buf;
 }
 
-/* Remove file or directory `pathname' after checking appropriate things.
-   Return 0 if `pathname' is removed, 1 if not.  */
+static __inline void
+fspec_init_file (struct File_spec *fs, const char *filename)
+{
+  fs->filename = (char *) filename;
+  fs->have_full_mode = 0;
+  fs->have_filetype_mode = 0;
+}
 
-static int
-rm (void)
+static __inline void
+fspec_init_dp (struct File_spec *fs, struct dirent *dp)
 {
-  struct stat path_stats;
-  char *base = base_name (pathname);
+  fs->filename = dp->d_name;
+  fs->have_full_mode = 0;
+  fs->have_filetype_mode = 0;
+  fs->inum = D_INO (dp);
 
-  if (base[0] == '.' && (base[1] == '\0'
-                            || (base[1] == '.' && base[2] == '\0')))
+#if D_TYPE_IN_DIRENT && defined (DT_UNKNOWN)
+  if (filetype_mode != DT_UNKNOWN)
     {
-      error (0, 0, _("cannot remove `.' or `..'"));
-      return 1;
+      fs->have_filetype_mode = 1;
+      fs->mode = filetype_mode;
     }
+#endif
+}
+
+static __inline int
+fspec_get_full_mode (struct File_spec *fs, mode_t *full_mode)
+{
+  struct stat stat_buf;
 
-  if (lstat (pathname, &path_stats)
-      /* The following or-clause is solely for systems like SunOS 4.1.3
-         with (broken) lstat that interpret a zero-length file name
-        argument as something meaningful.  For such systems, manually
-        set errno to ENOENT.  */
-      || (pathname[0] == '\0' && (errno = ENOENT)))
+  if (fs->have_full_mode)
     {
-      if (errno == ENOENT && ignore_missing_files)
-       return 0;
-      error (0, errno, "%s", pathname);
-      return 1;
+      *full_mode = fs->mode;
+      return 0;
     }
 
-  if (S_ISDIR (path_stats.st_mode) && !unlink_dirs)
-    return remove_dir (&path_stats);
+  if (lstat (fs->filename, &stat_buf))
+    return 1;
+
+  fs->have_full_mode = 1;
+  fs->have_filetype_mode = 1;
+  fs->mode = stat_buf.st_mode;
+  fs->inum = stat_buf.st_ino;
+
+  *full_mode = stat_buf.st_mode;
+  return 0;
+}
+
+static __inline int
+fspec_get_filetype_mode (struct File_spec *fs, mode_t *filetype_mode)
+{
+  int fail;
+
+  if (fs->have_filetype_mode)
+    {
+      *filetype_mode = fs->mode;
+      fail = 0;
+    }
   else
-    return remove_file (&path_stats);
+    {
+      fail = fspec_get_full_mode (fs, filetype_mode);
+    }
+
+  return fail;
+}
+
+static __inline mode_t
+fspec_filetype_mode (const struct File_spec *fs)
+{
+  assert (fs->have_filetype_mode);
+  return fs->mode;
+}
+
+/* Recursively remove all of the entries in the current directory.
+   Return an indication of the success of the operation.  */
+
+enum RM_status
+remove_cwd_entries (void)
+{
+  /* NOTE: this is static.  */
+  static DIR *dirp = NULL;
+
+  /* NULL or a malloc'd and initialized hash table of entries in the
+     current directory that have been processed but not removed --
+     due either to an error or to an interactive `no' response.  */
+  struct hash_table *ht = NULL;
+
+  struct dirent *dp;
+  enum RM_status status = RM_OK;
+
+  if (dirp)
+    {
+      if (CLOSEDIR (dirp))
+       {
+         /* FIXME-someday: but this is actually the previously opened dir.  */
+         error (0, errno, "%s", full_filename ("."));
+         status = RM_ERROR;
+       }
+    }
+
+  do
+    {
+      errno = 0;
+      dirp = opendir (".");
+      if (dirp == NULL)
+       {
+         if (errno != ENOENT || !ignore_missing_files)
+           {
+             error (0, errno, "%s", full_filename ("."));
+             status = RM_ERROR;
+           }
+         break;
+       }
+
+      while ((dp = readdir (dirp)) != NULL)
+       {
+         /* Skip this entry if it's `.' or `..'.  */
+         if (DOT_OR_DOTDOT (dp->d_name))
+           continue;
+
+         /* Skip this entry if it's in the table of ones we've already
+            processed.  */
+         if (ht && hash_find_item (ht, (dp)->d_name))
+           continue;
+
+         {
+           struct File_spec fs;
+           enum RM_status tmp_status;
+           fspec_init_dp (&fs, dp);
+           tmp_status = rm (&fs, 0);
+           /* Update status.  */
+           if (tmp_status > status)
+             status = tmp_status;
+           assert (VALID_STATUS (status));
+         }
+
+         /* If this entry was not removed (due either to an error or to
+            an interactive `no' response), record it in the hash table so
+            we don't consider it again if we reopen this directory later.  */
+         if (status != RM_OK)
+           {
+             void *old_item;
+             char *p;
+             int fail;
+
+             if (ht == NULL)
+               {
+                 ht = hash_init_table (NULL, HT_INITIAL_CAPACITY, 0, 0,
+                                       hash_string_1, hash_string_2,
+                                       hash_compare_strings);
+                 if (ht == NULL)
+                   error (1, 0, _("Memory exhausted"));
+               }
+             p = xmalloc (NLENGTH (dp) + 1);
+             stpncpy (p, (dp)->d_name, NLENGTH (dp));
+             fail = hash_insert_item (ht, p, &old_item);
+             assert (old_item == NULL);
+             if (fail)
+               error (1, 0, _("Memory exhausted"));
+           }
+
+         if (dirp == NULL)
+           break;
+       }
+    }
+  while (dirp == NULL);
+
+  if (CLOSEDIR (dirp))
+    {
+      error (0, errno, "%s", full_filename ("."));
+      status = 1;
+    }
+  dirp = NULL;
+
+  if (ht)
+    {
+      hash_free_items (ht);
+      hash_free_table (ht);
+      free (ht);
+    }
+
+  return status;
 }
 
 /* Query the user if appropriate, and if ok try to remove the
-   non-directory `pathname', which STATP contains info about.
-   Return 0 if `pathname' is removed, 1 if not.  */
+   file or directory specified by FS.  Return RM_OK if it is removed,
+   and RM_ERROR or RM_USER_DECLINED if not.
+   FIXME: describe IS_DIR parameter.  */
 
-static int
-remove_file (struct stat *statp)
+static enum RM_status
+remove_file (struct File_spec *fs)
 {
+  int asked = 0;
+  char *pathname = fs->filename;
+
   if (!ignore_missing_files && (interactive || stdin_tty)
-      && euidaccess (pathname, W_OK)
-#ifdef S_ISLNK
-      && !S_ISLNK (statp->st_mode)
-#endif
-      )
+      && euidaccess (pathname, W_OK) )
     {
-      fprintf (stderr, _("%s: remove %s`%s', overriding mode %04o? "),
-              program_name,
-              S_ISDIR (statp->st_mode) ? _("directory ") : "",
-              pathname,
-              (unsigned int) (statp->st_mode & 07777));
-      if (!yesno ())
-       return 1;
+      if (!S_ISLNK (fspec_filetype_mode (fs)))
+       {
+         error (0, 0,
+                (S_ISDIR (fspec_filetype_mode (fs))
+                 ? _("remove write-protected directory `%s'? ")
+                 : _("remove write-protected file `%s'? ")),
+                full_filename (pathname));
+         if (!yesno ())
+           return RM_USER_DECLINED;
+
+         asked = 1;
+       }
     }
-  else if (interactive)
+
+  if (!asked && interactive)
     {
-      fprintf (stderr, _("%s: remove %s`%s'? "), program_name,
-              S_ISDIR (statp->st_mode) ? _("directory ") : "",
-              pathname);
+      error (0, 0,
+            (S_ISDIR (fspec_filetype_mode (fs))
+             ? _("remove directory `%s'? ")
+             : _("remove `%s'? ")),
+            full_filename (pathname));
       if (!yesno ())
-       return 1;
+       return RM_USER_DECLINED;
     }
 
   if (verbose)
-    printf ("%s\n", pathname);
+    printf ("%s\n", full_filename (pathname));
 
   if (unlink (pathname) && (errno != ENOENT || !ignore_missing_files))
     {
-      error (0, errno, "%s", pathname);
-      return 1;
+      error (0, errno, _("cannot unlink `%s'"), full_filename (pathname));
+      return RM_ERROR;
     }
-  return 0;
+  return RM_OK;
 }
 
-/* If not in recursive mode, print an error message and return 1.
+/* If not in recursive mode, print an error message and return RM_ERROR.
    Otherwise, query the user if appropriate, then try to recursively
    remove directory `pathname', which STATP contains info about.
-   Return 0 if `pathname' is removed, 1 if not.  */
+   Return 0 if `pathname' is removed, 1 if not.
+   FIXME: describe need_save_cwd parameter.  */
 
-static int
-remove_dir (struct stat *statp)
+static enum RM_status
+remove_dir (struct File_spec *fs, int need_save_cwd)
 {
-  int err;
+  enum RM_status status;
+  struct saved_cwd cwd;
+  char *dir_name = fs->filename;
+  const char *fmt = NULL;
 
   if (!recursive)
     {
-      error (0, 0, _("%s: is a directory"), pathname);
-      return 1;
+      error (0, 0, _("%s: is a directory"), full_filename (dir_name));
+      return RM_ERROR;
     }
 
   if (!ignore_missing_files && (interactive || stdin_tty)
-      && euidaccess (pathname, W_OK))
+      && euidaccess (dir_name, W_OK))
     {
-      fprintf (stderr,
-              _("%s: descend directory `%s', overriding mode %04o? "),
-              program_name, pathname,
-              (unsigned int) (statp->st_mode & 07777));
-      if (!yesno ())
-       return 1;
+      fmt = _("directory `%s' is write protected; descend into it anyway? ");
     }
   else if (interactive)
     {
-      fprintf (stderr, _("%s: descend directory `%s'? "),
-              program_name, pathname);
+      fmt = _("descend into directory `%s'? ");
+    }
+
+  if (fmt)
+    {
+      error (0, 0, fmt, full_filename (dir_name));
       if (!yesno ())
-       return 1;
+       return RM_USER_DECLINED;
     }
 
   if (verbose)
-    printf ("%s\n", pathname);
+    printf ("%s\n", full_filename (dir_name));
+
+  /* Save cwd if needed.  */
+  if (need_save_cwd && save_cwd (&cwd))
+    return RM_ERROR;
+
+  /* Make target directory the current one.  */
+  if (chdir (dir_name) < 0)
+    {
+      error (0, errno, _("cannot change to directory %s"),
+            full_filename (dir_name));
+      if (need_save_cwd)
+       free_cwd (&cwd);
+      return RM_ERROR;
+    }
+
+  push_dir (dir_name);
 
-  err = clear_directory (statp);
+  /* Save a copy of dir_name.  Otherwise, remove_cwd_entries may clobber
+     dir_name because dir_name is just a pointer to the dir entry's d_name
+     field, and remove_cwd_entries may close the directory.  */
+  ASSIGN_STRDUPA (dir_name, dir_name);
+
+  status = remove_cwd_entries ();
+
+  pop_dir ();
+
+  /* Restore cwd.  */
+  if (need_save_cwd)
+    {
+      if (restore_cwd (&cwd, NULL, NULL))
+       {
+         free_cwd (&cwd);
+         return RM_ERROR;
+       }
+      free_cwd (&cwd);
+    }
+  else if (chdir ("..") < 0)
+    {
+      error (0, errno, _("cannot change back to directory %s via `..'"),
+            full_filename (dir_name));
+      return RM_ERROR;
+    }
 
   if (interactive)
     {
-      if (err)
-       fprintf (stderr, _("%s: remove directory `%s' (might be nonempty)? "),
-                program_name, pathname);
-      else
-       fprintf (stderr, _("%s: remove directory `%s'? "),
-                program_name, pathname);
+      error (0, 0, _("remove directory `%s'%s? "), full_filename (dir_name),
+            (status != RM_OK ? _(" (might be nonempty)") : ""));
       if (!yesno ())
-       return 1;
+       {
+         return RM_USER_DECLINED;
+       }
     }
 
-  if (rmdir (pathname) && (errno != ENOENT || !ignore_missing_files))
+  if (rmdir (dir_name) && (errno != ENOENT || !ignore_missing_files))
     {
-      error (0, errno, "%s", pathname);
-      return 1;
+      error (0, errno, _("cannot remove directory `%s'"),
+            full_filename (dir_name));
+      return RM_ERROR;
     }
-  return 0;
+
+  return RM_OK;
 }
 
-/* Read directory `pathname' and remove all of its entries,
-   avoiding use of chdir.
-   On entry, STATP points to the results of stat on `pathname'.
-   Return 0 for success, error count for failure.
-   Upon return, `pathname' will have the same contents as before,
-   but its address might be different; in that case, `pnsize' will
-   be larger, as well.  */
+/* Remove the file or directory specified by FS after checking appropriate
+   things.  Return RM_OK if it is removed, and RM_ERROR or RM_USER_DECLINED
+   if not.  If USER_SPECIFIED_NAME is non-zero, then the name part of FS may
+   be `.', `..', or may contain slashes.  Otherwise, it must be a simple file
+   name (and hence must specify a file in the current directory).  */
 
-static int
-clear_directory (struct stat *statp)
+static enum RM_status
+rm (struct File_spec *fs, int user_specified_name)
 {
-  DIR *dirp;
-  struct dirent *dp;
-  char *name_space;            /* Copy of directory's filenames.  */
-  char *namep;                 /* Current entry in `name_space'.  */
-  unsigned name_size;          /* Bytes allocated for `name_space'.  */
-  int name_length;             /* Length of filename in `namep' plus '\0'.  */
-  int pathname_length;         /* Length of `pathname'.  */
-  ino_t *inode_space;          /* Copy of directory's inodes.  */
-  ino_t *inodep;               /* Current entry in `inode_space'.  */
-  unsigned n_inodes_allocated; /* There is space for this many inodes
-                                         in `inode_space'.  */
-  int err = 0;                 /* Return status.  */
-  struct pathstack pathframe;  /* New top of stack.  */
-  struct pathstack *pp;                /* Temporary.  */
-
-  name_size = statp->st_size;
-  name_space = (char *) xmalloc (name_size);
-
-  n_inodes_allocated = (statp->st_size + sizeof (ino_t) - 1) / sizeof (ino_t);
-  inode_space = (ino_t *) xmalloc (n_inodes_allocated * sizeof (ino_t));
+  mode_t filetype_mode;
 
-  do
+  if (user_specified_name)
     {
-      namep = name_space;
-      inodep = inode_space;
+      char *base = base_name (fs->filename);
 
-      errno = 0;
-      dirp = opendir (pathname);
-      if (dirp == NULL)
+      if (DOT_OR_DOTDOT (base))
        {
-         if (errno != ENOENT || !ignore_missing_files)
-           {
-             error (0, errno, "%s", pathname);
-             err = 1;
-           }
-         free (name_space);
-         free (inode_space);
-         return err;
+         error (0, 0, _("cannot remove `.' or `..'"));
+         return RM_ERROR;
        }
+    }
 
-      while ((dp = readdir (dirp)) != NULL)
-       {
-         /* Skip "." and "..".  */
-         if (dp->d_name[0] != '.'
-             || (dp->d_name[1] != '\0'
-                 && (dp->d_name[1] != '.' || dp->d_name[2] != '\0')))
-           {
-             unsigned size_needed = (namep - name_space) + NLENGTH (dp) + 2;
-
-             if (size_needed > name_size)
-               {
-                 char *new_name_space;
+  if (fspec_get_filetype_mode (fs, &filetype_mode))
+    {
+      if (ignore_missing_files && errno == ENOENT)
+       return RM_OK;
 
-                 while (size_needed > name_size)
-                   name_size += 1024;
+      error (0, errno, _("cannot remove `%s'"), full_filename (fs->filename));
+      return RM_ERROR;
+    }
 
-                 new_name_space = xrealloc (name_space, name_size);
-                 namep += new_name_space - name_space;
-                 name_space = new_name_space;
-               }
-             namep = stpcpy (namep, dp->d_name) + 1;
+  if (S_ISDIR (filetype_mode))
+    {
+      int fail;
+      struct active_dir_ent *old_ent;
+
+      /* Insert this directory in the active_dir_map.
+        If there is already a directory in the map with the same inum,
+        then there's *probably* a directory cycle.  This test can get
+        a false positive if two directories have the same inode number
+        but different device numbers and one directory contains the
+        other.  But since people don't often try to delete hierarchies
+        containing mount points, and when they do, duplicate inode
+        numbers are not that likely, this isn't worth detecting.  */
+      fail = hash_insert_item (active_dir_map,
+                              make_active_dir_ent (fs->inum, current_depth ()),
+                              (void **) &old_ent);
+      if (fail)
+       error (1, 0, _("Memory exhausted"));
+
+      if (old_ent)
+       {
+         error (0, 0, _("\
+WARNING: Circular directory structure.\n\
+This almost certainly means that you have a corrupted file system.\n\
+NOTIFY YOUR SYSTEM MANAGER.\n\
+The following two directories have the same inode number:\n"));
+         print_nth_dir (stderr, current_depth ());
+         fputc ('\n', stderr);
+         print_nth_dir (stderr, old_ent->depth);
+         fputc ('\n', stderr);
+         fflush (stderr);
 
-             if (inodep == inode_space + n_inodes_allocated)
-               {
-                 ino_t *new_inode_space;
+         free (old_ent);
 
-                 n_inodes_allocated += 1024;
-                 new_inode_space = (ino_t *) xrealloc (inode_space,
-                                       n_inodes_allocated * sizeof (ino_t));
-                 inodep += new_inode_space - inode_space;
-                 inode_space = new_inode_space;
-               }
-             *inodep++ = D_INO (dp);
+         if (interactive)
+           {
+             error (0, 0, _("continue? "));
+             if (yesno ())
+               return RM_ERROR;
            }
+         exit (1);
        }
-      *namep = '\0';
-      if (CLOSEDIR (dirp))
-       {
-         error (0, errno, "%s", pathname);
-         err = 1;
-       }
+    }
 
-      pathname_length = strlen (pathname);
+  if (!S_ISDIR (filetype_mode) || unlink_dirs)
+    {
+      return remove_file (fs);
+    }
+  else
+    {
+      int need_save_cwd = user_specified_name;
+      enum RM_status status;
+      struct active_dir_ent tmp;
+      struct active_dir_ent *old_ent;
 
-      for (namep = name_space, inodep = inode_space; *namep != '\0';
-          namep += name_length, inodep++)
-       {
-         name_length = strlen (namep) + 1;
+      if (need_save_cwd)
+       need_save_cwd = (strchr (fs->filename, '/') != NULL);
 
-         /* Handle arbitrarily long filenames.  */
-         if (pathname_length + 1 + name_length > pnsize)
-           {
-             char *new_pathname;
-
-             pnsize = (pathname_length + 1 + name_length) * 2;
-             new_pathname = xrealloc (pathname, pnsize);
-             /* Update all pointers in the stack to use the new area.  */
-             for (pp = pathstack; pp != NULL; pp = pp->next)
-               pp->pathp += new_pathname - pathname;
-             pathname = new_pathname;
-           }
+      status = remove_dir (fs, need_save_cwd);
 
-         /* Add a new frame to the top of the path stack.  */
-         pathframe.pathp = pathname + pathname_length;
-         pathframe.inum = *inodep;
-         pathframe.next = pathstack;
-         pathstack = &pathframe;
+      /* Remove this directory from the active_dir_map.  */
+      tmp.inum = fs->inum;
+      hash_delete_item (active_dir_map, &tmp, (void **) &old_ent);
+      assert (old_ent != NULL);
+      free (old_ent);
 
-         /* Append '/' and the filename to current pathname, take care of
-            the file (which could result in recursive calls), and take
-            the filename back off.  */
+      return status;
+    }
+}
 
-         *pathstack->pathp = '/';
-         strcpy (pathstack->pathp + 1, namep);
+int
+main (int argc, char **argv)
+{
+  int fail = 0;
+  int c;
 
-         /* If the i-number has already appeared, there's an error.  */
-         if (duplicate_entry (pathstack->next, pathstack->inum))
-           err++;
-         else if (rm ())
-           err++;
+  program_name = argv[0];
+  setlocale (LC_ALL, "");
+  bindtextdomain (PACKAGE, LOCALEDIR);
+  textdomain (PACKAGE);
 
-         *pathstack->pathp = '\0';
-         pathstack = pathstack->next;  /* Pop the stack.  */
+  verbose = ignore_missing_files = recursive = interactive
+    = unlink_dirs = 0;
+
+  while ((c = getopt_long (argc, argv, "dfirvR", long_opts, NULL)) != -1)
+    {
+      switch (c)
+       {
+       case 0:         /* Long option.  */
+         break;
+       case 'd':
+         unlink_dirs = 1;
+         break;
+       case 'f':
+         interactive = 0;
+         ignore_missing_files = 1;
+         break;
+       case 'i':
+         interactive = 1;
+         ignore_missing_files = 0;
+         break;
+       case 'r':
+       case 'R':
+         recursive = 1;
+         break;
+       case 'v':
+         verbose = 1;
+         break;
+       default:
+         usage (1);
        }
     }
-  /* Keep trying while there are still files to remove.  */
-  while (namep > name_space && err == 0);
-
-  free (name_space);
-  free (inode_space);
-  return err;
-}
 
-/* If STACK does not already have an entry with the same i-number as INUM,
-   return 0. Otherwise, ask the user whether to continue;
-   if yes, return 1, and if no, exit.
-   This assumes that no one tries to remove filesystem mount points;
-   doing so could cause duplication of i-numbers that would not indicate
-   a corrupted file system.  */
+  if (show_version)
+    {
+      printf ("rm (%s) %s\n", GNU_PACKAGE, VERSION);
+      exit (0);
+    }
 
-static int
-duplicate_entry (struct pathstack *stack, ino_t inum)
-{
-#ifdef D_INO_IN_DIRENT
-  struct pathstack *p;
+  if (show_help)
+    usage (0);
 
-  for (p = stack; p != NULL; p = p->next)
+  if (optind == argc)
     {
-      if (p->inum == inum)
+      if (ignore_missing_files)
+       exit (0);
+      else
        {
-         fprintf (stderr, _("\
-%s: WARNING: Circular directory structure.\n\
-This almost certainly means that you have a corrupted file system.\n\
-NOTIFY YOUR SYSTEM MANAGER.\n\
-Cycle detected:\n\
-%s\n\
-is the same file as\n"), program_name, pathname);
-         *p->pathp = '\0';     /* Truncate pathname.  */
-         fprintf (stderr, "%s\n", pathname);
-         *p->pathp = '/';      /* Put it back.  */
-         if (interactive)
-           {
-             fprintf (stderr, _("%s: continue? "), program_name);
-             if (!yesno ())
-               exit (1);
-             return 1;
-           }
-         else
-           exit (1);
+         error (0, 0, _("too few arguments"));
+         usage (1);
        }
     }
-#endif /* D_INO_IN_DIRENT */
-  return 0;
-}
 
-static void
-usage (int status)
-{
-  if (status != 0)
-    fprintf (stderr, _("Try `%s --help' for more information.\n"),
-            program_name);
-  else
+  stdin_tty = isatty (STDIN_FILENO);
+
+  /* Initialize dir-stack obstacks.  */
+  obstack_init (&dir_stack);
+  obstack_init (&len_stack);
+
+  active_dir_map = hash_init_table (NULL, ACTIVE_DIR_INITIAL_CAPACITY, 0, 0,
+                                   hash_active_dir_ent_1,
+                                   hash_active_dir_ent_2,
+                                   hash_compare_active_dir_ents);
+
+  for (; optind < argc; optind++)
     {
-      printf (_("Usage: %s [OPTION]... FILE...\n"), program_name);
-      printf (_("\
-Remove (unlink) the FILE(s).\n\
-\n\
-  -d, --directory       unlink directory, even if non-empty (super-user only)\n\
-  -f, --force           ignore nonexistent files, never prompt\n\
-  -i, --interactive     prompt before any removal\n\
-  -r, -R, --recursive   remove the contents of directories recursively\n\
-  -v, --verbose         explain what is being done\n\
-      --help            display this help and exit\n\
-      --version         output version information and exit\n\
-"));
-      puts (_("\nReport bugs to <fileutils-bugs@gnu.ai.mit.edu>."));
+      struct File_spec fs;
+      enum RM_status status;
+
+      /* Stripping slashes is harmless for rmdir;
+        if the arg is not a directory, it will fail with ENOTDIR.  */
+      strip_trailing_slashes (argv[optind]);
+      fspec_init_file (&fs, argv[optind]);
+      status = rm (&fs, 1);
+      assert (VALID_STATUS (status));
+      if (status == RM_ERROR)
+       fail = 1;
     }
-  exit (status);
+
+  exit (fail);
 }