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