shell: implement seq -w and report -f as unsupported - #37926
Conversation
The seq builtin parsed -w/--fixed-width and then ignored it, and did not parse -f at all, so a format string was rejected as an invalid number. Both flags are in its usage string. -w now prints every value with the most decimals any operand was written with, zero-padded (after the sign) to the wider of the two bounds, as BSD and GNU seq do. -f/--format fails with the shell's standard unsupported option message, as the other builtins do for flags their usage string lists but they do not implement.
WalkthroughThe Changesseq formatting behavior
Possibly related PRs
Suggested reviewers: Mergeability Score: 🟠 High · up to Fixed-width sequences can buffer an unexpectedly large amount of output before writing it, allowing crafted inputs to exhaust memory and terminate the shell. The implementation should stream output or enforce a recoverable size limit before merge. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 11:46 PM PT - Aug 12th, 2026
❌ @robobun, your commit 5b07156 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 37926That installs a local version of the PR into your bun-37926 --bun |
|
Status: reproduced on bun 1.4.0 and a current debug build of main with which prints CI: the diff is green on every lane that ran (build 93665: 176 of 181 jobs passed, |
There was a problem hiding this comment.
LGTM — self-contained implementation of seq -w plus a proper unsupported-option message for -f, with thorough test coverage.
What was reviewed
-wwidth/precision derivation inFixedWidth::newanddecimal_places: bounds-based width, operand-based precision, and the 1074-decimal cap guardingcore::fmt'su16::MAXpanic — all check out against the stated invariants.- The non-
-woutput path is byte-for-byte unchanged;decimalsis tracked unconditionally but only consumed underfixed_width. -frouting throughBuiltin::fail_parse/ParseError::Unsupportedmatches how other builtins report unimplemented advertised flags;--format=foofalls through to the operand parser like--separator=fooalready does, so behavior is consistent.- Verified the referenced helpers (
bun_core::fmt::count,parse_decimal,strings::index_of_any,unsupported_flag) exist with matching signatures.
Extended reasoning...
Overview
This PR implements the -w / --fixed-width flag for the seq shell builtin (previously parsed and silently ignored) and routes -f / --format through the shared ParseError::Unsupported path so users get an actionable message instead of invalid argument. Changes are confined to src/runtime/shell/builtin/seq.rs (net +~100 lines: two new struct fields, a FixedWidth helper, a decimal_places parser, and a new arm in the flag loop) and 22 new test cases in test/js/bun/shell/commands/seq.test.ts.
Security risks
None. This is output formatting for a shell builtin that operates on already-validated finite f32 operands. The one adversarial-input concern — a user-written exponent like 1e-70000 requesting a precision above core::fmt's u16::MAX panic threshold — is explicitly handled by the MAX_FIXED_WIDTH_DECIMALS cap and covered by a test. decimal_places only runs on strings parse_f32 has already accepted, and its arithmetic is done in i64 and clamped before the u32 cast, so there is no overflow path. No unsafe, no allocation ownership changes, no FFI.
Level of scrutiny
Low-to-moderate. The change is additive to a small, isolated builtin; the non--w code path is unchanged. The -w semantics are derived from BSD/GNU seq and the PR description walks through why width-from-bounds and precision-from-operands is correct — I checked the reasoning holds (every printed value lies in [min(start,end), max(start,end)], and at fixed precision no value renders wider than the wider bound). The -f change replaces one error message with a more accurate one and follows the existing pattern used by cat/cp/touch/mkdir.
Other factors
Test coverage is unusually thorough for a builtin flag: integers up/down, single operand, negative bounds, sign-after-zero-padding, fractional increments, exponent-notation operands (both directions), interaction with -s/-t, captured stdout, and the precision-cap edge case. All referenced in-tree helpers (bun_core::fmt::count, bun_core::fmt::parse_decimal, bun_core::strings::index_of_any/index_of_char_usize, interpreter::unsupported_flag, Builtin::fail_parse) exist with the signatures used. The PR description already flags the mechanical overlap with #37923 and #33995. No prior human review comments to address.
There was a problem hiding this comment.
LGTM — self-contained seq builtin change, well-covered by tests.
What was reviewed:
-wimplementation:FixedWidth::newcomputes width frommax(len(start), len(end))at fixed precision; the non--wpath is unchanged.decimal_places: only runs on operandsparse_f32already accepted, and the result is clamped toMAX_FIXED_WIDTH_DECIMALS(1074) before reachingcore::fmt, so theu16::MAXprecision panic is unreachable (covered by the1e-70000test).-fhandling:starts_with(b"-f")cannot collide with negative operands or the other short flags; routes through the sharedfail_parse/ParseError::Unsupportedpath likecat/cp/touch.- Comment-cop threads were addressed in 804573d and are all resolved.
Extended reasoning...
Overview
This PR touches exactly two files: src/runtime/shell/builtin/seq.rs (implementing -w and reporting -f as unsupported) and test/js/bun/shell/commands/seq.test.ts (23 new test cases). The -w flag was previously consumed and ignored; it now zero-pads output to a fixed width derived from the wider of the two bounds rendered at the maximum decimal precision seen across all operands. -f/--format now goes through the shared Builtin::fail_parse + ParseError::Unsupported path instead of falling through to "invalid argument".
Security risks
None. This is output formatting for a shell builtin. No untrusted length is used to size an allocation without a bound: decimals is capped at 1074 before being passed as a core::fmt precision, which both prevents the documented u16::MAX panic and bounds per-value output size. No new unsafe, no FFI, no syscalls added.
Level of scrutiny
Low-to-medium. The change is confined to one builtin's flag parsing and output rendering; the pre-existing loop, state machine, and I/O paths are unchanged. The new code follows the file's established patterns (Self::state_mut via the BuiltinState trait, bun_core::fmt/bun_core::strings helpers, Builtin::fail_parse for unsupported flags). I verified that the referenced helpers (bun_core::fmt::count, parse_decimal, index_of_any, index_of_char_usize, unsupported_flag, fail_parse) exist with the expected signatures.
Other factors
Test coverage is thorough for a feature this size: up/down/single-operand, negative values, sign placement, decimals from the increment, exponent spellings, interaction with -s/-t, both output paths (captured stdout and file redirect), the precision cap, and all four -f spellings. The comment-cop bot flagged verbose comments; the author trimmed them in 804573d and each thread is marked resolved with a stated rationale for what was kept. No CODEOWNERS entry covers src/runtime/shell/. The one CI failure (FreeBSD aarch64 build-bun) is in scripts/build/ci.ts infrastructure, not this change. The PR notes overlap with #37923 and #33995 and states both are mechanically resolvable in either merge order.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/runtime/shell/builtin/seq.rs`:
- Around line 214-221: Bound output accumulation in the fixed-width formatting
path around fixed_width and out so untrusted width/decimal inputs cannot grow
the buffer without limit before I/O. Enforce a recoverable maximum before
appending each rendered value, or flush bounded chunks incrementally, while
preserving seq output formatting and progression behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f65b6f44-33ef-4fdc-8957-813bec0bfbcb
📒 Files selected for processing (2)
src/runtime/shell/builtin/seq.rstest/js/bun/shell/commands/seq.test.ts
There was a problem hiding this comment.
LGTM — self-contained implementation of seq -w plus routing -f through the shared ParseError::Unsupported path.
What was reviewed:
-wwidth/precision derivation:decimal_placeshandles.,e±N, and clamps;FixedWidth::newcaps at 1074 socore::fmtcan't panic on width/precision >u16::MAX.- Verified
arg.starts_with(b"-f")cannot shadow a valid negative-float operand, andparse_decimal::<i32>accepts a leading+so1e+3exponents parse. - Non-
-woutput path is unchanged; 23 new tests cover both output paths, signs, exponent spellings, flag combinations, and the precision cap.
Extended reasoning...
Overview
Two files: src/runtime/shell/builtin/seq.rs gains a working -w/--fixed-width (new fixed_width: bool and decimals: u32 fields, a FixedWidth layout struct, and a decimal_places helper) and a -f/--format arm that returns Builtin::fail_parse(.., ParseError::Unsupported(..)). test/js/bun/shell/commands/seq.test.ts adds a seq -w describe block plus -f cases (23 new tests).
Security risks
None. This is output formatting in a shell builtin; no filesystem, network, or privilege boundaries are touched. The one user-controlled size input (implied decimal places from operand text) is clamped to 1074 before reaching core::fmt, and decimal_places clamps its intermediate to u32::MAX in i64 before casting.
Level of scrutiny
Low-to-medium. The change is confined to one builtin's rendering path, uses existing in-tree helpers (bun_core::fmt::count, parse_decimal, strings::index_of_any, Builtin::fail_parse/unsupported_flag), and leaves the non--w branch byte-identical. I traced operand reassignment for the 1/2/3-operand forms and confirmed FixedWidth::new reads the final start/end; checked that parse_int (behind parse_decimal) accepts a leading + so 1e+3 doesn't fall into the unwrap_or(0) branch; and confirmed -f's starts_with(b"-f") cannot collide with any operand parse_f32 would accept.
Other factors
Test coverage is thorough per REVIEW.md's variant-matrix guidance: up/down, single operand, unreached end bound, negative bounds and sign placement, fractional increments, exponent spellings, -s/-t interaction, captured stdout vs. file redirect, and the 1074-decimal cap. The comment-cop bot flagged verbose comments on an earlier revision; the author trimmed them in 804573d and all threads are resolved. No human reviewer has requested changes and there is no CODEOWNERS entry for src/runtime/shell/.
|
Heads-up from #39265, which makes seq's |
Problem
seqbuiltin's usage string advertises-wand-f format(src/runtime/shell/Builtin.rs:193), but neither does anything useful.-w/--fixed-widthis consumed by the flag loop and then ignored (src/runtime/shell/builtin/seq.rs:83, the arm only dididx += 1), soseq -w 8 11prints8 9 10 11where BSD and GNU seq print08 09 10 11. The original Zig builtin stored afixed_widthbool it never read, so this has been the case since the builtin was added.-fis not parsed at all, so it falls through to the operand parser andseq -f %g 1 3fails withseq: invalid argument, which points at the wrong thing.Fix
-wsetsfixed_width; each operand's decimal places as written are tracked indecimals(decimal_places:0.25-> 2,2.5e-1-> 2,1.5e1-> 0);do_prints every value with{current:0width$.decimals$}, wherewidthis the wider ofstartandendrendered at that precision (FixedWidth::new). Output without-wis unchanged (seq 1 0.5 3still prints1 1.5 2 2.5 3).start + k*incrhas at most as many decimals as the most precise operand, so rendering at that precision prints each value exactly and gives all of them the same fractional part; every value lies betweenstartandend, and at a fixed precision a value never renders wider than the bound it lies inside of, so the wider bound is the width of the widest value. Rust's0flag pads after the sign, soseq -w -10 5 10gives-10 -05 000 005 010, which is what GNU seq prints; precision from the operands as written and width from the bounds is also how both BSD and GNU seq define-w(the only visible difference: GNU keeps leading zeros typed in an operand,seq -w 008 11pads to 3 there and to 2 here, as on BSD).decimalsis capped at 1074 when printing (MAX_FIXED_WIDTH_DECIMALS): no float has more fractional digits than that, andcore::fmtpanics on a precision or width aboveu16::MAX, which an operand like1e-70000(parses to 0, 70000 implied decimals) would otherwise reach.-f,-fFORMATand--formatgo throughBuiltin::fail_parsewithParseError::Unsupported, so they fail withseq: unsupported option, please open a GitHub issue -- -f. This is howcat,cp,touchandmkdiralready treat the flags their usage strings list but they do not implement; a printf-style-f(%e/%g/%awith flags, width and precision) is a feature of its own and is not added here. The usage string is left as is, like theirs.test/js/bun/shell/commands/seq.test.ts: 23 new cases (padding up and down, one operand, width taken from an unreached end bound, negative values, decimals from the increment, exponent spellings,-s/-t, both output paths (captured stdout and a>redirect), the precision cap, and the four-fspellings) fail on the released build and pass with this change (58/58).test/js/bun/shell/pipeline_stack.test.ts,test/js/bun/shell/shell-seq-condexpr.test.tsandtest/internal/source-lints/byte-search.test.tsstill pass;cargo clippy -p bun_runtimeandcargo fmtare clean.decimalsfield anddecimal_placeshelper (copied here as is, so whichever lands second only has to drop its duplicate;FixedWidth::newtakes the unscaled operands, which that PR still has at that point), and shell: accept--end-of-options delimiter in builtins #33995 adds a--arm next to the new-farm. Both are mechanical to resolve in either order.Background
seqinBun.$is always the builtin; it follows the BSD variant (its usage string is BSD's,seq 5 1counts down,-texists,-sis emitted after every value). Without-wit prints each value in its shortest round-trip form, so fractional sequences have values of varying width (1,1.5);-win both BSD and GNU seq switches to a fixed number of decimals and zero-pads, which is the only way every line can have the same width.Builtin::fail_parse/ParseError::Unsupported(src/runtime/shell/Builtin.rs,interpreter.rs) is the shared path that renders<cmd>: unsupported option, please open a GitHub issue -- <flag>and exits 1;seqhas a hand-rolled flag loop (options take arguments and operands may be negative numbers), so it calls that path directly instead of going throughparse_flags.f32;-wonly changes how values are rendered, so it is independent of that (and of shell: make the seq builtin count in f64 at the operands' decimal precision #37923, which changes it).Before / after
More
-woutput from this branch, each identical to GNU coreutils 9.7 (seq 11 8excepted, GNU prints nothing there):