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 .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::GenericError {
171 error: "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 .into(),
175 msg: "trash required to be true but not supported".into(),
176 span: Some(span),
177 help: None,
178 inner: vec![],
179 });
180 } else if trash {
181 return Err(ShellError::GenericError{
182 error: "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform"
183 .into(),
184 msg: "this option is only available if nu is built with the `trash-support` feature and the platform supports trash"
185 .into(),
186 span: Some(span),
187 help: None,
188 inner: vec![],
189 });
190 }
191 }
192
193 if paths.is_empty() {
194 return Err(ShellError::GenericError {
195 error: "rm requires target paths".into(),
196 msg: "needs parameter".into(),
197 span: Some(span),
198 help: None,
199 inner: vec![],
200 });
201 }
202
203 if unique_argument_check.is_some() && !(interactive_once || interactive) {
204 return Err(ShellError::GenericError {
205 error: "You are trying to remove your home dir".into(),
206 msg: "If you really want to remove your home dir, please use -I or -i".into(),
207 span: unique_argument_check,
208 help: None,
209 inner: vec![],
210 });
211 }
212
213 let targets_span = Span::new(
214 paths
215 .iter()
216 .map(|x| x.span.start)
217 .min()
218 .expect("targets were empty"),
219 paths
220 .iter()
221 .map(|x| x.span.end)
222 .max()
223 .expect("targets were empty"),
224 );
225
226 let (mut target_exists, mut empty_span) = (false, call.head);
227 let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
228
229 let glob_options = if all {
230 None
231 } else {
232 let glob_options = MatchOptions {
233 require_literal_leading_dot: true,
234 ..Default::default()
235 };
236
237 Some(glob_options)
238 };
239
240 for target in paths {
241 let path = expand_path_with(
242 target.item.as_ref(),
243 ¤tdir_path,
244 target.item.is_expand(),
245 );
246 if currentdir_path.to_string_lossy() == path.to_string_lossy()
247 || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
248 {
249 return Err(ShellError::GenericError {
250 error: "Cannot remove any parent directory".into(),
251 msg: "cannot remove any parent directory".into(),
252 span: Some(target.span),
253 help: None,
254 inner: vec![],
255 });
256 }
257
258 match nu_engine::glob_from(
259 &target,
260 ¤tdir_path,
261 call.head,
262 glob_options,
263 engine_state.signals().clone(),
264 ) {
265 Ok(files) => {
266 for file in files.1 {
267 match file {
268 Ok(f) => {
269 if !target_exists {
270 target_exists = true;
271 }
272
273 let name = f.display().to_string();
277 if name.ends_with("/.") || name.ends_with("/..") {
278 continue;
279 }
280
281 all_targets
282 .entry(nu_path::expand_path_with(
283 f,
284 ¤tdir_path,
285 target.item.is_expand(),
286 ))
287 .or_insert_with(|| target.span);
288 }
289 Err(e) => {
290 return Err(ShellError::GenericError {
291 error: format!("Could not remove {:}", path.to_string_lossy()),
292 msg: e.to_string(),
293 span: Some(target.span),
294 help: None,
295 inner: vec![],
296 });
297 }
298 }
299 }
300
301 if !target_exists && empty_span.eq(&call.head) {
303 empty_span = target.span;
304 }
305 }
306 Err(e) => {
307 if !(force
310 && matches!(
311 e,
312 ShellError::Io(IoError {
313 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
314 ..
315 })
316 ))
317 {
318 return Err(e);
319 }
320 }
321 };
322 }
323
324 if all_targets.is_empty() && !force {
325 return Err(ShellError::GenericError {
326 error: "File(s) not found".into(),
327 msg: "File(s) not found".into(),
328 span: Some(targets_span),
329 help: None,
330 inner: vec![],
331 });
332 }
333
334 if interactive_once {
335 let (interaction, confirmed) = try_interaction(
336 interactive_once,
337 format!("rm: remove {} files? ", all_targets.len()),
338 );
339 if let Err(e) = interaction {
340 return Err(ShellError::GenericError {
341 error: format!("Error during interaction: {e:}"),
342 msg: "could not move".into(),
343 span: None,
344 help: None,
345 inner: vec![],
346 });
347 } else if !confirmed {
348 return Ok(PipelineData::empty());
349 }
350 }
351
352 let iter = all_targets.into_iter().map(move |(f, span)| {
353 let is_empty = || match f.read_dir() {
354 Ok(mut p) => p.next().is_none(),
355 Err(_) => false,
356 };
357
358 if let Ok(metadata) = f.symlink_metadata() {
359 #[cfg(unix)]
360 let is_socket = metadata.file_type().is_socket();
361 #[cfg(unix)]
362 let is_fifo = metadata.file_type().is_fifo();
363
364 #[cfg(not(unix))]
365 let is_socket = false;
366 #[cfg(not(unix))]
367 let is_fifo = false;
368
369 if metadata.is_file()
370 || metadata.file_type().is_symlink()
371 || recursive
372 || is_socket
373 || is_fifo
374 || is_empty()
375 {
376 let (interaction, confirmed) = try_interaction(
377 interactive,
378 format!("rm: remove '{}'? ", f.to_string_lossy()),
379 );
380
381 let result = if let Err(e) = interaction {
382 Err(Error::other(&*e.to_string()))
383 } else if interactive && !confirmed {
384 Ok(())
385 } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
386 #[cfg(all(
387 feature = "trash-support",
388 not(any(target_os = "android", target_os = "ios"))
389 ))]
390 {
391 trash::delete(&f).map_err(|e: trash::Error| {
392 Error::other(format!("{e:?}\nTry '--permanent' flag"))
393 })
394 }
395
396 #[cfg(any(
399 not(feature = "trash-support"),
400 target_os = "android",
401 target_os = "ios"
402 ))]
403 {
404 unreachable!()
405 }
406 } else if metadata.is_symlink() {
407 #[cfg(windows)]
410 {
411 use std::os::windows::fs::FileTypeExt;
412 if metadata.file_type().is_symlink_dir() {
413 std::fs::remove_dir(&f)
414 } else {
415 std::fs::remove_file(&f)
416 }
417 }
418
419 #[cfg(not(windows))]
420 std::fs::remove_file(&f)
421 } else if metadata.is_file() || is_socket || is_fifo {
422 std::fs::remove_file(&f)
423 } else {
424 std::fs::remove_dir_all(&f)
425 };
426
427 if let Err(e) = result {
428 let original_error = e.to_string();
429 Err(ShellError::Io(IoError::new_with_additional_context(
430 e,
431 span,
432 f,
433 original_error,
434 )))
435 } else if verbose {
436 let msg = if interactive && !confirmed {
437 "not deleted"
438 } else {
439 "deleted"
440 };
441 Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
442 } else {
443 Ok(None)
444 }
445 } else {
446 let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
447 Err(ShellError::GenericError {
448 error,
449 msg: "cannot remove non-empty directory".into(),
450 span: Some(span),
451 help: None,
452 inner: vec![],
453 })
454 }
455 } else {
456 let error = format!("no such file or directory: {:}", f.to_string_lossy());
457 Err(ShellError::GenericError {
458 error,
459 msg: "no such file or directory".into(),
460 span: Some(span),
461 help: None,
462 inner: vec![],
463 })
464 }
465 });
466
467 let mut cmd_result = Ok(PipelineData::empty());
468 for result in iter {
469 engine_state.signals().check(&call.head)?;
470 match result {
471 Ok(None) => {}
472 Ok(Some(msg)) => eprintln!("{msg}"),
473 Err(err) => {
474 if !(force
475 && matches!(
476 err,
477 ShellError::Io(IoError {
478 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
479 ..
480 })
481 ))
482 {
483 if cmd_result.is_ok() {
484 cmd_result = Err(err);
485 } else {
486 report_shell_error(Some(stack), engine_state, &err)
487 }
488 }
489 }
490 }
491 }
492
493 cmd_result
494}