Skip to main content

command_stream/commands/
ls.rs

1//! Virtual `ls` command implementation
2
3use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult};
5use std::fs;
6use std::path::Path;
7
8/// Execute the ls command
9///
10/// Lists directory contents.
11pub async fn ls(ctx: CommandContext) -> CommandResult {
12    // Parse flags
13    let mut show_all = false;
14    let mut long_format = false;
15    let mut paths = Vec::new();
16
17    for arg in &ctx.args {
18        if arg == "-a" || arg == "--all" {
19            show_all = true;
20        } else if arg == "-l" {
21            long_format = true;
22        } else if arg == "-la" || arg == "-al" {
23            show_all = true;
24            long_format = true;
25        } else if arg.starts_with('-') {
26            if arg.contains('a') {
27                show_all = true;
28            }
29            if arg.contains('l') {
30                long_format = true;
31            }
32        } else {
33            paths.push(arg.clone());
34        }
35    }
36
37    // Default to current directory
38    if paths.is_empty() {
39        paths.push(".".to_string());
40    }
41
42    let cwd = ctx.get_cwd();
43    let mut outputs = Vec::new();
44
45    for path_str in paths {
46        let resolved_path = if Path::new(&path_str).is_absolute() {
47            Path::new(&path_str).to_path_buf()
48        } else {
49            cwd.join(&path_str)
50        };
51
52        trace_lazy("VirtualCommand", || {
53            format!("ls: listing {:?}", resolved_path)
54        });
55
56        if !resolved_path.exists() {
57            return CommandResult::error(format!(
58                "ls: cannot access '{}': No such file or directory\n",
59                path_str
60            ));
61        }
62
63        if resolved_path.is_file() {
64            outputs.push(format_entry(&resolved_path, long_format));
65        } else {
66            match fs::read_dir(&resolved_path) {
67                Ok(entries) => {
68                    let mut entry_strs = Vec::new();
69
70                    for entry in entries {
71                        if let Ok(entry) = entry {
72                            let name = entry.file_name().to_string_lossy().to_string();
73
74                            // Skip hidden files unless -a is specified
75                            if !show_all && name.starts_with('.') {
76                                continue;
77                            }
78
79                            if long_format {
80                                entry_strs.push(format_entry(&entry.path(), true));
81                            } else {
82                                entry_strs.push(name);
83                            }
84                        }
85                    }
86
87                    entry_strs.sort();
88                    outputs.push(entry_strs.join("\n"));
89                }
90                Err(e) => {
91                    return CommandResult::error(format!(
92                        "ls: cannot open '{}': {}\n",
93                        path_str, e
94                    ));
95                }
96            }
97        }
98    }
99
100    let output = outputs.join("\n");
101    if output.is_empty() {
102        CommandResult::success_empty()
103    } else {
104        CommandResult::success(format!("{}\n", output))
105    }
106}
107
108fn format_entry(path: &Path, long_format: bool) -> String {
109    let name = path
110        .file_name()
111        .map(|n| n.to_string_lossy().to_string())
112        .unwrap_or_else(|| path.display().to_string());
113
114    if !long_format {
115        return name;
116    }
117
118    // Long format: permissions, links, owner, group, size, date, name
119    let metadata = match fs::metadata(path) {
120        Ok(m) => m,
121        Err(_) => return name,
122    };
123
124    let file_type = if metadata.is_dir() { "d" } else { "-" };
125    let size = metadata.len();
126
127    // Simplified permissions
128    let perms = if metadata.is_dir() {
129        "drwxr-xr-x"
130    } else {
131        "-rw-r--r--"
132    };
133
134    format!("{} {:>8} {}", perms, size, name)
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use tempfile::tempdir;
141
142    #[tokio::test]
143    async fn test_ls_current_dir() {
144        let ctx = CommandContext::new(vec![]);
145        let result = ls(ctx).await;
146        assert!(result.is_success());
147    }
148
149    #[tokio::test]
150    async fn test_ls_with_path() {
151        let temp = tempdir().unwrap();
152        fs::write(temp.path().join("file.txt"), "test").unwrap();
153
154        let ctx = CommandContext::new(vec![temp.path().to_string_lossy().to_string()]);
155        let result = ls(ctx).await;
156
157        assert!(result.is_success());
158        assert!(result.stdout.contains("file.txt"));
159    }
160
161    #[tokio::test]
162    async fn test_ls_hidden_files() {
163        let temp = tempdir().unwrap();
164        fs::write(temp.path().join(".hidden"), "test").unwrap();
165        fs::write(temp.path().join("visible"), "test").unwrap();
166
167        // Without -a
168        let ctx = CommandContext::new(vec![temp.path().to_string_lossy().to_string()]);
169        let result = ls(ctx).await;
170        assert!(!result.stdout.contains(".hidden"));
171        assert!(result.stdout.contains("visible"));
172
173        // With -a
174        let ctx = CommandContext::new(vec![
175            "-a".to_string(),
176            temp.path().to_string_lossy().to_string(),
177        ]);
178        let result = ls(ctx).await;
179        assert!(result.stdout.contains(".hidden"));
180        assert!(result.stdout.contains("visible"));
181    }
182}