Skip to content

Commit

Permalink
cp: correct --verbose --parents output for dirs
Browse files Browse the repository at this point in the history
This commit corrects the behavior of `cp -r --parents --verbose` when
the source path is a directory, so that it prints the copied ancestor
directories. For example,

    $ mkdir -p a/b/c d
    $ cp -r --verbose --parents a/b/c d
    a -> d/a
    a/b -> d/a/b
    'a/b/c' -> 'd/a/b/c'
  • Loading branch information
jfinkels committed Dec 3, 2022
1 parent 171a8b3 commit 168837c
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 4 deletions.
70 changes: 66 additions & 4 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use uucore::uio_error;
use walkdir::{DirEntry, WalkDir};

use crate::{
copy_attributes, copy_file, copy_link, preserve_hardlinks, CopyResult, Error, Options,
TargetSlice,
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, preserve_hardlinks,
CopyResult, Error, Options, TargetSlice,
};

/// Ensure a Windows path starts with a `\\?`.
Expand Down Expand Up @@ -172,6 +172,27 @@ impl Entry {
}
}

/// Decide whether the given path ends with `/.`.
///
/// # Examples
///
/// ```rust,ignore
/// assert!(ends_with_slash_dot("/."));
/// assert!(ends_with_slash_dot("./."));
/// assert!(ends_with_slash_dot("a/."));
///
/// assert!(!ends_with_slash_dot("."));
/// assert!(!ends_with_slash_dot("./"));
/// assert!(!ends_with_slash_dot("a/.."));
/// ```
fn ends_with_slash_dot<P>(path: P) -> bool
where
P: AsRef<Path>,
{
// `path.ends_with(".")` does not seem to work
path.as_ref().display().to_string().ends_with("/.")
}

/// Copy a single entry during a directory traversal.
fn copy_direntry(
progress_bar: &Option<ProgressBar>,
Expand All @@ -196,7 +217,10 @@ fn copy_direntry(

// If the source is a directory and the destination does not
// exist, ...
if source_absolute.is_dir() && !local_to_target.exists() {
if source_absolute.is_dir()
&& !ends_with_slash_dot(&source_absolute)
&& !local_to_target.exists()
{
if target_is_file {
return Err("cannot overwrite non-directory with directory".into());
} else {
Expand All @@ -205,7 +229,10 @@ fn copy_direntry(
// `create_dir_all()` will have any benefit over
// `create_dir()`, since all the ancestor directories
// should have already been created.
fs::create_dir_all(local_to_target)?;
fs::create_dir_all(&local_to_target)?;
if options.verbose {
println!("{}", context_for(&source_relative, &local_to_target));
}
return Ok(());
}
}
Expand Down Expand Up @@ -324,6 +351,19 @@ pub(crate) fn copy_directory(
if let Some(parent) = root.parent() {
let new_target = target.join(parent);
std::fs::create_dir_all(&new_target)?;

if options.verbose {
// For example, if copying file `a/b/c` and its parents
// to directory `d/`, then print
//
// a -> d/a
// a/b -> d/a/b
//
for (x, y) in aligned_ancestors(root, &target.join(root)) {
println!("{} -> {}", x.display(), y.display());
}
}

new_target
} else {
target.to_path_buf()
Expand Down Expand Up @@ -393,3 +433,25 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {

Ok(pathbuf1.starts_with(pathbuf2))
}

#[cfg(test)]
mod tests {
use super::ends_with_slash_dot;

#[test]
fn test_ends_with_slash_dot() {
assert!(ends_with_slash_dot("/."));
assert!(ends_with_slash_dot("./."));
assert!(ends_with_slash_dot("../."));
assert!(ends_with_slash_dot("a/."));
assert!(ends_with_slash_dot("/a/."));

assert!(!ends_with_slash_dot(""));
assert!(!ends_with_slash_dot("."));
assert!(!ends_with_slash_dot("./"));
assert!(!ends_with_slash_dot(".."));
assert!(!ends_with_slash_dot("/.."));
assert!(!ends_with_slash_dot("a/.."));
assert!(!ends_with_slash_dot("/a/.."));
}
}
43 changes: 43 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,36 @@ fn test_cp_parents_2_link() {
assert!(at.file_exists("d/a/link/c"));
}

#[test]
fn test_cp_parents_2_dir() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir_all("a/b/c");
at.mkdir("d");
#[cfg(not(windows))]
let expected_stdout = "a -> d/a\na/b -> d/a/b\n'a/b/c' -> 'd/a/b/c'\n";
#[cfg(windows)]
let expected_stdout = "a -> d\\a\na/b -> d\\a/b\n'a/b/c' -> 'd\\a/b\\c'\n";
ucmd.args(&["--verbose", "-r", "--parents", "a/b/c", "d"])
.succeeds()
.stdout_only(expected_stdout);
assert!(at.dir_exists("d/a/b/c"));
}

#[test]
fn test_cp_parents_2_deep_dir() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir_all("a/b/c");
at.mkdir_all("d/e");
#[cfg(not(windows))]
let expected_stdout = "a -> d/e/a\na/b -> d/e/a/b\n'a/b/c' -> 'd/e/a/b/c'\n";
#[cfg(windows)]
let expected_stdout = "a -> d/e\\a\na/b -> d/e\\a/b\n'a/b/c' -> 'd/e\\a/b\\c'\n";
ucmd.args(&["--verbose", "-r", "--parents", "a/b/c", "d/e"])
.succeeds()
.stdout_only(expected_stdout);
assert!(at.dir_exists("d/e/a/b/c"));
}

#[test]
fn test_cp_copy_symlink_contents_recursive() {
let (at, mut ucmd) = at_and_ucmd!();
Expand Down Expand Up @@ -2379,3 +2409,16 @@ fn test_preserve_hardlink_attributes_in_directory() {
at.metadata("dest/src/link").ino()
);
}

#[test]
fn test_src_base_dot() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("x");
at.mkdir("y");
ucmd.current_dir("y")
.args(&["--verbose", "-r", "../x/.", "."])
.succeeds()
.no_stderr()
.no_stdout();
assert!(!at.dir_exists("y/x"));
}
16 changes: 16 additions & 0 deletions tests/common/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,9 @@ pub struct UCommand {
limits: Vec<(rlimit::Resource, u64, u64)>,
stderr_to_stdout: bool,
tmpd: Option<Rc<TempDir>>, // drop last

/// If set, this field sets the current working directory of the child.
working_dir: Option<String>,
}

impl UCommand {
Expand Down Expand Up @@ -998,6 +1001,7 @@ impl UCommand {
#[cfg(any(target_os = "linux", target_os = "android"))]
limits: vec![],
stderr_to_stdout: false,
working_dir: None,
};

if let Some(un) = util_name {
Expand Down Expand Up @@ -1114,20 +1118,31 @@ impl UCommand {
self
}

pub fn current_dir(&mut self, dir: &str) -> &mut Self {
self.working_dir = Some(self.get_full_fixture_path(dir));
self
}

/// Spawns the command, feeds the stdin if any, and returns the
/// child process immediately.
pub fn run_no_wait(&mut self) -> UChild {
assert!(!self.has_run, "{}", ALREADY_RUN);
self.has_run = true;
log_info("run", &self.comm_string);

let current_dir = match &self.working_dir {
Some(dir) => dir,
None => self.tmpd.as_ref().unwrap().path().to_str().unwrap(),
};

let mut captured_stdout = None;
let mut captured_stderr = None;
let command = if self.stderr_to_stdout {
let mut output = CapturedOutput::default();

let command = self
.raw
.current_dir(current_dir)
// TODO: use Stdio::null() as default to avoid accidental deadlocks ?
.stdin(self.stdin.take().unwrap_or_else(Stdio::piped))
.stdout(Stdio::from(output.try_clone().unwrap()))
Expand Down Expand Up @@ -1155,6 +1170,7 @@ impl UCommand {
};

self.raw
.current_dir(current_dir)
// TODO: use Stdio::null() as default to avoid accidental deadlocks ?
.stdin(self.stdin.take().unwrap_or_else(Stdio::piped))
.stdout(stdout)
Expand Down

0 comments on commit 168837c

Please sign in to comment.