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
2 changes: 2 additions & 0 deletions crates/bashkit/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod source;
mod strings;
mod system;
mod test;
mod textrev;
mod timeout;
mod vars;
mod wait;
Expand Down Expand Up @@ -105,6 +106,7 @@ pub use source::Source;
pub use strings::Strings;
pub use system::{Hostname, Id, Uname, Whoami, DEFAULT_HOSTNAME, DEFAULT_USERNAME};
pub use test::{Bracket, Test};
pub use textrev::{Rev, Tac};
pub use timeout::Timeout;
pub use vars::{Eval, Local, Readonly, Set, Shift, Shopt, Times, Unset};
pub use wait::Wait;
Expand Down
125 changes: 125 additions & 0 deletions crates/bashkit/src/builtins/textrev.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Text reversal builtins: tac (reverse line order) and rev (reverse characters per line)

use async_trait::async_trait;
use std::path::Path;

use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;

/// Read input from files or stdin, returning the raw text.
async fn read_input(ctx: &Context<'_>) -> std::result::Result<String, ExecResult> {
let mut files: Vec<&str> = Vec::new();
for arg in ctx.args {
if !arg.starts_with('-') {
files.push(arg);
}
}

let mut raw = String::new();
if files.is_empty() {
if let Some(stdin) = ctx.stdin {
raw.push_str(stdin);
}
} else {
for file in &files {
if *file == "-" {
if let Some(stdin) = ctx.stdin {
raw.push_str(stdin);
}
} else {
let path = if Path::new(file).is_absolute() {
file.to_string()
} else {
ctx.cwd.join(file).to_string_lossy().to_string()
};
match ctx.fs.read_file(Path::new(&path)).await {
Ok(content) => {
let text = String::from_utf8_lossy(&content);
raw.push_str(&text);
}
Err(e) => {
return Err(ExecResult::err(format!("tac: {}: {}\n", file, e), 1));
}
}
}
}
}
Ok(raw)
}

/// The tac builtin - concatenate and print files in reverse (line order).
///
/// Usage: tac [FILE...]
///
/// Prints lines in reverse order. Reads from stdin if no files given.
pub struct Tac;

#[async_trait]
impl Builtin for Tac {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let raw = match read_input(&ctx).await {
Ok(r) => r,
Err(e) => return Ok(e),
};

if raw.is_empty() {
return Ok(ExecResult::ok(String::new()));
}

let has_trailing_newline = raw.ends_with('\n');
let trimmed = if has_trailing_newline {
&raw[..raw.len() - 1]
} else {
&raw
};

let mut lines: Vec<&str> = trimmed.split('\n').collect();
lines.reverse();

let mut output = lines.join("\n");
output.push('\n');

Ok(ExecResult::ok(output))
}
}

/// The rev builtin - reverse characters of each line.
///
/// Usage: rev [FILE...]
///
/// Reverses characters on each line. Reads from stdin if no files given.
pub struct Rev;

#[async_trait]
impl Builtin for Rev {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let raw = match read_input(&ctx).await {
Ok(r) => r,
Err(e) => return Ok(e),
};

if raw.is_empty() {
return Ok(ExecResult::ok(String::new()));
}

let has_trailing_newline = raw.ends_with('\n');
let trimmed = if has_trailing_newline {
&raw[..raw.len() - 1]
} else {
&raw
};

let mut output = String::new();
for (i, line) in trimmed.split('\n').enumerate() {
if i > 0 {
output.push('\n');
}
let reversed: String = line.chars().rev().collect();
output.push_str(&reversed);
}
output.push('\n');

Ok(ExecResult::ok(output))
}
}
2 changes: 2 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ impl Interpreter {
builtins.insert("xxd".to_string(), Box::new(builtins::Xxd));
builtins.insert("hexdump".to_string(), Box::new(builtins::Hexdump));
builtins.insert("seq".to_string(), Box::new(builtins::Seq));
builtins.insert("tac".to_string(), Box::new(builtins::Tac));
builtins.insert("rev".to_string(), Box::new(builtins::Rev));
builtins.insert("sort".to_string(), Box::new(builtins::Sort));
builtins.insert("uniq".to_string(), Box::new(builtins::Uniq));
builtins.insert("cut".to_string(), Box::new(builtins::Cut));
Expand Down
92 changes: 92 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/textrev.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
### tac_basic
# tac reverses line order
printf "a\nb\nc\n" | tac
### expect
c
b
a
### end

### tac_single_line
# tac with single line
echo "hello" | tac
### expect
hello
### end

### tac_from_file
# tac reads from file
### bash_diff
printf "one\ntwo\nthree\n" > /tmp/tac_test
tac /tmp/tac_test
### expect
three
two
one
### end

### tac_numbered
# tac with numbered lines
printf "1\n2\n3\n4\n5\n" | tac
### expect
5
4
3
2
1
### end

### tac_empty_stdin
# tac with empty input produces no output
echo -n "" | tac
echo "done"
### expect
done
### end

### rev_basic
# rev reverses characters on each line
echo "hello" | rev
### expect
olleh
### end

### rev_multiple_lines
# rev reverses each line independently
printf "abc\ndef\nghi\n" | rev
### expect
cba
fed
ihg
### end

### rev_palindrome
# rev on palindrome outputs same word
echo "racecar" | rev
### expect
racecar
### end

### rev_from_file
# rev reads from file
### bash_diff
echo "hello world" > /tmp/rev_test
rev /tmp/rev_test
### expect
dlrow olleh
### end

### rev_empty_stdin
# rev with empty input produces no output
echo -n "" | rev
echo "done"
### expect
done
### end

### rev_spaces
# rev preserves and reverses spaces
echo "a b c" | rev
### expect
c b a
### end
7 changes: 4 additions & 3 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:** 1348 (1343 pass, 5 skip)
**Total spec test cases:** 1359 (1354 pass, 5 skip)

| Category | Cases | In CI | Pass | Skip | Notes |
|----------|-------|-------|------|------|-------|
| Bash (core) | 930 | Yes | 925 | 5 | `bash_spec_tests` in CI |
| Bash (core) | 941 | Yes | 936 | 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** | **1348** | **Yes** | **1343** | **5** | |
| **Total** | **1359** | **Yes** | **1354** | **5** | |

### Bash Spec Tests Breakdown

Expand Down Expand Up @@ -165,6 +165,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
| eval-bugs.test.sh | 4 | regression tests for eval/script bugs |
| script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes |
| seq.test.sh | 12 | `seq` numeric sequences, `-w`, `-s`, decrement, negative |
| textrev.test.sh | 11 | `tac` reverse line order, `rev` reverse characters |
| heredoc.test.sh | 10 | heredoc variable expansion, quoted delimiters, file redirects, `<<-` tab strip |
| string-ops.test.sh | 14 | string replacement (prefix/suffix anchored), `${var:?}`, case conversion |
| read-builtin.test.sh | 10 | `read` builtin, IFS splitting, `-r`, `-a` (array), `-n` (nchars), here-string |
Expand Down
Loading