Rust · 9495 bytes Raw Blame History
1 use std::fs;
2 use std::path::Path;
3
4 /// Execute the test builtin ([ or test)
5 ///
6 /// Returns true (0) if the test succeeds, false (1) if it fails
7 pub fn execute_test(args: &[String]) -> i32 {
8 // Handle [ command which requires trailing ]
9 let args = if args.last().map(|s| s.as_str()) == Some("]") {
10 &args[..args.len() - 1]
11 } else {
12 args
13 };
14
15 if args.is_empty() {
16 return 1; // Empty test is false
17 }
18
19 match evaluate_test(args) {
20 Ok(result) => if result { 0 } else { 1 },
21 Err(_) => 2, // Syntax error
22 }
23 }
24
25 fn evaluate_test(args: &[String]) -> Result<bool, String> {
26 if args.is_empty() {
27 return Ok(false);
28 }
29
30 // Handle negation
31 if args[0] == "!" {
32 return Ok(!evaluate_test(&args[1..])?);
33 }
34
35 // Single argument: non-empty string test
36 if args.len() == 1 {
37 return Ok(!args[0].is_empty());
38 }
39
40 // Two arguments: unary operators
41 if args.len() == 2 {
42 return evaluate_unary(&args[0], &args[1]);
43 }
44
45 // Three arguments: binary operators
46 if args.len() == 3 {
47 return evaluate_binary(&args[0], &args[1], &args[2]);
48 }
49
50 // Four arguments with negation
51 if args.len() == 4 && args[0] == "!" {
52 return Ok(!evaluate_binary(&args[1], &args[2], &args[3])?);
53 }
54
55 // Complex expressions with -a (and) or -o (or)
56 // Find the operator (rightmost for left-to-right evaluation)
57 for (i, arg) in args.iter().enumerate() {
58 if arg == "-o" && i > 0 && i < args.len() - 1 {
59 let left = evaluate_test(&args[..i])?;
60 let right = evaluate_test(&args[i + 1..])?;
61 return Ok(left || right);
62 }
63 }
64
65 for (i, arg) in args.iter().enumerate() {
66 if arg == "-a" && i > 0 && i < args.len() - 1 {
67 let left = evaluate_test(&args[..i])?;
68 let right = evaluate_test(&args[i + 1..])?;
69 return Ok(left && right);
70 }
71 }
72
73 Err("Invalid test syntax".to_string())
74 }
75
76 fn evaluate_unary(op: &str, arg: &str) -> Result<bool, String> {
77 match op {
78 // String tests
79 "-z" => Ok(arg.is_empty()),
80 "-n" => Ok(!arg.is_empty()),
81
82 // File existence and type tests
83 "-e" => Ok(Path::new(arg).exists()),
84 "-f" => Ok(Path::new(arg).is_file()),
85 "-d" => Ok(Path::new(arg).is_dir()),
86 "-L" | "-h" => Ok(is_symlink(arg)),
87 "-p" => Ok(is_fifo(arg)),
88 "-S" => Ok(is_socket(arg)),
89 "-b" => Ok(is_block_device(arg)),
90 "-c" => Ok(is_char_device(arg)),
91
92 // File permission tests
93 "-r" => Ok(is_readable(arg)),
94 "-w" => Ok(is_writable(arg)),
95 "-x" => Ok(is_executable(arg)),
96
97 // File size test
98 "-s" => Ok(file_has_size(arg)),
99
100 _ => Err(format!("Unknown unary operator: {}", op)),
101 }
102 }
103
104 fn evaluate_binary(left: &str, op: &str, right: &str) -> Result<bool, String> {
105 match op {
106 // String comparisons
107 "=" | "==" => Ok(left == right),
108 "!=" => Ok(left != right),
109
110 // Numeric comparisons
111 "-eq" => compare_numbers(left, right, |a, b| a == b),
112 "-ne" => compare_numbers(left, right, |a, b| a != b),
113 "-lt" => compare_numbers(left, right, |a, b| a < b),
114 "-le" => compare_numbers(left, right, |a, b| a <= b),
115 "-gt" => compare_numbers(left, right, |a, b| a > b),
116 "-ge" => compare_numbers(left, right, |a, b| a >= b),
117
118 // File comparisons
119 "-nt" => Ok(file_newer_than(left, right)),
120 "-ot" => Ok(file_older_than(left, right)),
121 "-ef" => Ok(same_file(left, right)),
122
123 _ => Err(format!("Unknown binary operator: {}", op)),
124 }
125 }
126
127 fn compare_numbers<F>(left: &str, right: &str, compare: F) -> Result<bool, String>
128 where
129 F: FnOnce(i64, i64) -> bool,
130 {
131 let left_num = left.parse::<i64>()
132 .map_err(|_| format!("Not a number: {}", left))?;
133 let right_num = right.parse::<i64>()
134 .map_err(|_| format!("Not a number: {}", right))?;
135
136 Ok(compare(left_num, right_num))
137 }
138
139 fn is_readable(path: &str) -> bool {
140 fs::metadata(path).is_ok()
141 }
142
143 fn is_writable(path: &str) -> bool {
144 if let Ok(metadata) = fs::metadata(path) {
145 !metadata.permissions().readonly()
146 } else {
147 false
148 }
149 }
150
151 #[cfg(unix)]
152 fn is_executable(path: &str) -> bool {
153 use std::os::unix::fs::PermissionsExt;
154 if let Ok(metadata) = fs::metadata(path) {
155 metadata.permissions().mode() & 0o111 != 0
156 } else {
157 false
158 }
159 }
160
161 #[cfg(not(unix))]
162 fn is_executable(_path: &str) -> bool {
163 // On non-Unix, we can't easily check execute permissions
164 Path::new(_path).exists()
165 }
166
167 fn file_has_size(path: &str) -> bool {
168 if let Ok(metadata) = fs::metadata(path) {
169 metadata.len() > 0
170 } else {
171 false
172 }
173 }
174
175 #[cfg(unix)]
176 fn is_symlink(path: &str) -> bool {
177 // Use symlink_metadata to not follow the symlink
178 if let Ok(metadata) = fs::symlink_metadata(path) {
179 metadata.file_type().is_symlink()
180 } else {
181 false
182 }
183 }
184
185 #[cfg(not(unix))]
186 fn is_symlink(path: &str) -> bool {
187 if let Ok(metadata) = fs::symlink_metadata(path) {
188 metadata.file_type().is_symlink()
189 } else {
190 false
191 }
192 }
193
194 #[cfg(unix)]
195 fn is_fifo(path: &str) -> bool {
196 use std::os::unix::fs::FileTypeExt;
197 if let Ok(metadata) = fs::metadata(path) {
198 metadata.file_type().is_fifo()
199 } else {
200 false
201 }
202 }
203
204 #[cfg(not(unix))]
205 fn is_fifo(_path: &str) -> bool {
206 false // FIFOs are Unix-specific
207 }
208
209 #[cfg(unix)]
210 fn is_socket(path: &str) -> bool {
211 use std::os::unix::fs::FileTypeExt;
212 if let Ok(metadata) = fs::metadata(path) {
213 metadata.file_type().is_socket()
214 } else {
215 false
216 }
217 }
218
219 #[cfg(not(unix))]
220 fn is_socket(_path: &str) -> bool {
221 false // Unix sockets are Unix-specific
222 }
223
224 #[cfg(unix)]
225 fn is_block_device(path: &str) -> bool {
226 use std::os::unix::fs::FileTypeExt;
227 if let Ok(metadata) = fs::metadata(path) {
228 metadata.file_type().is_block_device()
229 } else {
230 false
231 }
232 }
233
234 #[cfg(not(unix))]
235 fn is_block_device(_path: &str) -> bool {
236 false
237 }
238
239 #[cfg(unix)]
240 fn is_char_device(path: &str) -> bool {
241 use std::os::unix::fs::FileTypeExt;
242 if let Ok(metadata) = fs::metadata(path) {
243 metadata.file_type().is_char_device()
244 } else {
245 false
246 }
247 }
248
249 #[cfg(not(unix))]
250 fn is_char_device(_path: &str) -> bool {
251 false
252 }
253
254 fn file_newer_than(file1: &str, file2: &str) -> bool {
255 let meta1 = fs::metadata(file1);
256 let meta2 = fs::metadata(file2);
257
258 match (meta1, meta2) {
259 (Ok(m1), Ok(m2)) => {
260 match (m1.modified(), m2.modified()) {
261 (Ok(t1), Ok(t2)) => t1 > t2,
262 _ => false,
263 }
264 }
265 _ => false,
266 }
267 }
268
269 fn file_older_than(file1: &str, file2: &str) -> bool {
270 let meta1 = fs::metadata(file1);
271 let meta2 = fs::metadata(file2);
272
273 match (meta1, meta2) {
274 (Ok(m1), Ok(m2)) => {
275 match (m1.modified(), m2.modified()) {
276 (Ok(t1), Ok(t2)) => t1 < t2,
277 _ => false,
278 }
279 }
280 _ => false,
281 }
282 }
283
284 #[cfg(unix)]
285 fn same_file(file1: &str, file2: &str) -> bool {
286 use std::os::unix::fs::MetadataExt;
287 let meta1 = fs::metadata(file1);
288 let meta2 = fs::metadata(file2);
289
290 match (meta1, meta2) {
291 (Ok(m1), Ok(m2)) => m1.dev() == m2.dev() && m1.ino() == m2.ino(),
292 _ => false,
293 }
294 }
295
296 #[cfg(not(unix))]
297 fn same_file(file1: &str, file2: &str) -> bool {
298 // On non-Unix, we can only do a basic path comparison
299 use std::path::PathBuf;
300 let p1 = PathBuf::from(file1).canonicalize();
301 let p2 = PathBuf::from(file2).canonicalize();
302 match (p1, p2) {
303 (Ok(path1), Ok(path2)) => path1 == path2,
304 _ => false,
305 }
306 }
307
308 #[cfg(test)]
309 mod tests {
310 use super::*;
311
312 #[test]
313 fn test_string_equal() {
314 assert_eq!(execute_test(&["hello".to_string(), "=".to_string(), "hello".to_string()]), 0);
315 assert_eq!(execute_test(&["hello".to_string(), "=".to_string(), "world".to_string()]), 1);
316 }
317
318 #[test]
319 fn test_string_not_equal() {
320 assert_eq!(execute_test(&["hello".to_string(), "!=".to_string(), "world".to_string()]), 0);
321 assert_eq!(execute_test(&["hello".to_string(), "!=".to_string(), "hello".to_string()]), 1);
322 }
323
324 #[test]
325 fn test_string_empty() {
326 assert_eq!(execute_test(&["-z".to_string(), "".to_string()]), 0);
327 assert_eq!(execute_test(&["-z".to_string(), "hello".to_string()]), 1);
328 assert_eq!(execute_test(&["-n".to_string(), "hello".to_string()]), 0);
329 assert_eq!(execute_test(&["-n".to_string(), "".to_string()]), 1);
330 }
331
332 #[test]
333 fn test_numeric_comparison() {
334 assert_eq!(execute_test(&["5".to_string(), "-eq".to_string(), "5".to_string()]), 0);
335 assert_eq!(execute_test(&["5".to_string(), "-eq".to_string(), "6".to_string()]), 1);
336 assert_eq!(execute_test(&["3".to_string(), "-lt".to_string(), "5".to_string()]), 0);
337 assert_eq!(execute_test(&["5".to_string(), "-lt".to_string(), "3".to_string()]), 1);
338 assert_eq!(execute_test(&["7".to_string(), "-gt".to_string(), "5".to_string()]), 0);
339 }
340
341 #[test]
342 fn test_negation() {
343 assert_eq!(execute_test(&["!".to_string(), "-z".to_string(), "hello".to_string()]), 0);
344 assert_eq!(execute_test(&["!".to_string(), "-z".to_string(), "".to_string()]), 1);
345 }
346 }
347