use nu_engine::command_prelude::*;
use nu_glob::MatchOptions;
use nu_protocol::{
NuGlob,
shell_error::{self, generic::GenericError, io::IoError},
};
use std::path::{Path, PathBuf};
use uu_cp::{BackupMode, CopyMode, CpError, UpdateMode};
use uucore::{localized_help_template, translate};
#[cfg(not(target_os = "windows"))]
const PATH_SEPARATOR: &str = "/";
#[cfg(target_os = "windows")]
const PATH_SEPARATOR: &str = "\\";
#[derive(Clone)]
pub struct UCp;
impl Command for UCp {
fn name(&self) -> &str {
"cp"
}
fn description(&self) -> &str {
"Copy files using uutils/coreutils cp."
}
fn search_terms(&self) -> Vec<&str> {
vec!["copy", "file", "files", "coreutils"]
}
fn signature(&self) -> Signature {
Signature::build("cp")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch("recursive", "Copy directories recursively.", Some('r'))
.switch(
"no-dereference",
"Copy symbolic links as symbolic links instead of their targets.",
Some('P'),
)
.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(
"update",
"Copy only when the SOURCE file is newer than the destination file or when the destination file is missing.",
Some('u')
)
.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)
.switch("all", "Copy hidden files if '*' is provided.", Some('a'))
.rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "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: "cp myfile dir_b",
result: None,
},
Example {
description: "Recursively copy dir_a to dir_b.",
example: "cp -r dir_a dir_b",
result: None,
},
Example {
description: "Recursively copy dir_a to dir_b, and print the feedbacks.",
example: "cp -r -v dir_a dir_b",
result: None,
},
Example {
description: "Move many files into a directory.",
example: "cp *.txt dir_a",
result: None,
},
Example {
description: "Copy only if source file is newer than target file.",
example: "cp -u myfile newfile",
result: None,
},
Example {
description: "Copy file preserving mode and timestamps attributes.",
example: "cp --preserve [ mode timestamps ] myfile newfile",
result: None,
},
Example {
description: "Copy file erasing all attributes.",
example: "cp --preserve [] myfile newfile",
result: None,
},
Example {
description: "Copy a symbolic link without copying the target.",
example: "cp --no-dereference link-to-file newlink",
result: None,
},
Example {
description: "Copy file to a directory three levels above its current location.",
example: "cp myfile ....",
result: None,
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let _ = localized_help_template("cp");
let interactive = call.has_flag(engine_state, stack, "interactive")?;
let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
(UpdateMode::IfOlder, CopyMode::Update)
} else {
(UpdateMode::All, 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 no_dereference = call.has_flag(engine_state, stack, "no-dereference")?;
let verbose = call.has_flag(engine_state, stack, "verbose")?;
let preserve: Option<Value> = call.get_flag(engine_state, stack, "preserve")?;
let all = call.has_flag(engine_state, stack, "all")?;
let debug = call.has_flag(engine_state, stack, "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 = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
if paths.is_empty() {
return Err(ShellError::Generic(
GenericError::new("Missing file operand", "Missing file operand", call.head)
.with_help("Please provide source and destination paths"),
));
}
if paths.len() == 1 {
return Err(ShellError::Generic(GenericError::new(
"Missing destination path",
format!(
"Missing destination path operand after {}",
paths[0].item.as_ref()
),
paths[0].span,
)));
}
let target = paths.pop().expect("Should not be reached?");
let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
target.item.to_string(),
));
let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
let target_path = nu_path::expand_path_with(target_path, &cwd, target.item.is_expand());
if target.item.as_ref().ends_with(PATH_SEPARATOR) && !target_path.is_dir() {
return Err(ShellError::Generic(GenericError::new(
"is not a directory",
"is not a directory",
target.span,
)));
};
let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
let glob_options = if all {
None
} else {
let glob_options = MatchOptions {
require_literal_leading_dot: true,
..Default::default()
};
Some(glob_options)
};
for mut p in paths {
p.item = p.item.strip_ansi_string_unlikely();
let exp_files: Vec<Result<PathBuf, ShellError>> = nu_engine::glob_from(
&p,
&cwd,
call.head,
glob_options,
engine_state.signals().clone(),
)
.map(|f| f.1)?
.collect();
if exp_files.is_empty() {
return Err(ShellError::Io(IoError::new(
shell_error::io::ErrorKind::FileNotFound,
p.span,
PathBuf::from(p.item.to_string()),
)));
};
let mut app_vals: Vec<PathBuf> = Vec::new();
for v in exp_files {
match v {
Ok(path) => {
if !recursive && source_path_is_dir(&path, !no_dereference) {
return Err(ShellError::Generic(
GenericError::new(
"could_not_copy_directory",
"resolves to a directory (not copied)",
p.span,
)
.with_help("Directories must be copied using \"--recursive\""),
));
};
app_vals.push(path)
}
Err(e) => return Err(e),
}
}
sources.push((app_vals, p.item.is_expand()));
}
for (sources, need_expand_tilde) in sources.iter_mut() {
for src in sources.iter_mut() {
if !src.is_absolute() {
*src = nu_path::expand_path_with(&*src, &cwd, *need_expand_tilde);
}
}
}
let sources: Vec<PathBuf> = sources.into_iter().flat_map(|x| x.0).collect();
let attributes = make_attributes(preserve)?;
let options = uu_cp::Options {
overwrite,
reflink_mode,
recursive,
debug,
attributes,
verbose: verbose || debug,
dereference: !recursive && !no_dereference,
progress_bar: progress,
attributes_only: false,
backup: BackupMode::None,
copy_contents: false,
cli_dereference: false,
copy_mode,
no_target_dir: false,
one_file_system: false,
parents: false,
sparse_mode: uu_cp::SparseMode::Auto,
strip_trailing_slashes: false,
backup_suffix: String::from("~"),
target_dir: None,
update,
set_selinux_context: false,
context: None,
};
if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
match error {
CpError::NotAllFilesCopied => {}
_ => {
return Err(ShellError::Generic(GenericError::new_internal(
format!("{error}"),
translate!(&error.to_string()),
)));
}
};
}
Ok(PipelineData::empty())
}
}
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 = "freebsd",
target_os = "android",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
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 {
Ok(uu_cp::Attributes::NONE)
}
}
fn source_path_is_dir(path: &Path, follow_symlink: bool) -> bool {
if follow_symlink {
return path.is_dir();
}
matches!(path.symlink_metadata(),
Ok(metadata) if metadata.file_type().is_dir()
)
}
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 = "freebsd",
target_os = "android",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
"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::*;
#[test]
fn test_examples() -> nu_test_support::Result {
nu_test_support::test().examples(UCp)
}
}