diff --git a/src/uu/cp/cp.md b/src/uu/cp/cp.md index 5f3cabc18b5..7485340f2ac 100644 --- a/src/uu/cp/cp.md +++ b/src/uu/cp/cp.md @@ -7,3 +7,19 @@ cp [OPTION]... -t DIRECTORY SOURCE... ``` Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. + +## After Help + +Do not copy a non-directory that has an existing destination with the same or newer modification timestamp; +instead, silently skip the file without failing. If timestamps are being preserved, the comparison is to the +source timestamp truncated to the resolutions of the destination file system and of the system calls used to +update timestamps; this avoids duplicate work if several `cp -pu` commands are executed with the same source +and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. Also, if +`--preserve=links` is also specified (like with `cp -au` for example), that will take precedence; consequently, +depending on the order that files are processed from the source, newer files in the destination may be replaced, +to mirror hard links in the source. which gives more control over which existing files in the destination are +replaced, and its value can be one of the following: + +* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. +* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. +* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3ccff03036e..fc21fa26d8d 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -40,7 +40,10 @@ use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError}; use uucore::fs::{ canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode, }; -use uucore::{crash, format_usage, help_about, help_usage, prompt_yes, show_error, show_warning}; +use uucore::update_control::{self, UpdateMode}; +use uucore::{ + crash, format_usage, help_about, help_section, help_usage, prompt_yes, show_error, show_warning, +}; use crate::copydir::copy_directory; @@ -224,13 +227,14 @@ pub struct Options { recursive: bool, backup_suffix: String, target_dir: Option, - update: bool, + update: UpdateMode, verbose: bool, progress_bar: bool, } const ABOUT: &str = help_about!("cp.md"); const USAGE: &str = help_usage!("cp.md"); +const AFTER_HELP: &str = help_section!("after help", "cp.md"); static EXIT_ERR: i32 = 1; @@ -264,7 +268,6 @@ mod options { pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; pub const SYMBOLIC_LINK: &str = "symbolic-link"; pub const TARGET_DIRECTORY: &str = "target-directory"; - pub const UPDATE: &str = "update"; pub const VERBOSE: &str = "verbose"; } @@ -295,6 +298,7 @@ pub fn uu_app() -> Command { .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .after_help(AFTER_HELP) .infer_long_args(true) .arg( Arg::new(options::TARGET_DIRECTORY) @@ -393,16 +397,8 @@ pub fn uu_app() -> Command { .arg(backup_control::arguments::backup()) .arg(backup_control::arguments::backup_no_args()) .arg(backup_control::arguments::suffix()) - .arg( - Arg::new(options::UPDATE) - .short('u') - .long(options::UPDATE) - .help( - "copy only when the SOURCE file is newer than the destination file \ - or when the destination file is missing", - ) - .action(ArgAction::SetTrue), - ) + .arg(update_control::arguments::update()) + .arg(update_control::arguments::update_no_args()) .arg( Arg::new(options::REFLINK) .long(options::REFLINK) @@ -641,7 +637,11 @@ impl CopyMode { Self::Link } else if matches.get_flag(options::SYMBOLIC_LINK) { Self::SymLink - } else if matches.get_flag(options::UPDATE) { + } else if matches + .get_one::(update_control::arguments::OPT_UPDATE) + .is_some() + || matches.get_flag(update_control::arguments::OPT_UPDATE_NO_ARG) + { Self::Update } else if matches.get_flag(options::ATTRIBUTES_ONLY) { Self::AttrOnly @@ -749,6 +749,7 @@ impl Options { Err(e) => return Err(Error::Backup(format!("{e}"))), Ok(mode) => mode, }; + let update_mode = update_control::determine_update_mode(matches); let backup_suffix = backup_control::determine_backup_suffix(matches); @@ -826,7 +827,7 @@ impl Options { || matches.get_flag(options::DEREFERENCE), one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM), parents: matches.get_flag(options::PARENTS), - update: matches.get_flag(options::UPDATE), + update: update_mode, verbose: matches.get_flag(options::VERBOSE), strip_trailing_slashes: matches.get_flag(options::STRIP_TRAILING_SLASHES), reflink_mode: { @@ -1473,7 +1474,9 @@ fn copy_file( symlinked_files: &mut HashSet, source_in_command_line: bool, ) -> CopyResult<()> { - if options.update && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) { + if (options.update == UpdateMode::ReplaceIfOlder || options.update == UpdateMode::ReplaceNone) + && options.overwrite == OverwriteMode::Interactive(ClobberMode::Standard) + { // `cp -i --update old new` when `new` exists doesn't copy anything // and exit with 0 return Ok(()); @@ -1630,22 +1633,38 @@ fn copy_file( } CopyMode::Update => { if dest.exists() { - let dest_metadata = fs::symlink_metadata(dest)?; - - let src_time = source_metadata.modified()?; - let dest_time = dest_metadata.modified()?; - if src_time <= dest_time { - return Ok(()); - } else { - copy_helper( - source, - dest, - options, - context, - source_is_symlink, - source_is_fifo, - symlinked_files, - )?; + match options.update { + update_control::UpdateMode::ReplaceAll => { + copy_helper( + source, + dest, + options, + context, + source_is_symlink, + source_is_fifo, + symlinked_files, + )?; + } + update_control::UpdateMode::ReplaceNone => return Ok(()), + update_control::UpdateMode::ReplaceIfOlder => { + let dest_metadata = fs::symlink_metadata(dest)?; + + let src_time = source_metadata.modified()?; + let dest_time = dest_metadata.modified()?; + if src_time <= dest_time { + return Ok(()); + } else { + copy_helper( + source, + dest, + options, + context, + source_is_symlink, + source_is_fifo, + symlinked_files, + )?; + } + } } } else { copy_helper( diff --git a/src/uu/mv/mv.md b/src/uu/mv/mv.md index 772e4bfaf4a..c31c6d07c40 100644 --- a/src/uu/mv/mv.md +++ b/src/uu/mv/mv.md @@ -5,5 +5,17 @@ mv [OPTION]... [-T] SOURCE DEST mv [OPTION]... SOURCE... DIRECTORY mv [OPTION]... -t DIRECTORY SOURCE... ``` - Move `SOURCE` to `DEST`, or multiple `SOURCE`(s) to `DIRECTORY`. + +## After Help + +Do not move a non-directory that has an existing destination with the same or newer modification timestamp; +instead, silently skip the file without failing. If the move is across file system boundaries, the comparison is +to the source timestamp truncated to the resolutions of the destination file system and of the system calls used +to update timestamps; this avoids duplicate work if several `mv -u` commands are executed with the same source +and destination. This option is ignored if the `-n` or `--no-clobber` option is also specified. which gives more control +over which existing files in the destination are replaced, and its value can be one of the following: + +* `all` This is the default operation when an `--update` option is not specified, and results in all existing files in the destination being replaced. +* `none` This is similar to the `--no-clobber` option, in that no files in the destination are replaced, but also skipping a file does not induce a failure. +* `older` This is the default operation when `--update` is specified, and results in files being replaced if they’re older than the corresponding source file. diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index d583a133821..1a7897a91a6 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -25,7 +25,8 @@ use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; use uucore::display::Quotable; use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError}; -use uucore::{format_usage, help_about, help_usage, prompt_yes, show}; +use uucore::update_control::{self, UpdateMode}; +use uucore::{format_usage, help_about, help_section, help_usage, prompt_yes, show}; use fs_extra::dir::{ get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions, @@ -38,7 +39,7 @@ pub struct Behavior { overwrite: OverwriteMode, backup: BackupMode, suffix: String, - update: bool, + update: UpdateMode, target_dir: Option, no_target_dir: bool, verbose: bool, @@ -55,6 +56,7 @@ pub enum OverwriteMode { const ABOUT: &str = help_about!("mv.md"); const USAGE: &str = help_usage!("mv.md"); +const AFTER_HELP: &str = help_section!("after help", "mv.md"); static OPT_FORCE: &str = "force"; static OPT_INTERACTIVE: &str = "interactive"; @@ -62,7 +64,6 @@ static OPT_NO_CLOBBER: &str = "no-clobber"; static OPT_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes"; static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; -static OPT_UPDATE: &str = "update"; static OPT_VERBOSE: &str = "verbose"; static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; @@ -96,6 +97,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let overwrite_mode = determine_overwrite_mode(&matches); let backup_mode = backup_control::determine_backup_mode(&matches)?; + let update_mode = update_control::determine_update_mode(&matches); if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup { return Err(UUsageError::new( @@ -120,7 +122,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { overwrite: overwrite_mode, backup: backup_mode, suffix: backup_suffix, - update: matches.get_flag(OPT_UPDATE), + update: update_mode, target_dir, no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY), verbose: matches.get_flag(OPT_VERBOSE), @@ -136,9 +138,8 @@ pub fn uu_app() -> Command { .version(crate_version!()) .about(ABOUT) .override_usage(format_usage(USAGE)) + .after_help(AFTER_HELP) .infer_long_args(true) - .arg(backup_control::arguments::backup()) - .arg(backup_control::arguments::backup_no_args()) .arg( Arg::new(OPT_FORCE) .short('f') @@ -166,7 +167,11 @@ pub fn uu_app() -> Command { .help("remove any trailing slashes from each SOURCE argument") .action(ArgAction::SetTrue), ) + .arg(backup_control::arguments::backup()) + .arg(backup_control::arguments::backup_no_args()) .arg(backup_control::arguments::suffix()) + .arg(update_control::arguments::update()) + .arg(update_control::arguments::update_no_args()) .arg( Arg::new(OPT_TARGET_DIRECTORY) .short('t') @@ -184,16 +189,6 @@ pub fn uu_app() -> Command { .help("treat DEST as a normal file") .action(ArgAction::SetTrue), ) - .arg( - Arg::new(OPT_UPDATE) - .short('u') - .long(OPT_UPDATE) - .help( - "move only when the SOURCE file is newer than the destination file \ - or when the destination file is missing", - ) - .action(ArgAction::SetTrue), - ) .arg( Arg::new(OPT_VERBOSE) .short('v') @@ -420,12 +415,24 @@ fn rename( let mut backup_path = None; if to.exists() { - if b.update && b.overwrite == OverwriteMode::Interactive { + if (b.update == UpdateMode::ReplaceIfOlder || b.update == UpdateMode::ReplaceNone) + && b.overwrite == OverwriteMode::Interactive + { // `mv -i --update old new` when `new` exists doesn't move anything // and exit with 0 return Ok(()); } + if b.update == UpdateMode::ReplaceNone { + return Ok(()); + } + + if (b.update == UpdateMode::ReplaceIfOlder) + && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? + { + return Ok(()); + } + match b.overwrite { OverwriteMode::NoClobber => { return Err(io::Error::new( @@ -445,10 +452,6 @@ fn rename( if let Some(ref backup_path) = backup_path { rename_with_fallback(to, backup_path, multi_progress)?; } - - if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? { - return Ok(()); - } } // "to" may no longer exist if it was backed up diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 00162ddbba5..e76e540c8d8 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -26,6 +26,7 @@ pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::quoting_style; pub use crate::mods::ranges; +pub use crate::mods::update_control; pub use crate::mods::version_cmp; // * string parsing modules diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 4b6c53f9531..71d288c69a5 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -6,6 +6,7 @@ pub mod error; pub mod os; pub mod panic; pub mod ranges; +pub mod update_control; pub mod version_cmp; // dir and vdir also need access to the quoting_style module pub mod quoting_style; diff --git a/src/uucore/src/lib/mods/update_control.rs b/src/uucore/src/lib/mods/update_control.rs new file mode 100644 index 00000000000..e46afd18522 --- /dev/null +++ b/src/uucore/src/lib/mods/update_control.rs @@ -0,0 +1,139 @@ +// This file is part of the uutils coreutils package. +// +// (c) John Shin +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Implement GNU-style update functionality. +//! +//! - pre-defined [`clap`-Arguments][1] for inclusion in utilities that +//! implement updates +//! - determination of the [update mode][2] +//! +//! Update-functionality is implemented by the following utilities: +//! +//! - `cp` +//! - `mv` +//! +//! +//! [1]: arguments +//! [2]: `determine_update_mode()` +//! +//! +//! # Usage example +//! +//! ``` +//! #[macro_use] +//! extern crate uucore; +//! +//! use clap::{Command, Arg, ArgMatches}; +//! use uucore::update_control::{self, UpdateMode}; +//! +//! fn main() { +//! let matches = Command::new("command") +//! .arg(update_control::arguments::update()) +//! .arg(update_control::arguments::update_no_args()) +//! .get_matches_from(vec![ +//! "command", "--update=older" +//! ]); +//! +//! let update_mode = update_control::determine_update_mode(&matches); +//! +//! // handle cases +//! if update_mode == UpdateMode::ReplaceIfOlder { +//! // do +//! } else { +//! unreachable!() +//! } +//! } +//! ``` +use clap::ArgMatches; + +// Available update mode +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum UpdateMode { + // --update=`all`, `` + ReplaceAll, + // --update=`none` + ReplaceNone, + // --update=`older` + // -u + ReplaceIfOlder, +} + +pub mod arguments { + use clap::ArgAction; + + pub static OPT_UPDATE: &str = "update"; + pub static OPT_UPDATE_NO_ARG: &str = "u"; + + // `--update` argument, defaults to `older` if no values are provided + pub fn update() -> clap::Arg { + clap::Arg::new(OPT_UPDATE) + .long("update") + .help("move only when the SOURCE file is newer than the destination file or when the destination file is missing") + .value_parser(["none", "all", "older"]) + .num_args(0..=1) + .default_missing_value("older") + .require_equals(true) + .overrides_with("update") + .action(clap::ArgAction::Set) + } + + // `-u` argument + pub fn update_no_args() -> clap::Arg { + clap::Arg::new(OPT_UPDATE_NO_ARG) + .short('u') + .help("like --update but does not accept an argument") + .action(ArgAction::SetTrue) + } +} + +/// Determine the "mode" for the update operation to perform, if any. +/// +/// Parses the backup options and converts them to an instance of +/// `UpdateMode` for further processing. +/// +/// Takes [`clap::ArgMatches`] as argument which **must** contain the options +/// from [`arguments::update()`] or [`arguments::update_no_args()`]. Otherwise +/// the `ReplaceAll` mode is returned unconditionally. +/// +/// # Examples +/// +/// Here's how one would integrate the update mode determination into an +/// application. +/// +/// ``` +/// #[macro_use] +/// extern crate uucore; +/// use uucore::update_control::{self, UpdateMode}; +/// use clap::{Command, Arg, ArgMatches}; +/// +/// fn main() { +/// let matches = Command::new("command") +/// .arg(update_control::arguments::update()) +/// .arg(update_control::arguments::update_no_args()) +/// .get_matches_from(vec![ +/// "command", "--update=all" +/// ]); +/// +/// let update_mode = update_control::determine_update_mode(&matches); +/// assert_eq!(update_mode, UpdateMode::ReplaceAll) +/// } +pub fn determine_update_mode(matches: &ArgMatches) -> UpdateMode { + if let Some(mode) = matches.get_one::(arguments::OPT_UPDATE) { + match mode.as_str() { + "all" => UpdateMode::ReplaceAll, + "none" => UpdateMode::ReplaceNone, + "older" => UpdateMode::ReplaceIfOlder, + _ => unreachable!("other args restricted by clap"), + } + } else if matches.get_flag(arguments::OPT_UPDATE_NO_ARG) { + // short form of this option is equivalent to using --update=older + UpdateMode::ReplaceIfOlder + } else { + // no option was present + UpdateMode::ReplaceAll + } +} diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index b77ad474e6a..005efa14d1b 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -244,6 +244,192 @@ fn test_cp_arg_update_interactive_error() { .no_stdout(); } +#[test] +fn test_cp_arg_update_none() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "How are you?\n") +} + +#[test] +fn test_cp_arg_update_all() { + let (at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HOW_ARE_YOU_SOURCE) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!( + at.read(TEST_HOW_ARE_YOU_SOURCE), + at.read(TEST_HELLO_WORLD_SOURCE) + ) +} + +#[test] +fn test_cp_arg_update_older_dest_not_older_than_src() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_dest_not_older_file1"; + let new = "test_cp_arg_update_dest_not_older_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n") +} + +#[test] +fn test_cp_arg_update_older_dest_older_than_src() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_dest_older_file1"; + let new = "test_cp_arg_update_dest_older_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), "new content\n") +} + +#[test] +fn test_cp_arg_update_short_no_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_short_no_overwrite_file1"; + let new = "test_cp_arg_update_short_no_overwrite_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n") +} + +#[test] +fn test_cp_arg_update_short_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_short_overwrite_file1"; + let new = "test_cp_arg_update_short_overwrite_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), "new content\n") +} + +#[test] +fn test_cp_arg_update_none_then_all() { + // take last if multiple update args are supplied, + // update=all wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_none_then_all_file1"; + let new = "test_cp_arg_update_none_then_all_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=none") + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "old content\n") +} + +#[test] +fn test_cp_arg_update_all_then_none() { + // take last if multiple update args are supplied, + // update=none wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_cp_arg_update_all_then_none_file1"; + let new = "test_cp_arg_update_all_then_none_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=all") + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n") +} + #[test] fn test_cp_arg_interactive() { let (at, mut ucmd) = at_and_ucmd!(); diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 0195957353a..ce9e26a819e 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1,5 +1,7 @@ use crate::common::util::TestScenario; use filetime::FileTime; +use std::thread::sleep; +use std::time::Duration; #[test] fn test_invalid_arg() { @@ -716,6 +718,208 @@ fn test_mv_update_option() { assert!(!at.file_exists(file_b)); } +#[test] +fn test_mv_arg_update_none() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_mv_arg_update_none_file1"; + let file2 = "test_mv_arg_update_none_file2"; + let file1_content = "file1 content\n"; + let file2_content = "file2 content\n"; + + at.write(file1, file1_content); + at.write(file2, file2_content); + + ucmd.arg(file1) + .arg(file2) + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(file2), file2_content) +} + +#[test] +fn test_mv_arg_update_all() { + let (at, mut ucmd) = at_and_ucmd!(); + + let file1 = "test_mv_arg_update_none_file1"; + let file2 = "test_mv_arg_update_none_file2"; + let file1_content = "file1 content\n"; + let file2_content = "file2 content\n"; + + at.write(file1, file1_content); + at.write(file2, file2_content); + + ucmd.arg(file1) + .arg(file2) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(file2), file1_content) +} + +#[test] +fn test_mv_arg_update_older_dest_not_older() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=older") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), new_content) +} + +#[test] +fn test_mv_arg_update_none_then_all() { + // take last if multiple update args are supplied, + // update=all wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_then_all_file1"; + let new = "test_mv_arg_update_none_then_all_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=none") + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "old content\n") +} + +#[test] +fn test_mv_arg_update_all_then_none() { + // take last if multiple update args are supplied, + // update=none wins in this case + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_all_then_none_file1"; + let new = "test_mv_arg_update_all_then_none_file2"; + let old_content = "old content\n"; + let new_content = "new content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("--update=all") + .arg("--update=none") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), "new content\n") +} + +#[test] +fn test_mv_arg_update_older_dest_older() { + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("--update=all") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), new_content) +} + +#[test] +fn test_mv_arg_update_short_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(new) + .arg(old) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(old), new_content) +} + +#[test] +fn test_mv_arg_update_short_no_overwrite() { + // same as --update=older + let (at, mut ucmd) = at_and_ucmd!(); + + let old = "test_mv_arg_update_none_file1"; + let new = "test_mv_arg_update_none_file2"; + let old_content = "file1 content\n"; + let new_content = "file2 content\n"; + + at.write(old, old_content); + + sleep(Duration::from_secs(1)); + + at.write(new, new_content); + + ucmd.arg(old) + .arg(new) + .arg("-u") + .succeeds() + .no_stderr() + .no_stdout(); + + assert_eq!(at.read(new), new_content) +} + #[test] fn test_mv_target_dir() { let (at, mut ucmd) = at_and_ucmd!();