Skip to main content

command_stream/commands/
cat.rs

1//! Virtual `cat` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs;
6
7/// Execute the cat command
8///
9/// Concatenates and displays file contents.
10pub async fn cat(ctx: CommandContext) -> CommandResult {
11    if ctx.args.is_empty() {
12        // Read from stdin if no files specified
13        if let Some(ref stdin) = ctx.stdin {
14            if !stdin.is_empty() {
15                return CommandResult::success(stdin.clone());
16            }
17        }
18        return CommandResult::success_empty();
19    }
20
21    let cwd = ctx.get_cwd();
22    let mut outputs = Vec::new();
23
24    for file in &ctx.args {
25        // Check for cancellation before processing each file
26        if ctx.is_cancelled() {
27            trace_lazy("VirtualCommand", || {
28                "cat: cancelled while processing files".to_string()
29            });
30            return CommandResult::error_with_code("", 130); // SIGINT exit code
31        }
32
33        trace_lazy("VirtualCommand", || format!("cat: reading file {:?}", file));
34
35        let resolved_path = VirtualUtils::resolve_path(file, Some(&cwd));
36
37        match fs::read_to_string(&resolved_path) {
38            Ok(content) => {
39                outputs.push(content);
40            }
41            Err(e) => {
42                let error_msg = if e.kind() == std::io::ErrorKind::NotFound {
43                    format!("cat: {}: No such file or directory\n", file)
44                } else if e.kind() == std::io::ErrorKind::IsADirectory
45                    || (e.kind() == std::io::ErrorKind::Other
46                        && e.to_string().contains("directory"))
47                {
48                    format!("cat: {}: Is a directory\n", file)
49                } else {
50                    format!("cat: {}: {}\n", file, e)
51                };
52                return CommandResult::error(error_msg);
53            }
54        }
55    }
56
57    let output = outputs.join("");
58    trace_lazy("VirtualCommand", || {
59        format!("cat: success, bytes read: {}", output.len())
60    });
61
62    CommandResult::success(output)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::io::Write;
69    use tempfile::NamedTempFile;
70
71    #[tokio::test]
72    async fn test_cat_file() {
73        let mut temp = NamedTempFile::new().unwrap();
74        writeln!(temp, "Hello, World!").unwrap();
75
76        let ctx = CommandContext::new(vec![temp.path().to_string_lossy().to_string()]);
77        let result = cat(ctx).await;
78
79        assert!(result.is_success());
80        assert_eq!(result.stdout, "Hello, World!\n");
81    }
82
83    #[tokio::test]
84    async fn test_cat_stdin() {
85        let mut ctx = CommandContext::new(vec![]);
86        ctx.stdin = Some("stdin content".to_string());
87
88        let result = cat(ctx).await;
89        assert!(result.is_success());
90        assert_eq!(result.stdout, "stdin content");
91    }
92
93    #[tokio::test]
94    async fn test_cat_nonexistent() {
95        let ctx = CommandContext::new(vec!["/nonexistent/file/12345".to_string()]);
96        let result = cat(ctx).await;
97
98        assert!(!result.is_success());
99        assert!(result.stderr.contains("No such file or directory"));
100    }
101
102    #[tokio::test]
103    async fn test_cat_multiple_files() {
104        let mut temp1 = NamedTempFile::new().unwrap();
105        let mut temp2 = NamedTempFile::new().unwrap();
106        write!(temp1, "file1").unwrap();
107        write!(temp2, "file2").unwrap();
108
109        let ctx = CommandContext::new(vec![
110            temp1.path().to_string_lossy().to_string(),
111            temp2.path().to_string_lossy().to_string(),
112        ]);
113        let result = cat(ctx).await;
114
115        assert!(result.is_success());
116        assert_eq!(result.stdout, "file1file2");
117    }
118}