Skip to content

feat(grep): treat - operand as standard input - #331

Open
trieloff wants to merge 1 commit into
vercel-labs:mainfrom
trieloff:feat/grep-stdin-operand
Open

feat(grep): treat - operand as standard input#331
trieloff wants to merge 1 commit into
vercel-labs:mainfrom
trieloff:feat/grep-stdin-operand

Conversation

@trieloff

Copy link
Copy Markdown
Contributor

Summary

grep did not accept - as a FILE operand. Found while reviewing #327 (grep -f), which added -f - for reading patterns from stdin and made the neighbouring gap obvious. There is no issue for it.

$ printf 'apple\n' | grep apple -
grep: -: No such file or directory     # just-bash, exit 2
apple                                  # GNU grep 3.12, exit 0

- is a POSIX-documented operand meaning standard input, and the busybox grep spec suite already exercises it — six cases were parked in spec-tests/grep/skips.ts with the reason "- stdin arg not supported".

This PR implements the GNU semantics. Every expectation below was verified against GNU grep 3.12 (/opt/homebrew/opt/grep/libexec/gnubin/grep), never macOS BSD grep.

Details

The label. GNU prints (standard input) wherever a real file name would appear — the multi-file file:line prefix, -l/-L listings, -c counts, and the file-line form of context lines. (Context output with -A/-B/-C is not exercised in the new tests: just-bash omits GNU's -- separator between context groups, a pre-existing divergence that predates this PR — grep -A1 pat f1 f2 on main already omits it, - or no -.)

$ printf 'apple stdin\n' | ggrep -n apple f1.txt - f2.txt
f1.txt:1:apple
(standard input):2:apple stdin
f2.txt:2:apple pie

A lone - gets no prefix (one operand), and -h suppresses it exactly as it does for files. -l/-L still print (standard input) under -h, since those are the output rather than a prefix.

Stream semantics for repeated -. stdin is a stream, so only the first - sees content; every later one reads EOF:

$ printf 'apple\n' | ggrep -c apple - - -
(standard input):1
(standard input):0
(standard input):0

Implemented by marking each queued - entry with stdinAtEof at operand-collection time, so the parallel batch executor stays deterministic.

- with -r. GNU only forces the file-name prefix under -r when recursion can actually descend into a directory, and stdin is never a directory: grep -r pat - prints bare lines, while grep -r pat - sub prefixes both sources (two operands). showFilename now requires a real path target before -r forces the prefix. This deliberately leaves the pre-existing grep -r pat single-file.txt divergence (just-bash prefixes, GNU does not) untouched — out of scope here.

- bypasses name-based filtering. Glob expansion, recursion and --include/--exclude all filter on a file name; stdin has none, so GNU applies none of them. printf 'apple\n' | ggrep --exclude='*' apple - still prints apple.

Empty vs absent stdin. Empty stdin is a valid empty stream: no output, -c prints 0, exit 1 — same as GNU with < /dev/null. When just-bash has no stdin at all, - reads empty, consistent with how cat - already behaves in this repo.

Exit codes. Unchanged rules, now fed by stdin too: 0 on match, 1 on none, 2 when any operand errored. A missing operand alongside - still searches stdin and exits 2; -q short-circuits to 0 on a match even then, per SUSv3 (busybox spec case at busybox-grep.tests:55).

Two adjacent pre-existing divergences are deliberately not addressed (both reproduce on main without -): grep pat - dir exits 0 where GNU exits 2 (directory operands don't set the error flag), and -q suppresses the No such file or directory diagnostic that GNU still prints to stderr — the new -q test asserts just-bash's current (empty) stderr, and the corresponding fixture routes stderr to /dev/null, so neither pins the GNU wording.

-H is not implemented in just-bash at all (it is rejected as an unknown option, on main too). Out of scope; -h interaction is covered.

Composition with #327 (grep -f). Both -f - and a - operand consume stdin, and in GNU the first consumer wins — patterns are read before input, so -f - drains the stream and the - operand then reads EOF:

$ printf 'a\n' | ggrep -f - -
$ echo $?
1

The two PRs are independent (this one is based on upstream/main, not on #327). #327 renames filesoperands in the arg loop and introduces stdinUsedForPatterns; this PR adds a let stdinConsumed = false next to the operand-expansion loop. Whichever lands second needs one mechanical edit: initialise stdinConsumed from stdinUsedForPatterns instead of false, which reproduces GNU's first-consumer-wins behaviour for free. No other overlap — #327 touches the pattern-collection half of the function, this PR touches the operand-expansion and per-file-read half.

Composition with #314 (repeated -e). #314 also renames filesoperands and adds combinePatterns; it does not touch operand expansion, showFilename or the per-file read. Both PRs delete from spec-tests/grep/skips.ts, but from non-adjacent hunks (#314 removes the "Multiple -e patterns" block, this PR the "- for stdin argument position" block). Rebase is mechanical.

Tests

GNU grep version used for every expectation and every fixture: GNU grep 3.12 (Homebrew, /opt/homebrew/opt/grep/libexec/gnubin/grep).

  • packages/just-bash/src/commands/grep/grep.stdin-operand.test.ts28 unit tests, 286 lines, full-string stdout/stderr assertions plus exit codes. Covers the exact repro, the (standard input) label across -n/-l/-L/-c/-o/-v, -h, repeated - (2× and 3×), -m, -r prefixing, --include/--exclude exemption, -- terminator, -e, - as PATTERN, empty stdin, absent stdin, and the missing-operand + -q exit codes.

  • packages/just-bash/src/comparison-tests/grep-stdin-operand.comparison.test.ts + fixtures/grep-stdin-operand.comparison.fixtures.json27 comparison cases / 27 fixtures, all "locked": true so BSD-grep boxes cannot silently re-record them. The re-record command is documented in the test file header:

    PATH=/opt/homebrew/opt/grep/libexec/gnubin:$PATH \
      RECORD_FIXTURES=force pnpm test:run \
      src/comparison-tests/grep-stdin-operand.comparison.test.ts
    
  • packages/just-bash/src/spec-tests/grep/skips.ts4 busybox spec tests unskipped and now passing: grep - (specify stdin), grep - nofile (specify stdin and nonexisting file), grep -q - nofile (... match), grep -L exitcode 0 #2. Two entries stay skipped with corrected reasons: grep - infile (the spec runner rewrites input to /tmp/input, so the file-name prefix differs — a runner artifact, not a grep bug) and grep -s nofile - (-s is not implemented).

Verification numbers on this branch:

  • pnpm typecheck — clean; pnpm lint (biome + workflow security + lint:banned) — clean; pnpm knip — clean (2 pre-existing configuration hints only).
  • pnpm test:comparison — 36 files, 594 passed, 0 failed.
  • grep + rg + search-engine + grep spec suites — 33 files, 1158 passed, 90 skipped, 0 failed (grep spec alone: 332/332).
  • pnpm test:run14342 passed, 97 skipped, 6 failed. The 6 failures are pre-existing python3/WASM sandbox tests (just-bash.bundle, code-exec-exploit-regression, defense-in-depth-independence, python-sqlite-information-disclosure ×2, worker-protocol-runtime-desync); confirmed identical (6 failed / 26 passed across those 5 files) on a stashed, unmodified tree.

Not touched: -L's exit-code inversion (GNU keys -L's exit status off whether a line was selected, not off whether anything was printed) is being fixed separately. Every -L case here is one where both the current and the corrected rule agree.

🤖 Generated with Claude Code

https://claude.ai/code/session_019DayCPuYZEmv4VszJXTHT3

`grep PATTERN -` failed with "grep: -: No such file or directory" instead
of reading stdin. GNU treats `-` as a FILE operand naming standard input,
labelled `(standard input)` wherever a file name would appear.

- `-` reads stdin and is labelled `(standard input)` in the multi-file
  prefix and in -l/-L/-c output; a lone `-` gets no prefix, and -h
  suppresses it like any other file name.
- stdin is a stream: repeated `-` operands see it drained by the first,
  so `grep -c pat - -` prints `(standard input):N` then
  `(standard input):0`.
- `-` bypasses glob expansion, -r recursion and --include/--exclude,
  which all filter on a file name stdin does not have. Under -r the
  file-name prefix is now forced only when a real path is searched, so
  `grep -r pat -` prints bare lines like GNU.
- Empty stdin is a valid empty stream (exit 1, `-c` prints 0); absent
  stdin reads as empty, matching `cat -`.

Unskips 4 busybox spec tests and corrects the reason on 2 that stay
skipped for unrelated causes (spec-runner path rewriting, missing -s).

All expectations verified against GNU grep 3.12; comparison fixtures are
recorded from it and locked.

Signed-off-by: Lars Trieloff <lars@trieloff.net>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

@trieloff is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@trieloff
trieloff marked this pull request as ready for review July 30, 2026 10:21
@trieloff
trieloff requested a review from cramforce as a code owner July 30, 2026 10:21
Copilot AI review requested due to automatic review settings July 30, 2026 10:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants