mirror of
https://github.com/nushell/nushell.git
synced 2024-12-23 23:49:44 +01:00
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::{
|
||||
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::*;
|
||||
|
@ -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");
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user