1use super::util::try_interaction;
2use nu_engine::command_prelude::*;
3use nu_glob::MatchOptions;
4use nu_path::expand_path_with;
5use nu_protocol::{
6 NuGlob, report_shell_error,
7 shell_error::{self, generic::GenericError, io::IoError},
8};
9#[cfg(unix)]
10use std::os::unix::prelude::FileTypeExt;
11use std::{collections::HashMap, io::Error, path::PathBuf};
12
13const TRASH_SUPPORTED: bool = cfg!(all(
14 feature = "trash-support",
15 not(any(target_os = "android", target_os = "ios"))
16));
17
18#[derive(Clone)]
19pub struct Rm;
20
21impl Command for Rm {
22 fn name(&self) -> &str {
23 "rm"
24 }
25
26 fn description(&self) -> &str {
27 "Remove files and directories."
28 }
29
30 fn search_terms(&self) -> Vec<&str> {
31 vec!["delete", "remove", "del", "erase"]
32 }
33
34 fn signature(&self) -> Signature {
35 Signature::build("rm")
36 .input_output_types(vec![(Type::Nothing, Type::Nothing)])
37 .rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The file paths(s) to remove.")
38 .switch(
39 "trash",
40 "Move to the platform's trash instead of permanently deleting. not used on android and ios.",
41 Some('t'),
42 )
43 .switch(
44 "permanent",
45 "Delete permanently, ignoring the 'always_trash' config option. always enabled on android and ios.",
46 Some('p'),
47 )
48 .switch("recursive", "Delete subdirectories recursively.", Some('r'))
49 .switch("force", "Suppress error when no file.", Some('f'))
50 .switch("verbose", "Print names of deleted files.", Some('v'))
51 .switch("interactive", "Ask user to confirm action.", Some('i'))
52 .switch(
53 "interactive-once",
54 "Ask user to confirm action only once.",
55 Some('I'),
56 )
57 .switch("all", "Remove hidden files if '*' is provided.", Some('a'))
58 .category(Category::FileSystem)
59 }
60
61 fn run(
62 &self,
63 engine_state: &EngineState,
64 stack: &mut Stack,
65 call: &Call,
66 _input: PipelineData,
67 ) -> Result<PipelineData, ShellError> {
68 rm(engine_state, stack, call)
69 }
70
71 fn examples(&self) -> Vec<Example<'_>> {
72 let mut examples = vec![Example {
73 description: "Delete, or move a file to the trash (based on the 'always_trash' config option).",
74 example: "rm file.txt",
75 result: None,
76 }];
77 if TRASH_SUPPORTED {
78 examples.append(&mut vec![
79 Example {
80 description: "Move a file to the trash.",
81 example: "rm --trash file.txt",
82 result: None,
83 },
84 Example {
85 description:
86 "Delete a file permanently, even if the 'always_trash' config option is true.",
87 example: "rm --permanent file.txt",
88 result: None,
89 },
90 ]);
91 }
92 examples.push(Example {
93 description: "Delete a file, ignoring 'file not found' errors.",
94 example: "rm --force file.txt",
95 result: None,
96 });
97 examples.push(Example {
98 description: "Delete all 0KB files in the current directory.",
99 example: "ls | where size == 0KB and type == file | each { rm $in.name } | null",
100 result: None,
101 });
102 examples
103 }
104}
105
106fn rm(
107 engine_state: &EngineState,
108 stack: &mut Stack,
109 call: &Call,
110) -> Result<PipelineData, ShellError> {
111 let trash = call.has_flag(engine_state, stack, "trash")?;
112 let permanent = call.has_flag(engine_state, stack, "permanent")?;
113 let recursive = call.has_flag(engine_state, stack, "recursive")?;
114 let force = call.has_flag(engine_state, stack, "force")?;
115 let verbose = call.has_flag(engine_state, stack, "verbose")?;
116 let interactive = call.has_flag(engine_state, stack, "interactive")?;
117 let interactive_once = call.has_flag(engine_state, stack, "interactive-once")? && !interactive;
118 let all = call.has_flag(engine_state, stack, "all")?;
119 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
120
121 if paths.is_empty() {
122 return Err(ShellError::MissingParameter {
123 param_name: "requires file paths".to_string(),
124 span: call.head,
125 });
126 }
127
128 let mut unique_argument_check = None;
129
130 let currentdir_path = engine_state.cwd(Some(stack))?.into_std_path_buf();
131
132 let home: Option<String> = nu_path::home_dir().map(|path| {
133 {
134 if path.exists() {
135 nu_path::absolute_with(&path, ¤tdir_path).unwrap_or(path.into())
136 } else {
137 path.into()
138 }
139 }
140 .to_string_lossy()
141 .into()
142 });
143
144 for (idx, path) in paths.clone().into_iter().enumerate() {
145 if let Some(ref home) = home
146 && expand_path_with(path.item.as_ref(), ¤tdir_path, path.item.is_expand())
147 .to_string_lossy()
148 .as_ref()
149 == home.as_str()
150 {
151 unique_argument_check = Some(path.span);
152 }
153 let corrected_path = Spanned {
154 item: match path.item {
155 NuGlob::DoNotExpand(s) => {
156 NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s))
157 }
158 NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)),
159 },
160 span: path.span,
161 };
162 let _ = std::mem::replace(&mut paths[idx], corrected_path);
163 }
164
165 let span = call.head;
166 let rm_always_trash = stack.get_config(engine_state).rm.always_trash;
167
168 if !TRASH_SUPPORTED {
169 if rm_always_trash {
170 return Err(ShellError::Generic(GenericError::new(
171 "Cannot execute `rm`; the current configuration specifies \
172 `always_trash = true`, but the current nu executable was not \
173 built with feature `trash_support`.",
174 "trash required to be true but not supported",
175 span,
176 )));
177 } else if trash {
178 return Err(ShellError::Generic(GenericError::new(
179 "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform",
180 "this option is only available if nu is built with the `trash-support` feature and the platform supports trash",
181 span,
182 )));
183 }
184 }
185
186 if paths.is_empty() {
187 return Err(ShellError::Generic(GenericError::new(
188 "rm requires target paths",
189 "needs parameter",
190 span,
191 )));
192 }
193
194 if unique_argument_check.is_some() && !(interactive_once || interactive) {
195 return Err(ShellError::Generic(GenericError::new(
196 "You are trying to remove your home dir",
197 "If you really want to remove your home dir, please use -I or -i",
198 unique_argument_check.unwrap_or(call.head),
199 )));
200 }
201
202 let targets_span = Span::new(
203 paths
204 .iter()
205 .map(|x| x.span.start)
206 .min()
207 .expect("targets were empty"),
208 paths
209 .iter()
210 .map(|x| x.span.end)
211 .max()
212 .expect("targets were empty"),
213 );
214
215 let (mut target_exists, mut empty_span) = (false, call.head);
216 let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
217
218 let glob_options = if all {
219 None
220 } else {
221 let glob_options = MatchOptions {
222 require_literal_leading_dot: true,
223 ..Default::default()
224 };
225
226 Some(glob_options)
227 };
228
229 for target in paths {
230 let path = expand_path_with(
231 target.item.as_ref(),
232 ¤tdir_path,
233 target.item.is_expand(),
234 );
235
236 let raw = target.item.as_ref();
240 if raw.ends_with('/') || raw.ends_with(std::path::MAIN_SEPARATOR) {
241 let without_sep = raw
242 .trim_end_matches('/')
243 .trim_end_matches(std::path::MAIN_SEPARATOR);
244 let symlink_check =
245 expand_path_with(without_sep, ¤tdir_path, target.item.is_expand());
246 if symlink_check
247 .symlink_metadata()
248 .map(|m| m.file_type().is_symlink())
249 .unwrap_or(false)
250 {
251 return Err(ShellError::Generic(
252 GenericError::new(
253 format!("Cannot remove `{}`: is a directory", raw),
254 "is a directory",
255 target.span,
256 )
257 .with_help(format!(
258 "use `rm {}` without the trailing slash to remove the symlink itself",
259 without_sep
260 )),
261 ));
262 }
263 }
264
265 if currentdir_path.to_string_lossy() == path.to_string_lossy()
266 || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
267 {
268 return Err(ShellError::Generic(GenericError::new(
269 "Cannot remove any parent directory",
270 "cannot remove any parent directory",
271 target.span,
272 )));
273 }
274
275 match nu_engine::glob_from(
276 &target,
277 ¤tdir_path,
278 call.head,
279 glob_options,
280 engine_state.signals().clone(),
281 ) {
282 Ok(files) => {
283 for file in files.1 {
284 match file {
285 Ok(f) => {
286 if !target_exists {
287 target_exists = true;
288 }
289
290 let name = f.display().to_string();
294 if name.ends_with("/.") || name.ends_with("/..") {
295 continue;
296 }
297
298 all_targets
299 .entry(nu_path::expand_path_with(
300 f,
301 ¤tdir_path,
302 target.item.is_expand(),
303 ))
304 .or_insert_with(|| target.span);
305 }
306 Err(e) => {
307 return Err(ShellError::Generic(GenericError::new(
308 format!("Could not remove {:}", path.to_string_lossy()),
309 e.to_string(),
310 target.span,
311 )));
312 }
313 }
314 }
315
316 if !target_exists && empty_span.eq(&call.head) {
318 empty_span = target.span;
319 }
320 }
321 Err(e) => {
322 if !(force
325 && matches!(
326 e,
327 ShellError::Io(IoError {
328 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
329 ..
330 })
331 ))
332 {
333 return Err(e);
334 }
335 }
336 };
337 }
338
339 if all_targets.is_empty() && !force {
340 return Err(ShellError::Generic(GenericError::new(
341 "File(s) not found",
342 "File(s) not found",
343 targets_span,
344 )));
345 }
346
347 if interactive_once {
348 let (interaction, confirmed) = try_interaction(
349 interactive_once,
350 format!("rm: remove {} files? ", all_targets.len()),
351 );
352 if let Err(e) = interaction {
353 return Err(ShellError::Generic(GenericError::new_internal(
354 format!("Error during interaction: {e:}"),
355 "could not move",
356 )));
357 } else if !confirmed {
358 return Ok(PipelineData::empty());
359 }
360 }
361
362 let iter = all_targets.into_iter().map(move |(f, span)| {
363 let is_empty = || match f.read_dir() {
364 Ok(mut p) => p.next().is_none(),
365 Err(_) => false,
366 };
367
368 if let Ok(metadata) = f.symlink_metadata() {
369 #[cfg(unix)]
370 let is_socket = metadata.file_type().is_socket();
371 #[cfg(unix)]
372 let is_fifo = metadata.file_type().is_fifo();
373
374 #[cfg(not(unix))]
375 let is_socket = false;
376 #[cfg(not(unix))]
377 let is_fifo = false;
378
379 if metadata.is_file()
380 || metadata.file_type().is_symlink()
381 || recursive
382 || is_socket
383 || is_fifo
384 || is_empty()
385 {
386 let (interaction, confirmed) = try_interaction(
387 interactive,
388 format!("rm: remove '{}'? ", f.to_string_lossy()),
389 );
390
391 let result = if let Err(e) = interaction {
392 Err(Error::other(&*e.to_string()))
393 } else if interactive && !confirmed {
394 Ok(())
395 } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
396 #[cfg(all(
397 feature = "trash-support",
398 not(any(target_os = "android", target_os = "ios"))
399 ))]
400 {
401 trash::delete(&f).map_err(|e: trash::Error| {
402 Error::other(format!("{e:?}\nTry '--permanent' flag"))
403 })
404 }
405
406 #[cfg(any(
409 not(feature = "trash-support"),
410 target_os = "android",
411 target_os = "ios"
412 ))]
413 {
414 unreachable!()
415 }
416 } else if metadata.is_symlink() {
417 #[cfg(windows)]
420 {
421 use std::os::windows::fs::FileTypeExt;
422 if metadata.file_type().is_symlink_dir() {
423 std::fs::remove_dir(&f)
424 } else {
425 std::fs::remove_file(&f)
426 }
427 }
428
429 #[cfg(not(windows))]
430 std::fs::remove_file(&f)
431 } else if metadata.is_file() || is_socket || is_fifo {
432 std::fs::remove_file(&f)
433 } else {
434 std::fs::remove_dir_all(&f)
435 };
436
437 if let Err(e) = result {
438 let original_error = e.to_string();
439 Err(ShellError::Io(IoError::new_with_additional_context(
440 e,
441 span,
442 f,
443 original_error,
444 )))
445 } else if verbose {
446 let msg = if interactive && !confirmed {
447 "not deleted"
448 } else {
449 "deleted"
450 };
451 Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
452 } else {
453 Ok(None)
454 }
455 } else {
456 let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
457 Err(ShellError::Generic(GenericError::new(
458 error,
459 "cannot remove non-empty directory",
460 span,
461 )))
462 }
463 } else {
464 let error = format!("no such file or directory: {:}", f.to_string_lossy());
465 Err(ShellError::Generic(GenericError::new(
466 error,
467 "no such file or directory",
468 span,
469 )))
470 }
471 });
472
473 let mut cmd_result = Ok(PipelineData::empty());
474 for result in iter {
475 engine_state.signals().check(&call.head)?;
476 match result {
477 Ok(None) => {}
478 Ok(Some(msg)) => eprintln!("{msg}"),
479 Err(err) => {
480 if !(force
481 && matches!(
482 err,
483 ShellError::Io(IoError {
484 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
485 ..
486 })
487 ))
488 {
489 if cmd_result.is_ok() {
490 cmd_result = Err(err);
491 } else {
492 report_shell_error(Some(stack), engine_state, &err)
493 }
494 }
495 }
496 }
497 }
498
499 cmd_result
500}