fix(shell): reject non-finite seq args and handle empty condexpr args (#26993)

## Summary
- **`seq inf` / `seq nan` / `seq -inf` hang**: `std.fmt.parseFloat`
accepts non-finite float values like `inf`, `nan`, `-inf`, but the loop
`while (current <= this._end)` never terminates when `_end` is infinity.
Now rejects non-finite values after parsing.
- **`[[ -d "" ]]` out-of-bounds panic**: Empty string expansion produces
no args in the args list, but `doStat()` unconditionally accesses
`args.items[0]`. Now checks `args.items.len == 0` before calling
`doStat()` and returns exit code 1 (path doesn't exist).

## Test plan
- [x] `seq inf`, `seq nan`, `seq -inf` return exit code 1 with "invalid
argument" instead of hanging
- [x] `[[ -d "" ]]` and `[[ -f "" ]]` return exit code 1 instead of
panicking
- [x] `seq 3` still works normally (produces 1, 2, 3)
- [x] `[[ -d /tmp ]]`, `[[ -f /etc/hostname ]]` still work correctly
- [x] Tests pass with `bun bd test`, seq tests fail with
`USE_SYSTEM_BUN=1`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dylan Conway
2026-02-13 18:01:40 -08:00
committed by GitHub
parent c57af9df38
commit c19dcb3181
3 changed files with 90 additions and 0 deletions

View File

@@ -46,12 +46,14 @@ pub fn start(this: *@This()) Yield {
const maybe1 = iter.next().?;
const int1 = std.fmt.parseFloat(f32, bun.sliceTo(maybe1, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int1)) return this.fail("seq: invalid argument\n");
this._end = int1;
if (this._start > this._end) this.increment = -1;
const maybe2 = iter.next();
if (maybe2 == null) return this.do();
const int2 = std.fmt.parseFloat(f32, bun.sliceTo(maybe2.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int2)) return this.fail("seq: invalid argument\n");
this._start = int1;
this._end = int2;
if (this._start < this._end) this.increment = 1;
@@ -60,6 +62,7 @@ pub fn start(this: *@This()) Yield {
const maybe3 = iter.next();
if (maybe3 == null) return this.do();
const int3 = std.fmt.parseFloat(f32, bun.sliceTo(maybe3.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int3)) return this.fail("seq: invalid argument\n");
this._start = int1;
this.increment = int2;
this._end = int3;

View File

@@ -168,6 +168,8 @@ fn commandImplStart(this: *CondExpr) Yield {
.@"-d",
.@"-f",
=> {
// Empty string expansion produces no args; the path doesn't exist.
if (this.args.items.len == 0) return this.parent.childDone(this, 1);
this.state = .waiting_stat;
return this.doStat();
},

View File

@@ -0,0 +1,85 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("seq inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq nan does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq nan\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq -inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq -- -inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -d "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -d "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -f "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -f "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);