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            .category(Category::FileSystem)
58    }
59
60    fn run(
61        &self,
62        engine_state: &EngineState,
63        stack: &mut Stack,
64        call: &Call,
65        _input: PipelineData,
66    ) -> Result<PipelineData, ShellError> {
67        rm(engine_state, stack, call)
68    }
69
70    fn examples(&self) -> Vec<Example<'_>> {
71        let mut examples = vec![Example {
72            description: "Delete, or move a file to the trash (based on the 'always_trash' config option)",
73            example: "rm file.txt",
74            result: None,
75        }];
76        if TRASH_SUPPORTED {
77            examples.append(&mut vec![
78                Example {
79                    description: "Move a file to the trash",
80                    example: "rm --trash file.txt",
81                    result: None,
82                },
83                Example {
84                    description:
85                        "Delete a file permanently, even if the 'always_trash' config option is true",
86                    example: "rm --permanent file.txt",
87                    result: None,
88                },
89            ]);
90        }
91        examples.push(Example {
92            description: "Delete a file, ignoring 'file not found' errors",
93            example: "rm --force file.txt",
94            result: None,
95        });
96        examples.push(Example {
97            description: "Delete all 0KB files in the current directory",
98            example: "ls | where size == 0KB and type == file | each { rm $in.name } | null",
99            result: None,
100        });
101        examples
102    }
103}
104
105fn rm(
106    engine_state: &EngineState,
107    stack: &mut Stack,
108    call: &Call,
109) -> Result<PipelineData, ShellError> {
110    let trash = call.has_flag(engine_state, stack, "trash")?;
111    let permanent = call.has_flag(engine_state, stack, "permanent")?;
112    let recursive = call.has_flag(engine_state, stack, "recursive")?;
113    let force = call.has_flag(engine_state, stack, "force")?;
114    let verbose = call.has_flag(engine_state, stack, "verbose")?;
115    let interactive = call.has_flag(engine_state, stack, "interactive")?;
116    let interactive_once = call.has_flag(engine_state, stack, "interactive-once")? && !interactive;
117
118    let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
119
120    if paths.is_empty() {
121        return Err(ShellError::MissingParameter {
122            param_name: "requires file paths".to_string(),
123            span: call.head,
124        });
125    }
126
127    let mut unique_argument_check = None;
128
129    let currentdir_path = engine_state.cwd(Some(stack))?.into_std_path_buf();
130
131    let home: Option<String> = nu_path::home_dir().map(|path| {
132        {
133            if path.exists() {
134                nu_path::absolute_with(&path, &currentdir_path).unwrap_or(path.into())
135            } else {
136                path.into()
137            }
138        }
139        .to_string_lossy()
140        .into()
141    });
142
143    for (idx, path) in paths.clone().into_iter().enumerate() {
144        if let Some(ref home) = home
145            && expand_path_with(path.item.as_ref(), &currentdir_path, path.item.is_expand())
146                .to_string_lossy()
147                .as_ref()
148                == home.as_str()
149        {
150            unique_argument_check = Some(path.span);
151        }
152        let corrected_path = Spanned {
153            item: match path.item {
154                NuGlob::DoNotExpand(s) => {
155                    NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s))
156                }
157                NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)),
158            },
159            span: path.span,
160        };
161        let _ = std::mem::replace(&mut paths[idx], corrected_path);
162    }
163
164    let span = call.head;
165    let rm_always_trash = stack.get_config(engine_state).rm.always_trash;
166
167    if !TRASH_SUPPORTED {
168        if rm_always_trash {
169            return Err(ShellError::GenericError {
170                error: "Cannot execute `rm`; the current configuration specifies \
171                    `always_trash = true`, but the current nu executable was not \
172                    built with feature `trash_support`."
173                    .into(),
174                msg: "trash required to be true but not supported".into(),
175                span: Some(span),
176                help: None,
177                inner: vec![],
178            });
179        } else if trash {
180            return Err(ShellError::GenericError{
181                error: "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform"
182                    .into(),
183                msg: "this option is only available if nu is built with the `trash-support` feature and the platform supports trash"
184                    .into(),
185                span: Some(span),
186                help: None,
187                inner: vec![],
188            });
189        }
190    }
191
192    if paths.is_empty() {
193        return Err(ShellError::GenericError {
194            error: "rm requires target paths".into(),
195            msg: "needs parameter".into(),
196            span: Some(span),
197            help: None,
198            inner: vec![],
199        });
200    }
201
202    if unique_argument_check.is_some() && !(interactive_once || interactive) {
203        return Err(ShellError::GenericError {
204            error: "You are trying to remove your home dir".into(),
205            msg: "If you really want to remove your home dir, please use -I or -i".into(),
206            span: unique_argument_check,
207            help: None,
208            inner: vec![],
209        });
210    }
211
212    let targets_span = Span::new(
213        paths
214            .iter()
215            .map(|x| x.span.start)
216            .min()
217            .expect("targets were empty"),
218        paths
219            .iter()
220            .map(|x| x.span.end)
221            .max()
222            .expect("targets were empty"),
223    );
224
225    let (mut target_exists, mut empty_span) = (false, call.head);
226    let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
227
228    for target in paths {
229        let path = expand_path_with(
230            target.item.as_ref(),
231            &currentdir_path,
232            target.item.is_expand(),
233        );
234        if currentdir_path.to_string_lossy() == path.to_string_lossy()
235            || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
236        {
237            return Err(ShellError::GenericError {
238                error: "Cannot remove any parent directory".into(),
239                msg: "cannot remove any parent directory".into(),
240                span: Some(target.span),
241                help: None,
242                inner: vec![],
243            });
244        }
245
246        match nu_engine::glob_from(
247            &target,
248            &currentdir_path,
249            call.head,
250            Some(MatchOptions {
251                require_literal_leading_dot: true,
252                ..Default::default()
253            }),
254            engine_state.signals().clone(),
255        ) {
256            Ok(files) => {
257                for file in files.1 {
258                    match file {
259                        Ok(f) => {
260                            if !target_exists {
261                                target_exists = true;
262                            }
263
264                            // It is not appropriate to try and remove the
265                            // current directory or its parent when using
266                            // glob patterns.
267                            let name = f.display().to_string();
268                            if name.ends_with("/.") || name.ends_with("/..") {
269                                continue;
270                            }
271
272                            all_targets
273                                .entry(nu_path::expand_path_with(
274                                    f,
275                                    &currentdir_path,
276                                    target.item.is_expand(),
277                                ))
278                                .or_insert_with(|| target.span);
279                        }
280                        Err(e) => {
281                            return Err(ShellError::GenericError {
282                                error: format!("Could not remove {:}", path.to_string_lossy()),
283                                msg: e.to_string(),
284                                span: Some(target.span),
285                                help: None,
286                                inner: vec![],
287                            });
288                        }
289                    }
290                }
291
292                // Target doesn't exists
293                if !target_exists && empty_span.eq(&call.head) {
294                    empty_span = target.span;
295                }
296            }
297            Err(e) => {
298                // glob_from may canonicalize path and return an error when a directory is not found
299                // nushell should suppress the error if `--force` is used.
300                if !(force
301                    && matches!(
302                        e,
303                        ShellError::Io(IoError {
304                            kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
305                            ..
306                        })
307                    ))
308                {
309                    return Err(e);
310                }
311            }
312        };
313    }
314
315    if all_targets.is_empty() && !force {
316        return Err(ShellError::GenericError {
317            error: "File(s) not found".into(),
318            msg: "File(s) not found".into(),
319            span: Some(targets_span),
320            help: None,
321            inner: vec![],
322        });
323    }
324
325    if interactive_once {
326        let (interaction, confirmed) = try_interaction(
327            interactive_once,
328            format!("rm: remove {} files? ", all_targets.len()),
329        );
330        if let Err(e) = interaction {
331            return Err(ShellError::GenericError {
332                error: format!("Error during interaction: {e:}"),
333                msg: "could not move".into(),
334                span: None,
335                help: None,
336                inner: vec![],
337            });
338        } else if !confirmed {
339            return Ok(PipelineData::empty());
340        }
341    }
342
343    let iter = all_targets.into_iter().map(move |(f, span)| {
344        let is_empty = || match f.read_dir() {
345            Ok(mut p) => p.next().is_none(),
346            Err(_) => false,
347        };
348
349        if let Ok(metadata) = f.symlink_metadata() {
350            #[cfg(unix)]
351            let is_socket = metadata.file_type().is_socket();
352            #[cfg(unix)]
353            let is_fifo = metadata.file_type().is_fifo();
354
355            #[cfg(not(unix))]
356            let is_socket = false;
357            #[cfg(not(unix))]
358            let is_fifo = false;
359
360            if metadata.is_file()
361                || metadata.file_type().is_symlink()
362                || recursive
363                || is_socket
364                || is_fifo
365                || is_empty()
366            {
367                let (interaction, confirmed) = try_interaction(
368                    interactive,
369                    format!("rm: remove '{}'? ", f.to_string_lossy()),
370                );
371
372                let result = if let Err(e) = interaction {
373                    Err(Error::other(&*e.to_string()))
374                } else if interactive && !confirmed {
375                    Ok(())
376                } else if TRASH_SUPPORTED && (trash || (rm_always_trash && !permanent)) {
377                    #[cfg(all(
378                        feature = "trash-support",
379                        not(any(target_os = "android", target_os = "ios"))
380                    ))]
381                    {
382                        trash::delete(&f).map_err(|e: trash::Error| {
383                            Error::other(format!("{e:?}\nTry '--permanent' flag"))
384                        })
385                    }
386
387                    // Should not be reachable since we error earlier if
388                    // these options are given on an unsupported platform
389                    #[cfg(any(
390                        not(feature = "trash-support"),
391                        target_os = "android",
392                        target_os = "ios"
393                    ))]
394                    {
395                        unreachable!()
396                    }
397                } else if metadata.is_symlink() {
398                    // In Windows, symlink pointing to a directory can be removed using
399                    // std::fs::remove_dir instead of std::fs::remove_file.
400                    #[cfg(windows)]
401                    {
402                        use std::os::windows::fs::FileTypeExt;
403                        if metadata.file_type().is_symlink_dir() {
404                            std::fs::remove_dir(&f)
405                        } else {
406                            std::fs::remove_file(&f)
407                        }
408                    }
409
410                    #[cfg(not(windows))]
411                    std::fs::remove_file(&f)
412                } else if metadata.is_file() || is_socket || is_fifo {
413                    std::fs::remove_file(&f)
414                } else {
415                    std::fs::remove_dir_all(&f)
416                };
417
418                if let Err(e) = result {
419                    let original_error = e.to_string();
420                    Err(ShellError::Io(IoError::new_with_additional_context(
421                        e,
422                        span,
423                        f,
424                        original_error,
425                    )))
426                } else if verbose {
427                    let msg = if interactive && !confirmed {
428                        "not deleted"
429                    } else {
430                        "deleted"
431                    };
432                    Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
433                } else {
434                    Ok(None)
435                }
436            } else {
437                let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
438                Err(ShellError::GenericError {
439                    error,
440                    msg: "cannot remove non-empty directory".into(),
441                    span: Some(span),
442                    help: None,
443                    inner: vec![],
444                })
445            }
446        } else {
447            let error = format!("no such file or directory: {:}", f.to_string_lossy());
448            Err(ShellError::GenericError {
449                error,
450                msg: "no such file or directory".into(),
451                span: Some(span),
452                help: None,
453                inner: vec![],
454            })
455        }
456    });
457
458    let mut cmd_result = Ok(PipelineData::empty());
459    for result in iter {
460        engine_state.signals().check(&call.head)?;
461        match result {
462            Ok(None) => {}
463            Ok(Some(msg)) => eprintln!("{msg}"),
464            Err(err) => {
465                if !(force
466                    && matches!(
467                        err,
468                        ShellError::Io(IoError {
469                            kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound, ..),
470                            ..
471                        })
472                    ))
473                {
474                    if cmd_result.is_ok() {
475                        cmd_result = Err(err);
476                    } else {
477                        report_shell_error(Some(stack), engine_state, &err)
478                    }
479                }
480            }
481        }
482    }
483
484    cmd_result
485}