Skip to content

Commit e3a6025

Browse files
chaliyclaude
andauthored
feat(builtins): implement file comparison test operators (#267)
## Summary - Implement `-nt` (newer than), `-ot` (older than), and `-ef` (same file) test operators - Works in both `[ ]` test command and `[[ ]]` conditionals - Uses VFS metadata timestamps for `-nt`/`-ot`, canonical path comparison for `-ef` - Handles edge cases: nonexistent files, both nonexistent, same path ## Test plan - [x] `cargo clippy --all-targets --all-features -- -D warnings` passes - [x] `cargo test --all-features --test spec_tests bash_spec_tests` passes - [x] 10 new spec tests (Bash 981, Total 1399) - [x] Tests marked `bash_diff` where timing-dependent Co-authored-by: Claude <noreply@anthropic.com>
1 parent 88177b6 commit e3a6025

File tree

4 files changed

+155
-8
lines changed

4 files changed

+155
-8
lines changed

crates/bashkit/src/builtins/test.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fn evaluate_expression<'a>(
110110
}
111111
3 => {
112112
// Binary operators
113-
evaluate_binary(&args[0], &args[1], &args[2])
113+
evaluate_binary(&args[0], &args[1], &args[2], fs).await
114114
}
115115
_ => false,
116116
}
@@ -198,7 +198,7 @@ async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>) -> bool {
198198
}
199199

200200
/// Evaluate a binary test expression
201-
fn evaluate_binary(left: &str, op: &str, right: &str) -> bool {
201+
async fn evaluate_binary(left: &str, op: &str, right: &str, fs: &Arc<dyn FileSystem>) -> bool {
202202
match op {
203203
// String comparisons
204204
"=" | "==" => left == right,
@@ -214,8 +214,34 @@ fn evaluate_binary(left: &str, op: &str, right: &str) -> bool {
214214
"-gt" => parse_int(left) > parse_int(right),
215215
"-ge" => parse_int(left) >= parse_int(right),
216216

217-
// File comparisons (not implemented)
218-
"-nt" | "-ot" | "-ef" => false,
217+
// File comparisons
218+
"-nt" => {
219+
// file1 is newer than file2
220+
let left_meta = fs.stat(Path::new(left)).await;
221+
let right_meta = fs.stat(Path::new(right)).await;
222+
match (left_meta, right_meta) {
223+
(Ok(lm), Ok(rm)) => lm.modified > rm.modified,
224+
(Ok(_), Err(_)) => true, // left exists, right doesn't → left is newer
225+
_ => false,
226+
}
227+
}
228+
"-ot" => {
229+
// file1 is older than file2
230+
let left_meta = fs.stat(Path::new(left)).await;
231+
let right_meta = fs.stat(Path::new(right)).await;
232+
match (left_meta, right_meta) {
233+
(Ok(lm), Ok(rm)) => lm.modified < rm.modified,
234+
(Err(_), Ok(_)) => true, // left doesn't exist, right does → left is older
235+
_ => false,
236+
}
237+
}
238+
"-ef" => {
239+
// file1 and file2 refer to the same file (same path after resolution)
240+
// In VFS without inodes, compare canonical paths
241+
let left_path = super::resolve_path(&std::path::PathBuf::from("/"), left);
242+
let right_path = super::resolve_path(&std::path::PathBuf::from("/"), right);
243+
left_path == right_path
244+
}
219245

220246
_ => false,
221247
}

crates/bashkit/src/interpreter/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,35 @@ impl Interpreter {
12271227
>= args[2].parse::<i64>().unwrap_or(0)
12281228
}
12291229
"=~" => self.regex_match(&args[0], &args[2]),
1230+
"-nt" => {
1231+
let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
1232+
let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
1233+
match (lm, rm) {
1234+
(Ok(l), Ok(r)) => l.modified > r.modified,
1235+
(Ok(_), Err(_)) => true,
1236+
_ => false,
1237+
}
1238+
}
1239+
"-ot" => {
1240+
let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
1241+
let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
1242+
match (lm, rm) {
1243+
(Ok(l), Ok(r)) => l.modified < r.modified,
1244+
(Err(_), Ok(_)) => true,
1245+
_ => false,
1246+
}
1247+
}
1248+
"-ef" => {
1249+
let lp = crate::builtins::resolve_path(
1250+
&std::path::PathBuf::from("/"),
1251+
&args[0],
1252+
);
1253+
let rp = crate::builtins::resolve_path(
1254+
&std::path::PathBuf::from("/"),
1255+
&args[2],
1256+
);
1257+
lp == rp
1258+
}
12301259
_ => false,
12311260
}
12321261
}

crates/bashkit/tests/spec_cases/bash/test-operators.test.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,95 @@ both true
123123
### expect
124124
one true
125125
### end
126+
127+
### test_file_newer_than
128+
# Test -nt (file1 newer than file2)
129+
### bash_diff
130+
echo "old" > /tmp/old.txt
131+
sleep 0.01
132+
echo "new" > /tmp/new.txt
133+
[ /tmp/new.txt -nt /tmp/old.txt ] && echo "newer"
134+
### expect
135+
newer
136+
### end
137+
138+
### test_file_older_than
139+
# Test -ot (file1 older than file2)
140+
### bash_diff
141+
echo "first" > /tmp/first.txt
142+
sleep 0.01
143+
echo "second" > /tmp/second.txt
144+
[ /tmp/first.txt -ot /tmp/second.txt ] && echo "older"
145+
### expect
146+
older
147+
### end
148+
149+
### test_file_nt_nonexistent
150+
# -nt returns true if left exists and right doesn't
151+
echo "exists" > /tmp/exists_nt.txt
152+
[ /tmp/exists_nt.txt -nt /tmp/nonexistent_nt ] && echo "newer"
153+
### expect
154+
newer
155+
### end
156+
157+
### test_file_ot_nonexistent
158+
# -ot returns true if left doesn't exist and right does
159+
echo "exists" > /tmp/exists_ot.txt
160+
[ /tmp/nonexistent_ot -ot /tmp/exists_ot.txt ] && echo "older"
161+
### expect
162+
older
163+
### end
164+
165+
### test_file_ef_same_path
166+
# -ef returns true for same file
167+
echo "data" > /tmp/ef_test.txt
168+
[ /tmp/ef_test.txt -ef /tmp/ef_test.txt ] && echo "same"
169+
### expect
170+
same
171+
### end
172+
173+
### test_file_ef_different_path
174+
# -ef returns false for different files
175+
echo "a" > /tmp/ef_a.txt
176+
echo "b" > /tmp/ef_b.txt
177+
[ /tmp/ef_a.txt -ef /tmp/ef_b.txt ] || echo "different"
178+
### expect
179+
different
180+
### end
181+
182+
### test_file_nt_both_nonexistent
183+
# -nt returns false if both don't exist
184+
[ /tmp/no1 -nt /tmp/no2 ] || echo "false"
185+
### expect
186+
false
187+
### end
188+
189+
### test_cond_nt
190+
# [[ ]] also supports -nt
191+
### bash_diff
192+
echo "old" > /tmp/c_old.txt
193+
sleep 0.01
194+
echo "new" > /tmp/c_new.txt
195+
[[ /tmp/c_new.txt -nt /tmp/c_old.txt ]] && echo "newer"
196+
### expect
197+
newer
198+
### end
199+
200+
### test_cond_ot
201+
# [[ ]] also supports -ot
202+
### bash_diff
203+
echo "first" > /tmp/c_first.txt
204+
sleep 0.01
205+
echo "second" > /tmp/c_second.txt
206+
[[ /tmp/c_first.txt -ot /tmp/c_second.txt ]] && echo "older"
207+
### expect
208+
older
209+
### end
210+
211+
### test_cond_ef
212+
# [[ ]] also supports -ef
213+
echo "data" > /tmp/c_ef.txt
214+
[[ /tmp/c_ef.txt -ef /tmp/c_ef.txt ]] && echo "same"
215+
### expect
216+
same
217+
### end

specs/009-implementation-status.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
103103

104104
## Spec Test Coverage
105105

106-
**Total spec test cases:** 1389 (1384 pass, 5 skip)
106+
**Total spec test cases:** 1399 (1394 pass, 5 skip)
107107

108108
| Category | Cases | In CI | Pass | Skip | Notes |
109109
|----------|-------|-------|------|------|-------|
110-
| Bash (core) | 971 | Yes | 966 | 5 | `bash_spec_tests` in CI |
110+
| Bash (core) | 981 | Yes | 976 | 5 | `bash_spec_tests` in CI |
111111
| AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g |
112112
| Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect |
113113
| Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E |
114114
| JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env |
115115
| Python | 57 | Yes | 57 | 0 | embedded Python (Monty) |
116-
| **Total** | **1389** | **Yes** | **1384** | **5** | |
116+
| **Total** | **1399** | **Yes** | **1394** | **5** | |
117117

118118
### Bash Spec Tests Breakdown
119119

@@ -154,7 +154,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
154154
| sleep.test.sh | 6 | |
155155
| sortuniq.test.sh | 32 | sort and uniq, `-z` zero-terminated, `-m` merge |
156156
| source.test.sh | 21 | source/., function loading, PATH search, positional params |
157-
| test-operators.test.sh | 17 | file/string tests |
157+
| test-operators.test.sh | 27 | file/string tests, `-nt`/`-ot`/`-ef` file comparisons |
158158
| time.test.sh | 11 | Wall-clock only (user/sys always 0) |
159159
| timeout.test.sh | 17 | |
160160
| variables.test.sh | 97 | includes special vars, prefix env, PIPESTATUS, trap EXIT, `${var@Q}`, `\<newline>` line continuation, PWD/HOME/USER/HOSTNAME/BASH_VERSION/SECONDS, `set -x` xtrace, `shopt` builtin, nullglob, `set -o`/`set +o` display, `trap -p` |

0 commit comments

Comments
 (0)