1use crate::{
2 cmd::glob::{GlobOptions, expand_path},
3 error::to_shell_err,
4 globals::{get_pwd, get_vfs},
5};
6use nu_engine::CallExt;
7use nu_protocol::{
8 Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
9 engine::{Command, EngineState, Stack},
10};
11use std::sync::Arc;
12use vfs::VfsFileType;
13
14#[derive(Clone)]
15pub struct Rm;
16
17impl Command for Rm {
18 fn name(&self) -> &str {
19 "rm"
20 }
21
22 fn signature(&self) -> Signature {
23 Signature::build("rm")
24 .required(
25 "path",
26 SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
27 "path to file or directory to remove",
28 )
29 .switch(
30 "recursive",
31 "remove directories and their contents recursively",
32 Some('r'),
33 )
34 .input_output_type(Type::Nothing, Type::Nothing)
35 .category(Category::FileSystem)
36 }
37
38 fn description(&self) -> &str {
39 "remove a file or directory from the virtual filesystem."
40 }
41
42 fn run(
43 &self,
44 engine_state: &EngineState,
45 stack: &mut Stack,
46 call: &nu_protocol::engine::Call,
47 _input: PipelineData,
48 ) -> Result<PipelineData, ShellError> {
49 let path_value: Value = call.req(engine_state, stack, 0)?;
50 let recursive = call.has_flag(engine_state, stack, "recursive")?;
51
52 let path_str = match path_value {
53 Value::String { val, .. } | Value::Glob { val, .. } => val,
54 _ => {
55 return Err(ShellError::GenericError {
56 error: "invalid path".into(),
57 msg: "path must be a string or glob pattern".into(),
58 span: Some(call.head),
59 help: None,
60 inner: vec![],
61 });
62 }
63 };
64
65 // Prevent removing root
66 if path_str == "/" {
67 return Err(ShellError::GenericError {
68 error: "cannot remove root".to_string(),
69 msg: "refusing to remove root directory".to_string(),
70 span: Some(call.head),
71 help: None,
72 inner: vec![],
73 });
74 }
75
76 // Expand path (glob or single) into list of paths
77 let is_absolute = path_str.starts_with('/');
78 let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
79
80 let options = GlobOptions {
81 max_depth: None,
82 no_dirs: false,
83 no_files: false,
84 };
85
86 let matches = expand_path(&path_str, base_path.clone(), options)?;
87
88 // Remove all matching paths
89 for rel_path in matches {
90 let target = base_path.join(&rel_path).map_err(to_shell_err(call.head))?;
91 let meta = target.metadata().map_err(to_shell_err(call.head))?;
92 match meta.file_type {
93 VfsFileType::File => {
94 target.remove_file().map_err(to_shell_err(call.head))?;
95 }
96 VfsFileType::Directory => {
97 (if recursive {
98 target.remove_dir_all()
99 } else {
100 target.remove_dir()
101 })
102 .map_err(to_shell_err(call.head))?;
103 }
104 }
105 }
106
107 Ok(PipelineData::Empty)
108 }
109}