1use super::util::try_interaction;
2#[allow(deprecated)]
3use nu_engine::{command_prelude::*, env::current_dir};
4use nu_glob::MatchOptions;
5use nu_path::expand_path_with;
6use nu_protocol::{
7 NuGlob, report_shell_error,
8 shell_error::{self, io::IoError},
9};
10#[cfg(unix)]
11use std::os::unix::prelude::FileTypeExt;
12use std::{collections::HashMap, io::Error, path::PathBuf};
13
14const TRASH_SUPPORTED: bool = cfg!(all(
15 feature = "trash-support",
16 not(any(target_os = "android", target_os = "ios"))
17));
18
19#[derive(Clone)]
20pub struct Rm;
21
22impl Command for Rm {
23 fn name(&self) -> &str {
24 "rm"
25 }
26
27 fn description(&self) -> &str {
28 "Remove files and directories."
29 }
30
31 fn search_terms(&self) -> Vec<&str> {
32 vec!["delete", "remove"]
33 }
34
35 fn signature(&self) -> Signature {
36 Signature::build("rm")
37 .input_output_types(vec![(Type::Nothing, Type::Nothing)])
38 .rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The file paths(s) to remove.")
39 .switch(
40 "trash",
41 "move to the platform's trash instead of permanently deleting. not used on android and ios",
42 Some('t'),
43 )
44 .switch(
45 "permanent",
46 "delete permanently, ignoring the 'always_trash' config option. always enabled on android and ios",
47 Some('p'),
48 )
49 .switch("recursive", "delete subdirectories recursively", Some('r'))
50 .switch("force", "suppress error when no file", Some('f'))
51 .switch("verbose", "print names of deleted files", Some('v'))
52 .switch("interactive", "ask user to confirm action", Some('i'))
53 .switch(
54 "interactive-once",
55 "ask user to confirm action only once",
56 Some('I'),
57 )
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
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 #[allow(deprecated)]
131 let currentdir_path = current_dir(engine_state, stack)?;
132
133 let home: Option<String> = nu_path::home_dir().map(|path| {
134 {
135 if path.exists() {
136 nu_path::canonicalize_with(&path, ¤tdir_path).unwrap_or(path.into())
137 } else {
138 path.into()
139 }
140 }
141 .to_string_lossy()
142 .into()
143 });
144
145 for (idx, path) in paths.clone().into_iter().enumerate() {
146 if let Some(ref home) = home
147 && expand_path_with(path.item.as_ref(), ¤tdir_path, path.item.is_expand())
148 .to_string_lossy()
149 .as_ref()
150 == home.as_str()
151 {
152 unique_argument_check = Some(path.span);
153 }
154 let corrected_path = Spanned {
155 item: match path.item {
156 NuGlob::DoNotExpand(s) => {
157 NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s))
158 }
159 NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)),
160 },
161 span: path.span,
162 };
163 let _ = std::mem::replace(&mut paths[idx], corrected_path);
164 }
165
166 let span = call.head;
167 let rm_always_trash = stack.get_config(engine_state).rm.always_trash;
168
169 if !TRASH_SUPPORTED {
170 if rm_always_trash {
171 return Err(ShellError::GenericError {
172 error: "Cannot execute `rm`; the current configuration specifies \
173 `always_trash = true`, but the current nu executable was not \
174 built with feature `trash_support`."
175 .into(),
176 msg: "trash required to be true but not supported".into(),
177 span: Some(span),
178 help: None,
179 inner: vec![],
180 });
181 } else if trash {
182 return Err(ShellError::GenericError{
183 error: "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform"
184 .into(),
185 msg: "this option is only available if nu is built with the `trash-support` feature and the platform supports trash"
186 .into(),
187 span: Some(span),
188 help: None,
189 inner: vec![],
190 });
191 }
192 }
193
194 if paths.is_empty() {
195 return Err(ShellError::GenericError {
196 error: "rm requires target paths".into(),
197 msg: "needs parameter".into(),
198 span: Some(span),
199 help: None,
200 inner: vec![],
201 });
202 }
203
204 if unique_argument_check.is_some() && !(interactive_once || interactive) {
205 return Err(ShellError::GenericError {
206 error: "You are trying to remove your home dir".into(),
207 msg: "If you really want to remove your home dir, please use -I or -i".into(),
208 span: unique_argument_check,
209 help: None,
210 inner: vec![],
211 });
212 }
213
214 let targets_span = Span::new(
215 paths
216 .iter()
217 .map(|x| x.span.start)
218 .min()
219 .expect("targets were empty"),
220 paths
221 .iter()
222 .map(|x| x.span.end)
223 .max()
224 .expect("targets were empty"),
225 );
226
227 let (mut target_exists, mut empty_span) = (false, call.head);
228 let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
229
230 for target in paths {
231 let path = expand_path_with(
232 target.item.as_ref(),
233 ¤tdir_path,
234 target.item.is_expand(),
235 );
236 if currentdir_path.to_string_lossy() == path.to_string_lossy()
237 || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
238 {
239 return Err(ShellError::GenericError {
240 error: "Cannot remove any parent directory".into(),
241 msg: "cannot remove any parent directory".into(),
242 span: Some(target.span),
243 help: None,
244 inner: vec![],
245 });
246 }
247
248 match nu_engine::glob_from(
249 &target,
250 ¤tdir_path,
251 call.head,
252 Some(MatchOptions {
253 require_literal_leading_dot: true,
254 ..Default::default()
255 }),
256 engine_state.signals().clone(),
257 ) {
258 Ok(files) => {
259 for file in files.1 {
260 match file {
261 Ok(f) => {
262 if !target_exists {
263 target_exists = true;
264 }
265
266 let name = f.display().to_string();
270 if name.ends_with("/.") || name.ends_with("/..") {
271 continue;
272 }
273
274 all_targets
275 .entry(nu_path::expand_path_with(
276 f,
277 ¤tdir_path,
278 target.item.is_expand(),
279 ))
280 .or_insert_with(|| target.span);
281 }
282 Err(e) => {
283 return Err(ShellError::GenericError {
284 error: format!("Could not remove {:}", path.to_string_lossy()),
285 msg: e.to_string(),
286 span: Some(target.span),
287 help: None,
288 inner: vec![],
289 });
290 }
291 }
292 }
293
294 if !target_exists && empty_span.eq(&call.head) {
296 empty_span = target.span;
297 }
298 }
299 Err(e) => {
300 if !(force
303 && matches!(
304 e,
305 ShellError::Io(IoError {
306 kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
307 ..
308 })
309 ))
310 {
311 return Err(e);
312 }
313 }
314 };
315 }
316
317 if all_targets.is_empty() && !force {
318 return Err(ShellError::GenericError {
319 error: "File(s) not found".into(),
320 msg: "File(s) not found".into(),
321 span: Some(targets_span),
322 help: None,
323 inner: vec![],
324 });
325 }
326
327 if interactive_once {
328 let (interaction, confirmed) = try_interaction(
329 interactive_once,
330 format!("rm: remove {} files? ", all_targets.len()),
331 );
332 if let Err(e) = interaction {
333 return Err(ShellError::GenericError {
334 error: format!("Error during interaction: {e:}"),
335 msg: "could not move".into(),
336 span: None,
337 help: None,
338 inner: vec![],
339 });
340 } else if !confirmed {
341 return Ok(PipelineData::empty());
342 }
343 }
344
345 let iter = all_targets.into_iter().map(move |(f, span)| {
346 let is_empty = || match f.read_dir() {
347 Ok(mut p) => p.next().is_none(),
348 Err(_) => false,
349 };
350
351 if let Ok(metadata) = f.symlink_metadata() {
352 #[cfg(unix)]
353 let is_socket = metadata.file_type().is_socket();
354 #[cfg(unix)]
355 let is_fifo = metadata.file_type().is_fifo();
356
357 #[cfg(not(unix))]
358 let is_socket = false;
359 #[cfg(not(unix))]
360 let is_fifo = false;
361
362 if metadata.is_file()
363 || metadata.file_type().is_symlink()
364 || recursive
365 || is_socket
366 || is_fifo
367 || is_empty()
368 {
369 let (interaction, confirmed) = try_interaction(
370 interactive,
371 format!("rm: remove '{}'? ", f.to_string_lossy()),
372 );
373
374 let result = if let Err(e) = interaction {
375 Err(Error::other(&*e.to_string()))
376 } else if interactive && !confirmed {
377 Ok(())
378 } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
379 #[cfg(all(
380 feature = "trash-support",
381 not(any(target_os = "android", target_os = "ios"))
382 ))]
383 {
384 trash::delete(&f).map_err(|e: trash::Error| {
385 Error::other(format!("{e:?}\nTry '--permanent' flag"))
386 })
387 }
388
389 #[cfg(any(
392 not(feature = "trash-support"),
393 target_os = "android",
394 target_os = "ios"
395 ))]
396 {
397 unreachable!()
398 }
399 } else if metadata.is_symlink() {
400 #[cfg(windows)]
403 {
404 f.metadata().and_then(|metadata| {
405 if metadata.is_dir() {
406 std::fs::remove_dir(&f)
407 } else {
408 std::fs::remove_file(&f)
409 }
410 })
411 }
412
413 #[cfg(not(windows))]
414 std::fs::remove_file(&f)
415 } else if metadata.is_file() || is_socket || is_fifo {
416 std::fs::remove_file(&f)
417 } else {
418 std::fs::remove_dir_all(&f)
419 };
420
421 if let Err(e) = result {
422 Err(ShellError::Io(IoError::new(e, span, f)))
423 } else if verbose {
424 let msg = if interactive && !confirmed {
425 "not deleted"
426 } else {
427 "deleted"
428 };
429 Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
430 } else {
431 Ok(None)
432 }
433 } else {
434 let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
435 Err(ShellError::GenericError {
436 error,
437 msg: "cannot remove non-empty directory".into(),
438 span: Some(span),
439 help: None,
440 inner: vec![],
441 })
442 }
443 } else {
444 let error = format!("no such file or directory: {:}", f.to_string_lossy());
445 Err(ShellError::GenericError {
446 error,
447 msg: "no such file or directory".into(),
448 span: Some(span),
449 help: None,
450 inner: vec![],
451 })
452 }
453 });
454
455 for result in iter {
456 engine_state.signals().check(&call.head)?;
457 match result {
458 Ok(None) => {}
459 Ok(Some(msg)) => eprintln!("{msg}"),
460 Err(err) => report_shell_error(engine_state, &err),
461 }
462 }
463
464 Ok(PipelineData::empty())
465}