1 /* rmdir -- remove directories
2 
3    Copyright (C) 1990-2023 Free Software Foundation, Inc.
4 
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9 
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
17 
18 /* Options:
19    -p, --parent		Remove any parent dirs that are explicitly mentioned
20                         in an argument, if they become empty after the
21                         argument file is removed.
22 
23    David MacKenzie <djm@ai.mit.edu>  */
24 
25 #include <config.h>
26 #include <stdio.h>
27 #include <getopt.h>
28 #include <sys/types.h>
29 
30 #include "system.h"
31 #include "prog-fprintf.h"
32 
33 /* The official name of this program (e.g., no 'g' prefix).  */
34 #define PROGRAM_NAME "rmdir"
35 
36 #define AUTHORS proper_name ("David MacKenzie")
37 
38 /* If true, remove empty parent directories.  */
39 static bool remove_empty_parents;
40 
41 /* If true, don't treat failure to remove a nonempty directory
42    as an error.  */
43 static bool ignore_fail_on_non_empty;
44 
45 /* If true, output a diagnostic for every directory processed.  */
46 static bool verbose;
47 
48 /* For long options that have no equivalent short option, use a
49    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
50 enum
51 {
52   IGNORE_FAIL_ON_NON_EMPTY_OPTION = CHAR_MAX + 1
53 };
54 
55 static struct option const longopts[] =
56 {
57   /* Don't name this '--force' because it's not close enough in meaning
58      to e.g. rm's -f option.  */
59   {"ignore-fail-on-non-empty", no_argument, nullptr,
60    IGNORE_FAIL_ON_NON_EMPTY_OPTION},
61 
62   {"path", no_argument, nullptr, 'p'},  /* Deprecated.  */
63   {"parents", no_argument, nullptr, 'p'},
64   {"verbose", no_argument, nullptr, 'v'},
65   {GETOPT_HELP_OPTION_DECL},
66   {GETOPT_VERSION_OPTION_DECL},
67   {nullptr, 0, nullptr, 0}
68 };
69 
70 /* Return true if ERROR_NUMBER is one of the values associated
71    with a failed rmdir due to non-empty target directory.  */
72 static bool
errno_rmdir_non_empty(int error_number)73 errno_rmdir_non_empty (int error_number)
74 {
75   return error_number == ENOTEMPTY || error_number == EEXIST;
76 }
77 
78 /* Return true if when rmdir fails with errno == ERROR_NUMBER
79    the directory may be non empty.  */
80 static bool
errno_may_be_non_empty(int error_number)81 errno_may_be_non_empty (int error_number)
82 {
83   switch (error_number)
84     {
85     case EACCES:
86     case EPERM:
87     case EROFS:
88     case EBUSY:
89       return true;
90     default:
91       return false;
92     }
93 }
94 
95 /* Return true if an rmdir failure with errno == error_number
96    for DIR is ignorable.  */
97 static bool
ignorable_failure(int error_number,char const * dir)98 ignorable_failure (int error_number, char const *dir)
99 {
100   return (ignore_fail_on_non_empty
101           && (errno_rmdir_non_empty (error_number)
102               || (errno_may_be_non_empty (error_number)
103                   && directory_status (AT_FDCWD, dir) == DS_NONEMPTY)));
104 }
105 
106 /* Remove any empty parent directories of DIR.
107    If DIR contains slash characters, at least one of them
108    (beginning with the rightmost) is replaced with a NUL byte.
109    Return true if successful.  */
110 
111 static bool
remove_parents(char * dir)112 remove_parents (char *dir)
113 {
114   char *slash;
115   bool ok = true;
116 
117   strip_trailing_slashes (dir);
118   while (true)
119     {
120       slash = strrchr (dir, '/');
121       if (slash == nullptr)
122         break;
123       /* Remove any characters after the slash, skipping any extra
124          slashes in a row. */
125       while (slash > dir && *slash == '/')
126         --slash;
127       slash[1] = 0;
128 
129       /* Give a diagnostic for each attempted removal if --verbose.  */
130       if (verbose)
131         prog_fprintf (stdout, _("removing directory, %s"), quoteaf (dir));
132 
133       ok = (rmdir (dir) == 0);
134       int rmdir_errno = errno;
135 
136       if (! ok)
137         {
138           /* Stop quietly if --ignore-fail-on-non-empty. */
139           if (ignorable_failure (rmdir_errno, dir))
140             {
141               ok = true;
142             }
143           else
144             {
145               char const *error_msg;
146               if (rmdir_errno != ENOTDIR)
147                 {
148                   /* Barring race conditions,
149                      DIR is expected to be a directory.  */
150                   error_msg = N_("failed to remove directory %s");
151                 }
152               else
153                 {
154                   /* A path component could be a symbolic link */
155                   error_msg = N_("failed to remove %s");
156                 }
157               error (0, rmdir_errno, _(error_msg), quoteaf (dir));
158             }
159           break;
160         }
161     }
162   return ok;
163 }
164 
165 void
usage(int status)166 usage (int status)
167 {
168   if (status != EXIT_SUCCESS)
169     emit_try_help ();
170   else
171     {
172       printf (_("Usage: %s [OPTION]... DIRECTORY...\n"), program_name);
173       fputs (_("\
174 Remove the DIRECTORY(ies), if they are empty.\n\
175 \n\
176 "), stdout);
177       fputs (_("\
178       --ignore-fail-on-non-empty\n\
179                     ignore each failure to remove a non-empty directory\n\
180 "), stdout);
181       fputs (_("\
182   -p, --parents     remove DIRECTORY and its ancestors;\n\
183                     e.g., 'rmdir -p a/b' is similar to 'rmdir a/b a'\n\
184 \n\
185 "), stdout);
186       fputs (_("\
187   -v, --verbose     output a diagnostic for every directory processed\n\
188 "), stdout);
189       fputs (HELP_OPTION_DESCRIPTION, stdout);
190       fputs (VERSION_OPTION_DESCRIPTION, stdout);
191       emit_ancillary_info (PROGRAM_NAME);
192     }
193   exit (status);
194 }
195 
196 int
main(int argc,char ** argv)197 main (int argc, char **argv)
198 {
199   bool ok = true;
200   int optc;
201 
202   initialize_main (&argc, &argv);
203   set_program_name (argv[0]);
204   setlocale (LC_ALL, "");
205   bindtextdomain (PACKAGE, LOCALEDIR);
206   textdomain (PACKAGE);
207 
208   atexit (close_stdout);
209 
210   remove_empty_parents = false;
211 
212   while ((optc = getopt_long (argc, argv, "pv", longopts, nullptr)) != -1)
213     {
214       switch (optc)
215         {
216         case 'p':
217           remove_empty_parents = true;
218           break;
219         case IGNORE_FAIL_ON_NON_EMPTY_OPTION:
220           ignore_fail_on_non_empty = true;
221           break;
222         case 'v':
223           verbose = true;
224           break;
225         case_GETOPT_HELP_CHAR;
226         case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
227         default:
228           usage (EXIT_FAILURE);
229         }
230     }
231 
232   if (optind == argc)
233     {
234       error (0, 0, _("missing operand"));
235       usage (EXIT_FAILURE);
236     }
237 
238   for (; optind < argc; ++optind)
239     {
240       char *dir = argv[optind];
241 
242       /* Give a diagnostic for each attempted removal if --verbose.  */
243       if (verbose)
244         prog_fprintf (stdout, _("removing directory, %s"), quoteaf (dir));
245 
246       if (rmdir (dir) != 0)
247         {
248           int rmdir_errno = errno;
249           if (ignorable_failure (rmdir_errno, dir))
250             continue;
251 
252           /* Distinguish the case for a symlink with trailing slash.
253              On Linux, rmdir(2) confusingly does not follow the symlink,
254              thus giving the errno ENOTDIR, while on other systems the symlink
255              is followed.  We don't provide consistent behavior here,
256              but at least we provide a more accurate error message.  */
257           bool custom_error = false;
258           if (rmdir_errno == ENOTDIR)
259             {
260               char const *last_unix_slash = strrchr (dir, '/');
261               if (last_unix_slash && (*(last_unix_slash + 1) == '\0'))
262                 {
263                   struct stat st;
264                   int ret = stat (dir, &st);
265                   /* Some other issue following, or is actually a directory. */
266                   if ((ret != 0 && errno != ENOTDIR)
267                       || (ret == 0 && S_ISDIR (st.st_mode)))
268                     {
269                       /* Ensure the last component was a symlink.  */
270                       char *dir_arg = xstrdup (dir);
271                       strip_trailing_slashes (dir);
272                       ret = lstat (dir, &st);
273                       if (ret == 0 && S_ISLNK (st.st_mode))
274                         {
275                           error (0, 0,
276                                  _("failed to remove %s:"
277                                    " Symbolic link not followed"),
278                                  quoteaf (dir_arg));
279                           custom_error = true;
280                         }
281                       free (dir_arg);
282                     }
283                 }
284             }
285 
286           if (! custom_error)
287             error (0, rmdir_errno, _("failed to remove %s"), quoteaf (dir));
288 
289           ok = false;
290         }
291       else if (remove_empty_parents)
292         {
293           ok &= remove_parents (dir);
294         }
295     }
296 
297   return ok ? EXIT_SUCCESS : EXIT_FAILURE;
298 }
299