Skip to content

Commit 6413437

Browse files
Merge pull request #90 from Aaronontheweb/feature/v03-bash-heredoc-substitution
Discover Bash heredoc substitutions
2 parents ee1e189 + be016a1 commit 6413437

17 files changed

Lines changed: 1133 additions & 78 deletions

IMPLEMENTATION_PLAN.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,18 @@ priorities.
265265
depth limits, comment-safe delimiter scanning, and fail-closed command
266266
identities, background lists, assignment prefixes, backticks, heredocs,
267267
and malformed interiors.
268-
- [ ] Extend Bash substitution discovery to iterables and expanding heredoc
269-
bodies, then add the corresponding Netclaw approval-matrix cases.
268+
- [x] Extend Bash substitution discovery to expanding heredoc bodies. The
269+
bounded slice recognizes quoted, escaped, mixed, and tab-stripping
270+
delimiters; preserves exact body/terminator provenance; surfaces nested
271+
substitutions in authored order with isolated state; and rejects header
272+
tails, queued heredocs, backticks, arithmetic, continuations, incomplete
273+
interiors, and depth overflow atomically. Direct and executable-corpus
274+
cases pin exact syntax, command ancestry, spans, completeness, and
275+
literal-versus-expanding behavior; real-Bash output and parse-only
276+
oracles independently pin the bounded semantic boundary.
277+
- [ ] Extend Bash substitution discovery to iterables with the complete
278+
`for ... in` vertical slice, then add the Bash substitution cases to the
279+
Netclaw approval matrix.
270280
- [ ] Complete PowerShell `$()` discovery in `foreach` expressions and add the
271281
Netclaw approval-matrix cases. The simple-command slice is delivered for
272282
ordinary, adjacent, quoted, here-string, redirect, standalone,
@@ -276,9 +286,9 @@ priorities.
276286
- [ ] Deliver Bash `for ... in` and PowerShell `foreach` as the first two
277287
language-specific vertical slices, then extract only the shared analysis
278288
proven by both implementations.
279-
- [ ] Preserve the existing Bash heredoc grammar, fix quoted-delimiter
280-
adjacency, expose body/delimiter/expansion/completeness facts, and add a
281-
separately tested Bash `<<<` here-string redirect slice.
289+
- [ ] Build on the delivered bounded Bash heredoc grammar and quoted-delimiter
290+
adjacency by exposing public body/delimiter/expansion/completeness facts,
291+
then add a separately tested Bash `<<<` here-string redirect slice.
282292
- [ ] Near the end of v0.3 delivery, expand the Web sample with curated complex
283293
Bash and PowerShell inputs and deterministic Mermaid views of syntax,
284294
occurrences, compatibility clauses, ancestry, redirects, and fail-closed

SPEC.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,17 @@ quoted_string := single-quoted | double-quoted
978978
- v0.2 recognizes heredocs (`<<EOF ... EOF`) as redirect syntax while the
979979
body is skipped. Stable v0.3 preserves delimiter, body, expansion mode,
980980
tab-stripping mode, and completeness through `HereDocumentAnalysis`.
981+
The bounded grammar accepts one terminal `<<` / `<<-` redirect on a command
982+
header, with optional whitespace or a trailing comment after the delimiter.
983+
Additional header tokens, pipelines, and queued heredocs are unparseable
984+
until their body-association grammar is modeled. Quote removal determines
985+
the delimiter spelling; any quoted or escaped delimiter fragment makes the
986+
body literal. In an expanding body, unescaped `$()` substitutions are
987+
executable even when their spelling is surrounded by quote characters,
988+
because heredoc body quotes are data rather than shell quoting syntax.
989+
Escaped substitutions remain literal. Legacy backticks, arithmetic
990+
expansion, line continuations that could hide a substitution boundary, and
991+
incomplete substitutions make the whole result unparseable.
981992
- Redirect targets matching the POSIX fd-dup / fd-close shorthand —
982993
`&N`, `&N-`, or `&-` (where `N` is one or more decimal digits) — are
983994
NOT path-resolved. The parser carries the raw token (e.g. `&1`) on
@@ -1068,7 +1079,8 @@ The lexer produces tokens consumed by the parser. Token kinds:
10681079
- **OPERATOR**`&&`, `||`, `;`, `|`, `>`, `>>`, `<`, `2>`, `2>>`,
10691080
`(`, `)`, `<<`, `<<-`.
10701081
- **WHITESPACE** — one or more spaces, tabs, or newlines (newlines inside
1071-
a skipped heredoc body are not tokenized). A whitespace run that
1082+
a heredoc body are not emitted as ordinary tokens; the delimiter token
1083+
retains the body's resolver fragments and authored extent). A whitespace run that
10721084
contains a newline — including the newline after a heredoc terminator —
10731085
is flagged as a **statement separator**; the parser retains those
10741086
tokens past `FilterSignificant` and splits clauses on them per §4. A
@@ -1081,6 +1093,9 @@ The lexer produces tokens consumed by the parser. Token kinds:
10811093
and `\X` escapes via a shared opaque-region scanner. The parser
10821094
consumes this token as `Arg{ Kind=DynamicSkip, IsPath=false,
10831095
Resolved=null }` per locked interpretation #2.
1096+
Expanding-heredoc substitutions use the same opaque fragment semantics but
1097+
remain attached to the delimiter token rather than entering the ordinary
1098+
command-token stream.
10841099
- **UNPARSEABLE_SENTINEL**`$((expr))` arithmetic expansion or
10851100
`${var//pat/repl}` complex parameter expansion. The lexer skips past
10861101
the matching close (`))` or `}` respectively) and emits a sentinel

TOOLING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ and parser slice exists. The PII audit scans both corpus trees.
4040
### PwshCorpusTool
4141

4242
`tools/PwshCorpusTool` is the PowerShell corpus authoring aid
43-
(SPEC.POWERSHELL.md §13). It is a dev-only console app (not packed).
43+
(SPEC.POWERSHELL.md §13). Its shared serializer also exposes an ad-hoc Bash
44+
check so structural Bash entries do not need hand-authored spans. It is a
45+
dev-only console app (not packed).
4446

4547
| Command | Purpose |
4648
|---|---|
4749
| `dotnet run --project tools/PwshCorpusTool -- generate` | Regenerate every `Corpus/powershell/NNN_slug.json` from the curated `CorpusManifest`. Run after any parser change that shifts PowerShell AST output. |
4850
| `dotnet run --project tools/PwshCorpusTool -- check "<command>"` | Print the parser's expected-AST JSON block for a command beside the real-`pwsh` oracle verdict — the fastest way to author or debug a binding-category entry. |
51+
| `dotnet run --project tools/PwshCorpusTool -- check-bash "<command>"` | Print exact Bash `clauses`, `syntax`, and `commands` expectations using the corpus resolver settings. |
4952

5053
The curated inputs live in `tools/PwshCorpusTool/CorpusManifest.cs`; the
5154
`expected` AST is generated from `PwshParser`, and `PwshOracleTests`

openspec/changes/v0-3-structured-shell-analysis/tasks.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333
- [x] 3.9 Add corpus expectations for syntax shape, occurrences, roles, and completeness for existing constructs.
3434
- [ ] 3.10 Implement Bash `$()` discovery in supported argument words, redirect values, iterables, and expanding heredoc bodies; retain literal/escaped spellings and fail closed on command-name substitutions, legacy backticks, or incomplete interiors.
3535
- [x] 3.10a Implement the simple-command argument and redirect-target slice, including comment-safe boundaries and fail-closed unsupported interiors.
36+
- [x] 3.10b Implement the bounded expanding-heredoc slice with quote-removed delimiters, literal quoted/escaped bodies, tab stripping, exact provenance, and fail-closed unsupported header/body forms.
3637
- [ ] 3.11 Implement PowerShell `$()` discovery in supported words, redirect values, foreach expressions, call-operator dynamic identities, standalone expression statements, double-quoted strings, and expandable here-strings; never invent invocation from standalone output, retain literal/escaped spellings, and fail closed on trailing command-style arguments, call-operator script blocks, or unsupported execution-bearing `@()` / `@{}` forms.
3738
- [x] 3.11a Implement words, redirect values, call-operator dynamic identities, standalone statements, expandable strings/here-strings, and parent-versus-child host payload provenance; fail closed on arbitrary expression values and unsupported execution-bearing `@()` / `@{}` forms.
3839
- [ ] 3.12 Pin substitution parentage, authored sibling indices, innermost-first ordering, Bash-isolated versus PowerShell-current-scope state, unknown-state propagation, nesting/depth limits, and incomplete dynamic identities in direct tests.
3940
- [x] 3.12a Pin the Bash argument/redirect slice, isolated cwd behavior, wrapper provenance, and the shared structural-depth budget.
4041
- [x] 3.12b Pin the PowerShell simple-command slice, current-scope exact and unknown cwd propagation, parent/child wrapper provenance, expression boundaries, and the shared structural-depth budget.
42+
- [x] 3.12c Pin expanding-heredoc sibling/nested ordering, exact spans, isolated state, delimiter modes, escape parity, depth limits, and atomic failure.
4143
- [ ] 3.13 Promote ordinary, multiple, nested, iterator, redirect, quoted, escaped, stateful, malformed, and hidden-execution substitution cases into both executable corpora and the Netclaw approval matrix.
4244
- [x] 3.13a Promote the Bash ordinary, multiple, nested, redirect, quoted, escaped, stateful, malformed, and hidden-execution cases into its executable corpus.
4345
- [x] 3.13b Promote the PowerShell ordinary, multiple, nested, redirect, quoted, escaped, stateful, malformed, expression-boundary, and hidden-execution cases into its executable corpus.
46+
- [x] 3.13c Promote expanding, literal, tab-stripped, multiple, and malformed Bash heredoc cases with full structural expectations into the executable corpus.
4447

4548
## 4. Explicit Redirect Semantics
4649

@@ -106,10 +109,11 @@
106109
## 10. Heredoc / Here-String Slice and Separately Gated Follow-ups
107110

108111
- [x] 10.1 Specify heredoc delimiter adjacency and quoting, expansion mode, body provenance, substitutions, tab stripping, completeness, and Bash here-string semantics.
109-
- [ ] 10.2 Preserve existing `<<` / `<<-` behavior and fix quoted-delimiter adjacency without regressing the v0.2 compatibility redirect.
112+
- [x] 10.2 Preserve existing `<<` / `<<-` behavior and fix quoted-delimiter adjacency without regressing the v0.2 compatibility redirect.
110113
- [ ] 10.3 Add explicit heredoc delimiter/body/expansion/completeness facts and surface every supported substitution command.
111114
- [ ] 10.4 Add Bash `<<<` here-string tokenization, explicit redirect facts, bounded operand analysis, and trailing-newline semantics.
112115
- [ ] 10.5 Add direct, malformed, quoted/unquoted, tab-stripped, dynamic, and substitution-bearing corpus cases plus real-Bash parse-only validation.
116+
- [x] 10.5a Add direct, executable-corpus, real-Bash output, and real-Bash parse-only coverage for the bounded substitution-discovery slice; explicit redirect facts and the full heredoc matrix remain pending.
113117
- [ ] 10.6 After stable v0.3, specify process-substitution command discovery and the unknown produced descriptor/path value before enabling it.
114118
- [ ] 10.7 After stable v0.3, specify background-list concurrency, ordering, and shell-state boundaries before enabling single `&`.
115119
- [ ] 10.8 Specify C-style loop and arithmetic hidden-execution behavior before enabling either construct.

0 commit comments

Comments
 (0)