Skip to content

fix(builtins): GNU parity for sort, xargs, nl, fold, od, du and 20 more coreutils (166 BashBox cases) - #2585

Merged
chaliy merged 36 commits into
mainfrom
claude/project-thread-eafupr-bb-coreutils
Oct 7, 2026
Merged

chaliy merged 36 commits into
mainfrom
claude/project-thread-eafupr-bb-coreutils

Conversation

@chaliy

@chaliy chaliy commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Михайло · project thread

Follow-up to #2575 (the BashBox corpus import). Also updates builtin_fs_error_tests: sort on a missing file now exits 2, as GNU sort does.

What changed

This unskips 166 of the 169 BashBox coreutils cases. Bashkit's output now matches GNU bash/coreutils for all of them.

  • Option parsing: a new shared GNU-style option parser handles bundles, attached values, long options and their prefixes, and --. sort, uniq, tr, cut, expand/unexpand, xargs, seq, paste, nl, fold, comm, du, date, base64, head/tail, basename and rmdir use it.
  • sort / uniq: -R, -i, -d, -V, key modifiers, -u with numeric keys, and ties broken bytewise.
  • xargs: -I with empty input exits 0, and 28 cases covering the rest of xargs now pass.
  • nl: logical-page sections, -d/-h/-f/-l/-p and pBRE.
  • fold: GNU's column model.
  • tac: -s, -b and -r, which used to fail with "not implemented".
  • od: a port of GNU's block layout. The default output is now -t o2, as in GNU.
  • find: -fprint, -fprint0, -fprintf and -fls.
  • du: paths are printed relative to the operand, and -h rounds like GNU.
  • stat: %N.
  • sleep: unit suffixes, hex values, and several operands that add up.
  • printf, echo, tr, sed, grep, seq, date, base64: fixes for escapes, flags and edge cases.
  • Bugs found on the way:
    • cp -r failed on every directory.
    • mktemp kept the X's, made relative templates absolute and created missing parents.
    • base64 -w0 printed a trailing newline.

Three cases stay skipped on purpose:

  • Two need a newline in a file name, which the VFS refuses by design.
  • One uses a printf width of 5,000,000, beyond the 10,000 cap.

Why

These are real GNU-parity bugs that the imported BashBox corpus exposed.

Before / After

On the corpus branch, the spec suite has 446 skipped cases. With this PR:

Bash Spec Tests: Total: 3441 | Passed: 3127 | Failed: 0 | Skipped: 314
Comparison: 2912/2914 tests match real bash (99.9%)

The 2 comparison mismatches are the existing empty_heredoc/heredoc_empty host-bash cases and are unrelated. cargo clippy -p bashkit --all-targets --features http_client,ssh -- -D warnings is clean, and cargo test -p bashkit --lib passes 2850 tests.

Risk

  • Medium. A lot of builtin argument parsing changed.
  • Default od output and du path display now follow GNU, so scripts that relied on the old bashkit output will see different text. A few unit tests were updated to the GNU behaviour.

Checklist

  • Tests added or updated
  • Backward compatibility considered (no backward compatibility needed for internal code, per AGENTS.md; the user-visible output changes are listed above)

Generated by Claude Code

chaliy added 28 commits October 7, 2026 16:35
Copies (script, stdout) pairs from shipfastlabs/bashbox (MIT) into
spec_cases/bash/bashbox-*.test.sh, keeping only cases whose recorded
output matches host bash. 624 pass and join the bash comparison gate;
420 are marked TODO skips: real gaps where bashkit differs from bash.
Attribution in NOTICE; process in knowledge/operations/testing.md.
Rewrite sort comparison after GNU sort.c: fields keep leading blanks,
keys with ordering options do not inherit global ones, -u/-s drop the
last-resort comparison, exact -n, real -g/-h/-M/-V/-R/-d/-i, long
options, --sort=WORD, ignored tuning options. uniq gains -s, -w, -D,
-z, long options and the OUTPUT operand.
Add a shared GNU getopt_long-style parser (bundles, attached values,
long prefixes). tr decodes \NNN/\a.. escapes before ranges and classes,
supports [=c=], -t and --complement/--delete/--squeeze-repeats. cut
accepts -sf1 bundles, long options, -d '' as NUL, keeps empty lines and
prints delimiter-less lines whole.
Tab lists accumulate across -t, accept blanks, /N and +N suffixes and
obsolete -N,M forms; expand emits one space past the last stop and
supports -i; unexpand ports the GNU pending-blank state machine
(single blank before a stop, tabs absorbing blanks, -t implies -a,
--first-only) and no longer appends a stray newline.
Parse items like findutils read_line/read_string: quotes and backslashes
group, NUL truncates an item, empty quoted words are kept, -L lines
ending in a blank continue, -E/-e logical EOF. Add -a, -d escapes
(\xNN, \NNN, strtoul forms), -L/-l, -i, -s, -x, -t/--verbose and
GNU -I/-L/-n override order. -I keeps trailing blanks and enforces -s.
grep now compiles BRE/ERE through sed's positional translator (literal
+ ? | in BRE, leading * literal, back-references via the backtracking
engine), splits patterns on newlines, implements -w as no word
constituent on either side of the match, and -o skips empty matches.
printf prints \c in the format literally (only %b stops there), accepts
flags/width/precision on %b and %q (space padding, precision on the
rendered text), supports bash %Q (precision before quoting) and treats a
leading -- as end of options. echo -e emits octal/hex escapes as raw
bytes, keeps \x without digits literal, adds \e, \u, \U, and \c now
suppresses all further output including the newline.
Default output uses the larger fraction-digit count of FIRST and
INCREMENT, -w zero-pads after the sign to the wider of FIRST/LAST,
-f renders through the printf formatter, values are FIRST + i*STEP,
and --separator/--format/--equal-width are accepted. Option parsing
stops at a negative-number operand but still lets -s take "-1".
Every "-" operand reads one shared stdin stream so "paste - -" takes
lines in turns, -z/--zero-terminated uses NUL records, and options go
through the shared GNU getopt parser.
Lines equal to the section delimiter x3/x2/x1 switch to header/body/
footer, print empty, and reset numbering unless -p. A one-char -d keeps
the second ":". -l joins blank lines with a counter that survives
section changes, and options gain GNU long forms.
TAB advances to the next multiple of 8, backspace moves left, CR resets
the column, a char that alone overflows an empty line stays on it, and
-s re-scans the carried-over text after breaking at the last blank.
Adds -NUM, long options and an invalid-width error.
Replaces the "not yet implemented" error. An empty -s means NUL, as in
GNU tac, and each FILE is reversed on its own.
Operands are C strtod numbers (leading blanks, hex and hex floats, inf)
with an optional s/m/h/d suffix and are summed; "--" ends options and
any other dash argument is an invalid option, as with getopt.
Each -t spec and shorthand prints its own aligned line per block in
command-line order, the default is -t o2, short blocks are zero-filled,
repeated blocks collapse to "*" unless -v, -Ax uses 6 digits and the
final address is always printed. Float types are rejected explicitly.
Target files are created or truncated before the walk, even when nothing
matches, and receive the action output when the walk ends.
Prints directories (files with -a) under the operand name, adds -b, -c,
-d, -k, -m, -B, --si and long options, rounds human sizes up like GNU,
and reports missing operands while continuing with the rest.
Accepts -ud@N style bundles and attached values, --universal, and
unambiguous long prefixes; the first +FORMAT operand is the format.
Also parses options with the shared GNU getopt helper, honours
-i/--ignore-garbage when decoding, and rejects a bad wrap size.
cp -r/-R/-a now copies directory trees (symlinks recreated), refuses to
copy a directory into itself, and skips directories without -r. rmdir
adds -v and --ignore-fail-on-non-empty, walks the operand prefixes for
-p and keeps going after a failure. mktemp replaces the trailing run of
3+ X characters (text after the last X is the suffix), keeps relative
templates relative to the cwd, no longer creates missing parent
directories, and -q silences every failure.
GNU builds -I commands only when an item arrives, so the -s check runs
per item ("command too long") and empty input exits 0. Also drops the
locale-quoted mktemp message from the fileops spec comparison.
@chaliy chaliy self-assigned this Oct 7, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 96ca3a9 Commit Preview URL

Branch Preview URL
Oct 07 2026, 10:45 PM

Base automatically changed from claude/project-thread-eafupr-bashbox-corpus to main October 7, 2026 18:40
chaliy added 8 commits October 7, 2026 18:54
…-eafupr-bb-coreutils

# Conflicts:
#	THIRD_PARTY_LICENSES/README.md
#	crates/bashkit/src/builtins/seq.rs
#	crates/bashkit/tests/spec_cases/bash/bashbox-base64.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-comm.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-date.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-echo.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-expand.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-find-predicates.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-find.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-fold.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-grep.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-nl.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-od.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-options.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-paste.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-path-name.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-printf.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-rmdir.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-sed.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-seq.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-sleep.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-sort.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-stat.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-tac.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-temp-path.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-tr.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-unexpand.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-uniq.test.sh
#	crates/bashkit/tests/spec_cases/bash/bashbox-xargs.test.sh
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	crates/bashkit/src/builtins/cuttr.rs
…-eafupr-bb-coreutils

# Conflicts:
#	crates/bashkit/src/builtins/paste.rs
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	crates/bashkit/src/builtins/sed/tests.rs
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	knowledge/log.md
…-eafupr-bb-coreutils

# Conflicts:
#	knowledge/log.md
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.

1 participant