nushell/crates/nu-command/src/filesystem/ucp.rs
YizhePKU bdb6daa4b5
Migrate to a new PWD API (#12603)
This is the first PR towards migrating to a new `$env.PWD` API that
returns potentially un-canonicalized paths. Refer to PR #12515 for
motivations.

## New API: `EngineState::cwd()`

The goal of the new API is to cover both parse-time and runtime use
case, and avoid unintentional misuse. It takes an `Option<Stack>` as
argument, which if supplied, will search for `$env.PWD` on the stack in
additional to the engine state. I think with this design, there's less
confusion over parse-time and runtime environments. If you have access
to a stack, just supply it; otherwise supply `None`.

## Deprecation of other PWD-related APIs

Other APIs are re-implemented using `EngineState::cwd()` and properly
documented. They're marked deprecated, but their behavior is unchanged.
Unused APIs are deleted, and code that accesses `$env.PWD` directly
without using an API is rewritten.

Deprecated APIs:

* `EngineState::current_work_dir()`
* `StateWorkingSet::get_cwd()`
* `env::current_dir()`
* `env::current_dir_str()`
* `env::current_dir_const()`
* `env::current_dir_str_const()`

Other changes:

* `EngineState::get_cwd()` (deleted)
* `StateWorkingSet::list_env()` (deleted)
* `repl::do_run_cmd()` (rewritten with `env::current_dir_str()`)

## `cd` and `pwd` now use logical paths by default

This pulls the changes from PR #12515. It's currently somewhat broken
because using non-canonicalized paths exposed a bug in our path
normalization logic (Issue #12602). Once that is fixed, this should
work.

## Future plans

This PR needs some tests. Which test helpers should I use, and where
should I put those tests?

I noticed that unquoted paths are expanded within `eval_filepath()` and
`eval_directory()` before they even reach the `cd` command. This means
every paths is expanded twice. Is this intended?

Once this PR lands, the plan is to review all usages of the deprecated
APIs and migrate them to `EngineState::cwd()`. In the meantime, these
usages are annotated with `#[allow(deprecated)]` to avoid breaking CI.

---------

Co-authored-by: Jakub Žádník <kubouch@gmail.com>
2024-05-03 14:33:09 +03:00

402 lines
14 KiB
Rust

use super::util::get_rest_for_glob_pattern;
#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir};
use std::path::PathBuf;
use uu_cp::{BackupMode, CopyMode, UpdateMode};
// TODO: related to uucore::error::set_exit_code(EXIT_ERR)
// const EXIT_ERR: i32 = 1;
#[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 usage(&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("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)
.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 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,
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let interactive = call.has_flag(engine_state, stack, "interactive")?;
let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? {
(UpdateMode::ReplaceIfOlder, CopyMode::Update)
} 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 {
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 = "freebsd",
target_os = "android",
target_os = "macos"
))]
let reflink_mode = uu_cp::ReflinkMode::Auto;
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "android",
target_os = "macos"
)))]
let reflink_mode = uu_cp::ReflinkMode::Never;
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
if paths.is_empty() {
return Err(ShellError::GenericError {
error: "Missing file operand".into(),
msg: "Missing file operand".into(),
span: Some(call.head),
help: Some("Please provide source and destination paths".into()),
inner: vec![],
});
}
if paths.len() == 1 {
return Err(ShellError::GenericError {
error: "Missing destination path".into(),
msg: format!(
"Missing destination path operand after {}",
paths[0].item.as_ref()
),
span: Some(paths[0].span),
help: None,
inner: vec![],
});
}
let target = paths.pop().expect("Should not be reached?");
let target_path = PathBuf::from(&nu_utils::strip_ansi_string_unlikely(
target.item.to_string(),
));
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
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::GenericError {
error: "is not a directory".into(),
msg: "is not a directory".into(),
span: Some(target.span),
help: None,
inner: vec![],
});
};
// paths now contains the sources
let mut sources: Vec<(Vec<PathBuf>, bool)> = Vec::new();
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, None)
.map(|f| f.1)?
.collect();
if exp_files.is_empty() {
return Err(ShellError::FileNotFound {
file: p.item.to_string(),
span: p.span,
});
};
let mut app_vals: Vec<PathBuf> = Vec::new();
for v in exp_files {
match v {
Ok(path) => {
if !recursive && path.is_dir() {
return Err(ShellError::GenericError {
error: "could_not_copy_directory".into(),
msg: "resolves to a directory (not copied)".into(),
span: Some(p.span),
help: Some(
"Directories must be copied using \"--recursive\"".into(),
),
inner: vec![],
});
};
app_vals.push(path)
}
Err(e) => return Err(e),
}
}
sources.push((app_vals, p.item.is_expand()));
}
// Make sure to send absolute paths to avoid uu_cp looking for cwd in std::env which is not
// supported in Nushell
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,
progress_bar: progress,
attributes_only: false,
backup: BackupMode::NoBackup,
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,
};
if let Err(error) = uu_cp::copy(&sources, &target_path, &options) {
match error {
// code should still be EXIT_ERR as does GNU cp
uu_cp::Error::NotAllFilesCopied => {}
_ => {
return Err(ShellError::GenericError {
error: format!("{}", error),
msg: format!("{}", error),
span: None,
help: None,
inner: vec![],
})
}
};
// TODO: What should we do in place of set_exit_code?
// uucore::error::set_exit_code(EXIT_ERR);
}
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 {
// By default preseerve only mode
Ok(uu_cp::Attributes {
mode: ATTR_SET,
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "android",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
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 = "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() {
use crate::test_examples;
test_examples(UCp {})
}
}