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, ¤tdir_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(), ¤tdir_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 ¤tdir_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 ¤tdir_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 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 ¤tdir_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 if !target_exists && empty_span.eq(&call.head) {
299 empty_span = target.span;
300 }
301 }
302 Err(e) => {
303 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 #[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 #[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}