command_stream/commands/
ls.rs1use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult};
5use std::fs;
6use std::path::Path;
7
8pub async fn ls(ctx: CommandContext) -> CommandResult {
12 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 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 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 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 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 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 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}