Add file attribute handling flag to cp (#11491)

# Description
This PR adds possibility to preserve/strip attributes from files when
using `cp` (via uu_cp::Attributes). To achieve this a single `--preserve
<list of attributes>` flag is added. This is different from how
coreutils and uutils cp function, but I believe this is better for
nushell.

Coreutils cp has three options `-p`, `--preserve` and `--no-presevce`.
The logic of these two options is not straightforward. As far as I
understand it is:
1. By default only mode attributes are preserved
2. `--preserve` option adds to default preserved attributes specified
ones (e.g. `--preserve=xattr,timestamps` will preserve mode, timestamps
and xattr)
3. `-p` is the same as `--preserve=mode,ownership,timestamps`
4. `--no-preserve` option rejects specified attributes (having priority
over `--preserve`)

However (in my opinion) the `--no-preserve` option is not needed,
because its only use seems to be rejecting attributes preserved by
default. But there is no need for this in nushell, because `--preserve`
can be specified with empty list as argument (whereas coreutils cp will
display a `cp: ambiguous argument ‘’ for ‘--preserve’` error if
`--preserve` is used with empty string as argument).

So to simplify this command is suggest (and implemented) only the
`--preserve` with the following logic:
1. By default mode attribute is preserved (as in coreutils cp)
2. `--preserve [ ... ]` will overwrite default with whatever is
specified in list (empty list meaning preserve nothing)

This way cp without `--preserve` behaves the same as coreutils `cp`, but
instead of using combinations of `--preserve` and `--no-preserve` one
needs to use `--preserve [ ... ]` with all attributes specified
explicitly. This seems more user-friendly to me as it does not require
remembering what the attributes preserved by default are and rejecting
them manually. However I see the possible problem with behavior
different from coreutils implementation, so some feedback is
apprecieated!

# User-Facing Changes
Users can now preserve or reject file attributes when using `cp`

# Tests + Formatting
Added tests manipulating mode and timestamps attributes.
This commit is contained in:
Artemiy 2024-01-12 21:02:55 +03:00 committed by GitHub
parent 724818030d
commit 387c5462e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 177 additions and 2 deletions

View File

@ -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<Value> = 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<Value>) -> Result<uu_cp::Attributes, ShellError> {
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::*;

View File

@ -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");
});
}