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:
parent
724818030d
commit
387c5462e9
|
@ -4,7 +4,7 @@ use nu_glob::GlobResult;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Call,
|
ast::Call,
|
||||||
engine::{Command, EngineState, Stack},
|
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 std::path::PathBuf;
|
||||||
use uu_cp::{BackupMode, CopyMode, UpdateMode};
|
use uu_cp::{BackupMode, CopyMode, UpdateMode};
|
||||||
|
@ -53,6 +53,14 @@ impl Command for UCp {
|
||||||
)
|
)
|
||||||
.switch("progress", "display a progress bar", Some('p'))
|
.switch("progress", "display a progress bar", Some('p'))
|
||||||
.switch("no-clobber", "do not overwrite an existing file", Some('n'))
|
.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)
|
.switch("debug", "explain how a file is copied. Implies -v", None)
|
||||||
.rest("paths", SyntaxShape::Filepath, "Copy SRC file/s to DEST.")
|
.rest("paths", SyntaxShape::Filepath, "Copy SRC file/s to DEST.")
|
||||||
.allow_variants_without_examples(true)
|
.allow_variants_without_examples(true)
|
||||||
|
@ -86,6 +94,16 @@ impl Command for UCp {
|
||||||
example: "cp -u a b",
|
example: "cp -u a b",
|
||||||
result: None,
|
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 {
|
} else {
|
||||||
(UpdateMode::ReplaceAll, CopyMode::Copy)
|
(UpdateMode::ReplaceAll, CopyMode::Copy)
|
||||||
};
|
};
|
||||||
|
|
||||||
let force = call.has_flag(engine_state, stack, "force")?;
|
let force = call.has_flag(engine_state, stack, "force")?;
|
||||||
let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
|
let no_clobber = call.has_flag(engine_state, stack, "no-clobber")?;
|
||||||
let progress = call.has_flag(engine_state, stack, "progress")?;
|
let progress = call.has_flag(engine_state, stack, "progress")?;
|
||||||
let recursive = call.has_flag(engine_state, stack, "recursive")?;
|
let recursive = call.has_flag(engine_state, stack, "recursive")?;
|
||||||
let verbose = call.has_flag(engine_state, stack, "verbose")?;
|
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 debug = call.has_flag(engine_state, stack, "debug")?;
|
||||||
let overwrite = if no_clobber {
|
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 target_path = nu_path::expand_path_with(&target_path, &cwd);
|
||||||
|
|
||||||
|
let attributes = make_attributes(preserve)?;
|
||||||
|
|
||||||
let options = uu_cp::Options {
|
let options = uu_cp::Options {
|
||||||
overwrite,
|
overwrite,
|
||||||
reflink_mode,
|
reflink_mode,
|
||||||
recursive,
|
recursive,
|
||||||
debug,
|
debug,
|
||||||
|
attributes,
|
||||||
verbose: verbose || debug,
|
verbose: verbose || debug,
|
||||||
dereference: !recursive,
|
dereference: !recursive,
|
||||||
progress_bar: progress,
|
progress_bar: progress,
|
||||||
|
@ -231,7 +254,6 @@ impl Command for UCp {
|
||||||
parents: false,
|
parents: false,
|
||||||
sparse_mode: uu_cp::SparseMode::Auto,
|
sparse_mode: uu_cp::SparseMode::Auto,
|
||||||
strip_trailing_slashes: false,
|
strip_trailing_slashes: false,
|
||||||
attributes: uu_cp::Attributes::NONE,
|
|
||||||
backup_suffix: String::from("~"),
|
backup_suffix: String::from("~"),
|
||||||
target_dir: None,
|
target_dir: None,
|
||||||
update,
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -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) {
|
fn copies_files_with_glob_metachars_nw(#[case] src_name: &str) {
|
||||||
copies_files_with_glob_metachars(src_name);
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user