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 @@ -51,6 +51,7 @@ mod pipeline;
mod printf;
mod read;
mod sed;
mod seq;
mod sleep;
mod sortuniq;
mod source;
Expand Down Expand Up @@ -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;
Expand Down
182 changes: 182 additions & 0 deletions crates/bashkit/src/builtins/seq.rs
Original file line number Diff line number Diff line change
@@ -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<ExecResult> {
let mut separator = "\n".to_string();
let mut equal_width = false;
let mut nums: Vec<String> = 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))
}
}
1 change: 1 addition & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
120 changes: 120 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/seq.test.sh
Original file line number Diff line number Diff line change
@@ -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
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:** 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

Expand Down Expand Up @@ -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 |
Expand Down
Loading