Skip to main content

coreutils_rs/rm/
core.rs

1use std::io;
2use std::path::Path;
3
4#[cfg(unix)]
5use std::os::unix::fs::MetadataExt;
6
7/// How interactive prompting should behave.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum InteractiveMode {
10    /// Never prompt.
11    Never,
12    /// Prompt once before removing more than 3 files or when recursive.
13    Once,
14    /// Prompt before every removal.
15    Always,
16}
17
18/// Whether to protect the root directory from recursive removal.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PreserveRoot {
21    /// Refuse to remove '/' (default).
22    Yes,
23    /// Refuse to remove '/' and also reject arguments on different mount points.
24    All,
25    /// Allow removing '/'.
26    No,
27}
28
29/// Configuration for the rm operation.
30#[derive(Debug)]
31pub struct RmConfig {
32    /// Ignore nonexistent files, never prompt.
33    pub force: bool,
34    /// Interactive prompting mode.
35    pub interactive: InteractiveMode,
36    /// Remove directories and their contents recursively.
37    pub recursive: bool,
38    /// Remove empty directories.
39    pub dir: bool,
40    /// Print a message for each removed file.
41    pub verbose: bool,
42    /// Root protection mode.
43    pub preserve_root: PreserveRoot,
44    /// When used with -r, skip directories on different file systems.
45    pub one_file_system: bool,
46}
47
48impl Default for RmConfig {
49    fn default() -> Self {
50        Self {
51            force: false,
52            interactive: InteractiveMode::Never,
53            recursive: false,
54            dir: false,
55            verbose: false,
56            preserve_root: PreserveRoot::Yes,
57            one_file_system: false,
58        }
59    }
60}
61
62/// Prompt the user on stderr and return true if they answer 'y' or 'Y'.
63fn prompt_yes(msg: &str) -> bool {
64    eprint!("{}", msg);
65    let mut answer = String::new();
66    if io::stdin().read_line(&mut answer).is_err() {
67        return false;
68    }
69    let trimmed = answer.trim();
70    trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes")
71}
72
73/// Remove a single path according to the given configuration.
74///
75/// Returns `Ok(true)` on success, `Ok(false)` on non-fatal failure (e.g. the
76/// user declined a prompt, or the path was skipped), and `Err` on I/O errors
77/// that should propagate.
78pub fn rm_path(path: &Path, config: &RmConfig) -> Result<bool, io::Error> {
79    // Check preserve-root: canonicalize to detect '/' even through symlinks.
80    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
81    if canonical == Path::new("/") {
82        if matches!(config.preserve_root, PreserveRoot::Yes | PreserveRoot::All) {
83            eprintln!("rm: it is dangerous to operate recursively on '/'");
84            eprintln!("rm: use --no-preserve-root to override this failsafe");
85            return Ok(false);
86        }
87    }
88
89    let meta = match std::fs::symlink_metadata(path) {
90        Ok(m) => m,
91        Err(e) => {
92            if config.force && e.kind() == io::ErrorKind::NotFound {
93                return Ok(true);
94            }
95            eprintln!("rm: cannot remove '{}': {}", path.display(), e);
96            return Ok(false);
97        }
98    };
99
100    if meta.is_dir() {
101        if config.recursive {
102            if config.interactive == InteractiveMode::Always
103                && !prompt_yes(&format!(
104                    "rm: descend into directory '{}'? ",
105                    path.display()
106                ))
107            {
108                return Ok(false);
109            }
110            #[cfg(unix)]
111            let root_dev = meta.dev();
112            #[cfg(not(unix))]
113            let root_dev = 0u64;
114            let ok = rm_recursive(path, config, root_dev)?;
115            Ok(ok)
116        } else if config.dir {
117            if config.interactive == InteractiveMode::Always
118                && !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
119            {
120                return Ok(false);
121            }
122            match std::fs::remove_dir(path) {
123                Ok(()) => {
124                    if config.verbose {
125                        eprintln!("removed directory '{}'", path.display());
126                    }
127                    Ok(true)
128                }
129                Err(e) => {
130                    eprintln!("rm: cannot remove '{}': {}", path.display(), e);
131                    Ok(false)
132                }
133            }
134        } else {
135            eprintln!("rm: cannot remove '{}': Is a directory", path.display());
136            Ok(false)
137        }
138    } else {
139        if config.interactive == InteractiveMode::Always
140            && !prompt_yes(&format!("rm: remove file '{}'? ", path.display()))
141        {
142            return Ok(false);
143        }
144        match std::fs::remove_file(path) {
145            Ok(()) => {
146                if config.verbose {
147                    eprintln!("removed '{}'", path.display());
148                }
149                Ok(true)
150            }
151            Err(e) => {
152                eprintln!("rm: cannot remove '{}': {}", path.display(), e);
153                Ok(false)
154            }
155        }
156    }
157}
158
159/// Recursively remove a directory tree.
160/// Uses parallel removal via rayon when not in interactive mode.
161fn rm_recursive(path: &Path, config: &RmConfig, root_dev: u64) -> Result<bool, io::Error> {
162    // For non-interactive mode, use parallel recursive removal
163    if config.interactive == InteractiveMode::Never && !config.verbose {
164        let success = std::sync::atomic::AtomicBool::new(true);
165        rm_recursive_parallel(path, config, root_dev, &success);
166        // Remove the directory itself after children are removed
167        if let Err(e) = std::fs::remove_dir(path) {
168            eprintln!("rm: cannot remove '{}': {}", path.display(), e);
169            return Ok(false);
170        }
171        return Ok(success.load(std::sync::atomic::Ordering::Relaxed));
172    }
173
174    let mut success = true;
175
176    let entries = match std::fs::read_dir(path) {
177        Ok(rd) => rd,
178        Err(e) => {
179            eprintln!("rm: cannot remove '{}': {}", path.display(), e);
180            return Ok(false);
181        }
182    };
183
184    for entry in entries {
185        let entry = entry?;
186        let child_path = entry.path();
187        let child_meta = match std::fs::symlink_metadata(&child_path) {
188            Ok(m) => m,
189            Err(e) => {
190                eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
191                success = false;
192                continue;
193            }
194        };
195
196        #[cfg(unix)]
197        let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
198        #[cfg(not(unix))]
199        let skip_fs = false;
200
201        if skip_fs {
202            continue;
203        }
204
205        if child_meta.is_dir() {
206            if config.interactive == InteractiveMode::Always
207                && !prompt_yes(&format!(
208                    "rm: descend into directory '{}'? ",
209                    child_path.display()
210                ))
211            {
212                success = false;
213                continue;
214            }
215            if !rm_recursive(&child_path, config, root_dev)? {
216                success = false;
217            }
218        } else {
219            if config.interactive == InteractiveMode::Always
220                && !prompt_yes(&format!("rm: remove file '{}'? ", child_path.display()))
221            {
222                success = false;
223                continue;
224            }
225            match std::fs::remove_file(&child_path) {
226                Ok(()) => {
227                    if config.verbose {
228                        eprintln!("removed '{}'", child_path.display());
229                    }
230                }
231                Err(e) => {
232                    eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
233                    success = false;
234                }
235            }
236        }
237    }
238
239    // Now remove the (hopefully empty) directory itself.
240    if config.interactive == InteractiveMode::Always
241        && !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
242    {
243        return Ok(false);
244    }
245
246    match std::fs::remove_dir(path) {
247        Ok(()) => {
248            if config.verbose {
249                eprintln!("removed directory '{}'", path.display());
250            }
251        }
252        Err(e) => {
253            eprintln!("rm: cannot remove '{}': {}", path.display(), e);
254            success = false;
255        }
256    }
257
258    Ok(success)
259}
260
261/// Parallel recursive removal for non-interactive, non-verbose mode.
262fn rm_recursive_parallel(
263    path: &Path,
264    config: &RmConfig,
265    root_dev: u64,
266    success: &std::sync::atomic::AtomicBool,
267) {
268    let entries = match std::fs::read_dir(path) {
269        Ok(rd) => rd,
270        Err(e) => {
271            if !config.force {
272                eprintln!("rm: cannot remove '{}': {}", path.display(), e);
273            }
274            success.store(false, std::sync::atomic::Ordering::Relaxed);
275            return;
276        }
277    };
278
279    let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
280
281    use rayon::prelude::*;
282    entries.par_iter().for_each(|entry| {
283        let child_path = entry.path();
284        let child_meta = match std::fs::symlink_metadata(&child_path) {
285            Ok(m) => m,
286            Err(e) => {
287                if !config.force {
288                    eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
289                }
290                success.store(false, std::sync::atomic::Ordering::Relaxed);
291                return;
292            }
293        };
294
295        #[cfg(unix)]
296        let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
297        #[cfg(not(unix))]
298        let skip_fs = false;
299
300        if skip_fs {
301            return;
302        }
303
304        if child_meta.is_dir() {
305            rm_recursive_parallel(&child_path, config, root_dev, success);
306            if let Err(e) = std::fs::remove_dir(&child_path) {
307                if !config.force {
308                    eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
309                }
310                success.store(false, std::sync::atomic::Ordering::Relaxed);
311            }
312        } else if let Err(e) = std::fs::remove_file(&child_path) {
313            if !config.force {
314                eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
315            }
316            success.store(false, std::sync::atomic::Ordering::Relaxed);
317        }
318    });
319}