Skip to main content

command_stream/commands/
rm.rs

1//! Virtual `rm` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs;
6
7/// Execute the rm command
8///
9/// Removes files and directories.
10pub async fn rm(ctx: CommandContext) -> CommandResult {
11    if ctx.args.is_empty() {
12        return VirtualUtils::missing_operand_error("rm");
13    }
14
15    // Parse flags
16    let mut recursive = false;
17    let mut force = false;
18    let mut paths = Vec::new();
19
20    for arg in &ctx.args {
21        if arg == "-r" || arg == "-R" || arg == "--recursive" {
22            recursive = true;
23        } else if arg == "-f" || arg == "--force" {
24            force = true;
25        } else if arg == "-rf" || arg == "-fr" {
26            recursive = true;
27            force = true;
28        } else if arg.starts_with('-') {
29            // Check for combined flags like -rf
30            if arg.contains('r') || arg.contains('R') {
31                recursive = true;
32            }
33            if arg.contains('f') {
34                force = true;
35            }
36        } else {
37            paths.push(arg.clone());
38        }
39    }
40
41    if paths.is_empty() {
42        return VirtualUtils::missing_operand_error("rm");
43    }
44
45    let cwd = ctx.get_cwd();
46
47    for path_str in paths {
48        let resolved_path = VirtualUtils::resolve_path(&path_str, Some(&cwd));
49
50        trace_lazy("VirtualCommand", || {
51            format!(
52                "rm: removing {:?}, recursive: {}, force: {}",
53                resolved_path, recursive, force
54            )
55        });
56
57        if !resolved_path.exists() {
58            if !force {
59                return CommandResult::error(format!(
60                    "rm: cannot remove '{}': No such file or directory\n",
61                    path_str
62                ));
63            }
64            continue;
65        }
66
67        let result = if resolved_path.is_dir() {
68            if recursive {
69                fs::remove_dir_all(&resolved_path)
70            } else {
71                return CommandResult::error(format!(
72                    "rm: cannot remove '{}': Is a directory\n",
73                    path_str
74                ));
75            }
76        } else {
77            fs::remove_file(&resolved_path)
78        };
79
80        if let Err(e) = result {
81            if !force {
82                return CommandResult::error(format!("rm: cannot remove '{}': {}\n", path_str, e));
83            }
84        }
85    }
86
87    CommandResult::success_empty()
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::io::Write;
94    use tempfile::{tempdir, NamedTempFile};
95
96    #[tokio::test]
97    async fn test_rm_file() {
98        let mut temp = NamedTempFile::new().unwrap();
99        writeln!(temp, "test").unwrap();
100        let path = temp.path().to_path_buf();
101
102        // Keep the file but get the path
103        let path_str = path.to_string_lossy().to_string();
104        drop(temp);
105
106        // Create file again
107        fs::write(&path, "test").unwrap();
108
109        let ctx = CommandContext::new(vec![path_str.clone()]);
110        let result = rm(ctx).await;
111
112        assert!(result.is_success());
113        assert!(!path.exists());
114    }
115
116    #[tokio::test]
117    async fn test_rm_directory_recursive() {
118        let temp = tempdir().unwrap();
119        let dir = temp.path().join("subdir");
120        fs::create_dir(&dir).unwrap();
121        fs::write(dir.join("file.txt"), "test").unwrap();
122
123        let ctx = CommandContext::new(vec!["-r".to_string(), dir.to_string_lossy().to_string()]);
124        let result = rm(ctx).await;
125
126        assert!(result.is_success());
127        assert!(!dir.exists());
128    }
129
130    #[tokio::test]
131    async fn test_rm_nonexistent_force() {
132        let ctx = CommandContext::new(vec![
133            "-f".to_string(),
134            "/nonexistent/file/12345".to_string(),
135        ]);
136        let result = rm(ctx).await;
137
138        assert!(result.is_success());
139    }
140
141    #[tokio::test]
142    async fn test_rm_nonexistent_no_force() {
143        let ctx = CommandContext::new(vec!["/nonexistent/file/12345".to_string()]);
144        let result = rm(ctx).await;
145
146        assert!(!result.is_success());
147    }
148}