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 if currentdir_path.to_string_lossy() == path.to_string_lossy()
236 || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
237 {
238 return Err(ShellError::Generic(GenericError::new(
239 "Cannot remove any parent directory",
240 "cannot remove any parent directory",
241 target.span,
242 )));
243 }
244
245 match nu_engine::glob_from(
246 &target,
247 ¤tdir_path,
248 call.head,
249 glob_options,
250 engine_state.signals().clone(),
251 ) {
252 Ok(files) => {
253 for file in files.1 {
254 match file {
255 Ok(f) => {
256 if !target_exists {
257 target_exists = true;
258 }
259
260 let name = f.display().to_string();
264 if name.ends_with("/.") || name.ends_with("/..") {
265 continue;
266 }
267
268 all_targets
269 .entry(nu_path::expand_path_with(
270 f,
271 ¤tdir_path,
272 target.item.is_expand(),
273 ))
274 .or_insert_with(|| target.span);
275 }
276 Err(e) => {
277 return Err(ShellError::Generic(GenericError::new(
278 format!("Could not remove {:}", path.to_string_lossy()),
279 e.to_string(),
280 target.span,
281 )));
282 }
283 }
284 }
285
286 if !target_exists && empty_span.eq(&call.head) {
288 empty_span = target.span;
289 }
290 }
291 Err(e) => {
292 if !(force
295 && matches!(
296 e,
297 ShellError::Io(IoError {
298 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
299 ..
300 })
301 ))
302 {
303 return Err(e);
304 }
305 }
306 };
307 }
308
309 if all_targets.is_empty() && !force {
310 return Err(ShellError::Generic(GenericError::new(
311 "File(s) not found",
312 "File(s) not found",
313 targets_span,
314 )));
315 }
316
317 if interactive_once {
318 let (interaction, confirmed) = try_interaction(
319 interactive_once,
320 format!("rm: remove {} files? ", all_targets.len()),
321 );
322 if let Err(e) = interaction {
323 return Err(ShellError::Generic(GenericError::new_internal(
324 format!("Error during interaction: {e:}"),
325 "could not move",
326 )));
327 } else if !confirmed {
328 return Ok(PipelineData::empty());
329 }
330 }
331
332 let iter = all_targets.into_iter().map(move |(f, span)| {
333 let is_empty = || match f.read_dir() {
334 Ok(mut p) => p.next().is_none(),
335 Err(_) => false,
336 };
337
338 if let Ok(metadata) = f.symlink_metadata() {
339 #[cfg(unix)]
340 let is_socket = metadata.file_type().is_socket();
341 #[cfg(unix)]
342 let is_fifo = metadata.file_type().is_fifo();
343
344 #[cfg(not(unix))]
345 let is_socket = false;
346 #[cfg(not(unix))]
347 let is_fifo = false;
348
349 if metadata.is_file()
350 || metadata.file_type().is_symlink()
351 || recursive
352 || is_socket
353 || is_fifo
354 || is_empty()
355 {
356 let (interaction, confirmed) = try_interaction(
357 interactive,
358 format!("rm: remove '{}'? ", f.to_string_lossy()),
359 );
360
361 let result = if let Err(e) = interaction {
362 Err(Error::other(&*e.to_string()))
363 } else if interactive && !confirmed {
364 Ok(())
365 } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
366 #[cfg(all(
367 feature = "trash-support",
368 not(any(target_os = "android", target_os = "ios"))
369 ))]
370 {
371 trash::delete(&f).map_err(|e: trash::Error| {
372 Error::other(format!("{e:?}\nTry '--permanent' flag"))
373 })
374 }
375
376 #[cfg(any(
379 not(feature = "trash-support"),
380 target_os = "android",
381 target_os = "ios"
382 ))]
383 {
384 unreachable!()
385 }
386 } else if metadata.is_symlink() {
387 #[cfg(windows)]
390 {
391 use std::os::windows::fs::FileTypeExt;
392 if metadata.file_type().is_symlink_dir() {
393 std::fs::remove_dir(&f)
394 } else {
395 std::fs::remove_file(&f)
396 }
397 }
398
399 #[cfg(not(windows))]
400 std::fs::remove_file(&f)
401 } else if metadata.is_file() || is_socket || is_fifo {
402 std::fs::remove_file(&f)
403 } else {
404 std::fs::remove_dir_all(&f)
405 };
406
407 if let Err(e) = result {
408 let original_error = e.to_string();
409 Err(ShellError::Io(IoError::new_with_additional_context(
410 e,
411 span,
412 f,
413 original_error,
414 )))
415 } else if verbose {
416 let msg = if interactive && !confirmed {
417 "not deleted"
418 } else {
419 "deleted"
420 };
421 Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
422 } else {
423 Ok(None)
424 }
425 } else {
426 let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
427 Err(ShellError::Generic(GenericError::new(
428 error,
429 "cannot remove non-empty directory",
430 span,
431 )))
432 }
433 } else {
434 let error = format!("no such file or directory: {:}", f.to_string_lossy());
435 Err(ShellError::Generic(GenericError::new(
436 error,
437 "no such file or directory",
438 span,
439 )))
440 }
441 });
442
443 let mut cmd_result = Ok(PipelineData::empty());
444 for result in iter {
445 engine_state.signals().check(&call.head)?;
446 match result {
447 Ok(None) => {}
448 Ok(Some(msg)) => eprintln!("{msg}"),
449 Err(err) => {
450 if !(force
451 && matches!(
452 err,
453 ShellError::Io(IoError {
454 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
455 ..
456 })
457 ))
458 {
459 if cmd_result.is_ok() {
460 cmd_result = Err(err);
461 } else {
462 report_shell_error(Some(stack), engine_state, &err)
463 }
464 }
465 }
466 }
467 }
468
469 cmd_result
470}