mirror of
https://github.com/nushell/nushell.git
synced 2025-07-07 18:07:02 +02:00
use uutils/coreutils cp command in place of nushell's cp command (#10097)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description Hi. Basically, this is a continuation of the work that @fdncred started. Given some nice discussions on #9463 , and [merged uutils PR](https://github.com/uutils/coreutils/pull/5152) from @tertsdiepraam we have decided to give the `cp` command the `crawl` stage as it was named. > [!NOTE] Given that the `uutils` crate has not made the release for the merged PR, just make sure you checkout latest and put it in the required place to make this PR work. The aim of this PR is for is to see how to move forward using `uutils` crate. In order to getting this started, I have made the current `nushell cp tests` pass along with some extra ones I copied over from the `uutils` repo. With all of that being said, things that would be nice to decide, and keep working on: Crawl: - Handling of certain `named` flags, with their long and short forms(e.g. --update, --reflink, --preserve, etc), and using default values. Maybe `-u` can already have a `default_missing_value`. - Should we maybe just support one single option `switch` flags (see `--backup` in code) as a contrast to the other named args. - Complete test coverage from `uutils`. They had > 100 tests, and I could only port like 12 as they are a bit time consuming given they cannot be straight up copy pasted. Maybe we do not need all >100, but maybe the more relevant to what we want. - Refactor this code Walk: - Non fatal errors on `copy` from `utils`. Currently it just sends it to stdout but errors have no span - Better integration An added possibility is the addition of `SyntaxShape::OneOf()` for `Named` arguments which was briefly mentioned in the discord server, but that is still to be decided. This could greatly improve some of the integration. This would enable something like `cp --preserve [all timestamp]` or `cp --preserve all` to both work. I did not want to keep holding on this, and wait till I was happy with the code because I think its nice if everyone can start up and suggest refactors, but the main important part now was getting it out the door, as if I take my sweet time this will take way longer 😛 <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting Make sure you've run and fixed any issues with these commands: - [X] cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - [X] cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - [X] cargo test --workspace` to check that all tests pass - [X] cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` 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. --> --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
@ -10,6 +10,7 @@ mod rm;
|
||||
mod save;
|
||||
mod start;
|
||||
mod touch;
|
||||
mod ucp;
|
||||
mod util;
|
||||
mod watch;
|
||||
|
||||
@ -25,4 +26,5 @@ pub use rm::Rm;
|
||||
pub use save::Save;
|
||||
pub use start::Start;
|
||||
pub use touch::Touch;
|
||||
pub use ucp::UCp;
|
||||
pub use watch::Watch;
|
||||
|
244
crates/nu-command/src/filesystem/ucp.rs
Normal file
244
crates/nu-command/src/filesystem/ucp.rs
Normal file
@ -0,0 +1,244 @@
|
||||
use nu_path::expand_to_real_path;
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Call, Expr},
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use uu_cp::{BackupMode, UpdateMode};
|
||||
|
||||
// TODO: related to uucore::error::set_exit_code(EXIT_ERR)
|
||||
// const EXIT_ERR: i32 = 1;
|
||||
const GLOB_PARAMS: nu_glob::MatchOptions = nu_glob::MatchOptions {
|
||||
case_sensitive: true,
|
||||
require_literal_separator: false,
|
||||
require_literal_leading_dot: false,
|
||||
recursive_match_hidden_dir: true,
|
||||
};
|
||||
|
||||
pub fn collect_filepath_arguments(call: &Call) -> Vec<Spanned<String>> {
|
||||
call.arguments
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
Argument::Positional(expression) => {
|
||||
if let Expr::Filepath(p) = &expression.expr {
|
||||
let pth = Spanned {
|
||||
item: nu_utils::strip_ansi_string_unlikely(p.clone()),
|
||||
span: expression.span,
|
||||
};
|
||||
Some(pth)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<Spanned<String>>>()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UCp;
|
||||
|
||||
impl Command for UCp {
|
||||
fn name(&self) -> &str {
|
||||
"ucp"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Copy files using uutils/coreutils cp."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["copy", "file", "files"]
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("ucp")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
.switch("recursive", "copy directories recursively", Some('r'))
|
||||
.switch("verbose", "explicitly state what is being done", Some('v'))
|
||||
.switch(
|
||||
"force",
|
||||
"if an existing destination file cannot be opened, remove it and try
|
||||
again (this option is ignored when the -n option is also used).
|
||||
currently not implemented for windows",
|
||||
Some('f'),
|
||||
)
|
||||
.switch("interactive", "ask before overwriting files", Some('i'))
|
||||
.switch("progress", "display a progress bar", Some('p'))
|
||||
.switch("no-clobber", "do not overwrite an existing file", Some('n'))
|
||||
.switch("debug", "explain how a file is copied. Implies -v", None)
|
||||
.rest("paths", SyntaxShape::Filepath, "Copy SRC file/s to DEST")
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::FileSystem)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Copy myfile to dir_b",
|
||||
example: "ucp myfile dir_b",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Recursively copy dir_a to dir_b",
|
||||
example: "ucp -r dir_a dir_b",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Recursively copy dir_a to dir_b, and print the feedbacks",
|
||||
example: "ucp -r -v dir_a dir_b",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Move many files into a directory",
|
||||
example: "ucp *.txt dir_a",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let interactive = call.has_flag("interactive");
|
||||
let force = call.has_flag("force");
|
||||
let no_clobber = call.has_flag("no-clobber");
|
||||
let progress = call.has_flag("progress");
|
||||
let recursive = call.has_flag("recursive");
|
||||
let verbose = call.has_flag("verbose");
|
||||
|
||||
let debug = call.has_flag("debug");
|
||||
let overwrite = if no_clobber {
|
||||
uu_cp::OverwriteMode::NoClobber
|
||||
} else if interactive {
|
||||
if force {
|
||||
uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Force)
|
||||
} else {
|
||||
uu_cp::OverwriteMode::Interactive(uu_cp::ClobberMode::Standard)
|
||||
}
|
||||
} else if force {
|
||||
uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Force)
|
||||
} else {
|
||||
uu_cp::OverwriteMode::Clobber(uu_cp::ClobberMode::Standard)
|
||||
};
|
||||
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
|
||||
let reflink_mode = uu_cp::ReflinkMode::Auto;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
|
||||
let reflink_mode = uu_cp::ReflinkMode::Never;
|
||||
let mut paths = collect_filepath_arguments(call);
|
||||
if paths.is_empty() {
|
||||
return Err(ShellError::GenericError(
|
||||
"Missing file operand".into(),
|
||||
"Missing file operand".into(),
|
||||
Some(call.head),
|
||||
Some("Please provide source and destination paths".into()),
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
if paths.len() == 1 {
|
||||
return Err(ShellError::GenericError(
|
||||
"Missing destination path".into(),
|
||||
format!("Missing destination path operand after {}", paths[0].item),
|
||||
Some(paths[0].span),
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
let target = paths.pop().expect("Should not be reached?");
|
||||
let target_path = PathBuf::from(&target.item);
|
||||
if target.item.ends_with('/') && !target_path.is_dir() {
|
||||
return Err(ShellError::GenericError(
|
||||
"is not a directory".into(),
|
||||
"is not a directory".into(),
|
||||
Some(target.span),
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
};
|
||||
// paths now contains the sources
|
||||
let sources: Vec<Vec<PathBuf>> = paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
// Need to expand too make it work with globbing
|
||||
let expanded_src = expand_to_real_path(&p.item);
|
||||
match nu_glob::glob_with(&expanded_src.to_string_lossy(), GLOB_PARAMS) {
|
||||
Ok(files) => {
|
||||
let f = files.filter_map(Result::ok).collect::<Vec<PathBuf>>();
|
||||
if f.is_empty() {
|
||||
return Err(ShellError::FileNotFound(p.span));
|
||||
}
|
||||
Ok(f)
|
||||
}
|
||||
Err(e) => Err(ShellError::GenericError(
|
||||
e.to_string(),
|
||||
"invalid pattern".to_string(),
|
||||
Some(p.span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<Vec<PathBuf>>, ShellError>>()?;
|
||||
|
||||
let sources = sources.into_iter().flatten().collect::<Vec<PathBuf>>();
|
||||
let options = uu_cp::Options {
|
||||
overwrite,
|
||||
reflink_mode,
|
||||
recursive,
|
||||
debug,
|
||||
verbose: verbose || debug,
|
||||
dereference: !recursive,
|
||||
progress_bar: progress,
|
||||
attributes_only: false,
|
||||
backup: BackupMode::NoBackup,
|
||||
copy_contents: false,
|
||||
cli_dereference: false,
|
||||
copy_mode: uu_cp::CopyMode::Copy,
|
||||
no_target_dir: false,
|
||||
one_file_system: false,
|
||||
parents: false,
|
||||
sparse_mode: uu_cp::SparseMode::Auto,
|
||||
strip_trailing_slashes: false,
|
||||
attributes: uu_cp::Attributes::NONE,
|
||||
backup_suffix: String::from("~"),
|
||||
target_dir: None,
|
||||
update: UpdateMode::ReplaceAll,
|
||||
};
|
||||
|
||||
if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
|
||||
match error {
|
||||
// code should still be EXIT_ERR as does GNU cp
|
||||
uu_cp::Error::NotAllFilesCopied => {}
|
||||
_ => {
|
||||
return Err(ShellError::GenericError(
|
||||
format!("{}", error),
|
||||
format!("{}", error),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
};
|
||||
// TODO: What should we do in place of set_exit_code?
|
||||
// uucore::error::set_exit_code(EXIT_ERR);
|
||||
}
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(UCp {})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user