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 { rm(engine_state, stack, call) } fn examples(&self) -> Vec { 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 { 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 = 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 = 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()) }