mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
cp: implement backup control with tests
This commit is contained in:
parent
7240b12895
commit
a8a1ec7faf
2 changed files with 247 additions and 27 deletions
|
@ -47,6 +47,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::backup_control::{self, BackupMode};
|
||||||
use uucore::fs::resolve_relative_path;
|
use uucore::fs::resolve_relative_path;
|
||||||
use uucore::fs::{canonicalize, CanonicalizeMode};
|
use uucore::fs::{canonicalize, CanonicalizeMode};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
@ -169,14 +170,6 @@ pub enum TargetType {
|
||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
|
||||||
pub enum BackupMode {
|
|
||||||
ExistingBackup,
|
|
||||||
NoBackup,
|
|
||||||
NumberedBackup,
|
|
||||||
SimpleBackup,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum CopyMode {
|
pub enum CopyMode {
|
||||||
Link,
|
Link,
|
||||||
SymLink,
|
SymLink,
|
||||||
|
@ -201,7 +194,7 @@ pub enum Attribute {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Options {
|
pub struct Options {
|
||||||
attributes_only: bool,
|
attributes_only: bool,
|
||||||
backup: bool,
|
backup: BackupMode,
|
||||||
copy_contents: bool,
|
copy_contents: bool,
|
||||||
copy_mode: CopyMode,
|
copy_mode: CopyMode,
|
||||||
dereference: bool,
|
dereference: bool,
|
||||||
|
@ -222,6 +215,7 @@ pub struct Options {
|
||||||
|
|
||||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
|
static ABOUT: &str = "Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.";
|
||||||
|
static LONG_HELP: &str = "";
|
||||||
static EXIT_OK: i32 = 0;
|
static EXIT_OK: i32 = 0;
|
||||||
static EXIT_ERR: i32 = 1;
|
static EXIT_ERR: i32 = 1;
|
||||||
|
|
||||||
|
@ -301,6 +295,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let matches = App::new(executable!())
|
let matches = App::new(executable!())
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
|
.after_help(&*format!("{}\n{}", LONG_HELP, backup_control::BACKUP_CONTROL_LONG_HELP))
|
||||||
.usage(&usage[..])
|
.usage(&usage[..])
|
||||||
.arg(Arg::with_name(OPT_TARGET_DIRECTORY)
|
.arg(Arg::with_name(OPT_TARGET_DIRECTORY)
|
||||||
.short("t")
|
.short("t")
|
||||||
|
@ -364,12 +359,17 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
.arg(Arg::with_name(OPT_BACKUP)
|
.arg(Arg::with_name(OPT_BACKUP)
|
||||||
.short("b")
|
.short("b")
|
||||||
.long(OPT_BACKUP)
|
.long(OPT_BACKUP)
|
||||||
.help("make a backup of each existing destination file"))
|
.help("make a backup of each existing destination file")
|
||||||
|
.takes_value(true)
|
||||||
|
.require_equals(true)
|
||||||
|
.min_values(0)
|
||||||
|
.possible_values(backup_control::BACKUP_CONTROL_VALUES)
|
||||||
|
.value_name("CONTROL")
|
||||||
|
)
|
||||||
.arg(Arg::with_name(OPT_SUFFIX)
|
.arg(Arg::with_name(OPT_SUFFIX)
|
||||||
.short("S")
|
.short("S")
|
||||||
.long(OPT_SUFFIX)
|
.long(OPT_SUFFIX)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.default_value("~")
|
|
||||||
.value_name("SUFFIX")
|
.value_name("SUFFIX")
|
||||||
.help("override the usual backup suffix"))
|
.help("override the usual backup suffix"))
|
||||||
.arg(Arg::with_name(OPT_UPDATE)
|
.arg(Arg::with_name(OPT_UPDATE)
|
||||||
|
@ -585,7 +585,24 @@ impl Options {
|
||||||
|| matches.is_present(OPT_RECURSIVE_ALIAS)
|
|| matches.is_present(OPT_RECURSIVE_ALIAS)
|
||||||
|| matches.is_present(OPT_ARCHIVE);
|
|| matches.is_present(OPT_ARCHIVE);
|
||||||
|
|
||||||
let backup = matches.is_present(OPT_BACKUP) || (matches.occurrences_of(OPT_SUFFIX) > 0);
|
let backup_mode = backup_control::determine_backup_mode(
|
||||||
|
matches.is_present(OPT_BACKUP),
|
||||||
|
matches.value_of(OPT_BACKUP),
|
||||||
|
);
|
||||||
|
let backup_suffix = backup_control::determine_backup_suffix(matches.value_of(OPT_SUFFIX));
|
||||||
|
|
||||||
|
let overwrite = OverwriteMode::from_matches(matches);
|
||||||
|
|
||||||
|
if overwrite == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup {
|
||||||
|
show_error!(
|
||||||
|
"options --backup and --no-clobber are mutually exclusive\n\
|
||||||
|
Try '{} --help' for more information.",
|
||||||
|
executable!()
|
||||||
|
);
|
||||||
|
return Err(Error::Error(
|
||||||
|
"options --backup and --no-clobber are mutually exclusive".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Parse target directory options
|
// Parse target directory options
|
||||||
let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY);
|
let no_target_dir = matches.is_present(OPT_NO_TARGET_DIRECTORY);
|
||||||
|
@ -631,9 +648,7 @@ impl Options {
|
||||||
|| matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
|
|| matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS)
|
||||||
|| matches.is_present(OPT_ARCHIVE),
|
|| matches.is_present(OPT_ARCHIVE),
|
||||||
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),
|
|
||||||
parents: matches.is_present(OPT_PARENTS),
|
parents: matches.is_present(OPT_PARENTS),
|
||||||
backup_suffix: matches.value_of(OPT_SUFFIX).unwrap().to_string(),
|
|
||||||
update: matches.is_present(OPT_UPDATE),
|
update: matches.is_present(OPT_UPDATE),
|
||||||
verbose: matches.is_present(OPT_VERBOSE),
|
verbose: matches.is_present(OPT_VERBOSE),
|
||||||
strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES),
|
strip_trailing_slashes: matches.is_present(OPT_STRIP_TRAILING_SLASHES),
|
||||||
|
@ -654,7 +669,9 @@ impl Options {
|
||||||
ReflinkMode::Never
|
ReflinkMode::Never
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
backup,
|
backup: backup_mode,
|
||||||
|
backup_suffix: backup_suffix,
|
||||||
|
overwrite: overwrite,
|
||||||
no_target_dir,
|
no_target_dir,
|
||||||
preserve_attributes,
|
preserve_attributes,
|
||||||
recursive,
|
recursive,
|
||||||
|
@ -1090,14 +1107,10 @@ fn context_for(src: &Path, dest: &Path) -> String {
|
||||||
format!("'{}' -> '{}'", src.display(), dest.display())
|
format!("'{}' -> '{}'", src.display(), dest.display())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements a relatively naive backup that is not as full featured
|
/// Implements a simple backup copy for the destination file.
|
||||||
/// as GNU cp. No CONTROL version control method argument is taken
|
/// TODO: for the backup, should this function be replaced by `copy_file(...)`?
|
||||||
/// for backups.
|
fn backup_dest(dest: &Path, backup_path: &PathBuf) -> CopyResult<PathBuf> {
|
||||||
/// TODO: Add version control methods
|
fs::copy(dest, &backup_path)?;
|
||||||
fn backup_file(path: &Path, suffix: &str) -> CopyResult<PathBuf> {
|
|
||||||
let mut backup_path = path.to_path_buf().into_os_string();
|
|
||||||
backup_path.push(suffix);
|
|
||||||
fs::copy(path, &backup_path)?;
|
|
||||||
Ok(backup_path.into())
|
Ok(backup_path.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1108,8 +1121,9 @@ fn handle_existing_dest(source: &Path, dest: &Path, options: &Options) -> CopyRe
|
||||||
|
|
||||||
options.overwrite.verify(dest)?;
|
options.overwrite.verify(dest)?;
|
||||||
|
|
||||||
if options.backup {
|
let backup_path = backup_control::get_backup_path(options.backup, dest, &options.backup_suffix);
|
||||||
backup_file(dest, &options.backup_suffix)?;
|
if let Some(backup_path) = backup_path {
|
||||||
|
backup_dest(dest, &backup_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
match options.overwrite {
|
match options.overwrite {
|
||||||
|
|
|
@ -214,8 +214,8 @@ fn test_cp_arg_symlink() {
|
||||||
fn test_cp_arg_no_clobber() {
|
fn test_cp_arg_no_clobber() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
.arg("--no-clobber")
|
|
||||||
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.arg("--no-clobber")
|
||||||
.succeeds();
|
.succeeds();
|
||||||
|
|
||||||
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n");
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n");
|
||||||
|
@ -305,7 +305,23 @@ fn test_cp_arg_backup() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
.arg("--backup")
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.arg("-b")
|
||||||
|
.succeeds();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_arg_backup_arg_first() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
.succeeds();
|
.succeeds();
|
||||||
|
|
||||||
|
@ -321,6 +337,7 @@ fn test_cp_arg_suffix() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg("-b")
|
||||||
.arg("--suffix")
|
.arg("--suffix")
|
||||||
.arg(".bak")
|
.arg(".bak")
|
||||||
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
@ -333,6 +350,195 @@ fn test_cp_arg_suffix() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_custom_backup_suffix_via_env() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let suffix = "super-suffix-of-the-century";
|
||||||
|
|
||||||
|
ucmd.arg("-b")
|
||||||
|
.env("SIMPLE_BACKUP_SUFFIX", suffix)
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}{}", TEST_HOW_ARE_YOU_SOURCE, suffix)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_numbered_with_t() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=t")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_numbered() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=numbered")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_existing() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=existing")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_nil() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=nil")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_numbered_if_existing_backup_existing() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let existing_backup = &*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE);
|
||||||
|
at.touch(existing_backup);
|
||||||
|
|
||||||
|
ucmd.arg("--backup=existing")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert!(at.file_exists(TEST_HOW_ARE_YOU_SOURCE));
|
||||||
|
assert!(at.file_exists(existing_backup));
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}.~2~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_numbered_if_existing_backup_nil() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let existing_backup = &*format!("{}.~1~", TEST_HOW_ARE_YOU_SOURCE);
|
||||||
|
|
||||||
|
at.touch(existing_backup);
|
||||||
|
ucmd.arg("--backup=nil")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert!(at.file_exists(TEST_HOW_ARE_YOU_SOURCE));
|
||||||
|
assert!(at.file_exists(existing_backup));
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}.~2~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_simple() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=simple")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_never() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=never")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert_eq!(
|
||||||
|
at.read(&*format!("{}~", TEST_HOW_ARE_YOU_SOURCE)),
|
||||||
|
"How are you?\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_none() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=none")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert!(!at.file_exists(&format!("{}~", TEST_HOW_ARE_YOU_SOURCE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cp_backup_off() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
ucmd.arg("--backup=off")
|
||||||
|
.arg(TEST_HELLO_WORLD_SOURCE)
|
||||||
|
.arg(TEST_HOW_ARE_YOU_SOURCE)
|
||||||
|
.succeeds()
|
||||||
|
.no_stderr();
|
||||||
|
|
||||||
|
assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n");
|
||||||
|
assert!(!at.file_exists(&format!("{}~", TEST_HOW_ARE_YOU_SOURCE)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cp_deref_conflicting_options() {
|
fn test_cp_deref_conflicting_options() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue