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