Skip to content

Commit

Permalink
Merge pull request #4796 from shinhs0506/mv-cp-update
Browse files Browse the repository at this point in the history
mv, cp: add support for --update=none,all,older
  • Loading branch information
cakebaker authored May 3, 2023
2 parents 82eb04c + 923a62c commit a97199f
Show file tree
Hide file tree
Showing 9 changed files with 635 additions and 54 deletions.
16 changes: 16 additions & 0 deletions src/uu/cp/cp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
83 changes: 51 additions & 32 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -224,13 +227,14 @@ pub struct Options {
recursive: bool,
backup_suffix: String,
target_dir: Option<PathBuf>,
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;

Expand Down Expand Up @@ -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";
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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::<String>(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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -1473,7 +1474,9 @@ fn copy_file(
symlinked_files: &mut HashSet<FileInformation>,
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(());
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 13 additions & 1 deletion src/uu/mv/mv.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 24 additions & 21 deletions src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,7 +39,7 @@ pub struct Behavior {
overwrite: OverwriteMode,
backup: BackupMode,
suffix: String,
update: bool,
update: UpdateMode,
target_dir: Option<OsString>,
no_target_dir: bool,
verbose: bool,
Expand All @@ -55,14 +56,14 @@ 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";
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";
Expand Down Expand Up @@ -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(
Expand All @@ -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),
Expand All @@ -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')
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/uucore/src/lib/mods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading

0 comments on commit a97199f

Please sign in to comment.