mirror of
https://github.com/nushell/nushell.git
synced 2025-04-11 14:58:21 +02:00
# Description Allows `Stack` to have a modified local `Config`, which is updated immediately when `$env.config` is assigned to. This means that even within a script, commands that come after `$env.config` changes will always see those changes in `Stack::get_config()`. Also fixed a lot of cases where `engine_state.get_config()` was used even when `Stack` was available. Closes #13324. # User-Facing Changes - Config changes apply immediately after the assignment is executed, rather than whenever config is read by a command that needs it. - Potentially slower performance when executing a lot of lines that change `$env.config` one after another. Recommended to get `$env.config` into a `mut` variable first and do modifications, then assign it back. - Much faster performance when executing a script that made modifications to `$env.config`, as the changes are only parsed once. # Tests + Formatting All passing. # After Submitting - [ ] release notes
464 lines
16 KiB
Rust
464 lines
16 KiB
Rust
use super::util::{get_rest_for_glob_pattern, try_interaction};
|
|
#[allow(deprecated)]
|
|
use nu_engine::{command_prelude::*, env::current_dir};
|
|
use nu_glob::MatchOptions;
|
|
use nu_path::expand_path_with;
|
|
use nu_protocol::{report_error_new, 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 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;
|
|
|
|
#[allow(deprecated)]
|
|
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 = stack.get_config(engine_state).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);
|
|
}
|
|
}
|
|
|
|
let iter = 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 {
|
|
Err(Error::new(ErrorKind::Other, &*e.to_string()))
|
|
} 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 '--permanent' 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());
|
|
Err(ShellError::RemoveNotPossible { msg, span })
|
|
} else if verbose {
|
|
let msg = if interactive && !confirmed {
|
|
"not deleted"
|
|
} else {
|
|
"deleted"
|
|
};
|
|
Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else {
|
|
let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
|
|
Err(ShellError::GenericError {
|
|
error,
|
|
msg: "cannot remove non-empty directory".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
})
|
|
}
|
|
} else {
|
|
let error = format!("no such file or directory: {:}", f.to_string_lossy());
|
|
Err(ShellError::GenericError {
|
|
error,
|
|
msg: "no such file or directory".into(),
|
|
span: Some(span),
|
|
help: None,
|
|
inner: vec![],
|
|
})
|
|
}
|
|
});
|
|
|
|
for result in iter {
|
|
engine_state.signals().check(call.head)?;
|
|
match result {
|
|
Ok(None) => {}
|
|
Ok(Some(msg)) => eprintln!("{msg}"),
|
|
Err(err) => report_error_new(engine_state, &err),
|
|
}
|
|
}
|
|
|
|
Ok(PipelineData::empty())
|
|
}
|