mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 19:47:45 +00:00
Merge pull request #1528 from sylvestre/cp-no-deref
feature(cp) implement -P/--no-deference
This commit is contained in:
commit
700d51a92b
2 changed files with 238 additions and 10 deletions
|
@ -31,10 +31,13 @@ use winapi::um::fileapi::CreateFileW;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use winapi::um::fileapi::GetFileInformationByHandle;
|
use winapi::um::fileapi::GetFileInformationByHandle;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches};
|
use clap::{App, Arg, ArgMatches};
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
use quick_error::ResultExt;
|
use quick_error::ResultExt;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::env;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
@ -53,6 +56,7 @@ use std::os::windows::ffi::OsStrExt;
|
||||||
use std::path::{Path, PathBuf, StripPrefixError};
|
use std::path::{Path, PathBuf, StripPrefixError};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
use uucore::fs::resolve_relative_path;
|
||||||
use uucore::fs::{canonicalize, CanonicalizeMode};
|
use uucore::fs::{canonicalize, CanonicalizeMode};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
@ -207,6 +211,7 @@ pub struct Options {
|
||||||
copy_contents: bool,
|
copy_contents: bool,
|
||||||
copy_mode: CopyMode,
|
copy_mode: CopyMode,
|
||||||
dereference: bool,
|
dereference: bool,
|
||||||
|
no_dereference: bool,
|
||||||
no_target_dir: bool,
|
no_target_dir: bool,
|
||||||
one_file_system: bool,
|
one_file_system: bool,
|
||||||
overwrite: OverwriteMode,
|
overwrite: OverwriteMode,
|
||||||
|
@ -414,6 +419,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.value_name("ATTR_LIST")
|
.value_name("ATTR_LIST")
|
||||||
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE])
|
.conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_ARCHIVE])
|
||||||
.help("don't preserve the specified attributes"))
|
.help("don't preserve the specified attributes"))
|
||||||
|
.arg(Arg::with_name(OPT_NO_DEREFERENCE)
|
||||||
|
.short("-P")
|
||||||
|
.long(OPT_NO_DEREFERENCE)
|
||||||
|
.conflicts_with(OPT_DEREFERENCE)
|
||||||
|
.help("never follow symbolic links in SOURCE"))
|
||||||
|
|
||||||
// TODO: implement the following args
|
// TODO: implement the following args
|
||||||
.arg(Arg::with_name(OPT_ARCHIVE)
|
.arg(Arg::with_name(OPT_ARCHIVE)
|
||||||
|
@ -433,11 +443,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.long(OPT_DEREFERENCE)
|
.long(OPT_DEREFERENCE)
|
||||||
.conflicts_with(OPT_NO_DEREFERENCE)
|
.conflicts_with(OPT_NO_DEREFERENCE)
|
||||||
.help("NotImplemented: always follow symbolic links in SOURCE"))
|
.help("NotImplemented: always follow symbolic links in SOURCE"))
|
||||||
.arg(Arg::with_name(OPT_NO_DEREFERENCE)
|
|
||||||
.short("-P")
|
|
||||||
.long(OPT_NO_DEREFERENCE)
|
|
||||||
.conflicts_with(OPT_DEREFERENCE)
|
|
||||||
.help("NotImplemented: never follow symbolic links in SOURCE"))
|
|
||||||
.arg(Arg::with_name(OPT_PARENTS)
|
.arg(Arg::with_name(OPT_PARENTS)
|
||||||
.long(OPT_PARENTS)
|
.long(OPT_PARENTS)
|
||||||
.help("NotImplemented: use full source file name under DIRECTORY"))
|
.help("NotImplemented: use full source file name under DIRECTORY"))
|
||||||
|
@ -565,7 +570,6 @@ impl Options {
|
||||||
OPT_COPY_CONTENTS,
|
OPT_COPY_CONTENTS,
|
||||||
OPT_NO_DEREFERENCE_PRESERVE_LINKS,
|
OPT_NO_DEREFERENCE_PRESERVE_LINKS,
|
||||||
OPT_DEREFERENCE,
|
OPT_DEREFERENCE,
|
||||||
OPT_NO_DEREFERENCE,
|
|
||||||
OPT_PARENTS,
|
OPT_PARENTS,
|
||||||
OPT_SPARSE,
|
OPT_SPARSE,
|
||||||
OPT_STRIP_TRAILING_SLASHES,
|
OPT_STRIP_TRAILING_SLASHES,
|
||||||
|
@ -627,6 +631,7 @@ impl Options {
|
||||||
copy_contents: matches.is_present(OPT_COPY_CONTENTS),
|
copy_contents: matches.is_present(OPT_COPY_CONTENTS),
|
||||||
copy_mode: CopyMode::from_matches(matches),
|
copy_mode: CopyMode::from_matches(matches),
|
||||||
dereference: matches.is_present(OPT_DEREFERENCE),
|
dereference: matches.is_present(OPT_DEREFERENCE),
|
||||||
|
no_dereference: matches.is_present(OPT_NO_DEREFERENCE),
|
||||||
one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM),
|
one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM),
|
||||||
overwrite: OverwriteMode::from_matches(matches),
|
overwrite: OverwriteMode::from_matches(matches),
|
||||||
parents: matches.is_present(OPT_PARENTS),
|
parents: matches.is_present(OPT_PARENTS),
|
||||||
|
@ -807,6 +812,7 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<()
|
||||||
let dest = construct_dest_path(source, target, &target_type, options)?;
|
let dest = construct_dest_path(source, target, &target_type, options)?;
|
||||||
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
|
preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_hard_link {
|
if !found_hard_link {
|
||||||
if let Err(error) = copy_source(source, target, &target_type, options) {
|
if let Err(error) = copy_source(source, target, &target_type, options) {
|
||||||
show_error!("{}", error);
|
show_error!("{}", error);
|
||||||
|
@ -866,6 +872,27 @@ fn copy_source(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn adjust_canonicalization<'a>(p: &'a Path) -> Cow<'a, Path> {
|
||||||
|
// In some cases, \\? can be missing on some Windows paths. Add it at the
|
||||||
|
// beginning unless the path is prefixed with a device namespace.
|
||||||
|
const VERBATIM_PREFIX: &str = r#"\\?"#;
|
||||||
|
const DEVICE_NS_PREFIX: &str = r#"\\."#;
|
||||||
|
|
||||||
|
let has_prefix = p
|
||||||
|
.components()
|
||||||
|
.next()
|
||||||
|
.and_then(|comp| comp.as_os_str().to_str())
|
||||||
|
.map(|p_str| p_str.starts_with(VERBATIM_PREFIX) || p_str.starts_with(DEVICE_NS_PREFIX))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if has_prefix {
|
||||||
|
p.into()
|
||||||
|
} else {
|
||||||
|
Path::new(VERBATIM_PREFIX).join(p).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the contents of the directory `root` and recursively copy the
|
/// Read the contents of the directory `root` and recursively copy the
|
||||||
/// contents to `target`.
|
/// contents to `target`.
|
||||||
///
|
///
|
||||||
|
@ -898,9 +925,35 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult
|
||||||
let mut hard_links: Vec<(String, u64)> = vec![];
|
let mut hard_links: Vec<(String, u64)> = vec![];
|
||||||
|
|
||||||
for path in WalkDir::new(root) {
|
for path in WalkDir::new(root) {
|
||||||
let path = or_continue!(or_continue!(path).path().canonicalize());
|
let p = or_continue!(path);
|
||||||
|
let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink();
|
||||||
|
let path = if options.no_dereference && is_symlink {
|
||||||
|
// we are dealing with a symlink. Don't follow it
|
||||||
|
match env::current_dir() {
|
||||||
|
Ok(cwd) => cwd.join(resolve_relative_path(p.path())),
|
||||||
|
Err(e) => crash!(1, "failed to get current directory {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
or_continue!(p.path().canonicalize())
|
||||||
|
};
|
||||||
|
|
||||||
let local_to_root_parent = match root_parent {
|
let local_to_root_parent = match root_parent {
|
||||||
Some(parent) => or_continue!(path.strip_prefix(&parent)).to_path_buf(),
|
Some(parent) => {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// On Windows, some pathes are starting with \\?
|
||||||
|
// but not always, so, make sure that we are consistent for strip_prefix
|
||||||
|
// See https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file for more info
|
||||||
|
let parent_can = adjust_canonicalization(parent);
|
||||||
|
let path_can = adjust_canonicalization(&path);
|
||||||
|
|
||||||
|
or_continue!(&path_can.strip_prefix(&parent_can)).to_path_buf()
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
or_continue!(path.strip_prefix(&parent)).to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
None => path.clone(),
|
None => path.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1070,7 +1123,6 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match options.copy_mode {
|
match options.copy_mode {
|
||||||
CopyMode::Link => {
|
CopyMode::Link => {
|
||||||
fs::hard_link(source, dest).context(&*context_for(source, dest))?;
|
fs::hard_link(source, dest).context(&*context_for(source, dest))?;
|
||||||
|
@ -1156,9 +1208,26 @@ fn copy_helper(source: &Path, dest: &Path, options: &Options) -> CopyResult<()>
|
||||||
ReflinkMode::Never => {}
|
ReflinkMode::Never => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if options.no_dereference && fs::symlink_metadata(&source)?.file_type().is_symlink() {
|
||||||
|
// Here, we will copy the symlink itself (actually, just recreate it)
|
||||||
|
let link = fs::read_link(&source)?;
|
||||||
|
let dest: Cow<'_, Path> = if dest.is_dir() {
|
||||||
|
match source.file_name() {
|
||||||
|
Some(name) => dest.join(name).into(),
|
||||||
|
None => crash!(
|
||||||
|
EXIT_ERR,
|
||||||
|
"cannot stat ‘{}’: No such file or directory",
|
||||||
|
source.display()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dest.into()
|
||||||
|
};
|
||||||
|
symlink_file(&link, &dest, &*context_for(&link, &dest))?;
|
||||||
} else {
|
} else {
|
||||||
fs::copy(source, dest).context(&*context_for(source, dest))?;
|
fs::copy(source, dest).context(&*context_for(source, dest))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,18 @@ use crate::common::util::*;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::fs::set_permissions;
|
use std::fs::set_permissions;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
use std::os::unix::fs;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::fs::symlink_file;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
use std::env;
|
||||||
|
|
||||||
static TEST_EXISTING_FILE: &str = "existing_file.txt";
|
static TEST_EXISTING_FILE: &str = "existing_file.txt";
|
||||||
static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt";
|
static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt";
|
||||||
|
static TEST_HELLO_WORLD_SOURCE_SYMLINK: &str = "hello_world.txt.link";
|
||||||
static TEST_HELLO_WORLD_DEST: &str = "copy_of_hello_world.txt";
|
static TEST_HELLO_WORLD_DEST: &str = "copy_of_hello_world.txt";
|
||||||
static TEST_HOW_ARE_YOU_SOURCE: &str = "how_are_you.txt";
|
static TEST_HOW_ARE_YOU_SOURCE: &str = "how_are_you.txt";
|
||||||
static TEST_HOW_ARE_YOU_DEST: &str = "hello_dir/how_are_you.txt";
|
static TEST_HOW_ARE_YOU_DEST: &str = "hello_dir/how_are_you.txt";
|
||||||
|
@ -11,7 +21,7 @@ static TEST_COPY_TO_FOLDER: &str = "hello_dir/";
|
||||||
static TEST_COPY_TO_FOLDER_FILE: &str = "hello_dir/hello_world.txt";
|
static TEST_COPY_TO_FOLDER_FILE: &str = "hello_dir/hello_world.txt";
|
||||||
static TEST_COPY_FROM_FOLDER: &str = "hello_dir_with_file/";
|
static TEST_COPY_FROM_FOLDER: &str = "hello_dir_with_file/";
|
||||||
static TEST_COPY_FROM_FOLDER_FILE: &str = "hello_dir_with_file/hello_world.txt";
|
static TEST_COPY_FROM_FOLDER_FILE: &str = "hello_dir_with_file/hello_world.txt";
|
||||||
static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new/";
|
static TEST_COPY_TO_FOLDER_NEW: &str = "hello_dir_new";
|
||||||
static TEST_COPY_TO_FOLDER_NEW_FILE: &str = "hello_dir_new/hello_world.txt";
|
static TEST_COPY_TO_FOLDER_NEW_FILE: &str = "hello_dir_new/hello_world.txt";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -328,3 +338,152 @@ fn test_cp_arg_suffix() {
|
||||||
"How are you?\n"
|
"How are you?\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_no_deref() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _r = fs::symlink(
|
||||||
|
TEST_HELLO_WORLD_SOURCE,
|
||||||
|
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
|
||||||
|
);
|
||||||
|
#[cfg(windows)]
|
||||||
|
let _r = symlink_file(
|
||||||
|
TEST_HELLO_WORLD_SOURCE,
|
||||||
|
at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK),
|
||||||
|
);
|
||||||
|
//using -P option
|
||||||
|
let result = scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("-P")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE_SYMLINK)
|
||||||
|
.arg(TEST_COPY_TO_FOLDER)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Check that the exit code represents a successful copy.
|
||||||
|
let exit_success = result.success;
|
||||||
|
assert!(exit_success);
|
||||||
|
let path_to_new_symlink = at
|
||||||
|
.subdir
|
||||||
|
.join(TEST_COPY_TO_FOLDER)
|
||||||
|
.join(TEST_HELLO_WORLD_SOURCE_SYMLINK);
|
||||||
|
assert!(at.is_symlink(
|
||||||
|
&path_to_new_symlink
|
||||||
|
.clone()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap()
|
||||||
|
));
|
||||||
|
// Check the content of the destination file that was copied.
|
||||||
|
assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n");
|
||||||
|
let path_to_check = path_to_new_symlink.to_str().unwrap();
|
||||||
|
assert_eq!(at.read(&path_to_check), "Hello, World!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// For now, disable the test on Windows. Symlinks aren't well support on Windows.
|
||||||
|
// It works on Unix for now and it works locally when run from a powershell
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn test_cp_no_deref_folder_to_folder() {
|
||||||
|
let scene = TestScenario::new(util_name!());
|
||||||
|
let at = &scene.fixtures;
|
||||||
|
|
||||||
|
let cwd = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER);
|
||||||
|
|
||||||
|
// Change the cwd to have a correct symlink
|
||||||
|
assert!(env::set_current_dir(&path_to_new_symlink).is_ok());
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _r = fs::symlink(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK);
|
||||||
|
#[cfg(windows)]
|
||||||
|
let _r = symlink_file(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK);
|
||||||
|
|
||||||
|
// Back to the initial cwd (breaks the other tests)
|
||||||
|
assert!(env::set_current_dir(&cwd).is_ok());
|
||||||
|
|
||||||
|
//using -P -R option
|
||||||
|
let result = scene
|
||||||
|
.ucmd()
|
||||||
|
.arg("-P")
|
||||||
|
.arg("-R")
|
||||||
|
.arg("-v")
|
||||||
|
.arg(TEST_COPY_FROM_FOLDER)
|
||||||
|
.arg(TEST_COPY_TO_FOLDER_NEW)
|
||||||
|
.run();
|
||||||
|
println!("cp output {}", result.stdout);
|
||||||
|
|
||||||
|
// Check that the exit code represents a successful copy.
|
||||||
|
let exit_success = result.success;
|
||||||
|
assert!(exit_success);
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let scene2 = TestScenario::new("ls");
|
||||||
|
let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run();
|
||||||
|
println!("ls source {}", result.stdout);
|
||||||
|
|
||||||
|
let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW);
|
||||||
|
|
||||||
|
let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run();
|
||||||
|
println!("ls dest {}", result.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// No action as this test is disabled but kept in case we want to
|
||||||
|
// try to make it work in the future.
|
||||||
|
let a = Command::new("cmd").args(&["/C", "dir"]).output();
|
||||||
|
println!("output {:#?}", a);
|
||||||
|
|
||||||
|
let a = Command::new("cmd")
|
||||||
|
.args(&["/C", "dir", &at.as_string()])
|
||||||
|
.output();
|
||||||
|
println!("output {:#?}", a);
|
||||||
|
|
||||||
|
let a = Command::new("cmd")
|
||||||
|
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
println!("output {:#?}", a);
|
||||||
|
|
||||||
|
let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER);
|
||||||
|
|
||||||
|
let a = Command::new("cmd")
|
||||||
|
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
println!("output {:#?}", a);
|
||||||
|
|
||||||
|
let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW);
|
||||||
|
|
||||||
|
let a = Command::new("cmd")
|
||||||
|
.args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
println!("output {:#?}", a);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_to_new_symlink = at
|
||||||
|
.subdir
|
||||||
|
.join(TEST_COPY_TO_FOLDER_NEW)
|
||||||
|
.join(TEST_HELLO_WORLD_SOURCE_SYMLINK);
|
||||||
|
assert!(at.is_symlink(
|
||||||
|
&path_to_new_symlink
|
||||||
|
.clone()
|
||||||
|
.into_os_string()
|
||||||
|
.into_string()
|
||||||
|
.unwrap()
|
||||||
|
));
|
||||||
|
|
||||||
|
let path_to_new = at.subdir.join(TEST_COPY_TO_FOLDER_NEW_FILE);
|
||||||
|
|
||||||
|
// Check the content of the destination file that was copied.
|
||||||
|
let path_to_check = path_to_new.to_str().unwrap();
|
||||||
|
assert_eq!(at.read(path_to_check), "Hello, World!\n");
|
||||||
|
|
||||||
|
// Check the content of the symlink
|
||||||
|
let path_to_check = path_to_new_symlink.to_str().unwrap();
|
||||||
|
assert_eq!(at.read(&path_to_check), "Hello, World!\n");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue