diff --git a/crates/bashkit/src/builtins/mod.rs b/crates/bashkit/src/builtins/mod.rs index 2f98bde3..867d360c 100644 --- a/crates/bashkit/src/builtins/mod.rs +++ b/crates/bashkit/src/builtins/mod.rs @@ -51,6 +51,7 @@ mod pipeline; mod printf; mod read; mod sed; +mod seq; mod sleep; mod sortuniq; mod source; @@ -97,6 +98,7 @@ pub use pipeline::{Tee, Watch, Xargs}; pub use printf::Printf; pub use read::Read; pub use sed::Sed; +pub use seq::Seq; pub use sleep::Sleep; pub use sortuniq::{Sort, Uniq}; pub use source::Source; diff --git a/crates/bashkit/src/builtins/seq.rs b/crates/bashkit/src/builtins/seq.rs new file mode 100644 index 00000000..1812b22a --- /dev/null +++ b/crates/bashkit/src/builtins/seq.rs @@ -0,0 +1,182 @@ +//! seq builtin - print a sequence of numbers + +use async_trait::async_trait; + +use super::{Builtin, Context}; +use crate::error::Result; +use crate::interpreter::ExecResult; + +/// The seq builtin - print a sequence of numbers. +/// +/// Usage: seq [OPTION]... LAST +/// seq [OPTION]... FIRST LAST +/// seq [OPTION]... FIRST INCREMENT LAST +/// +/// Options: +/// -s STRING Use STRING as separator (default: newline) +/// -w Equalize width by padding with leading zeroes +pub struct Seq; + +#[async_trait] +impl Builtin for Seq { + async fn execute(&self, ctx: Context<'_>) -> Result { + let mut separator = "\n".to_string(); + let mut equal_width = false; + let mut nums: Vec = Vec::new(); + + let mut i = 0; + while i < ctx.args.len() { + match ctx.args[i].as_str() { + "-s" => { + i += 1; + if i < ctx.args.len() { + separator = ctx.args[i].clone(); + } + } + "-w" => equal_width = true, + arg if arg.starts_with("-s") => { + // -sSEP (no space) + separator = arg[2..].to_string(); + } + _ => { + nums.push(ctx.args[i].clone()); + } + } + i += 1; + } + + if nums.is_empty() { + return Ok(ExecResult::err("seq: missing operand\n".to_string(), 1)); + } + + let (first, increment, last) = match nums.len() { + 1 => { + let last: f64 = match nums[0].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[0]), + 1, + )); + } + }; + (1.0_f64, 1.0_f64, last) + } + 2 => { + let first: f64 = match nums[0].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[0]), + 1, + )); + } + }; + let last: f64 = match nums[1].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[1]), + 1, + )); + } + }; + (first, 1.0, last) + } + _ => { + let first: f64 = match nums[0].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[0]), + 1, + )); + } + }; + let increment: f64 = match nums[1].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[1]), + 1, + )); + } + }; + let last: f64 = match nums[2].parse() { + Ok(v) => v, + Err(_) => { + return Ok(ExecResult::err( + format!("seq: invalid floating point argument: '{}'\n", nums[2]), + 1, + )); + } + }; + (first, increment, last) + } + }; + + if increment == 0.0 { + return Ok(ExecResult::err("seq: zero increment\n".to_string(), 1)); + } + + // Determine if all values are integers + let all_integer = first.fract() == 0.0 && increment.fract() == 0.0 && last.fract() == 0.0; + + // Calculate width for -w flag + let width = if equal_width && all_integer { + let first_w = format!("{}", first as i64).len(); + let last_w = format!("{}", last as i64).len(); + first_w.max(last_w) + } else { + 0 + }; + + let mut output = String::new(); + let mut current = first; + let mut first_item = true; + + // Safety: limit iterations to prevent infinite loops + let max_iterations = 1_000_000; + let mut count = 0; + + loop { + if increment > 0.0 && current > last + f64::EPSILON { + break; + } + if increment < 0.0 && current < last - f64::EPSILON { + break; + } + count += 1; + if count > max_iterations { + break; + } + + if !first_item { + output.push_str(&separator); + } + first_item = false; + + if all_integer { + let val = current as i64; + if equal_width { + output.push_str(&format!("{:0>width$}", val, width = width)); + } else { + output.push_str(&format!("{}", val)); + } + } else { + // Format float, removing trailing zeros + let formatted = format!("{:.10}", current); + let trimmed = formatted.trim_end_matches('0').trim_end_matches('.'); + output.push_str(trimmed); + } + + current += increment; + } + + if !output.is_empty() { + output.push('\n'); + } + + Ok(ExecResult::ok(output)) + } +} diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index b97b687d..65407e9f 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -257,6 +257,7 @@ impl Interpreter { builtins.insert("od".to_string(), Box::new(builtins::Od)); 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("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)); diff --git a/crates/bashkit/tests/spec_cases/bash/seq.test.sh b/crates/bashkit/tests/spec_cases/bash/seq.test.sh new file mode 100644 index 00000000..02f35d15 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/seq.test.sh @@ -0,0 +1,120 @@ +### seq_single_arg +# seq LAST - count from 1 to LAST +seq 5 +### expect +1 +2 +3 +4 +5 +### end + +### seq_two_args +# seq FIRST LAST +seq 3 6 +### expect +3 +4 +5 +6 +### end + +### seq_three_args +# seq FIRST INCREMENT LAST +seq 1 2 9 +### expect +1 +3 +5 +7 +9 +### end + +### seq_decrement +# seq counting down +seq 5 -1 1 +### expect +5 +4 +3 +2 +1 +### end + +### seq_negative +# seq with negative numbers +seq -2 2 +### expect +-2 +-1 +0 +1 +2 +### end + +### seq_equal_width +# seq -w pads with leading zeros +seq -w 1 10 +### expect +01 +02 +03 +04 +05 +06 +07 +08 +09 +10 +### end + +### seq_separator +# seq -s uses custom separator +seq -s ", " 1 5 +### expect +1, 2, 3, 4, 5 +### end + +### seq_single_value +# seq 1 produces just 1 +seq 1 +### expect +1 +### end + +### seq_no_output +# seq where FIRST > LAST produces no output +seq 5 1 +echo "done" +### expect +done +### end + +### seq_in_subst +# seq output captured in command substitution +result=$(seq 3) +echo "$result" +### expect +1 +2 +3 +### end + +### seq_step_two +# seq with step of 2 +seq 0 2 8 +### expect +0 +2 +4 +6 +8 +### end + +### seq_missing_operand +# seq with no args should error +seq 2>/dev/null +echo $? +### expect +1 +### end diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 51e19364..56533212 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 1336 (1331 pass, 5 skip) +**Total spec test cases:** 1348 (1343 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 918 | Yes | 913 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 930 | Yes | 925 | 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** | **1336** | **Yes** | **1331** | **5** | | +| **Total** | **1348** | **Yes** | **1343** | **5** | | ### Bash Spec Tests Breakdown @@ -164,6 +164,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | ln.test.sh | 5 | `ln -s`, `-f`, symlink creation | | 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 | | 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 |