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.flatten() {
73 let name = entry.file_name().to_string_lossy().to_string();
74
75 if !show_all && name.starts_with('.') {
77 continue;
78 }
79
80 if long_format {
81 entry_strs.push(format_entry(&entry.path(), true));
82 } else {
83 entry_strs.push(name);
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 size = metadata.len();
125
126 let perms = if metadata.is_dir() {
128 "drwxr-xr-x"
129 } else {
130 "-rw-r--r--"
131 };
132
133 format!("{} {:>8} {}", perms, size, name)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use tempfile::tempdir;
140
141 #[tokio::test]
142 async fn test_ls_current_dir() {
143 let ctx = CommandContext::new(vec![]);
144 let result = ls(ctx).await;
145 assert!(result.is_success());
146 }
147
148 #[tokio::test]
149 async fn test_ls_with_path() {
150 let temp = tempdir().unwrap();
151 fs::write(temp.path().join("file.txt"), "test").unwrap();
152
153 let ctx = CommandContext::new(vec![temp.path().to_string_lossy().to_string()]);
154 let result = ls(ctx).await;
155
156 assert!(result.is_success());
157 assert!(result.stdout.contains("file.txt"));
158 }
159
160 #[tokio::test]
161 async fn test_ls_hidden_files() {
162 let temp = tempdir().unwrap();
163 fs::write(temp.path().join(".hidden"), "test").unwrap();
164 fs::write(temp.path().join("visible"), "test").unwrap();
165
166 let ctx = CommandContext::new(vec![temp.path().to_string_lossy().to_string()]);
168 let result = ls(ctx).await;
169 assert!(!result.stdout.contains(".hidden"));
170 assert!(result.stdout.contains("visible"));
171
172 let ctx = CommandContext::new(vec![
174 "-a".to_string(),
175 temp.path().to_string_lossy().to_string(),
176 ]);
177 let result = ls(ctx).await;
178 assert!(result.stdout.contains(".hidden"));
179 assert!(result.stdout.contains("visible"));
180 }
181}