add -u flag to cp, mv command (#9214)

# Description
Closes: #7853

I found that I want this feature too...

So I take over it, sorry for that @VincenzoCarlino 

# User-Facing Changes

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` to check that
you're using the standard code style
- `cargo test --workspace` to check that all tests pass
- `cargo run -- crates/nu-std/tests/run.nu` to run the tests for the
standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
WindSoilder 2023-05-21 00:48:57 +08:00 committed by GitHub
parent 9b139330f8
commit 5a34671343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 5 deletions

View File

@ -57,6 +57,10 @@ impl Command for Cp {
"show successful copies in addition to failed copies (default:false)",
Some('v'),
)
.switch("update",
"copy only when the SOURCE file is newer than the destination file or when the destination file is missing",
Some('u')
)
// TODO: add back in additional features
// .switch("force", "suppress error when no file", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i'))
@ -88,6 +92,7 @@ impl Command for Cp {
let verbose = call.has_flag("verbose");
let interactive = call.has_flag("interactive");
let progress = call.has_flag("progress");
let update_mode = call.has_flag("update");
let current_dir_path = current_dir(engine_state, stack)?;
let source = current_dir_path.join(src.item.as_str());
@ -177,6 +182,12 @@ impl Command for Cp {
if src.is_file() {
let dst =
canonicalize_with(dst.as_path(), &current_dir_path).unwrap_or(dst);
// ignore when source file is not newer than target file
if update_mode && super::util::is_older(&src, &dst) {
continue;
}
let res = if src == dst {
let message = format!(
"src {source:?} and dst {destination:?} are identical(not copied)"
@ -361,6 +372,11 @@ impl Command for Cp {
example: "cp *.txt dir_a",
result: None,
},
Example {
description: "Copy only if source file is newer than target file",
example: "cp -u a b",
result: None,
},
]
}
}

View File

@ -53,6 +53,11 @@ impl Command for Mv {
)
.switch("force", "overwrite the destination.", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i'))
.switch("update",
"move only when the SOURCE file is newer than the destination file(with -f) or when the destination file is missing",
Some('u')
)
// TODO: add back in additional features
.category(Category::FileSystem)
}
@ -75,6 +80,7 @@ impl Command for Mv {
let verbose = call.has_flag("verbose");
let interactive = call.has_flag("interactive");
let force = call.has_flag("force");
let update_mode = call.has_flag("update");
let ctrlc = engine_state.ctrlc.clone();
@ -191,6 +197,7 @@ impl Command for Mv {
span: spanned_destination.span,
},
interactive,
update_mode,
);
if let Err(error) = result {
Some(Value::Error {
@ -244,6 +251,7 @@ fn move_file(
spanned_from: Spanned<PathBuf>,
spanned_to: Spanned<PathBuf>,
interactive: bool,
update_mode: bool,
) -> Result<bool, ShellError> {
let Spanned {
item: from,
@ -305,10 +313,14 @@ fn move_file(
}
}
if update_mode && super::util::is_older(&from, &to) {
Ok(false)
} else {
match move_item(&from, from_span, &to) {
Ok(()) => Ok(true),
Err(e) => Err(e),
}
}
}
fn move_item(from: &Path, from_span: Span, to: &Path) -> Result<(), ShellError> {

View File

@ -132,3 +132,31 @@ fn get_interactive_confirmation(prompt: String) -> Result<bool, Box<dyn Error>>
Ok(false)
}
}
pub fn is_older(src: &Path, dst: &Path) -> bool {
if !dst.exists() {
return true;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let src_ctime = std::fs::metadata(src)
.map(|m| m.ctime())
.unwrap_or(i64::MIN);
let dst_ctime = std::fs::metadata(dst)
.map(|m| m.ctime())
.unwrap_or(i64::MAX);
src_ctime <= dst_ctime
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
let src_ctime = std::fs::metadata(src)
.map(|m| m.last_write_time())
.unwrap_or(u64::MIN);
let dst_ctime = std::fs::metadata(dst)
.map(|m| m.last_write_time())
.unwrap_or(u64::MAX);
src_ctime <= dst_ctime
}
}

View File

@ -1,7 +1,7 @@
use nu_test_support::fs::file_contents;
use nu_test_support::fs::{
files_exist_at, AbsoluteFile,
Stub::{EmptyFile, FileWithPermission},
Stub::{EmptyFile, FileWithContent, FileWithPermission},
};
use nu_test_support::nu;
use nu_test_support::playground::Playground;
@ -579,3 +579,33 @@ fn copy_file_with_read_permission_impl(progress: bool) {
);
});
}
#[test]
fn copy_file_with_update_flag() {
copy_file_with_update_flag_impl(false);
copy_file_with_update_flag_impl(true);
}
fn copy_file_with_update_flag_impl(progress: bool) {
Playground::setup("cp_test_19", |_dirs, sandbox| {
sandbox.with_files(vec![
EmptyFile("valid.txt"),
FileWithContent("newer_valid.txt", "body"),
]);
let progress_flag = if progress { "-p" } else { "" };
let actual = nu!(
cwd: sandbox.cwd(),
"cp {} -u valid.txt newer_valid.txt; open newer_valid.txt",
progress_flag,
);
assert!(actual.out.contains("body"));
// create a file after assert to make sure that newest_valid.txt is newest
std::thread::sleep(std::time::Duration::from_secs(1));
sandbox.with_files(vec![FileWithContent("newest_valid.txt", "newest_body")]);
let actual = nu!(cwd: sandbox.cwd(), "cp {} -u newest_valid.txt valid.txt; open valid.txt", progress_flag);
assert_eq!(actual.out, "newest_body");
});
}

View File

@ -1,4 +1,4 @@
use nu_test_support::fs::{files_exist_at, Stub::EmptyFile};
use nu_test_support::fs::{files_exist_at, Stub::EmptyFile, Stub::FileWithContent};
use nu_test_support::nu;
use nu_test_support::playground::Playground;
@ -464,3 +464,25 @@ fn mv_change_case_of_file() {
assert!(files_in_test_directory.contains(&new_file_name));
})
}
#[test]
fn mv_with_update_flag() {
Playground::setup("mv_with_update_flag", |_dirs, sandbox| {
sandbox.with_files(vec![
EmptyFile("valid.txt"),
FileWithContent("newer_valid.txt", "body"),
]);
let actual = nu!(
cwd: sandbox.cwd(),
"mv -uf valid.txt newer_valid.txt; open newer_valid.txt",
);
assert_eq!(actual.out, "body");
// create a file after assert to make sure that newest_valid.txt is newest
std::thread::sleep(std::time::Duration::from_secs(1));
sandbox.with_files(vec![FileWithContent("newest_valid.txt", "newest_body")]);
let actual = nu!(cwd: sandbox.cwd(), "mv -uf newest_valid.txt valid.txt; open valid.txt");
assert_eq!(actual.out, "newest_body");
});
}