Skip to main content

nu_command/filesystem/
rm.rs

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, &currentdir_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(), &currentdir_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            &currentdir_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            &currentdir_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                            // It is not appropriate to try and remove the
274                            // current directory or its parent when using
275                            // glob patterns.
276                            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                                    &currentdir_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                // Target doesn't exists
302                if !target_exists && empty_span.eq(&call.head) {
303                    empty_span = target.span;
304                }
305            }
306            Err(e) => {
307                // glob_from may canonicalize path and return an error when a directory is not found
308                // nushell should suppress the error if `--force` is used.
309                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                    // Should not be reachable since we error earlier if
397                    // these options are given on an unsupported platform
398                    #[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                    // In Windows, symlink pointing to a directory can be removed using
408                    // std::fs::remove_dir instead of std::fs::remove_file.
409                    #[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}