use std::io;
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InteractiveMode {
Never,
Once,
Always,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreserveRoot {
Yes,
All,
No,
}
#[derive(Debug)]
pub struct RmConfig {
pub force: bool,
pub interactive: InteractiveMode,
pub recursive: bool,
pub dir: bool,
pub verbose: bool,
pub preserve_root: PreserveRoot,
pub one_file_system: bool,
}
impl Default for RmConfig {
fn default() -> Self {
Self {
force: false,
interactive: InteractiveMode::Never,
recursive: false,
dir: false,
verbose: false,
preserve_root: PreserveRoot::Yes,
one_file_system: false,
}
}
}
fn prompt_yes(msg: &str) -> bool {
eprint!("{}", msg);
let mut answer = String::new();
if io::stdin().read_line(&mut answer).is_err() {
return false;
}
let trimmed = answer.trim();
trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes")
}
pub fn rm_path(path: &Path, config: &RmConfig) -> Result<bool, io::Error> {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if canonical == Path::new("/") {
if matches!(config.preserve_root, PreserveRoot::Yes | PreserveRoot::All) {
eprintln!("rm: it is dangerous to operate recursively on '/'");
eprintln!("rm: use --no-preserve-root to override this failsafe");
return Ok(false);
}
}
let meta = match std::fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) => {
if config.force && e.kind() == io::ErrorKind::NotFound {
return Ok(true);
}
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
return Ok(false);
}
};
if meta.is_dir() {
if config.recursive {
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!(
"rm: descend into directory '{}'? ",
path.display()
))
{
return Ok(false);
}
#[cfg(unix)]
let root_dev = meta.dev();
#[cfg(not(unix))]
let root_dev = 0u64;
let ok = rm_recursive(path, config, root_dev)?;
Ok(ok)
} else if config.dir {
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
{
return Ok(false);
}
match std::fs::remove_dir(path) {
Ok(()) => {
if config.verbose {
eprintln!("removed directory '{}'", path.display());
}
Ok(true)
}
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
Ok(false)
}
}
} else {
eprintln!("rm: cannot remove '{}': Is a directory", path.display());
Ok(false)
}
} else {
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!("rm: remove file '{}'? ", path.display()))
{
return Ok(false);
}
match std::fs::remove_file(path) {
Ok(()) => {
if config.verbose {
eprintln!("removed '{}'", path.display());
}
Ok(true)
}
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
Ok(false)
}
}
}
}
fn rm_recursive(path: &Path, config: &RmConfig, root_dev: u64) -> Result<bool, io::Error> {
if config.interactive == InteractiveMode::Never && !config.verbose {
let success = std::sync::atomic::AtomicBool::new(true);
rm_recursive_parallel(path, config, root_dev, &success);
if let Err(e) = std::fs::remove_dir(path) {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
return Ok(false);
}
return Ok(success.load(std::sync::atomic::Ordering::Relaxed));
}
let mut success = true;
let entries = match std::fs::read_dir(path) {
Ok(rd) => rd,
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
return Ok(false);
}
};
for entry in entries {
let entry = entry?;
let child_path = entry.path();
let child_meta = match std::fs::symlink_metadata(&child_path) {
Ok(m) => m,
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
success = false;
continue;
}
};
#[cfg(unix)]
let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
#[cfg(not(unix))]
let skip_fs = false;
if skip_fs {
continue;
}
if child_meta.is_dir() {
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!(
"rm: descend into directory '{}'? ",
child_path.display()
))
{
success = false;
continue;
}
if !rm_recursive(&child_path, config, root_dev)? {
success = false;
}
} else {
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!("rm: remove file '{}'? ", child_path.display()))
{
success = false;
continue;
}
match std::fs::remove_file(&child_path) {
Ok(()) => {
if config.verbose {
eprintln!("removed '{}'", child_path.display());
}
}
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
success = false;
}
}
}
}
if config.interactive == InteractiveMode::Always
&& !prompt_yes(&format!("rm: remove directory '{}'? ", path.display()))
{
return Ok(false);
}
match std::fs::remove_dir(path) {
Ok(()) => {
if config.verbose {
eprintln!("removed directory '{}'", path.display());
}
}
Err(e) => {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
success = false;
}
}
Ok(success)
}
fn rm_recursive_parallel(
path: &Path,
config: &RmConfig,
root_dev: u64,
success: &std::sync::atomic::AtomicBool,
) {
let entries = match std::fs::read_dir(path) {
Ok(rd) => rd,
Err(e) => {
if !config.force {
eprintln!("rm: cannot remove '{}': {}", path.display(), e);
}
success.store(false, std::sync::atomic::Ordering::Relaxed);
return;
}
};
let entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
use rayon::prelude::*;
entries.par_iter().for_each(|entry| {
let child_path = entry.path();
let child_meta = match std::fs::symlink_metadata(&child_path) {
Ok(m) => m,
Err(e) => {
if !config.force {
eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
}
success.store(false, std::sync::atomic::Ordering::Relaxed);
return;
}
};
#[cfg(unix)]
let skip_fs = config.one_file_system && child_meta.dev() != root_dev;
#[cfg(not(unix))]
let skip_fs = false;
if skip_fs {
return;
}
if child_meta.is_dir() {
rm_recursive_parallel(&child_path, config, root_dev, success);
if let Err(e) = std::fs::remove_dir(&child_path) {
if !config.force {
eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
}
success.store(false, std::sync::atomic::Ordering::Relaxed);
}
} else if let Err(e) = std::fs::remove_file(&child_path) {
if !config.force {
eprintln!("rm: cannot remove '{}': {}", child_path.display(), e);
}
success.store(false, std::sync::atomic::Ordering::Relaxed);
}
});
}