use clap::builder::{PossibleValue, ValueParser};
use clap::{Arg, ArgAction, Command, parser::ValueSource};
use indicatif::{ProgressBar, ProgressStyle};
use std::ffi::{OsStr, OsString};
use std::fs::{self, Metadata};
use std::io::{self, IsTerminal, stdin};
use std::ops::BitOr;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::MAIN_SEPARATOR;
use std::path::Path;
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult};
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
use uucore::translate;
use uucore::{format_usage, os_str_as_bytes, prompt_yes, show_error};
mod platform;
#[cfg(all(unix, not(target_os = "redox")))]
use platform::{safe_remove_dir_recursive, safe_remove_empty_dir, safe_remove_file};
#[derive(Debug, Error)]
enum RmError {
#[error("{}", translate!("rm-error-missing-operand", "util_name" => uucore::execution_phrase()))]
MissingOperand,
#[error("{}", translate!("rm-error-cannot-remove-no-such-file", "file" => _0.quote()))]
CannotRemoveNoSuchFile(OsString),
#[error("{}", translate!("rm-error-cannot-remove-permission-denied", "file" => _0.quote()))]
CannotRemovePermissionDenied(OsString),
#[error("{}", translate!("rm-error-cannot-remove-is-directory", "file" => _0.quote()))]
CannotRemoveIsDirectory(OsString),
#[error("{}", translate!("rm-error-dangerous-recursive-operation"))]
DangerousRecursiveOperation,
#[error("{}", translate!("rm-error-use-no-preserve-root"))]
UseNoPreserveRoot,
#[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0.quote()))]
RefusingToRemoveDirectory(OsString),
#[error("{}", translate!("rm-error-may-not-abbreviate-no-preserve-root"))]
MayNotAbbreviateNoPreserveRoot,
}
impl UError for RmError {}
fn verbose_removed_file(path: &Path, options: &Options) {
if options.verbose {
println!(
"{}",
translate!("rm-verbose-removed", "file" => uucore::fs::normalize_path(path).quote())
);
}
}
fn verbose_removed_directory(path: &Path, options: &Options) {
if options.verbose {
println!(
"{}",
translate!("rm-verbose-removed-directory", "file" => uucore::fs::normalize_path(path).quote())
);
}
}
fn show_removal_error(error: io::Error, path: &Path) -> bool {
if error.kind() == io::ErrorKind::PermissionDenied {
show_error!("cannot remove {}: Permission denied", path.quote());
} else {
let e =
error.map_err_context(|| translate!("rm-error-cannot-remove", "file" => path.quote()));
show_error!("{e}");
}
true
}
fn show_permission_denied_error(path: &Path) -> bool {
show_error!("cannot remove {}: Permission denied", path.quote());
true
}
fn remove_dir_with_feedback(path: &Path, options: &Options) -> bool {
match fs::remove_dir(path) {
Ok(_) => {
verbose_removed_directory(path, options);
false
}
Err(e) => show_removal_error(e, path),
}
}
#[derive(Eq, PartialEq, Clone, Copy)]
pub enum InteractiveMode {
Never,
Once,
Always,
PromptProtected,
}
impl From<&str> for InteractiveMode {
fn from(s: &str) -> Self {
match s {
"never" => Self::Never,
"once" => Self::Once,
"always" => Self::Always,
_ => unreachable!("should be prevented by clap"),
}
}
}
pub struct Options {
pub force: bool,
pub interactive: InteractiveMode,
#[allow(dead_code)]
pub one_fs: bool,
pub preserve_root: bool,
pub recursive: bool,
pub dir: bool,
pub verbose: bool,
pub progress: bool,
#[doc(hidden)]
pub __presume_input_tty: Option<bool>,
}
impl Default for Options {
fn default() -> Self {
Self {
force: false,
interactive: InteractiveMode::PromptProtected,
one_fs: false,
preserve_root: true,
recursive: false,
dir: false,
verbose: false,
progress: false,
__presume_input_tty: None,
}
}
}
static OPT_DIR: &str = "dir";
static OPT_INTERACTIVE: &str = "interactive";
static OPT_FORCE: &str = "force";
static OPT_NO_PRESERVE_ROOT: &str = "no-preserve-root";
static OPT_ONE_FILE_SYSTEM: &str = "one-file-system";
static OPT_PRESERVE_ROOT: &str = "preserve-root";
static OPT_PROMPT_ALWAYS: &str = "prompt-always";
static OPT_PROMPT_ONCE: &str = "prompt-once";
static OPT_RECURSIVE: &str = "recursive";
static OPT_VERBOSE: &str = "verbose";
static OPT_PROGRESS: &str = "progress";
static PRESUME_INPUT_TTY: &str = "-presume-input-tty";
static ARG_FILES: &str = "files";
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args: Vec<OsString> = args.collect();
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args.iter())?;
let files: Vec<_> = matches
.get_many::<OsString>(ARG_FILES)
.map(|v| v.map(OsString::as_os_str).collect())
.unwrap_or_default();
let force_flag = matches.get_flag(OPT_FORCE);
if files.is_empty() && !force_flag {
return Err(RmError::MissingOperand.into());
}
let force_prompt_never = force_flag && {
let force_index = matches.index_of(OPT_FORCE).unwrap_or(0);
![OPT_PROMPT_ALWAYS, OPT_PROMPT_ONCE, OPT_INTERACTIVE]
.iter()
.any(|flag| {
matches.value_source(flag) == Some(ValueSource::CommandLine)
&& matches.index_of(flag).unwrap_or(0) > force_index
})
};
let preserve_root = !matches.get_flag(OPT_NO_PRESERVE_ROOT);
let recursive = matches.get_flag(OPT_RECURSIVE);
let options = Options {
force: force_flag,
interactive: {
if force_prompt_never {
InteractiveMode::Never
} else if matches.get_flag(OPT_PROMPT_ALWAYS) {
InteractiveMode::Always
} else if matches.get_flag(OPT_PROMPT_ONCE) {
InteractiveMode::Once
} else if matches.contains_id(OPT_INTERACTIVE) {
InteractiveMode::from(matches.get_one::<String>(OPT_INTERACTIVE).unwrap().as_str())
} else {
InteractiveMode::PromptProtected
}
},
one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM),
preserve_root,
recursive,
dir: matches.get_flag(OPT_DIR),
verbose: matches.get_flag(OPT_VERBOSE),
progress: matches.get_flag(OPT_PROGRESS),
__presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) {
Some(true)
} else {
None
},
};
if !options.preserve_root && !args.iter().any(|arg| arg == "--no-preserve-root") {
return Err(RmError::MayNotAbbreviateNoPreserveRoot.into());
}
if options.interactive == InteractiveMode::Once && (options.recursive || files.len() > 3) {
let msg: String = format!(
"remove {} {}{}",
files.len(),
if files.len() > 1 {
"arguments"
} else {
"argument"
},
if options.recursive {
" recursively?"
} else {
"?"
}
);
if !prompt_yes!("{msg}") {
return Ok(());
}
}
if remove(&files, &options) {
return Err(1.into());
}
Ok(())
}
pub fn uu_app() -> Command {
Command::new("rm")
.version(uucore::crate_version!())
.about(translate!("rm-about"))
.help_template(uucore::localized_help_template(uucore::util_name()))
.override_usage(format_usage(&translate!("rm-usage")))
.after_help(translate!("rm-after-help"))
.infer_long_args(true)
.args_override_self(true)
.arg(
Arg::new(OPT_FORCE)
.short('f')
.long(OPT_FORCE)
.help(translate!("rm-help-force"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PROMPT_ALWAYS)
.short('i')
.help(translate!("rm-help-prompt-always"))
.overrides_with_all([OPT_PROMPT_ONCE, OPT_INTERACTIVE])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PROMPT_ONCE)
.short('I')
.help(translate!("rm-help-prompt-once"))
.overrides_with_all([OPT_PROMPT_ALWAYS, OPT_INTERACTIVE])
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_INTERACTIVE)
.long(OPT_INTERACTIVE)
.help(translate!("rm-help-interactive"))
.value_name("WHEN")
.value_parser(ShortcutValueParser::new([
PossibleValue::new("always").alias("yes"),
PossibleValue::new("once"),
PossibleValue::new("never").alias("no").alias("none"),
]))
.num_args(0..=1)
.require_equals(true)
.default_missing_value("always")
.overrides_with_all([OPT_PROMPT_ALWAYS, OPT_PROMPT_ONCE]),
)
.arg(
Arg::new(OPT_ONE_FILE_SYSTEM)
.long(OPT_ONE_FILE_SYSTEM)
.help(translate!("rm-help-one-file-system"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_NO_PRESERVE_ROOT)
.long(OPT_NO_PRESERVE_ROOT)
.help(translate!("rm-help-no-preserve-root"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PRESERVE_ROOT)
.long(OPT_PRESERVE_ROOT)
.help(translate!("rm-help-preserve-root"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_RECURSIVE)
.short('r')
.visible_short_alias('R')
.long(OPT_RECURSIVE)
.help(translate!("rm-help-recursive"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_DIR)
.short('d')
.long(OPT_DIR)
.help(translate!("rm-help-dir"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_VERBOSE)
.short('v')
.long(OPT_VERBOSE)
.help(translate!("rm-help-verbose"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PROGRESS)
.short('g')
.long(OPT_PROGRESS)
.help(translate!("rm-help-progress"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(PRESUME_INPUT_TTY)
.long("presume-input-tty")
.alias(PRESUME_INPUT_TTY)
.hide(true)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(ARG_FILES)
.action(ArgAction::Append)
.value_parser(ValueParser::os_string())
.num_args(1..)
.value_hint(clap::ValueHint::AnyPath),
)
}
fn create_progress_bar(files: &[&OsStr], recursive: bool) -> Option<ProgressBar> {
let total_files = count_files(files, recursive);
if total_files == 0 {
return None;
}
Some(
ProgressBar::new(total_files)
.with_style(
ProgressStyle::with_template(
"{msg}: [{elapsed_precise}] {wide_bar} {pos:>7}/{len:7} files",
)
.unwrap(),
)
.with_message(translate!("rm-progress-removing")),
)
}
fn count_files(paths: &[&OsStr], recursive: bool) -> u64 {
let mut total = 0;
for p in paths {
let path = Path::new(p);
if let Ok(md) = fs::symlink_metadata(path) {
if md.is_dir() && !is_symlink_dir(&md) {
if recursive {
total += count_files_in_directory(path);
}
} else {
total += 1;
}
}
}
total
}
fn count_files_in_directory(p: &Path) -> u64 {
let entries_count = fs::read_dir(p).map_or(0, |entries| {
entries
.flatten()
.map(|entry| match entry.file_type() {
Ok(ft) if ft.is_dir() => count_files_in_directory(&entry.path()),
Ok(_) => 1,
Err(_) => 0,
})
.sum()
});
1 + entries_count
}
pub fn remove(files: &[&OsStr], options: &Options) -> bool {
let mut had_err = false;
let mut progress_bar: Option<ProgressBar> = None;
let mut any_files_processed = false;
for filename in files {
let file = Path::new(filename);
if uucore::fs::path_ends_with_terminator(file)
&& options.recursive
&& options.preserve_root
&& is_root_path(file)
{
show_preserve_root_error(file);
had_err = true;
continue;
}
had_err = match file.symlink_metadata() {
Ok(metadata) => {
if options.progress && progress_bar.is_none() {
progress_bar = create_progress_bar(files, options.recursive);
}
any_files_processed = true;
if metadata.is_dir() {
handle_dir(file, options, progress_bar.as_ref())
} else if is_symlink_dir(&metadata) {
remove_dir(file, options, progress_bar.as_ref())
} else {
remove_file(file, options, progress_bar.as_ref())
}
}
Err(_e) => {
if options.force {
false
} else {
show_error!(
"{}",
RmError::CannotRemoveNoSuchFile(filename.to_os_string())
);
true
}
}
}
.bitor(had_err);
}
if let Some(pb) = progress_bar {
if any_files_processed {
pb.finish();
}
}
had_err
}
fn is_dir_empty(path: &Path) -> bool {
fs::read_dir(path).is_ok_and(|mut iter| iter.next().is_none())
}
#[cfg(unix)]
fn is_readable_metadata(metadata: &Metadata) -> bool {
let mode = metadata.permissions().mode();
(mode & 0o400) > 0
}
#[cfg(any(not(unix), target_os = "redox"))]
fn is_readable(_path: &Path) -> bool {
true
}
#[cfg(unix)]
fn is_writable_metadata(metadata: &Metadata) -> bool {
let mode = metadata.permissions().mode();
(mode & 0o200) > 0
}
#[cfg(not(unix))]
fn is_writable_metadata(_metadata: &Metadata) -> bool {
true
}
fn remove_dir_recursive(
path: &Path,
options: &Options,
progress_bar: Option<&ProgressBar>,
) -> bool {
if !path.is_dir() || path.is_symlink() {
return remove_file(path, options, progress_bar);
}
if options.interactive == InteractiveMode::Always
&& !is_dir_empty(path)
&& !prompt_descend(path)
{
return false;
}
#[cfg(all(unix, not(target_os = "redox")))]
{
safe_remove_dir_recursive(path, options, progress_bar)
}
#[cfg(any(not(unix), target_os = "redox"))]
{
if let Some(s) = path.to_str() {
if s.len() > 1000 {
match fs::remove_dir_all(path) {
Ok(_) => return false,
Err(e) => {
let e = e.map_err_context(
|| translate!("rm-error-cannot-remove", "file" => path.quote()),
);
show_error!("{e}");
return true;
}
}
}
}
let mut error = false;
match fs::read_dir(path) {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
}
Err(_) => error = true,
Ok(iter) => {
for entry in iter {
match entry {
Err(_) => error = true,
Ok(entry) => {
let child_error =
remove_dir_recursive(&entry.path(), options, progress_bar);
error = error || child_error;
}
}
}
}
}
if options.interactive == InteractiveMode::Always && !prompt_dir(path, options) {
return false;
}
match fs::remove_dir(path) {
Err(_) if !error && !is_readable(path) => {
show_permission_denied_error(path);
error = true;
}
Err(e) if !error => {
let e = e.map_err_context(
|| translate!("rm-error-cannot-remove", "file" => path.quote()),
);
show_error!("{e}");
error = true;
}
Err(_) => {
}
Ok(_) => verbose_removed_directory(path, options),
}
error
}
}
fn is_root_path(path: &Path) -> bool {
if path.has_root() && path.parent().is_none() {
return true;
}
if let Ok(canonical) = path.canonicalize() {
canonical.has_root() && canonical.parent().is_none()
} else {
false
}
}
fn show_preserve_root_error(path: &Path) {
let path_looks_like_root = path.has_root() && path.parent().is_none();
if path_looks_like_root {
show_error!("{}", RmError::DangerousRecursiveOperation);
} else {
show_error!(
"{}",
translate!("rm-error-dangerous-recursive-operation-same-as-root",
"path" => path.display())
);
}
show_error!("{}", RmError::UseNoPreserveRoot);
}
fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
let mut had_err = false;
let path = clean_trailing_slashes(path);
if path_is_current_or_parent_directory(path) {
show_error!(
"{}",
RmError::RefusingToRemoveDirectory(path.as_os_str().to_os_string())
);
return true;
}
let is_root = is_root_path(path);
if options.recursive && (!is_root || !options.preserve_root) {
had_err = remove_dir_recursive(path, options, progress_bar);
} else if options.dir && (!is_root || !options.preserve_root) {
had_err = remove_dir(path, options, progress_bar).bitor(had_err);
} else if options.recursive {
show_preserve_root_error(path);
had_err = true;
} else {
show_error!(
"{}",
RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string())
);
had_err = true;
}
had_err
}
fn remove_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
if !prompt_dir(path, options) {
return false;
}
if !options.dir && !options.recursive {
show_error!(
"{}",
RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string())
);
return true;
}
#[cfg(all(unix, not(target_os = "redox")))]
{
if let Some(result) = safe_remove_empty_dir(path, options, progress_bar) {
return result;
}
}
if let Some(pb) = progress_bar {
pb.inc(1);
}
remove_dir_with_feedback(path, options)
}
fn remove_file(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
if prompt_file(path, options) {
if let Some(pb) = progress_bar {
pb.inc(1);
}
#[cfg(all(unix, not(target_os = "redox")))]
{
if let Some(result) = safe_remove_file(path, options, progress_bar) {
return result;
}
}
match fs::remove_file(path) {
Ok(_) => {
verbose_removed_file(path, options);
}
Err(e) => {
if e.kind() == io::ErrorKind::PermissionDenied {
show_error!(
"{}",
RmError::CannotRemovePermissionDenied(path.as_os_str().to_os_string())
);
} else {
return show_removal_error(e, path);
}
return true;
}
}
}
false
}
fn prompt_dir(path: &Path, options: &Options) -> bool {
if options.interactive == InteractiveMode::Never {
return true;
}
if let Ok(metadata) = fs::metadata(path) {
handle_writable_directory(path, options, &metadata)
} else {
true
}
}
fn prompt_file(path: &Path, options: &Options) -> bool {
if options.interactive == InteractiveMode::Never {
return true;
}
let Ok(metadata) = fs::symlink_metadata(path) else {
return true;
};
if metadata.is_symlink() {
return options.interactive != InteractiveMode::Always
|| prompt_yes!("remove symbolic link {}?", path.quote());
}
if options.interactive == InteractiveMode::Always && is_writable_metadata(&metadata) {
return if metadata.len() == 0 {
prompt_yes!("remove regular empty file {}?", path.quote())
} else {
prompt_yes!("remove file {}?", path.quote())
};
}
prompt_file_permission_readonly(path, options, &metadata)
}
fn prompt_file_permission_readonly(path: &Path, options: &Options, metadata: &Metadata) -> bool {
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (stdin_ok, options.interactive) {
(false, InteractiveMode::PromptProtected) => true,
_ if is_writable_metadata(metadata) => true,
_ if metadata.len() == 0 => prompt_yes!(
"remove write-protected regular empty file {}?",
path.quote()
),
_ => prompt_yes!("remove write-protected regular file {}?", path.quote()),
}
}
fn path_is_current_or_parent_directory(path: &Path) -> bool {
let path_str = os_str_as_bytes(path.as_os_str());
let dir_separator = MAIN_SEPARATOR as u8;
if let Ok(path_bytes) = path_str {
return path_bytes == ([b'.'])
|| path_bytes == ([b'.', dir_separator])
|| path_bytes == ([b'.', b'.'])
|| path_bytes == ([b'.', b'.', dir_separator])
|| path_bytes.ends_with(&[dir_separator, b'.'])
|| path_bytes.ends_with(&[dir_separator, b'.', b'.'])
|| path_bytes.ends_with(&[dir_separator, b'.', dir_separator])
|| path_bytes.ends_with(&[dir_separator, b'.', b'.', dir_separator]);
}
false
}
#[cfg(unix)]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (
stdin_ok,
is_readable_metadata(metadata),
is_writable_metadata(metadata),
options.interactive,
) {
(false, _, _, InteractiveMode::PromptProtected) => true,
(false, false, false, InteractiveMode::Never) => true, (_, false, false, _) => prompt_yes!(
"attempt removal of inaccessible directory {}?",
path.quote()
),
(_, false, true, InteractiveMode::Always) => prompt_yes!(
"attempt removal of inaccessible directory {}?",
path.quote()
),
(_, true, false, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
(_, _, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
(_, _, _, _) => true,
}
}
#[cfg(windows)]
fn handle_writable_directory(path: &Path, options: &Options, metadata: &Metadata) -> bool {
use std::os::windows::prelude::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_READONLY;
let not_user_writable = (metadata.file_attributes() & FILE_ATTRIBUTE_READONLY) != 0;
let stdin_ok = options.__presume_input_tty.unwrap_or(false) || stdin().is_terminal();
match (stdin_ok, not_user_writable, options.interactive) {
(false, _, InteractiveMode::PromptProtected) => true,
(_, true, _) => prompt_yes!("remove write-protected directory {}?", path.quote()),
(_, _, InteractiveMode::Always) => prompt_yes!("remove directory {}?", path.quote()),
(_, _, _) => true,
}
}
#[cfg(not(windows))]
#[cfg(not(unix))]
fn handle_writable_directory(path: &Path, options: &Options, _metadata: &Metadata) -> bool {
if options.interactive == InteractiveMode::Always {
prompt_yes!("remove directory {}?", path.quote())
} else {
true
}
}
fn clean_trailing_slashes(path: &Path) -> &Path {
let path_str = os_str_as_bytes(path.as_os_str());
let dir_separator = MAIN_SEPARATOR as u8;
if let Ok(path_bytes) = path_str {
let mut idx = if path_bytes.len() > 1 {
path_bytes.len() - 1
} else {
return path;
};
if path_bytes[idx] == dir_separator {
for i in (1..path_bytes.len()).rev() {
if path_bytes[i - 1] != dir_separator {
idx = i;
break;
}
}
#[cfg(unix)]
return Path::new(OsStr::from_bytes(&path_bytes[0..=idx]));
#[cfg(not(unix))]
return Path::new(std::str::from_utf8(&path_bytes[0..=idx]).unwrap());
}
}
path
}
fn prompt_descend(path: &Path) -> bool {
prompt_yes!("descend into directory {}?", path.quote())
}
#[cfg(not(windows))]
fn is_symlink_dir(_metadata: &Metadata) -> bool {
false
}
#[cfg(windows)]
fn is_symlink_dir(metadata: &Metadata) -> bool {
use std::os::windows::prelude::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY;
metadata.file_type().is_symlink()
&& ((metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY) != 0)
}
mod tests {
#[test]
fn test_collapsible_slash_path() {
use std::path::Path;
use crate::clean_trailing_slashes;
let path = Path::new("/////");
assert_eq!(Path::new("/"), clean_trailing_slashes(path));
}
}