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