nu_command/filesystem/
rm.rs

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, &currentdir_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(), &currentdir_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            &currentdir_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            &currentdir_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                            // It is not appropriate to try and remove the
268                            // current directory or its parent when using
269                            // glob patterns.
270                            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                                    &currentdir_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                // Target doesn't exists
296                if !target_exists && empty_span.eq(&call.head) {
297                    empty_span = target.span;
298                }
299            }
300            Err(e) => {
301                // glob_from may canonicalize path and return an error when a directory is not found
302                // nushell should suppress the error if `--force` is used.
303                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                    // Should not be reachable since we error earlier if
391                    // these options are given on an unsupported platform
392                    #[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                    // In Windows, symlink pointing to a directory can be removed using
402                    // std::fs::remove_dir instead of std::fs::remove_file.
403                    #[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}