mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 19:47:45 +00:00
Merge pull request #4796 from shinhs0506/mv-cp-update
mv, cp: add support for --update=none,all,older
This commit is contained in:
commit
a97199f72a
9 changed files with 634 additions and 53 deletions
|
@ -7,3 +7,19 @@ cp [OPTION]... -t DIRECTORY SOURCE...
|
||||||
```
|
```
|
||||||
|
|
||||||
Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.
|
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.
|
||||||
|
|
|
@ -40,7 +40,10 @@ use uucore::error::{set_exit_code, UClapError, UError, UResult, UUsageError};
|
||||||
use uucore::fs::{
|
use uucore::fs::{
|
||||||
canonicalize, paths_refer_to_same_file, FileInformation, MissingHandling, ResolveMode,
|
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;
|
use crate::copydir::copy_directory;
|
||||||
|
|
||||||
|
@ -224,13 +227,14 @@ pub struct Options {
|
||||||
recursive: bool,
|
recursive: bool,
|
||||||
backup_suffix: String,
|
backup_suffix: String,
|
||||||
target_dir: Option<PathBuf>,
|
target_dir: Option<PathBuf>,
|
||||||
update: bool,
|
update: UpdateMode,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
progress_bar: bool,
|
progress_bar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ABOUT: &str = help_about!("cp.md");
|
const ABOUT: &str = help_about!("cp.md");
|
||||||
const USAGE: &str = help_usage!("cp.md");
|
const USAGE: &str = help_usage!("cp.md");
|
||||||
|
const AFTER_HELP: &str = help_section!("after help", "cp.md");
|
||||||
|
|
||||||
static EXIT_ERR: i32 = 1;
|
static EXIT_ERR: i32 = 1;
|
||||||
|
|
||||||
|
@ -264,7 +268,6 @@ mod options {
|
||||||
pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes";
|
pub const STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes";
|
||||||
pub const SYMBOLIC_LINK: &str = "symbolic-link";
|
pub const SYMBOLIC_LINK: &str = "symbolic-link";
|
||||||
pub const TARGET_DIRECTORY: &str = "target-directory";
|
pub const TARGET_DIRECTORY: &str = "target-directory";
|
||||||
pub const UPDATE: &str = "update";
|
|
||||||
pub const VERBOSE: &str = "verbose";
|
pub const VERBOSE: &str = "verbose";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +298,7 @@ pub fn uu_app() -> Command {
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.override_usage(format_usage(USAGE))
|
.override_usage(format_usage(USAGE))
|
||||||
|
.after_help(AFTER_HELP)
|
||||||
.infer_long_args(true)
|
.infer_long_args(true)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(options::TARGET_DIRECTORY)
|
Arg::new(options::TARGET_DIRECTORY)
|
||||||
|
@ -393,16 +397,8 @@ pub fn uu_app() -> Command {
|
||||||
.arg(backup_control::arguments::backup())
|
.arg(backup_control::arguments::backup())
|
||||||
.arg(backup_control::arguments::backup_no_args())
|
.arg(backup_control::arguments::backup_no_args())
|
||||||
.arg(backup_control::arguments::suffix())
|
.arg(backup_control::arguments::suffix())
|
||||||
.arg(
|
.arg(update_control::arguments::update())
|
||||||
Arg::new(options::UPDATE)
|
.arg(update_control::arguments::update_no_args())
|
||||||
.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(
|
.arg(
|
||||||
Arg::new(options::REFLINK)
|
Arg::new(options::REFLINK)
|
||||||
.long(options::REFLINK)
|
.long(options::REFLINK)
|
||||||
|
@ -641,7 +637,11 @@ impl CopyMode {
|
||||||
Self::Link
|
Self::Link
|
||||||
} else if matches.get_flag(options::SYMBOLIC_LINK) {
|
} else if matches.get_flag(options::SYMBOLIC_LINK) {
|
||||||
Self::SymLink
|
Self::SymLink
|
||||||
} else if matches.get_flag(options::UPDATE) {
|
} else if matches
|
||||||
|
.get_one::<String>(update_control::arguments::OPT_UPDATE)
|
||||||
|
.is_some()
|
||||||
|
|| matches.get_flag(update_control::arguments::OPT_UPDATE_NO_ARG)
|
||||||
|
{
|
||||||
Self::Update
|
Self::Update
|
||||||
} else if matches.get_flag(options::ATTRIBUTES_ONLY) {
|
} else if matches.get_flag(options::ATTRIBUTES_ONLY) {
|
||||||
Self::AttrOnly
|
Self::AttrOnly
|
||||||
|
@ -749,6 +749,7 @@ impl Options {
|
||||||
Err(e) => return Err(Error::Backup(format!("{e}"))),
|
Err(e) => return Err(Error::Backup(format!("{e}"))),
|
||||||
Ok(mode) => mode,
|
Ok(mode) => mode,
|
||||||
};
|
};
|
||||||
|
let update_mode = update_control::determine_update_mode(matches);
|
||||||
|
|
||||||
let backup_suffix = backup_control::determine_backup_suffix(matches);
|
let backup_suffix = backup_control::determine_backup_suffix(matches);
|
||||||
|
|
||||||
|
@ -826,7 +827,7 @@ impl Options {
|
||||||
|| matches.get_flag(options::DEREFERENCE),
|
|| matches.get_flag(options::DEREFERENCE),
|
||||||
one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
|
one_file_system: matches.get_flag(options::ONE_FILE_SYSTEM),
|
||||||
parents: matches.get_flag(options::PARENTS),
|
parents: matches.get_flag(options::PARENTS),
|
||||||
update: matches.get_flag(options::UPDATE),
|
update: update_mode,
|
||||||
verbose: matches.get_flag(options::VERBOSE),
|
verbose: matches.get_flag(options::VERBOSE),
|
||||||
strip_trailing_slashes: matches.get_flag(options::STRIP_TRAILING_SLASHES),
|
strip_trailing_slashes: matches.get_flag(options::STRIP_TRAILING_SLASHES),
|
||||||
reflink_mode: {
|
reflink_mode: {
|
||||||
|
@ -1473,7 +1474,9 @@ fn copy_file(
|
||||||
symlinked_files: &mut HashSet<FileInformation>,
|
symlinked_files: &mut HashSet<FileInformation>,
|
||||||
source_in_command_line: bool,
|
source_in_command_line: bool,
|
||||||
) -> CopyResult<()> {
|
) -> 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
|
// `cp -i --update old new` when `new` exists doesn't copy anything
|
||||||
// and exit with 0
|
// and exit with 0
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -1630,22 +1633,38 @@ fn copy_file(
|
||||||
}
|
}
|
||||||
CopyMode::Update => {
|
CopyMode::Update => {
|
||||||
if dest.exists() {
|
if dest.exists() {
|
||||||
let dest_metadata = fs::symlink_metadata(dest)?;
|
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 src_time = source_metadata.modified()?;
|
||||||
let dest_time = dest_metadata.modified()?;
|
let dest_time = dest_metadata.modified()?;
|
||||||
if src_time <= dest_time {
|
if src_time <= dest_time {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
copy_helper(
|
copy_helper(
|
||||||
source,
|
source,
|
||||||
dest,
|
dest,
|
||||||
options,
|
options,
|
||||||
context,
|
context,
|
||||||
source_is_symlink,
|
source_is_symlink,
|
||||||
source_is_fifo,
|
source_is_fifo,
|
||||||
symlinked_files,
|
symlinked_files,
|
||||||
)?;
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
copy_helper(
|
copy_helper(
|
||||||
|
|
|
@ -5,5 +5,17 @@ mv [OPTION]... [-T] SOURCE DEST
|
||||||
mv [OPTION]... SOURCE... DIRECTORY
|
mv [OPTION]... SOURCE... DIRECTORY
|
||||||
mv [OPTION]... -t DIRECTORY SOURCE...
|
mv [OPTION]... -t DIRECTORY SOURCE...
|
||||||
```
|
```
|
||||||
|
|
||||||
Move `SOURCE` to `DEST`, or multiple `SOURCE`(s) to `DIRECTORY`.
|
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.
|
||||||
|
|
|
@ -25,7 +25,8 @@ use std::path::{Path, PathBuf};
|
||||||
use uucore::backup_control::{self, BackupMode};
|
use uucore::backup_control::{self, BackupMode};
|
||||||
use uucore::display::Quotable;
|
use uucore::display::Quotable;
|
||||||
use uucore::error::{set_exit_code, FromIo, UError, UResult, USimpleError, UUsageError};
|
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::{
|
use fs_extra::dir::{
|
||||||
get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions,
|
get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions,
|
||||||
|
@ -38,7 +39,7 @@ pub struct Behavior {
|
||||||
overwrite: OverwriteMode,
|
overwrite: OverwriteMode,
|
||||||
backup: BackupMode,
|
backup: BackupMode,
|
||||||
suffix: String,
|
suffix: String,
|
||||||
update: bool,
|
update: UpdateMode,
|
||||||
target_dir: Option<OsString>,
|
target_dir: Option<OsString>,
|
||||||
no_target_dir: bool,
|
no_target_dir: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
@ -55,6 +56,7 @@ pub enum OverwriteMode {
|
||||||
|
|
||||||
const ABOUT: &str = help_about!("mv.md");
|
const ABOUT: &str = help_about!("mv.md");
|
||||||
const USAGE: &str = help_usage!("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_FORCE: &str = "force";
|
||||||
static OPT_INTERACTIVE: &str = "interactive";
|
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_STRIP_TRAILING_SLASHES: &str = "strip-trailing-slashes";
|
||||||
static OPT_TARGET_DIRECTORY: &str = "target-directory";
|
static OPT_TARGET_DIRECTORY: &str = "target-directory";
|
||||||
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
|
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
|
||||||
static OPT_UPDATE: &str = "update";
|
|
||||||
static OPT_VERBOSE: &str = "verbose";
|
static OPT_VERBOSE: &str = "verbose";
|
||||||
static OPT_PROGRESS: &str = "progress";
|
static OPT_PROGRESS: &str = "progress";
|
||||||
static ARG_FILES: &str = "files";
|
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 overwrite_mode = determine_overwrite_mode(&matches);
|
||||||
let backup_mode = backup_control::determine_backup_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 {
|
if overwrite_mode == OverwriteMode::NoClobber && backup_mode != BackupMode::NoBackup {
|
||||||
return Err(UUsageError::new(
|
return Err(UUsageError::new(
|
||||||
|
@ -120,7 +122,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||||
overwrite: overwrite_mode,
|
overwrite: overwrite_mode,
|
||||||
backup: backup_mode,
|
backup: backup_mode,
|
||||||
suffix: backup_suffix,
|
suffix: backup_suffix,
|
||||||
update: matches.get_flag(OPT_UPDATE),
|
update: update_mode,
|
||||||
target_dir,
|
target_dir,
|
||||||
no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
|
no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY),
|
||||||
verbose: matches.get_flag(OPT_VERBOSE),
|
verbose: matches.get_flag(OPT_VERBOSE),
|
||||||
|
@ -136,9 +138,8 @@ pub fn uu_app() -> Command {
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.about(ABOUT)
|
.about(ABOUT)
|
||||||
.override_usage(format_usage(USAGE))
|
.override_usage(format_usage(USAGE))
|
||||||
|
.after_help(AFTER_HELP)
|
||||||
.infer_long_args(true)
|
.infer_long_args(true)
|
||||||
.arg(backup_control::arguments::backup())
|
|
||||||
.arg(backup_control::arguments::backup_no_args())
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(OPT_FORCE)
|
Arg::new(OPT_FORCE)
|
||||||
.short('f')
|
.short('f')
|
||||||
|
@ -166,7 +167,11 @@ pub fn uu_app() -> Command {
|
||||||
.help("remove any trailing slashes from each SOURCE argument")
|
.help("remove any trailing slashes from each SOURCE argument")
|
||||||
.action(ArgAction::SetTrue),
|
.action(ArgAction::SetTrue),
|
||||||
)
|
)
|
||||||
|
.arg(backup_control::arguments::backup())
|
||||||
|
.arg(backup_control::arguments::backup_no_args())
|
||||||
.arg(backup_control::arguments::suffix())
|
.arg(backup_control::arguments::suffix())
|
||||||
|
.arg(update_control::arguments::update())
|
||||||
|
.arg(update_control::arguments::update_no_args())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(OPT_TARGET_DIRECTORY)
|
Arg::new(OPT_TARGET_DIRECTORY)
|
||||||
.short('t')
|
.short('t')
|
||||||
|
@ -184,16 +189,6 @@ pub fn uu_app() -> Command {
|
||||||
.help("treat DEST as a normal file")
|
.help("treat DEST as a normal file")
|
||||||
.action(ArgAction::SetTrue),
|
.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(
|
||||||
Arg::new(OPT_VERBOSE)
|
Arg::new(OPT_VERBOSE)
|
||||||
.short('v')
|
.short('v')
|
||||||
|
@ -420,12 +415,24 @@ fn rename(
|
||||||
let mut backup_path = None;
|
let mut backup_path = None;
|
||||||
|
|
||||||
if to.exists() {
|
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
|
// `mv -i --update old new` when `new` exists doesn't move anything
|
||||||
// and exit with 0
|
// and exit with 0
|
||||||
return Ok(());
|
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 {
|
match b.overwrite {
|
||||||
OverwriteMode::NoClobber => {
|
OverwriteMode::NoClobber => {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
|
@ -445,10 +452,6 @@ fn rename(
|
||||||
if let Some(ref backup_path) = backup_path {
|
if let Some(ref backup_path) = backup_path {
|
||||||
rename_with_fallback(to, backup_path, multi_progress)?;
|
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
|
// "to" may no longer exist if it was backed up
|
||||||
|
|
|
@ -26,6 +26,7 @@ pub use crate::mods::os;
|
||||||
pub use crate::mods::panic;
|
pub use crate::mods::panic;
|
||||||
pub use crate::mods::quoting_style;
|
pub use crate::mods::quoting_style;
|
||||||
pub use crate::mods::ranges;
|
pub use crate::mods::ranges;
|
||||||
|
pub use crate::mods::update_control;
|
||||||
pub use crate::mods::version_cmp;
|
pub use crate::mods::version_cmp;
|
||||||
|
|
||||||
// * string parsing modules
|
// * string parsing modules
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub mod error;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
pub mod panic;
|
pub mod panic;
|
||||||
pub mod ranges;
|
pub mod ranges;
|
||||||
|
pub mod update_control;
|
||||||
pub mod version_cmp;
|
pub mod version_cmp;
|
||||||
// dir and vdir also need access to the quoting_style module
|
// dir and vdir also need access to the quoting_style module
|
||||||
pub mod quoting_style;
|
pub mod quoting_style;
|
||||||
|
|
139
src/uucore/src/lib/mods/update_control.rs
Normal file
139
src/uucore/src/lib/mods/update_control.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// This file is part of the uutils coreutils package.
|
||||||
|
//
|
||||||
|
// (c) John Shin <shinhs0506@gmail.com>
|
||||||
|
//
|
||||||
|
// 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::<String>(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
|
||||||
|
}
|
||||||
|
}
|
|
@ -244,6 +244,192 @@ fn test_cp_arg_update_interactive_error() {
|
||||||
.no_stdout();
|
.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]
|
#[test]
|
||||||
fn test_cp_arg_interactive() {
|
fn test_cp_arg_interactive() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::common::util::TestScenario;
|
use crate::common::util::TestScenario;
|
||||||
use filetime::FileTime;
|
use filetime::FileTime;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_arg() {
|
fn test_invalid_arg() {
|
||||||
|
@ -716,6 +718,208 @@ fn test_mv_update_option() {
|
||||||
assert!(!at.file_exists(file_b));
|
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]
|
#[test]
|
||||||
fn test_mv_target_dir() {
|
fn test_mv_target_dir() {
|
||||||
let (at, mut ucmd) = at_and_ucmd!();
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue