Skip to content

Commit 30ccad6

Browse files
committed
rm: add the --progress option like with cp & mv
1 parent 08299c0 commit 30ccad6

File tree

7 files changed

+178
-15
lines changed

7 files changed

+178
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/extensions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ packages.
4040

4141
`mv` can display a progress bar when the `-g`/`--progress` flag is set.
4242

43+
## `rm`
44+
45+
`rm` can display a progress bar when the `-g`/`--progress` flag is set.
46+
4347
## `hashsum`
4448

4549
This utility does not exist in GNU coreutils. `hashsum` is a utility that

src/uu/rm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ thiserror = { workspace = true }
2222
clap = { workspace = true }
2323
uucore = { workspace = true, features = ["fs", "parser", "safe-traversal"] }
2424
fluent = { workspace = true }
25+
indicatif = { workspace = true }
2526

2627
[target.'cfg(unix)'.dependencies]
2728
libc = { workspace = true }

src/uu/rm/locales/en-US.ftl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ rm-help-preserve-root = do not remove '/' (default)
2828
rm-help-recursive = remove directories and their contents recursively
2929
rm-help-dir = remove empty directories
3030
rm-help-verbose = explain what is being done
31+
rm-help-progress = display a progress bar. Note: this feature is not supported by GNU coreutils.
32+
33+
# Progress messages
34+
rm-progress-removing = Removing
3135
3236
# Error messages
3337
rm-error-missing-operand = missing operand

src/uu/rm/locales/fr-FR.ftl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ rm-help-preserve-root = ne pas supprimer '/' (par défaut)
2828
rm-help-recursive = supprimer les répertoires et leur contenu récursivement
2929
rm-help-dir = supprimer les répertoires vides
3030
rm-help-verbose = expliquer ce qui est fait
31+
rm-help-progress = afficher une barre de progression. Note : cette fonctionnalité n'est pas supportée par GNU coreutils.
32+
33+
# Messages de progression
34+
rm-progress-removing = Suppression
3135
3236
# Messages d'erreur
3337
rm-error-missing-operand = opérande manquant

src/uu/rm/src/rm.rs

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
use clap::builder::{PossibleValue, ValueParser};
99
use clap::{Arg, ArgAction, Command, parser::ValueSource};
10+
use indicatif::{ProgressBar, ProgressStyle};
1011
use std::ffi::{OsStr, OsString};
1112
use std::fs::{self, Metadata};
12-
use std::io::{IsTerminal, stdin};
13+
use std::io::{self, IsTerminal, stdin};
1314
use std::ops::BitOr;
1415
#[cfg(unix)]
1516
use std::os::unix::ffi::OsStrExt;
@@ -108,6 +109,8 @@ pub struct Options {
108109
pub dir: bool,
109110
/// `-v`, `--verbose`
110111
pub verbose: bool,
112+
/// `-g`, `--progress`
113+
pub progress: bool,
111114
#[doc(hidden)]
112115
/// `---presume-input-tty`
113116
/// Always use `None`; GNU flag for testing use only
@@ -124,6 +127,7 @@ impl Default for Options {
124127
recursive: false,
125128
dir: false,
126129
verbose: false,
130+
progress: false,
127131
__presume_input_tty: None,
128132
}
129133
}
@@ -139,6 +143,7 @@ static OPT_PROMPT_ALWAYS: &str = "prompt-always";
139143
static OPT_PROMPT_ONCE: &str = "prompt-once";
140144
static OPT_RECURSIVE: &str = "recursive";
141145
static OPT_VERBOSE: &str = "verbose";
146+
static OPT_PROGRESS: &str = "progress";
142147
static PRESUME_INPUT_TTY: &str = "-presume-input-tty";
143148

144149
static ARG_FILES: &str = "files";
@@ -191,6 +196,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
191196
recursive: matches.get_flag(OPT_RECURSIVE),
192197
dir: matches.get_flag(OPT_DIR),
193198
verbose: matches.get_flag(OPT_VERBOSE),
199+
progress: matches.get_flag(OPT_PROGRESS),
194200
__presume_input_tty: if matches.get_flag(PRESUME_INPUT_TTY) {
195201
Some(true)
196202
} else {
@@ -309,6 +315,13 @@ pub fn uu_app() -> Command {
309315
.help(translate!("rm-help-verbose"))
310316
.action(ArgAction::SetTrue),
311317
)
318+
.arg(
319+
Arg::new(OPT_PROGRESS)
320+
.short('g')
321+
.long(OPT_PROGRESS)
322+
.help(translate!("rm-help-progress"))
323+
.action(ArgAction::SetTrue),
324+
)
312325
// From the GNU source code:
313326
// This is solely for testing.
314327
// Do not document.
@@ -333,6 +346,63 @@ pub fn uu_app() -> Command {
333346
)
334347
}
335348

349+
/// Count the total number of files and directories to be deleted.
350+
/// This function recursively counts all files and directories that will be processed.
351+
/// Files are not deduplicated when appearing in multiple sources. If `recursive` is set to `false`, the
352+
/// directories in `paths` will be ignored.
353+
fn count_files(paths: &[&OsStr], recursive: bool) -> io::Result<u64> {
354+
let mut total = 0;
355+
for p in paths {
356+
let path = Path::new(p);
357+
match fs::symlink_metadata(path) {
358+
Ok(md) => {
359+
if md.is_dir() && !is_symlink_dir(&md) {
360+
if recursive {
361+
total += count_files_in_directory(path)?;
362+
}
363+
} else {
364+
total += 1; // Count this file/symlink
365+
}
366+
}
367+
Err(_) => {
368+
// If we can't access the file, skip it for counting
369+
// This matches the behavior where -f suppresses errors for missing files
370+
}
371+
}
372+
}
373+
Ok(total)
374+
}
375+
376+
/// A helper for `count_files` specialized for directories.
377+
fn count_files_in_directory(p: &Path) -> io::Result<u64> {
378+
let mut total = 1; // Count the directory itself
379+
380+
match fs::read_dir(p) {
381+
Ok(entries) => {
382+
for entry in entries.flatten() {
383+
let path = entry.path();
384+
match entry.file_type() {
385+
Ok(file_type) => {
386+
if file_type.is_dir() {
387+
total += count_files_in_directory(&path)?;
388+
} else {
389+
total += 1; // Count this file
390+
}
391+
}
392+
Err(_) => {
393+
// Skip files we can't access
394+
}
395+
}
396+
}
397+
}
398+
Err(_) => {
399+
// Skip directories we can't read
400+
}
401+
}
402+
403+
Ok(total)
404+
}
405+
336406
// TODO: implement one-file-system (this may get partially implemented in walkdir)
337407
/// Remove (or unlink) the given files
338408
///
@@ -343,17 +413,35 @@ pub fn uu_app() -> Command {
343413
pub fn remove(files: &[&OsStr], options: &Options) -> bool {
344414
let mut had_err = false;
345415

416+
// Create progress bar if requested
417+
let progress_bar = if options.progress {
418+
count_files(files, options.recursive)
419+
.ok()
420+
.map(|total_files| {
421+
ProgressBar::new(total_files)
422+
.with_style(
423+
ProgressStyle::with_template(
424+
"{msg}: [{elapsed_precise}] {wide_bar} {pos:>7}/{len:7} files",
425+
)
426+
.unwrap(),
427+
)
428+
.with_message(translate!("rm-progress-removing"))
429+
})
430+
} else {
431+
None
432+
};
433+
346434
for filename in files {
347435
let file = Path::new(filename);
348436

349437
had_err = match file.symlink_metadata() {
350438
Ok(metadata) => {
351439
if metadata.is_dir() {
352-
handle_dir(file, options)
440+
handle_dir(file, options, progress_bar.as_ref())
353441
} else if is_symlink_dir(&metadata) {
354-
remove_dir(file, options)
442+
remove_dir(file, options, progress_bar.as_ref())
355443
} else {
356-
remove_file(file, options)
444+
remove_file(file, options, progress_bar.as_ref())
357445
}
358446
}
359447

@@ -376,6 +464,10 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
376464
.bitor(had_err);
377465
}
378466

467+
if let Some(pb) = progress_bar {
468+
pb.finish();
469+
}
470+
379471
had_err
380472
}
381473

@@ -482,7 +574,7 @@ fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options
482574
// Read directory entries using safe traversal
483575
let entries = match dir_fd.read_dir() {
484576
Ok(entries) => entries,
485-
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
577+
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
486578
// This is not considered an error - just like the original
487579
return false;
488580
}
@@ -570,15 +662,19 @@ fn safe_remove_dir_recursive_impl(path: &Path, dir_fd: &DirFd, options: &Options
570662
/// directory, remove all of its entries recursively and then remove the
571663
/// directory itself. In case of an error, print the error message to
572664
/// `stderr` and return `true`. If there were no errors, return `false`.
573-
fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
665+
fn remove_dir_recursive(
666+
path: &Path,
667+
options: &Options,
668+
progress_bar: Option<&ProgressBar>,
669+
) -> bool {
574670
// Base case 1: this is a file or a symbolic link.
575671
//
576672
// The symbolic link case is important because it could be a link to
577673
// a directory and we don't want to recurse. In particular, this
578674
// avoids an infinite recursion in the case of a link to the current
579675
// directory, like `ln -s . link`.
580676
if !path.is_dir() || path.is_symlink() {
581-
return remove_file(path, options);
677+
return remove_file(path, options, progress_bar);
582678
}
583679

584680
// Base case 2: this is a non-empty directory, but the user
@@ -622,7 +718,7 @@ fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
622718
// Recursive case: this is a directory.
623719
let mut error = false;
624720
match fs::read_dir(path) {
625-
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
721+
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
626722
// This is not considered an error.
627723
}
628724
Err(_) => error = true,
@@ -631,7 +727,8 @@ fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
631727
match entry {
632728
Err(_) => error = true,
633729
Ok(entry) => {
634-
let child_error = remove_dir_recursive(&entry.path(), options);
730+
let child_error =
731+
remove_dir_recursive(&entry.path(), options, progress_bar);
635732
error = error || child_error;
636733
}
637734
}
@@ -675,7 +772,7 @@ fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
675772
error
676773
}
677774

678-
fn handle_dir(path: &Path, options: &Options) -> bool {
775+
fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
679776
let mut had_err = false;
680777

681778
let path = clean_trailing_slashes(path);
@@ -689,9 +786,9 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
689786

690787
let is_root = path.has_root() && path.parent().is_none();
691788
if options.recursive && (!is_root || !options.preserve_root) {
692-
had_err = remove_dir_recursive(path, options);
789+
had_err = remove_dir_recursive(path, options, progress_bar);
693790
} else if options.dir && (!is_root || !options.preserve_root) {
694-
had_err = remove_dir(path, options).bitor(had_err);
791+
had_err = remove_dir(path, options, progress_bar).bitor(had_err);
695792
} else if options.recursive {
696793
show_error!("{}", RmError::DangerousRecursiveOperation);
697794
show_error!("{}", RmError::UseNoPreserveRoot);
@@ -710,7 +807,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool {
710807
/// Remove the given directory, asking the user for permission if necessary.
711808
///
712809
/// Returns true if it has encountered an error.
713-
fn remove_dir(path: &Path, options: &Options) -> bool {
810+
fn remove_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
714811
// Ask the user for permission.
715812
if !prompt_dir(path, options) {
716813
return false;
@@ -726,6 +823,11 @@ fn remove_dir(path: &Path, options: &Options) -> bool {
726823
}
727824

728825
// Try to remove the directory.
826+
// Update progress bar for directory removal
827+
if let Some(pb) = progress_bar {
828+
pb.inc(1);
829+
}
830+
729831
match fs::remove_dir(path) {
730832
Ok(_) => {
731833
if options.verbose {
@@ -745,8 +847,13 @@ fn remove_dir(path: &Path, options: &Options) -> bool {
745847
}
746848
}
747849

748-
fn remove_file(path: &Path, options: &Options) -> bool {
850+
fn remove_file(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
749851
if prompt_file(path, options) {
852+
// Update progress bar before removing the file
853+
if let Some(pb) = progress_bar {
854+
pb.inc(1);
855+
}
856+
750857
match fs::remove_file(path) {
751858
Ok(_) => {
752859
if options.verbose {
@@ -757,7 +864,7 @@ fn remove_file(path: &Path, options: &Options) -> bool {
757864
}
758865
}
759866
Err(e) => {
760-
if e.kind() == std::io::ErrorKind::PermissionDenied {
867+
if e.kind() == io::ErrorKind::PermissionDenied {
761868
// GNU compatibility (rm/fail-eacces.sh)
762869
show_error!(
763870
"{}",

tests/by-util/test_rm.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,3 +1078,45 @@ fn test_rm_recursive_long_path_safe_traversal() {
10781078
// Verify the directory is completely removed
10791079
assert!(!at.dir_exists("rm_deep"));
10801080
}
1081+
1082+
#[test]
1083+
fn test_progress_flag() {
1084+
let (at, mut ucmd) = at_and_ucmd!();
1085+
let file = "test_rm_progress_file";
1086+
1087+
at.touch(file);
1088+
1089+
// Test that -g/--progress flag is accepted
1090+
ucmd.arg("-g").arg(file).succeeds();
1091+
1092+
assert!(!at.file_exists(file));
1093+
}
1094+
1095+
#[test]
1096+
fn test_progress_with_recursive() {
1097+
let (at, mut ucmd) = at_and_ucmd!();
1098+
1099+
at.mkdir("test_dir");
1100+
at.touch("test_dir/file1");
1101+
at.touch("test_dir/file2");
1102+
at.mkdir("test_dir/subdir");
1103+
at.touch("test_dir/subdir/file3");
1104+
1105+
// Test progress with recursive removal
1106+
ucmd.arg("-rg").arg("test_dir").succeeds();
1107+
1108+
assert!(!at.dir_exists("test_dir"));
1109+
}
1110+
1111+
#[test]
1112+
fn test_progress_with_verbose() {
1113+
let (at, mut ucmd) = at_and_ucmd!();
1114+
let file = "test_rm_progress_verbose_file";
1115+
1116+
at.touch(file);
1117+
1118+
// Test that progress and verbose work together
1119+
ucmd.arg("-gv").arg(file).succeeds().stdout_contains(file);
1120+
1121+
assert!(!at.file_exists(file));
1122+
}

0 commit comments

Comments
 (0)