Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mv, cp: add support for --update=none,all,older #4796

Merged
merged 26 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ecde4b6
core: introduce update controller for mv and cp
shinhs0506 Apr 27, 2023
0e8dd89
mv: implement update=[switch]
shinhs0506 Apr 27, 2023
9dc84e9
cp: implement update=[switch]
shinhs0506 Apr 27, 2023
2f8df65
core mv cp: update help doc for 'update' functionality
shinhs0506 Apr 27, 2023
ed3ff10
cp: write tests for --update
shinhs0506 May 1, 2023
78412c5
mv: add tests for --update
shinhs0506 May 1, 2023
2f975e9
cp: move after help to md file
shinhs0506 May 1, 2023
f83468d
mv: move after help to md file
shinhs0506 May 1, 2023
85ded23
mv: resolve merge conflict
shinhs0506 May 1, 2023
b707b69
cp: remove long help
shinhs0506 May 1, 2023
c5327cf
core: add docs for update control
shinhs0506 May 1, 2023
92e1b3f
cp: fix documentation
shinhs0506 May 1, 2023
6625cfe
mv: fix documentation
shinhs0506 May 1, 2023
66a9169
cp: fix typos
shinhs0506 May 1, 2023
60c0b66
core: fix typo in update control
shinhs0506 May 2, 2023
36e93e1
core: add header notice for update control
shinhs0506 May 2, 2023
06d4603
core: fix typo in update control
shinhs0506 May 2, 2023
460d346
core: remove '' case for the update argument
shinhs0506 May 2, 2023
3b8f3d0
core: remove unnecessary if statement in update control
shinhs0506 May 2, 2023
6a10097
mv: simplify tests for update
shinhs0506 May 2, 2023
c0e4e4f
cp: simplify tests for update
shinhs0506 May 2, 2023
983fee0
cp: fix wrong test names for update
shinhs0506 May 2, 2023
918c36b
cp: write test for multiple update args
shinhs0506 May 2, 2023
8ad2fa3
mv: write test for multiple update args
shinhs0506 May 2, 2023
898628f
core: fix typo in update control
shinhs0506 May 2, 2023
923a62c
mv: fix function/file names in tests
cakebaker May 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
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,
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
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.
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1629,22 +1632,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;
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
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;
55 changes: 55 additions & 0 deletions src/uucore/src/lib/mods/update_control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use clap::ArgMatches;
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved

pub static UPDATE_CONTROL_VALUES: &[&str] = &["all", "none", "old", ""];

#[derive(Clone, Eq, PartialEq)]
pub enum UpdateMode {
ReplaceAll,
ReplaceNone,
ReplaceIfOlder,
}

pub mod arguments {
use clap::ArgAction;

pub static OPT_UPDATE: &str = "update";
pub static OPT_UPDATE_NO_ARG: &str = "u";

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"])
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
.num_args(0..=1)
.default_missing_value("older")
.require_equals(true)
.overrides_with("update")
.action(clap::ArgAction::Set)
}

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)
}
}

pub fn determine_update_mode(matches: &ArgMatches) -> UpdateMode {
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
if matches.contains_id(arguments::OPT_UPDATE) {
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
if let Some(mode) = matches.get_one::<String>(arguments::OPT_UPDATE) {
match mode.as_str() {
"all" | "" => UpdateMode::ReplaceAll,
shinhs0506 marked this conversation as resolved.
Show resolved Hide resolved
"none" => UpdateMode::ReplaceNone,
"older" => UpdateMode::ReplaceIfOlder,
_ => unreachable!("other args restricted by clap"),
}
} else {
unreachable!("other args restricted by clap")
}
} else if matches.get_flag(arguments::OPT_UPDATE_NO_ARG) {
UpdateMode::ReplaceIfOlder
} else {
UpdateMode::ReplaceAll
}
}
Loading