Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions crates/bashkit/src/builtins/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn evaluate_expression<'a>(
}
3 => {
// Binary operators
evaluate_binary(&args[0], &args[1], &args[2])
evaluate_binary(&args[0], &args[1], &args[2], fs).await
}
_ => false,
}
Expand Down Expand Up @@ -198,7 +198,7 @@ async fn evaluate_unary(op: &str, arg: &str, fs: &Arc<dyn FileSystem>) -> bool {
}

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

// File comparisons (not implemented)
"-nt" | "-ot" | "-ef" => false,
// File comparisons
"-nt" => {
// file1 is newer than file2
let left_meta = fs.stat(Path::new(left)).await;
let right_meta = fs.stat(Path::new(right)).await;
match (left_meta, right_meta) {
(Ok(lm), Ok(rm)) => lm.modified > rm.modified,
(Ok(_), Err(_)) => true, // left exists, right doesn't → left is newer
_ => false,
}
}
"-ot" => {
// file1 is older than file2
let left_meta = fs.stat(Path::new(left)).await;
let right_meta = fs.stat(Path::new(right)).await;
match (left_meta, right_meta) {
(Ok(lm), Ok(rm)) => lm.modified < rm.modified,
(Err(_), Ok(_)) => true, // left doesn't exist, right does → left is older
_ => false,
}
}
"-ef" => {
// file1 and file2 refer to the same file (same path after resolution)
// In VFS without inodes, compare canonical paths
let left_path = super::resolve_path(&std::path::PathBuf::from("/"), left);
let right_path = super::resolve_path(&std::path::PathBuf::from("/"), right);
left_path == right_path
}

_ => false,
}
Expand Down
29 changes: 29 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,35 @@ impl Interpreter {
>= args[2].parse::<i64>().unwrap_or(0)
}
"=~" => self.regex_match(&args[0], &args[2]),
"-nt" => {
let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
match (lm, rm) {
(Ok(l), Ok(r)) => l.modified > r.modified,
(Ok(_), Err(_)) => true,
_ => false,
}
}
"-ot" => {
let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
match (lm, rm) {
(Ok(l), Ok(r)) => l.modified < r.modified,
(Err(_), Ok(_)) => true,
_ => false,
}
}
"-ef" => {
let lp = crate::builtins::resolve_path(
&std::path::PathBuf::from("/"),
&args[0],
);
let rp = crate::builtins::resolve_path(
&std::path::PathBuf::from("/"),
&args[2],
);
lp == rp
}
_ => false,
}
}
Expand Down
92 changes: 92 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/test-operators.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,95 @@ both true
### expect
one true
### end

### test_file_newer_than
# Test -nt (file1 newer than file2)
### bash_diff
echo "old" > /tmp/old.txt
sleep 0.01
echo "new" > /tmp/new.txt
[ /tmp/new.txt -nt /tmp/old.txt ] && echo "newer"
### expect
newer
### end

### test_file_older_than
# Test -ot (file1 older than file2)
### bash_diff
echo "first" > /tmp/first.txt
sleep 0.01
echo "second" > /tmp/second.txt
[ /tmp/first.txt -ot /tmp/second.txt ] && echo "older"
### expect
older
### end

### test_file_nt_nonexistent
# -nt returns true if left exists and right doesn't
echo "exists" > /tmp/exists_nt.txt
[ /tmp/exists_nt.txt -nt /tmp/nonexistent_nt ] && echo "newer"
### expect
newer
### end

### test_file_ot_nonexistent
# -ot returns true if left doesn't exist and right does
echo "exists" > /tmp/exists_ot.txt
[ /tmp/nonexistent_ot -ot /tmp/exists_ot.txt ] && echo "older"
### expect
older
### end

### test_file_ef_same_path
# -ef returns true for same file
echo "data" > /tmp/ef_test.txt
[ /tmp/ef_test.txt -ef /tmp/ef_test.txt ] && echo "same"
### expect
same
### end

### test_file_ef_different_path
# -ef returns false for different files
echo "a" > /tmp/ef_a.txt
echo "b" > /tmp/ef_b.txt
[ /tmp/ef_a.txt -ef /tmp/ef_b.txt ] || echo "different"
### expect
different
### end

### test_file_nt_both_nonexistent
# -nt returns false if both don't exist
[ /tmp/no1 -nt /tmp/no2 ] || echo "false"
### expect
false
### end

### test_cond_nt
# [[ ]] also supports -nt
### bash_diff
echo "old" > /tmp/c_old.txt
sleep 0.01
echo "new" > /tmp/c_new.txt
[[ /tmp/c_new.txt -nt /tmp/c_old.txt ]] && echo "newer"
### expect
newer
### end

### test_cond_ot
# [[ ]] also supports -ot
### bash_diff
echo "first" > /tmp/c_first.txt
sleep 0.01
echo "second" > /tmp/c_second.txt
[[ /tmp/c_first.txt -ot /tmp/c_second.txt ]] && echo "older"
### expect
older
### end

### test_cond_ef
# [[ ]] also supports -ef
echo "data" > /tmp/c_ef.txt
[[ /tmp/c_ef.txt -ef /tmp/c_ef.txt ]] && echo "same"
### expect
same
### end
8 changes: 4 additions & 4 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See

## Spec Test Coverage

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

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

### Bash Spec Tests Breakdown

Expand Down Expand Up @@ -154,7 +154,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
| sleep.test.sh | 6 | |
| sortuniq.test.sh | 32 | sort and uniq, `-z` zero-terminated, `-m` merge |
| source.test.sh | 21 | source/., function loading, PATH search, positional params |
| test-operators.test.sh | 17 | file/string tests |
| test-operators.test.sh | 27 | file/string tests, `-nt`/`-ot`/`-ef` file comparisons |
| time.test.sh | 11 | Wall-clock only (user/sys always 0) |
| timeout.test.sh | 17 | |
| 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` |
Expand Down
Loading