# The large subset: the state of the union (0.03, 2026-11-03) This document summarises where `Proc::ShellSubset::Large` (the large parser) stands against dash and bash. Section 1 gives the rules, section 2 the order in which the rules apply, section 3 the figures, section 4 the work list (forms the shells agree on that the parser still rejects), and sections 5 and 6 the subtle and the blatant differences. It replaces the yardsticks of `large-subset-ceiling.md`, section 6, which sorted our parser's own rejection messages and so measured the parser against itself. Every figure here is of version 0.03 unless it names another version (the labels: `../README.md`, last section). Shells: dash 0.5.12 and `bash --posix` 5.2.21. "The manual" means the Bash Reference Manual of bash 5.2.21, section 6.11 "Bash POSIX Mode", taken from the Debian package `bash-doc`; "M:N" names its item N. Where this document cites a probe ("P:ID"), the probe's ID is in `dialect-cases/cases.txt` or `dialect-cases/large-subset-dialect-probes2.txt`. ## 1. The rules (the user's, 2026-11-02) - **R-agree.** Where dash and bash agree, the subset must accept the form and behave as they do. - **R-refuse.** Where both shells refuse a form, the subset refuses it too. "Refuse" covers both a syntax error (`-n`) and an error when the form runs. Forms both shells refuse count as blatant, where the detector can see them (it cannot see errors that happen only at run time, such as `${foo$n}`). - **R-blatant.** A difference is **blatant** when "either one of them fails or does not match the docs; or they behave in blatantly different ways". Any other difference in how dash and bash behave is **subtle**. - **R-program.** For the commands the subset runs as programs (`echo`, `printf`, `test`, `[`, `kill`, `time`, and `pwd`, `true`, `false`, where the shells agree anyway; the base parser's specification, `doc/parsers/shell-subset-spec.md`, section 4), the program beats any difference between the shells' builtins. "`test` beats blatant" was decided under item P5 of "Shell Corpus Gaps"; the user extended it to this list on 2026-11-02. In the measurement files these differences are class **X**, together with the differences that a decision of the user settles: the interactive-only builtins (`history`, `fc`, `bind`, `help`, `compopt`, `logout`, `set -o history`) do nothing, by decision B2. - **The yardsticks.** **Upper** ("nothing bogus found"): both shells parse the sample (`dash -n`, `bash --posix -n`), and it uses no blatant difference. **Lower** ("compatible"): within the upper yardstick, and it uses no subtle difference either. A class-X difference excludes nothing. - **Who decided.** Every verdict names its basis: P:ID (a probe), M:N (the manual), SC:CODE (ShellCheck says dash lacks the construct, so dash fails), J (Claude's judgement, not yet reviewed), or "user". J verdicts are kept as postponed text, so they count as open. Postponed verdicts are free-form text in `tools/large-subset-verdicts.txt` and count as **open** (O): outside the lower yardstick, inside the upper one. They can be rewritten in short form later without a rescan. ## 2. Which rule beats which Proposed order, highest first. Items marked **(question)** need the user's decision. 1. **R-refuse beats R-program** (the user, 2026-11-03: "refusing is better"; Q1 of "Shell Corpus Gaps"). `kill -SIGHUP PID`, `kill -s SIGHUP PID`, `kill -l SIGHUP` and `kill -sighup PID`: both shells refuse the `SIG` prefix (dash: "Illegal option -S"; bash as its manual says, M:38), while the program `/usr/bin/kill` accepts it. The parser rejects them (tests LS:246–250). 2. **R-agree against R-program: a caller's option decides.** `[ a \< b ]`: both shells answer 0, but the coreutils program fails ("binary operator expected"). Item R1 of "Shell Corpus Gaps" settled this: a caller's option decides, and without the option the form stays rejected. 3. **R-program beats R-blatant and subtle differences.** `echo 'a\nb'`: dash prints two lines; bash and `/bin/echo` print `a\nb`. The subset accepts it and prints `a\nb`. 4. **A program result that is itself bogus (question Q2 of "Shell Corpus Gaps").** `printf -v x %s hi`: bash sets `x`, and dash fails. The coreutils program prints `-v` and a warning about excess arguments, which matches neither shell and serves no purpose. Open (Q2): the user asked what the shells do; the answer, with the choice between rejecting it and keeping it, is under Q2 on the page. Also `[ -v NAME ]` and unary `[ -a FILE ]`: bash answers, while dash and coreutils fail with different messages. Under R-program the subset fails like coreutils; no change is proposed for these. 5. **R-blatant:** the subset rejects every form with a blatant difference (R-refuse, R-agree and R-program aside). 6. **Subtle differences:** the subset follows the choices of the page "Shell Dialect Choices" (default: dash's behaviour where it is cheap to give, otherwise reject). **Windows.** R-program assumes the programs exist: coreutils `[`, `/bin/echo`, `printf`, `kill`. A Windows port of `Proc::ShellSpawn` would lack them. It would then have to give the same answers itself (emulate the coreutils behaviour in Perl) or reject these commands. Until then, the rule ties the subset to POSIX systems with coreutils; this is recorded as known issue X21 in `doc/pspawn-known-issues.md`. ## 3. The figures Measured with 0.03 over the pinned corpora (`doc/test-pools.md`): 41,966 samples after leaving out 2,719 duplicates, the make book (578, a noisy extraction), 163 tldr lines made only of keystrokes, 59 tldr lines with keystrokes next to other text (Y3), and 16 ShellCheck test strings that are wrong where they stand (`return`, `local`, `break` outside their function or loop, which ShellCheck's own tests mark as errors). The figures were first taken in the early hours of 2026-11-03, before the release; they were measured again on the released 0.03 later that morning (a new scan of every sample), and every figure in this section and in sections 4 to 6 came out the same. | Source | Samples | Accepted | Lower | Upper | Rejected within lower | Rejected, subtle or open | Accepted outside upper | |---|---|---|---|---|---|---|---| | tldr | 29,450 | 99.2% | 99.2% | 99.3% | 23 | 7 | 1 | | NL2Bash | 10,624 | 93.9% | 94.5% | 94.8% | 76 | 24 | 3 | | bash Cookbook | 81 | 32.1% | 27.2% | 35.8% | 0 | 3 | 0 | | pure-sh-bible | 44 | 97.7% | 95.5% | 100.0% | 0 | 1 | 0 | | pure-bash-bible | 122 | 40.2% | 41.8% | 41.8% | 2 | 0 | 0 | | ShellCheck, clean | 879 | 60.3% | 62.6% | 66.0% | 34 | 16 | 0 | | ShellCheck, warned | 766 | 58.5% | 61.5% | 62.3% | 28 | 2 | 1 | With early-post-0.02 (2026-11-02: P1–P4 done, not yet P5, R1, Y2, Y3, B1–B3, Q1) the samples rejected within the lower yardstick were 304; with 0.03 they are 163. The parser accepts 258 samples whose only differences are class X (programs, and the interactive builtins of B2); one more accepted sample, `time read`, has a class-X difference and also a blatant one that the detector misreads (below). "Accepted" can exceed "Lower" because accepted samples with subtle differences count against the lower yardstick: the subset gives one of the two behaviours there. The 5 samples **accepted outside the upper yardstick** are misreadings by the detector, not parser errors: `[^…]` inside a quoted `printf` format, `$(( ((1+1)) ))` taken for the command `((`, an assignment `source=…`, and `time read` (the program `time` running the program `read`). ## 4. Still rejected although the shells agree (the work list) There are 163 samples: 159 with no difference found, and 4 whose only differences are class X. By the parser's message: | Count | What | Where it is decided | |---|---|---| | 38 | `read` with other arguments, mostly `read -p PROMPT` (dash 0.5.12 has `-p`: no difference) | P6 (unmarked) | | 17 | `jobs -p`, `jobs -l`, `jobs %N`: inside pipelines and `$( )` dash lists nothing and bash lists the jobs, which the detector does not see | to probe; a likely new subtle or blatant feature | | 16 + 4 | `alias` with a value or name from an expansion (`alias foo="echo $PWD"`), or options of zsh (`alias -g`); `alias` inside a compound command or an and-or list | rejected by the plan of B1 (the alias cannot be known when parsing) | | 13 | `${foo$n}`, `${{var}` and the like | both shells parse them but fail when they run: right by R-refuse; the detector cannot see run-time failures | | 9 | `set` with an option the parser does not take: `set -o OPTION` for options both shells have, `set -o` alone (it lists), `set -b`, `set +m`, `set -euo`, and the `set` of other programs (tmux's `set -g`, fish's `set -U`) | P6 | | 7 | `cd -P`, `cd -L` | P6 | | 6 | `trap f int`, `trap f 8` (lower-case signal names, signal numbers) | P6 | | 5 | `[ a \< b ]` and the like | R1: the caller's option `test_lt_gt` | | 4 | `ulimit` setting a limit, or `-a` | B3 reads limits only | | 3 | here-documents cut off by NL2Bash (`ssh host <<'EOF'` with no body) | both shells read them with an empty body; to decide | | the rest (41) | `exit 07`, `wait %1`, unquoted `export $v=$y`, a function named like a builtin, `NAME[` as a command, … | one by one | ## 5. Subtle differences and open verdicts **Between the shells** (0.03: 73 samples have a subtle difference as their worst one, 31 an open verdict). The counts per feature below include samples that also have a blatant difference, so they add up to more: - listings (`set`, `export -p`, `trap` alone), also the order of the `alias` listing: 33; - an assignment with an expansion plus a redirection, where dash redirects first (open): 24; - `local x` without a value: 18; - `getopts` after a missing argument: 16; - traces (`set -x`): 7; - `${x:?}` statuses, `command -v` statuses, `$PS1`: a few each. Open verdicts (postponed, free-form in `tools/large-subset-verdicts.txt`; the shells' behaviour is under Q3 on the page, with a question for the user): - `local` or `return` outside a function; - `$-`; - `$LINENO`; - the assignment-and-redirection order. **Between the shells and the programs or decisions (class X):** - `echo`: like bash. - `[ -v ]`, unary `[ -a ]`: coreutils fails like dash, unlike bash. - `[ a \< b ]`: coreutils fails, unlike both shells (R1: the option `test_lt_gt`). - `printf %q`: coreutils quotes as `'a b'`, bash as `a\ b`, and dash fails. - `printf -v`: open (Q2). - `time`: the program `/usr/bin/time`. It is not installed in Claude's workspace, so `time cmd` fails there with "not found", as in dash, which has no `time` builtin either. - `history` and the other interactive-only builtins (B2): nothing, as bash mostly does; dash has none of them ("not found"). The caller can choose dash's behaviour (`interactive => 'dash'`) or rejection (`interactive => 'reject'`). ## 6. Blatant differences With 0.03, 1,463 samples are outside the upper yardstick: 520 because a shell refuses to parse them, the rest because of a blatant difference. Except for the 5 detector misreadings of section 3, the parser rejects them all. The most frequent features: - bash-only builtins (`source`, `declare`, `let`, …); - process substitution; - arrays; - `[[ … ]]`; - options of `read`, `local` and `type` that dash lacks; - `$'…'`; - brace expansion; - `(( … ))`. **❰BUG❱ bash 5.2.21:** M:24 says "If a POSIX special builtin returns an error status, a non-interactive shell exits". In `bash --posix -c`, `shift 3` with fewer arguments and `export "1x=2"` print an error, return 1, and the shell goes on. `set -o nosuch` and `unset -x` do exit (status 2). This is a blatant difference by the definition ("does not match the docs"); known issue X22 in `doc/pspawn-known-issues.md`. The subset rejects these forms at parse time where it can see them; where it cannot, the executor follows dash and stops. ## Files - `tools/large-subset-features.pl`: the detector (features of a text) and the mapping of ShellCheck codes, each with verdict and basis; `-n TESTFILE…` prints the per-test record (redirect it to `t/parser/large-subset-shells.tsv`). - `tools/large-subset-verdicts.txt`: verdicts kept by hand, including the postponed free-form ones. - `tools/large-subset-yardsticks.pl`: the figures of section 3. `-o` writes one line per sample; `-c` caches ShellCheck's messages, so later runs need no ShellCheck. - `large-subset-dialect-features.md`: the feature list with every probe result. - `../test-pools.md`: how to rebuild every test pool and measurement.