Skip to main content

command_stream/commands/
touch.rs

1//! Virtual `touch` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs::{self, OpenOptions};
6use std::time::SystemTime;
7
8/// Execute the touch command
9///
10/// Updates file timestamps or creates empty files.
11pub async fn touch(ctx: CommandContext) -> CommandResult {
12    if ctx.args.is_empty() {
13        return VirtualUtils::missing_operand_error("touch");
14    }
15
16    let cwd = ctx.get_cwd();
17
18    for file in &ctx.args {
19        if file.starts_with('-') {
20            // Skip flags for now
21            continue;
22        }
23
24        let resolved_path = VirtualUtils::resolve_path(file, Some(&cwd));
25
26        trace_lazy("VirtualCommand", || {
27            format!("touch: touching {:?}", resolved_path)
28        });
29
30        if resolved_path.exists() {
31            // Update modification time
32            let now = SystemTime::now();
33            if let Err(e) =
34                filetime::set_file_mtime(&resolved_path, filetime::FileTime::from_system_time(now))
35            {
36                // Fallback: try to just open and close the file
37                if let Err(e2) = OpenOptions::new().write(true).open(&resolved_path) {
38                    return CommandResult::error(format!(
39                        "touch: cannot touch '{}': {}\n",
40                        file, e2
41                    ));
42                }
43            }
44        } else {
45            // Create the file
46            if let Some(parent) = resolved_path.parent() {
47                if !parent.exists() {
48                    if let Err(e) = fs::create_dir_all(parent) {
49                        return CommandResult::error(format!(
50                            "touch: cannot touch '{}': {}\n",
51                            file, e
52                        ));
53                    }
54                }
55            }
56
57            if let Err(e) = fs::File::create(&resolved_path) {
58                return CommandResult::error(format!("touch: cannot touch '{}': {}\n", file, e));
59            }
60        }
61    }
62
63    CommandResult::success_empty()
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use tempfile::tempdir;
70
71    #[tokio::test]
72    async fn test_touch_new_file() {
73        let temp = tempdir().unwrap();
74        let new_file = temp.path().join("new_file.txt");
75
76        let ctx = CommandContext::new(vec![new_file.to_string_lossy().to_string()]);
77        let result = touch(ctx).await;
78
79        assert!(result.is_success());
80        assert!(new_file.exists());
81    }
82
83    #[tokio::test]
84    async fn test_touch_existing_file() {
85        let temp = tempdir().unwrap();
86        let file = temp.path().join("existing.txt");
87        fs::write(&file, "test").unwrap();
88
89        let ctx = CommandContext::new(vec![file.to_string_lossy().to_string()]);
90        let result = touch(ctx).await;
91
92        assert!(result.is_success());
93    }
94
95    #[tokio::test]
96    async fn test_touch_missing_operand() {
97        let ctx = CommandContext::new(vec![]);
98        let result = touch(ctx).await;
99
100        assert!(!result.is_success());
101        assert!(result.stderr.contains("missing operand"));
102    }
103}