diff --git a/crates/nu-command/src/filesystem/ucp.rs b/crates/nu-command/src/filesystem/ucp.rs index 76481de0c3..8bb662375d 100644 --- a/crates/nu-command/src/filesystem/ucp.rs +++ b/crates/nu-command/src/filesystem/ucp.rs @@ -4,7 +4,7 @@ use nu_glob::GlobResult; use nu_protocol::{ ast::Call, engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, + Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, Value, }; use std::path::PathBuf; use uu_cp::{BackupMode, CopyMode, UpdateMode}; @@ -53,6 +53,14 @@ impl Command for UCp { ) .switch("progress", "display a progress bar", Some('p')) .switch("no-clobber", "do not overwrite an existing file", Some('n')) + .named( + "preserve", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "preserve only the specified attributes (empty list means no attributes preserved) + if not specified only mode is preserved + possible values: mode, ownership (unix only), timestamps, context, link, links, xattr", + None + ) .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) @@ -86,6 +94,16 @@ impl Command for UCp { example: "cp -u a b", result: None, }, + Example { + description: "Copy file preserving mode and timestamps attributes", + example: "cp --preserve [ mode timestamps ] a b", + result: None, + }, + Example { + description: "Copy file erasing all attributes", + example: "cp --preserve [] a b", + result: None, + }, ] } @@ -102,11 +120,13 @@ impl Command for UCp { } else { (UpdateMode::ReplaceAll, CopyMode::Copy) }; + let force = call.has_flag(engine_state, stack, "force")?; let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?; let progress = call.has_flag(engine_state, stack, "progress")?; let recursive = call.has_flag(engine_state, stack, "recursive")?; let verbose = call.has_flag(engine_state, stack, "verbose")?; + let preserve: Option = call.get_flag(engine_state, stack, "preserve")?; let debug = call.has_flag(engine_state, stack, "debug")?; let overwrite = if no_clobber { @@ -213,11 +233,14 @@ impl Command for UCp { let target_path = nu_path::expand_path_with(&target_path, &cwd); + let attributes = make_attributes(preserve)?; + let options = uu_cp::Options { overwrite, reflink_mode, recursive, debug, + attributes, verbose: verbose || debug, dereference: !recursive, progress_bar: progress, @@ -231,7 +254,6 @@ impl Command for UCp { 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, @@ -258,6 +280,86 @@ impl Command for UCp { } } +const ATTR_UNSET: uu_cp::Preserve = uu_cp::Preserve::No { explicit: true }; +const ATTR_SET: uu_cp::Preserve = uu_cp::Preserve::Yes { required: true }; + +fn make_attributes(preserve: Option) -> Result { + if let Some(preserve) = preserve { + let mut attributes = uu_cp::Attributes { + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + ownership: ATTR_UNSET, + mode: ATTR_UNSET, + timestamps: ATTR_UNSET, + context: ATTR_UNSET, + links: ATTR_UNSET, + xattr: ATTR_UNSET, + }; + parse_and_set_attributes_list(&preserve, &mut attributes)?; + + Ok(attributes) + } else { + // By default preseerve only mode + Ok(uu_cp::Attributes { + mode: ATTR_SET, + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + ownership: ATTR_UNSET, + timestamps: ATTR_UNSET, + context: ATTR_UNSET, + links: ATTR_UNSET, + xattr: ATTR_UNSET, + }) + } +} + +fn parse_and_set_attributes_list( + list: &Value, + attribute: &mut uu_cp::Attributes, +) -> Result<(), ShellError> { + match list { + Value::List { vals, .. } => { + for val in vals { + parse_and_set_attribute(val, attribute)?; + } + Ok(()) + } + _ => Err(ShellError::IncompatibleParametersSingle { + msg: "--preserve flag expects a list of strings".into(), + span: list.span(), + }), + } +} + +fn parse_and_set_attribute( + value: &Value, + attribute: &mut uu_cp::Attributes, +) -> Result<(), ShellError> { + match value { + Value::String { val, .. } => { + let attribute = match val.as_str() { + "mode" => &mut attribute.mode, + #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))] + "ownership" => &mut attribute.ownership, + "timestamps" => &mut attribute.timestamps, + "context" => &mut attribute.context, + "link" | "links" => &mut attribute.links, + "xattr" => &mut attribute.xattr, + _ => { + return Err(ShellError::IncompatibleParametersSingle { + msg: format!("--preserve flag got an unexpected attribute \"{}\"", val), + span: value.span(), + }); + } + }; + *attribute = ATTR_SET; + Ok(()) + } + _ => Err(ShellError::IncompatibleParametersSingle { + msg: "--preserve flag expects a list of strings".into(), + span: value.span(), + }), + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index e0f7faca8f..df8222bf84 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -1027,3 +1027,76 @@ fn copies_files_with_glob_metachars(#[case] src_name: &str) { fn copies_files_with_glob_metachars_nw(#[case] src_name: &str) { copies_files_with_glob_metachars(src_name); } + +#[cfg(not(windows))] +#[test] +fn test_cp_preserve_timestamps() { + // Preserve timestamp and mode + + Playground::setup("ucp_test_35", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("file.txt")]); + let actual = nu!( + cwd: dirs.test(), + " + chmod +x file.txt + cp --preserve [ mode timestamps ] file.txt other.txt + + let old_attrs = ls -l file.txt | get 0 | select mode accessed modified + let new_attrs = ls -l other.txt | get 0 | select mode accessed modified + + $old_attrs == $new_attrs + ", + ); + assert!(actual.err.is_empty()); + assert_eq!(actual.out, "true"); + }); +} + +#[cfg(not(windows))] +#[test] +fn test_cp_preserve_only_timestamps() { + // Preserve timestamps and discard all other attributes including mode + + Playground::setup("ucp_test_35", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("file.txt")]); + let actual = nu!( + cwd: dirs.test(), + " + chmod +x file.txt + cp --preserve [ timestamps ] file.txt other.txt + + let old_attrs = ls -l file.txt | get 0 | select mode accessed modified + let new_attrs = ls -l other.txt | get 0 | select mode accessed modified + + print (($old_attrs | select mode) != ($new_attrs | select mode)) + print (($old_attrs | select accessed modified) == ($new_attrs | select accessed modified)) + ", + ); + assert!(actual.err.is_empty()); + assert_eq!(actual.out, "truetrue"); + }); +} + +#[cfg(not(windows))] +#[test] +fn test_cp_preserve_nothing() { + // Preserve no attributes + + Playground::setup("ucp_test_35", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("file.txt")]); + let actual = nu!( + cwd: dirs.test(), + " + chmod +x file.txt + cp --preserve [] file.txt other.txt + + let old_attrs = ls -l file.txt | get 0 | select mode accessed modified + let new_attrs = ls -l other.txt | get 0 | select mode accessed modified + + $old_attrs != $new_attrs + ", + ); + assert!(actual.err.is_empty()); + assert_eq!(actual.out, "true"); + }); +}