forked from extern/nushell
# Description When implementing a `Command`, one must also import all the types present in the function signatures for `Command`. This makes it so that we often import the same set of types in each command implementation file. E.g., something like this: ```rust use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, }; ``` This PR adds the `nu_engine::command_prelude` module which contains the necessary and commonly used types to implement a `Command`: ```rust // command_prelude.rs pub use crate::CallExt; pub use nu_protocol::{ ast::{Call, CellPath}, engine::{Command, EngineState, Stack}, record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; ``` This should reduce the boilerplate needed to implement a command and also gives us a place to track the breadth of the `Command` API. I tried to be conservative with what went into the prelude modules, since it might be hard/annoying to remove items from the prelude in the future. Let me know if something should be included or excluded.
468 lines
17 KiB
Rust
468 lines
17 KiB
Rust
use super::util::{get_rest_for_glob_pattern, try_interaction};
|
|
use nu_engine::{command_prelude::*, env::current_dir};
|
|
use nu_glob::MatchOptions;
|
|
use nu_path::expand_path_with;
|
|
use nu_protocol::NuGlob;
|
|
#[cfg(unix)]
|
|
use std::os::unix::prelude::FileTypeExt;
|
|
use std::{
|
|
collections::HashMap,
|
|
io::{Error, ErrorKind},
|
|
path::PathBuf,
|
|
};
|
|
|
|
const TRASH_SUPPORTED: bool = cfg!(all(
|
|
feature = "trash-support",
|
|
not(any(target_os = "android", target_os = "ios"))
|
|
));
|
|
|
|
#[derive(Clone)]
|
|
pub struct Rm;
|
|
|
|
impl Command for Rm {
|
|
fn name(&self) -> &str {
|
|
"rm"
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"Remove files and directories."
|
|
}
|
|
|
|
fn search_terms(&self) -> Vec<&str> {
|
|
vec!["delete", "remove"]
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("rm")
|
|
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
|
.rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The file paths(s) to remove.")
|
|
.switch(
|
|
"trash",
|
|
"move to the platform's trash instead of permanently deleting. not used on android and ios",
|
|
Some('t'),
|
|
)
|
|
.switch(
|
|
"permanent",
|
|
"delete permanently, ignoring the 'always_trash' config option. always enabled on android and ios",
|
|
Some('p'),
|
|
)
|
|
.switch("recursive", "delete subdirectories recursively", Some('r'))
|
|
.switch("force", "suppress error when no file", Some('f'))
|
|
.switch("verbose", "print names of deleted files", Some('v'))
|
|
.switch("interactive", "ask user to confirm action", Some('i'))
|
|
.switch(
|
|
"interactive-once",
|
|
"ask user to confirm action only once",
|
|
Some('I'),
|
|
)
|
|
.category(Category::FileSystem)
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
_input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
rm(engine_state, stack, call)
|
|
}
|
|
|
|
fn examples(&self) -> Vec<Example> {
|
|
let mut examples = vec![Example {
|
|
description:
|
|
"Delete, or move a file to the trash (based on the 'always_trash' config option)",
|
|
example: "rm file.txt",
|
|
result: None,
|
|
}];
|
|
if TRASH_SUPPORTED {
|
|
examples.append(&mut vec![
|
|
Example {
|
|
description: "Move a file to the trash",
|
|
example: "rm --trash file.txt",
|
|
result: None,
|
|
},
|
|
Example {
|
|
description:
|
|
"Delete a file permanently, even if the 'always_trash' config option is true",
|
|
example: "rm --permanent file.txt",
|
|
result: None,
|
|
},
|
|
]);
|
|
}
|
|
examples.push(Example {
|
|
description: "Delete a file, ignoring 'file not found' errors",
|
|
example: "rm --force file.txt",
|
|
result: None,
|
|
});
|
|
examples.push(Example {
|
|
description: "Delete all 0KB files in the current directory",
|
|
example: "ls | where size == 0KB and type == file | each { rm $in.name } | null",
|
|
result: None,
|
|
});
|
|
examples
|
|
}
|
|
}
|
|
|
|
fn rm(
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let trash = call.has_flag(engine_state, stack, "trash")?;
|
|
let permanent = call.has_flag(engine_state, stack, "permanent")?;
|
|
let recursive = call.has_flag(engine_state, stack, "recursive")?;
|
|
let force = call.has_flag(engine_state, stack, "force")?;
|
|
let verbose = call.has_flag(engine_state, stack, "verbose")?;
|
|
let interactive = call.has_flag(engine_state, stack, "interactive")?;
|
|
let interactive_once = call.has_flag(engine_state, stack, "interactive-once")? && !interactive;
|
|
|
|
let ctrlc = engine_state.ctrlc.clone();
|
|
|
|
let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?;
|
|
|
|
if paths.is_empty() {
|
|
return Err(ShellError::MissingParameter {
|
|
param_name: "requires file paths".to_string(),
|
|
span: call.head,
|
|
});
|
|
}
|
|
|
|
let mut unique_argument_check = None;
|
|
|
|
let currentdir_path = current_dir(engine_state, stack)?;
|
|
|
|
let home: Option<String> = nu_path::home_dir().map(|path| {
|
|
{
|
|
if path.exists() {
|
|
match nu_path::canonicalize_with(&path, ¤tdir_path) {
|
|
Ok(canon_path) => canon_path,
|
|
Err(_) => path,
|
|
}
|
|
} else {
|
|
path
|
|
}
|
|
}
|
|
.to_string_lossy()
|
|
.into()
|
|
});
|
|
|
|
for (idx, path) in paths.clone().into_iter().enumerate() {
|
|
if let Some(ref home) = home {
|
|
if expand_path_with(path.item.as_ref(), ¤tdir_path, path.item.is_expand())
|
|
.to_string_lossy()
|
|
.as_ref()
|
|
== home.as_str()
|
|
{
|
|
unique_argument_check = Some(path.span);
|
|
}
|
|
}
|
|
let corrected_path = Spanned {
|
|
item: match path.item {
|
|
NuGlob::DoNotExpand(s) => {
|
|
NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s))
|
|
}
|
|
NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)),
|
|
},
|
|
span: path.span,
|
|
};
|
|
let _ = std::mem::replace(&mut paths[idx], corrected_path);
|
|
}
|
|
|
|
let span = call.head;
|
|
let rm_always_trash = engine_state.get_config().rm_always_trash;
|
|
|
|
if !TRASH_SUPPORTED {
|
|
if rm_always_trash {
|
|
return Err(ShellError::GenericError {
|
|
error: "Cannot execute `rm`; the current configuration specifies \
|
|
`always_trash = true`, but the current nu executable was not \
|
|
built with feature `trash_support`."
|
|
.into(),
|
|
msg: "trash required to be true but not supported".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
} else if trash {
|
|
return Err(ShellError::GenericError{
|
|
error: "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform"
|
|
.into(),
|
|
msg: "this option is only available if nu is built with the `trash-support` feature and the platform supports trash"
|
|
.into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
}
|
|
|
|
if paths.is_empty() {
|
|
return Err(ShellError::GenericError {
|
|
error: "rm requires target paths".into(),
|
|
msg: "needs parameter".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
if unique_argument_check.is_some() && !(interactive_once || interactive) {
|
|
return Err(ShellError::GenericError {
|
|
error: "You are trying to remove your home dir".into(),
|
|
msg: "If you really want to remove your home dir, please use -I or -i".into(),
|
|
span: unique_argument_check,
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
let targets_span = Span::new(
|
|
paths
|
|
.iter()
|
|
.map(|x| x.span.start)
|
|
.min()
|
|
.expect("targets were empty"),
|
|
paths
|
|
.iter()
|
|
.map(|x| x.span.end)
|
|
.max()
|
|
.expect("targets were empty"),
|
|
);
|
|
|
|
let (mut target_exists, mut empty_span) = (false, call.head);
|
|
let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
|
|
|
|
for target in paths {
|
|
let path = expand_path_with(
|
|
target.item.as_ref(),
|
|
¤tdir_path,
|
|
target.item.is_expand(),
|
|
);
|
|
if currentdir_path.to_string_lossy() == path.to_string_lossy()
|
|
|| currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
|
|
{
|
|
return Err(ShellError::GenericError {
|
|
error: "Cannot remove any parent directory".into(),
|
|
msg: "cannot remove any parent directory".into(),
|
|
span: Some(target.span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
match nu_engine::glob_from(
|
|
&target,
|
|
¤tdir_path,
|
|
call.head,
|
|
Some(MatchOptions {
|
|
require_literal_leading_dot: true,
|
|
..Default::default()
|
|
}),
|
|
) {
|
|
Ok(files) => {
|
|
for file in files.1 {
|
|
match file {
|
|
Ok(f) => {
|
|
if !target_exists {
|
|
target_exists = true;
|
|
}
|
|
|
|
// It is not appropriate to try and remove the
|
|
// current directory or its parent when using
|
|
// glob patterns.
|
|
let name = f.display().to_string();
|
|
if name.ends_with("/.") || name.ends_with("/..") {
|
|
continue;
|
|
}
|
|
|
|
all_targets
|
|
.entry(nu_path::expand_path_with(
|
|
f,
|
|
¤tdir_path,
|
|
target.item.is_expand(),
|
|
))
|
|
.or_insert_with(|| target.span);
|
|
}
|
|
Err(e) => {
|
|
return Err(ShellError::GenericError {
|
|
error: format!("Could not remove {:}", path.to_string_lossy()),
|
|
msg: e.to_string(),
|
|
span: Some(target.span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Target doesn't exists
|
|
if !target_exists && empty_span.eq(&call.head) {
|
|
empty_span = target.span;
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// glob_from may canonicalize path and return `DirectoryNotFound`
|
|
// nushell should suppress the error if `--force` is used.
|
|
if !(force && matches!(e, ShellError::DirectoryNotFound { .. })) {
|
|
return Err(e);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
if all_targets.is_empty() && !force {
|
|
return Err(ShellError::GenericError {
|
|
error: "File(s) not found".into(),
|
|
msg: "File(s) not found".into(),
|
|
span: Some(targets_span),
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
}
|
|
|
|
if interactive_once {
|
|
let (interaction, confirmed) = try_interaction(
|
|
interactive_once,
|
|
format!("rm: remove {} files? ", all_targets.len()),
|
|
);
|
|
if let Err(e) = interaction {
|
|
return Err(ShellError::GenericError {
|
|
error: format!("Error during interaction: {e:}"),
|
|
msg: "could not move".into(),
|
|
span: None,
|
|
help: None,
|
|
inner: vec![],
|
|
});
|
|
} else if !confirmed {
|
|
return Ok(PipelineData::Empty);
|
|
}
|
|
}
|
|
|
|
all_targets
|
|
.into_iter()
|
|
.map(move |(f, span)| {
|
|
let is_empty = || match f.read_dir() {
|
|
Ok(mut p) => p.next().is_none(),
|
|
Err(_) => false,
|
|
};
|
|
|
|
if let Ok(metadata) = f.symlink_metadata() {
|
|
#[cfg(unix)]
|
|
let is_socket = metadata.file_type().is_socket();
|
|
#[cfg(unix)]
|
|
let is_fifo = metadata.file_type().is_fifo();
|
|
|
|
#[cfg(not(unix))]
|
|
let is_socket = false;
|
|
#[cfg(not(unix))]
|
|
let is_fifo = false;
|
|
|
|
if metadata.is_file()
|
|
|| metadata.file_type().is_symlink()
|
|
|| recursive
|
|
|| is_socket
|
|
|| is_fifo
|
|
|| is_empty()
|
|
{
|
|
let (interaction, confirmed) = try_interaction(
|
|
interactive,
|
|
format!("rm: remove '{}'? ", f.to_string_lossy()),
|
|
);
|
|
|
|
let result = if let Err(e) = interaction {
|
|
let e = Error::new(ErrorKind::Other, &*e.to_string());
|
|
Err(e)
|
|
} else if interactive && !confirmed {
|
|
Ok(())
|
|
} else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
|
|
#[cfg(all(
|
|
feature = "trash-support",
|
|
not(any(target_os = "android", target_os = "ios"))
|
|
))]
|
|
{
|
|
trash::delete(&f).map_err(|e: trash::Error| {
|
|
Error::new(ErrorKind::Other, format!("{e:?}\nTry '--trash' flag"))
|
|
})
|
|
}
|
|
|
|
// Should not be reachable since we error earlier if
|
|
// these options are given on an unsupported platform
|
|
#[cfg(any(
|
|
not(feature = "trash-support"),
|
|
target_os = "android",
|
|
target_os = "ios"
|
|
))]
|
|
{
|
|
unreachable!()
|
|
}
|
|
} else if metadata.is_symlink() {
|
|
// In Windows, symlink pointing to a directory can be removed using
|
|
// std::fs::remove_dir instead of std::fs::remove_file.
|
|
#[cfg(windows)]
|
|
{
|
|
f.metadata().and_then(|metadata| {
|
|
if metadata.is_dir() {
|
|
std::fs::remove_dir(&f)
|
|
} else {
|
|
std::fs::remove_file(&f)
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
std::fs::remove_file(&f)
|
|
} else if metadata.is_file() || is_socket || is_fifo {
|
|
std::fs::remove_file(&f)
|
|
} else {
|
|
std::fs::remove_dir_all(&f)
|
|
};
|
|
|
|
if let Err(e) = result {
|
|
let msg = format!("Could not delete {:}: {e:}", f.to_string_lossy());
|
|
Value::error(ShellError::RemoveNotPossible { msg, span }, span)
|
|
} else if verbose {
|
|
let msg = if interactive && !confirmed {
|
|
"not deleted"
|
|
} else {
|
|
"deleted"
|
|
};
|
|
let val = format!("{} {:}", msg, f.to_string_lossy());
|
|
Value::string(val, span)
|
|
} else {
|
|
Value::nothing(span)
|
|
}
|
|
} else {
|
|
let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
|
|
Value::error(
|
|
ShellError::GenericError {
|
|
error,
|
|
msg: "cannot remove non-empty directory".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
},
|
|
span,
|
|
)
|
|
}
|
|
} else {
|
|
let error = format!("no such file or directory: {:}", f.to_string_lossy());
|
|
Value::error(
|
|
ShellError::GenericError {
|
|
error,
|
|
msg: "no such file or directory".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
},
|
|
span,
|
|
)
|
|
}
|
|
})
|
|
.filter(|x| !matches!(x.get_type(), Type::Nothing))
|
|
.into_pipeline_data(ctrlc)
|
|
.print_not_formatted(engine_state, false, true)?;
|
|
|
|
Ok(PipelineData::empty())
|
|
}
|