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::{
14 collections::HashMap,
15 io::{Error, ErrorKind},
16 path::PathBuf,
17};
18
19const TRASH_SUPPORTED: bool = cfg!(all(
20 feature = "trash-support",
21 not(any(target_os = "android", target_os = "ios"))
22));
23
24#[derive(Clone)]
25pub struct Rm;
26
27impl Command for Rm {
28 fn name(&self) -> &str {
29 "rm"
30 }
31
32 fn description(&self) -> &str {
33 "Remove files and directories."
34 }
35
36 fn search_terms(&self) -> Vec<&str> {
37 vec!["delete", "remove"]
38 }
39
40 fn signature(&self) -> Signature {
41 Signature::build("rm")
42 .input_output_types(vec![(Type::Nothing, Type::Nothing)])
43 .rest("paths", SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::String]), "The file paths(s) to remove.")
44 .switch(
45 "trash",
46 "move to the platform's trash instead of permanently deleting. not used on android and ios",
47 Some('t'),
48 )
49 .switch(
50 "permanent",
51 "delete permanently, ignoring the 'always_trash' config option. always enabled on android and ios",
52 Some('p'),
53 )
54 .switch("recursive", "delete subdirectories recursively", Some('r'))
55 .switch("force", "suppress error when no file", Some('f'))
56 .switch("verbose", "print names of deleted files", Some('v'))
57 .switch("interactive", "ask user to confirm action", Some('i'))
58 .switch(
59 "interactive-once",
60 "ask user to confirm action only once",
61 Some('I'),
62 )
63 .category(Category::FileSystem)
64 }
65
66 fn run(
67 &self,
68 engine_state: &EngineState,
69 stack: &mut Stack,
70 call: &Call,
71 _input: PipelineData,
72 ) -> Result<PipelineData, ShellError> {
73 rm(engine_state, stack, call)
74 }
75
76 fn examples(&self) -> Vec<Example> {
77 let mut examples = vec![Example {
78 description:
79 "Delete, or move a file to the trash (based on the 'always_trash' config option)",
80 example: "rm file.txt",
81 result: None,
82 }];
83 if TRASH_SUPPORTED {
84 examples.append(&mut vec![
85 Example {
86 description: "Move a file to the trash",
87 example: "rm --trash file.txt",
88 result: None,
89 },
90 Example {
91 description:
92 "Delete a file permanently, even if the 'always_trash' config option is true",
93 example: "rm --permanent file.txt",
94 result: None,
95 },
96 ]);
97 }
98 examples.push(Example {
99 description: "Delete a file, ignoring 'file not found' errors",
100 example: "rm --force file.txt",
101 result: None,
102 });
103 examples.push(Example {
104 description: "Delete all 0KB files in the current directory",
105 example: "ls | where size == 0KB and type == file | each { rm $in.name } | null",
106 result: None,
107 });
108 examples
109 }
110}
111
112fn rm(
113 engine_state: &EngineState,
114 stack: &mut Stack,
115 call: &Call,
116) -> Result<PipelineData, ShellError> {
117 let trash = call.has_flag(engine_state, stack, "trash")?;
118 let permanent = call.has_flag(engine_state, stack, "permanent")?;
119 let recursive = call.has_flag(engine_state, stack, "recursive")?;
120 let force = call.has_flag(engine_state, stack, "force")?;
121 let verbose = call.has_flag(engine_state, stack, "verbose")?;
122 let interactive = call.has_flag(engine_state, stack, "interactive")?;
123 let interactive_once = call.has_flag(engine_state, stack, "interactive-once")? && !interactive;
124
125 let mut paths = call.rest::<Spanned<NuGlob>>(engine_state, stack, 0)?;
126
127 if paths.is_empty() {
128 return Err(ShellError::MissingParameter {
129 param_name: "requires file paths".to_string(),
130 span: call.head,
131 });
132 }
133
134 let mut unique_argument_check = None;
135
136 #[allow(deprecated)]
137 let currentdir_path = current_dir(engine_state, stack)?;
138
139 let home: Option<String> = nu_path::home_dir().map(|path| {
140 {
141 if path.exists() {
142 nu_path::canonicalize_with(&path, ¤tdir_path).unwrap_or(path.into())
143 } else {
144 path.into()
145 }
146 }
147 .to_string_lossy()
148 .into()
149 });
150
151 for (idx, path) in paths.clone().into_iter().enumerate() {
152 if let Some(ref home) = home {
153 if expand_path_with(path.item.as_ref(), ¤tdir_path, path.item.is_expand())
154 .to_string_lossy()
155 .as_ref()
156 == home.as_str()
157 {
158 unique_argument_check = Some(path.span);
159 }
160 }
161 let corrected_path = Spanned {
162 item: match path.item {
163 NuGlob::DoNotExpand(s) => {
164 NuGlob::DoNotExpand(nu_utils::strip_ansi_string_unlikely(s))
165 }
166 NuGlob::Expand(s) => NuGlob::Expand(nu_utils::strip_ansi_string_unlikely(s)),
167 },
168 span: path.span,
169 };
170 let _ = std::mem::replace(&mut paths[idx], corrected_path);
171 }
172
173 let span = call.head;
174 let rm_always_trash = stack.get_config(engine_state).rm.always_trash;
175
176 if !TRASH_SUPPORTED {
177 if rm_always_trash {
178 return Err(ShellError::GenericError {
179 error: "Cannot execute `rm`; the current configuration specifies \
180 `always_trash = true`, but the current nu executable was not \
181 built with feature `trash_support`."
182 .into(),
183 msg: "trash required to be true but not supported".into(),
184 span: Some(span),
185 help: None,
186 inner: vec![],
187 });
188 } else if trash {
189 return Err(ShellError::GenericError{
190 error: "Cannot execute `rm` with option `--trash`; feature `trash-support` not enabled or on an unsupported platform"
191 .into(),
192 msg: "this option is only available if nu is built with the `trash-support` feature and the platform supports trash"
193 .into(),
194 span: Some(span),
195 help: None,
196 inner: vec![],
197 });
198 }
199 }
200
201 if paths.is_empty() {
202 return Err(ShellError::GenericError {
203 error: "rm requires target paths".into(),
204 msg: "needs parameter".into(),
205 span: Some(span),
206 help: None,
207 inner: vec![],
208 });
209 }
210
211 if unique_argument_check.is_some() && !(interactive_once || interactive) {
212 return Err(ShellError::GenericError {
213 error: "You are trying to remove your home dir".into(),
214 msg: "If you really want to remove your home dir, please use -I or -i".into(),
215 span: unique_argument_check,
216 help: None,
217 inner: vec![],
218 });
219 }
220
221 let targets_span = Span::new(
222 paths
223 .iter()
224 .map(|x| x.span.start)
225 .min()
226 .expect("targets were empty"),
227 paths
228 .iter()
229 .map(|x| x.span.end)
230 .max()
231 .expect("targets were empty"),
232 );
233
234 let (mut target_exists, mut empty_span) = (false, call.head);
235 let mut all_targets: HashMap<PathBuf, Span> = HashMap::new();
236
237 for target in paths {
238 let path = expand_path_with(
239 target.item.as_ref(),
240 ¤tdir_path,
241 target.item.is_expand(),
242 );
243 if currentdir_path.to_string_lossy() == path.to_string_lossy()
244 || currentdir_path.starts_with(format!("{}{}", target.item, std::path::MAIN_SEPARATOR))
245 {
246 return Err(ShellError::GenericError {
247 error: "Cannot remove any parent directory".into(),
248 msg: "cannot remove any parent directory".into(),
249 span: Some(target.span),
250 help: None,
251 inner: vec![],
252 });
253 }
254
255 match nu_engine::glob_from(
256 &target,
257 ¤tdir_path,
258 call.head,
259 Some(MatchOptions {
260 require_literal_leading_dot: true,
261 ..Default::default()
262 }),
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 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 ¤tdir_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 if !target_exists && empty_span.eq(&call.head) {
303 empty_span = target.span;
304 }
305 }
306 Err(e) => {
307 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::new(ErrorKind::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::new(ErrorKind::Other, format!("{e:?}\nTry '--permanent' flag"))
393 })
394 }
395
396 #[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 #[cfg(windows)]
410 {
411 f.metadata().and_then(|metadata| {
412 if metadata.is_dir() {
413 std::fs::remove_dir(&f)
414 } else {
415 std::fs::remove_file(&f)
416 }
417 })
418 }
419
420 #[cfg(not(windows))]
421 std::fs::remove_file(&f)
422 } else if metadata.is_file() || is_socket || is_fifo {
423 std::fs::remove_file(&f)
424 } else {
425 std::fs::remove_dir_all(&f)
426 };
427
428 if let Err(e) = result {
429 Err(ShellError::Io(IoError::new(e.kind(), span, f)))
430 } else if verbose {
431 let msg = if interactive && !confirmed {
432 "not deleted"
433 } else {
434 "deleted"
435 };
436 Ok(Some(format!("{} {:}", msg, f.to_string_lossy())))
437 } else {
438 Ok(None)
439 }
440 } else {
441 let error = format!("Cannot remove {:}. try --recursive", f.to_string_lossy());
442 Err(ShellError::GenericError {
443 error,
444 msg: "cannot remove non-empty directory".into(),
445 span: Some(span),
446 help: None,
447 inner: vec![],
448 })
449 }
450 } else {
451 let error = format!("no such file or directory: {:}", f.to_string_lossy());
452 Err(ShellError::GenericError {
453 error,
454 msg: "no such file or directory".into(),
455 span: Some(span),
456 help: None,
457 inner: vec![],
458 })
459 }
460 });
461
462 for result in iter {
463 engine_state.signals().check(call.head)?;
464 match result {
465 Ok(None) => {}
466 Ok(Some(msg)) => eprintln!("{msg}"),
467 Err(err) => report_shell_error(engine_state, &err),
468 }
469 }
470
471 Ok(PipelineData::empty())
472}