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