From 37d00ae89b912ba037a9b7052cae4a87ddfc362d Mon Sep 17 00:00:00 2001 From: Christopher Armstrong Date: Wed, 15 Oct 2025 12:25:45 -0400 Subject: [PATCH] cp: display symlink creation with --verbose cp: display removed files with --verbose --- src/uu/cp/locales/en-US.ftl | 1 + src/uu/cp/locales/fr-FR.ftl | 1 + src/uu/cp/src/copydir.rs | 12 ------- src/uu/cp/src/cp.rs | 26 +++++++++++---- tests/by-util/test_cp.rs | 64 +++++++++++++++++++++++++++++++++++-- 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl index bc39d6132..a0b95cf6c 100644 --- a/src/uu/cp/locales/en-US.ftl +++ b/src/uu/cp/locales/en-US.ftl @@ -108,6 +108,7 @@ cp-warning-source-specified-more-than-once = source { $file_type } { $source } s # Verbose and debug messages cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = skipped { $path } +cp-verbose-removed = removed { $path } cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload: { $offload }, reflink: { $reflink }, sparse detection: { $sparse } diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl index 2fea7cf4d..cc58eeab7 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -108,6 +108,7 @@ cp-warning-source-specified-more-than-once = { $file_type } source { $source } s # Messages verbeux et de débogage cp-verbose-copied = { $source } -> { $dest } cp-debug-skipped = { $path } ignoré +cp-verbose-removed = removed { $path } cp-verbose-created-directory = { $source } -> { $dest } cp-debug-copy-offload = copy offload : { $offload }, reflink : { $reflink }, sparse detection : { $sparse } diff --git a/src/uu/cp/src/copydir.rs b/src/uu/cp/src/copydir.rs index 7b4962297..bbd3aba62 100644 --- a/src/uu/cp/src/copydir.rs +++ b/src/uu/cp/src/copydir.rs @@ -29,7 +29,6 @@ use walkdir::{DirEntry, WalkDir}; use crate::{ CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes, copy_file, - copy_link, }; /// Ensure a Windows path starts with a `\\?`. @@ -259,17 +258,6 @@ fn copy_direntry( entry_is_dir_no_follow }; - // If the source is a symbolic link and the options tell us not to - // dereference the link, then copy the link object itself. - if source_is_symlink && !options.dereference { - return copy_link( - &entry.source_absolute, - &entry.local_to_target, - symlinked_files, - options, - ); - } - // If the source is a directory and the destination does not // exist, ... if source_is_dir && !entry.local_to_target.exists() { diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index dd0a4ebf6..c6c8789e2 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1937,7 +1937,6 @@ fn delete_dest_if_needed_and_allowed( let delete_dest = match options.overwrite { OverwriteMode::Clobber(cl) | OverwriteMode::Interactive(cl) => { match cl { - // FIXME: print that the file was removed if --verbose is enabled ClobberMode::Force => { // TODO // Using `readonly` here to check if `dest` needs to be deleted is not correct: @@ -1976,13 +1975,26 @@ fn delete_dest_if_needed_and_allowed( }; if delete_dest { - match fs::remove_file(dest) { - Ok(()) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => { - // target could have been deleted earlier (e.g. same-file with --remove-destination) + delete_path(dest, options) + } else { + Ok(()) + } +} + +fn delete_path(path: &Path, options: &Options) -> CopyResult<()> { + match fs::remove_file(path) { + Ok(()) => { + if options.verbose { + println!( + "{}", + translate!("cp-verbose-removed", "path" => path.quote()) + ); } - Err(err) => return Err(err.into()), } + Err(err) if err.kind() == io::ErrorKind::NotFound => { + // target could have been deleted earlier (e.g. same-file with --remove-destination) + } + Err(err) => return Err(err.into()), } Ok(()) @@ -2666,7 +2678,7 @@ fn copy_link( // we always need to remove the file to be able to create a symlink, // even if it is writeable. if dest.is_symlink() || dest.is_file() { - fs::remove_file(dest)?; + delete_path(dest, options)?; } symlink_file(&link, dest, symlinked_files)?; copy_attributes(source, dest, &options.attributes) diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 45691203f..aa91e4c4c 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -385,7 +385,6 @@ fn test_cp_arg_no_target_directory_with_recursive() { } #[test] -#[ignore = "disabled until https://github.com/uutils/coreutils/issues/7455 is fixed"] fn test_cp_arg_no_target_directory_with_recursive_target_does_not_exists() { let (at, mut ucmd) = at_and_ucmd!(); @@ -768,7 +767,7 @@ fn test_cp_f_i_verbose_non_writeable_destination_y() { .pipe_in("y") .succeeds() .stderr_is("cp: replace 'b', overriding mode 0000 (---------)? ") - .stdout_is("'a' -> 'b'\n"); + .stdout_is("removed 'b'\n'a' -> 'b'\n"); } #[test] @@ -7167,3 +7166,64 @@ fn test_cp_recurse_verbose_output() { .no_stderr() .stdout_is(output); } + +#[test] +fn test_cp_recurse_verbose_output_with_symlink() { + let source_dir = "source_dir"; + let target_dir = "target_dir"; + let file = "file"; + let symlink = "symlink"; + #[cfg(not(windows))] + let output = format!( + "'{source_dir}' -> '{target_dir}/'\n'{source_dir}/{symlink}' -> '{target_dir}/{symlink}'\n" + ); + #[cfg(windows)] + let output = format!( + "'{source_dir}' -> '{target_dir}\\'\n'{source_dir}\\{symlink}' -> '{target_dir}\\{symlink}'\n" + ); + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir(source_dir); + at.touch(file); + at.symlink_file(file, format!("{source_dir}/{symlink}").as_str()); + + ucmd.arg(source_dir) + .arg(target_dir) + .arg("-r") + .arg("--verbose") + .succeeds() + .no_stderr() + .stdout_is(output); +} + +#[test] +fn test_cp_recurse_verbose_output_with_symlink_already_exists() { + let source_dir = "source_dir"; + let target_dir = "target_dir"; + let file = "file"; + let symlink = "symlink"; + #[cfg(not(windows))] + let output = format!( + "removed '{target_dir}/{symlink}'\n'{source_dir}/{symlink}' -> '{target_dir}/{symlink}'\n" + ); + #[cfg(windows)] + let output = format!( + "removed '{target_dir}\\{symlink}'\n'{source_dir}\\{symlink}' -> '{target_dir}\\{symlink}'\n" + ); + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir(source_dir); + at.touch(file); + at.symlink_file(file, format!("{source_dir}/{symlink}").as_str()); + at.mkdir(target_dir); + at.symlink_file(file, format!("{target_dir}/{symlink}").as_str()); + + ucmd.arg(source_dir) + .arg(target_dir) + .arg("-r") + .arg("--verbose") + .arg("-T") + .succeeds() + .no_stderr() + .stdout_is(output); +}