Skip to content

Commit ddce4c7

Browse files
PR 4: BashResolver + per-verb path rules + flag-aware verb probe (#7)
* PR 4: BashResolver + per-verb path rules + flag-aware verb probe Implements SPEC §8 (resolver) and SPEC §7 (per-verb path-arg rules) + the flag-with-value-aware verb-chain probe deferred from PR 3. Locked interpretations #3 (Glob vs DynamicSkip distinction) and #8 (tar / docker -v default rule) materialized. What's wired: - Internal/Resolving/BashResolver.cs (SPEC §8 pipeline): * filesystem:: prefix strip * tilde + $HOME expansion (HomeDirectory lazy fallback to Environment.SpecialFolder.UserProfile) * other $VAR/${VAR} in path slot → Kind=DynamicSkip, IsPath=false * other $VAR in non-path slot → Kind=EnvVar * glob detection: Kind=Glob, IsPath=true in path slot (covering-dir heuristic preserved per locked #3) * relative-path join against WorkingDirectory (lazy fallback to Environment.CurrentDirectory) * LooksLikePath heuristic with curated extension list * IOException / path-format exception → DynamicSkip per SPEC §8 step 6 - Internal/Bash/Verbs/BashPerVerbRules.cs (SPEC §7): * Per-positional rules: chmod (mode/path), chown (user/path), chgrp (group/path), ln (all paths), find (path/predicates), grep/rg (pattern/paths), sed/awk (script/paths), tar (default rule per #8), curl/wget (URL not path; -o/-O values are paths), scp/rsync/sftp (all paths), cd-family (target path) * Default rule for FileVerbs without override: all non-flag positionals are paths * LooksLikePath fallback for non-FileVerbs * Flag-value path classification: git -C / --git-dir is path; curl -d/--data is body data not path; docker -v is single literal IsPath=false per locked #8; tar -f/-C are paths - BashCommandParser updates: * Flag-with-value-aware verb-chain probe — `git -C /repo log` → Verb.Tokens=["git", "log"] per SPEC §12 example (deferred from PR 3) * Resolver wired into Arg construction * Redirect.Target = Resolved when resolvable; IsDynamicSkip=true otherwise Tests: - BashResolverTests: 34 tests covering tilde/$HOME/env-var/glob/ filesystem::/abs/rel/LooksLikePath - BashPerVerbRulesTests: 34 tests covering each per-verb rule + flag- value classification - BashCommandParserTests: 9 new tests + refresh of PR 3 tests with HomeDirectory=/home/test, WorkingDirectory=/work - 12 existing corpus entries refreshed for path classification - 20 new corpus entries (51-70): * 51-60 dynamic-skip: $UNRESOLVED/foo, $REPO patterns, $HOME expansion, $LOG_FILE, ${PATH}/bin/foo, glob in path slot * 61-70 per-verb rules: chmod 755 path, chown user:group path, find /var/log -name "*.log", grep pattern path, sed script path, awk program path, curl -o path URL, wget -O path URL, git -C /repo log, tar -xf archive target Total tests: 296/296 passing. Public API surface unchanged. SPEC.md §8 updated: - Step 4 explicitly states Glob has IsPath=true in path-arg slot and IsPath=false elsewhere; preserves covering-dir heuristic - Step 6 narrows DynamicSkip to env-var-in-path-slot and resolver-throws cases (no longer overlaps with Glob) - Glob and DynamicSkip carry distinct signals per locked #3 PR 4 → PR 5 follow-ups tracked in tasks.md: - Subshell IsSubshell flag wiring (Segment.FromSubshell plumbed but unused) - cd-attribution propagation via internal-context-state piggybacked on parsing pipeline - locked #6 (cd $VAR propagation) DynamicSkip cwd signal * Fix(resolver): produce bash-style paths on Windows Path.GetFullPath is platform-aware. On Windows it expands `/etc/hosts` to `D:\etc\hosts` because Windows treats `/` as drive-relative. This breaks 53 tests on Test-windows-latest CI runner. Bash semantics use forward slashes universally regardless of host OS, so the resolver now string-only normalizes paths: - NormalizeToForwardSlashes: \\ → / (preserves UNC as //server/share) - NormalizePath: split on /, collapse . and .., dedupe slashes, preserve leading / (or //, or X: drive letter), join with / - TryResolveAbsolutePath uses these helpers instead of Path.GetFullPath - JoinPath uses string concat with explicit / instead of Path.Combine Linux: still 296/296 green. Windows runner should now pass — the `Expected /work/input vs Actual D:\work\input` failures all disappear because the resolver no longer touches the host's PathSeparator.
1 parent 325fa6f commit ddce4c7

42 files changed

Lines changed: 2553 additions & 287 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

IMPLEMENTATION_PLAN.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,28 @@ bulldoze priorities.
9191
- [x] CorpusRunnerTests skeleton + 50 corpus entries
9292
- [x] `BashParser.Parse` delegates to `BashCommandParser.Parse`
9393

94-
### 6. Resolver (SPEC §8)
95-
96-
- [ ] Tilde + `$HOME` expansion against `BashParserOptions.HomeDirectory`
97-
- [ ] All other `$VAR` / `${VAR}``DynamicSkip`
98-
- [ ] `filesystem::/path` prefix stripping
99-
- [ ] Glob detection (don't expand)
100-
- [ ] Relative path joining against `BashParserOptions.WorkingDirectory`
101-
- [ ] `LooksLikePath` heuristic per §8
102-
103-
### 7. Per-verb path-arg rules (SPEC §7)
104-
105-
- [ ] Default: every non-flag positional after the verb chain is a path
106-
- [ ] Per-verb overrides: `chmod`, `chown`, `chgrp`, `ln`, `find`,
107-
`grep`, `rg`, `sed`, `awk`, `tar`, `curl`/`wget`, `scp`/`rsync`,
108-
`cd`-family
109-
- [ ] Flag-with-value handling (`-o file`, `git -C /repo`, `--output=file`)
94+
### 6. Resolver (SPEC §8) — PR 4, complete
95+
96+
- [x] Tilde + `$HOME` expansion against `BashParserOptions.HomeDirectory`
97+
- [x] All other `$VAR` / `${VAR}``DynamicSkip` (in path slots) /
98+
`EnvVar` (in non-path slots)
99+
- [x] `filesystem::/path` prefix stripping
100+
- [x] Glob detection (don't expand); locked interp #3 distinguishes
101+
Glob (IsPath=true in path slot) vs DynamicSkip (IsPath=false)
102+
- [x] Relative path joining against `BashParserOptions.WorkingDirectory`
103+
- [x] `LooksLikePath` heuristic per §8 with curated extension list
104+
- [x] SPEC §8 step 4/6 overlap resolved
105+
106+
### 7. Per-verb path-arg rules (SPEC §7) — PR 4, complete
107+
108+
- [x] Default: every non-flag positional after the verb chain is a path
109+
- [x] Per-verb overrides: `chmod`, `chown`, `chgrp`, `ln`, `find`,
110+
`grep`, `rg`, `sed`, `awk`, `tar` (default fallback per #8),
111+
`curl`/`wget`, `scp`/`rsync`, `cd`-family
112+
- [x] Flag-with-value handling (`-o file`, `git -C /repo`,
113+
`--output=file`); `git -C /repo log` → Verb=["git", "log"]
114+
- [x] Flag-value path classification table (`git -C` is path; `curl -d`
115+
is body data; `docker -v` is single literal IsPath=false per #8)
110116

111117
### 8. cd-in-compound propagation (SPEC §9)
112118

SPEC.md

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -621,23 +621,35 @@ a normalized absolute path. Resolution order:
621621

622622
4. **Glob detection.** Tokens containing `*`, `?`, or `[` are marked
623623
`ArgKind.Glob`. The resolver does **not** expand globs. The token
624-
stays as-is in `Raw`; `Resolved` is null. Consumers that want the
625-
glob's "covering directory" can use `Path.GetDirectoryName(Raw)` to
626-
approximate.
624+
stays as-is in `Raw`; `Resolved` is null.
625+
626+
**In a path-arg slot:** `IsPath = true`. Consumers can apply the
627+
"covering directory" heuristic (`Path.GetDirectoryName(Raw)`) to
628+
reason about the directory the glob resolves under (e.g.
629+
`/tmp/*.bak``/tmp`).
630+
631+
**In a non-path slot:** `IsPath = false`.
632+
633+
Per locked interpretation #3, glob and DynamicSkip carry **distinct**
634+
signals — globs preserve a useful covering-dir hint that DynamicSkip
635+
tokens lack.
627636

628637
5. **Relative path resolution.** Tokens not starting with `/` (or `\\` on
629-
Windows) are joined to `BashParserOptions.WorkingDirectory`. If
630-
`WorkingDirectory` is null, the token stays relative and `Resolved`
631-
is null with `Kind = DynamicSkip`.
632-
633-
6. **Dynamic-skip predicates.** A token is `DynamicSkip` when:
634-
- It contains an unresolved env var reference (other than `$HOME`).
635-
- It contains glob metachars AND the resolver was asked for an
636-
absolute resolution.
638+
Windows, or a Windows drive letter `X:`) are joined to
639+
`BashParserOptions.WorkingDirectory` (lazy fallback to
640+
`Environment.CurrentDirectory` when null). On
641+
`IOException` / path-format exceptions during resolution, fall through
642+
to `Kind = DynamicSkip, IsPath = false, Resolved = null`.
643+
644+
6. **DynamicSkip predicates.** A token is `Kind = DynamicSkip,
645+
IsPath = false, Resolved = null` when:
646+
- It contains an unresolved env-var reference (other than `$HOME`)
647+
in a slot the verb's rule classifies as a path.
637648
- Resolution throws an `IOException` or path-format exception.
638649

639-
`DynamicSkip` tokens have `Resolved = null`. Consumers must not use
640-
`Raw` as a literal path.
650+
Globs do NOT downgrade to DynamicSkip — they carry their own Kind so
651+
consumers can still apply the covering-dir heuristic. Consumers must
652+
not use `Raw` as a literal path for `DynamicSkip` tokens.
641653

642654
### Path-shape heuristic
643655

openspec/changes/v0.1-locked-interpretations/tasks.md

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,58 @@ the SPEC.md sections that get updated alongside the implementation.
106106

107107
## 4. PR 4 — Resolver + per-verb rules (interpretations #3 + #8)
108108

109-
- [ ] 4.1 `Internal/Resolving/BashResolver.cs` per SPEC §8
110-
- [ ] 4.2 Tilde + `$HOME`; other env vars → `DynamicSkip, IsPath=false`
111-
- [ ] 4.3 `filesystem::/path` strip; glob detection per interpretation #3
109+
- [x] 4.1 `Internal/Resolving/BashResolver.cs` per SPEC §8: full pipeline
110+
(filesystem:: strip → tilde expansion → $HOME substitution → other
111+
env-var DynamicSkip → glob detection → path resolution against
112+
WorkingDirectory). Cross-platform-safe (Linux + Windows).
113+
- [x] 4.2 Tilde + `$HOME` (the only env var we expand); other env vars
114+
in path slot → `DynamicSkip, IsPath=false` per interpretation #3
115+
- [x] 4.3 `filesystem::/path` strip; glob detection per interpretation #3
112116
(`Kind=Glob, IsPath=true (in path slot), Resolved=null`)
113-
- [ ] 4.4 Relative-path joining against `WorkingDirectory`; `LooksLikePath`
114-
heuristic
115-
- [ ] 4.5 `Internal/Bash/Verbs/BashPerVerbRules.cs` per SPEC §7 with
116-
interpretation #8 fallback for tar (default rule) and docker -v
117-
(single literal arg, IsPath=false)
118-
- [ ] 4.6 Update `SPEC.md` §7 (note v0.1 limitations + reference issues),
119-
§8 (rewrite steps 4 & 6 to remove the overlap; explicit IsPath
120-
asymmetry per interpretation #3)
121-
- [ ] 4.7 File 2 GitHub issues: tar action-flag awareness; docker -v
122-
colon-split + Windows drive-letter handling
123-
- [ ] 4.8 Open OpenSpec change `path-resolver-rules` for the §7/§8 deltas
117+
- [x] 4.4 Relative-path joining against `WorkingDirectory` (lazy fallback
118+
to `Environment.CurrentDirectory`); `LooksLikePath` heuristic with
119+
curated extension list
120+
- [x] 4.5 `Internal/Bash/Verbs/BashPerVerbRules.cs` per SPEC §7 + flag-value
121+
classification table:
122+
- Per-positional rules for `chmod`, `chown`, `chgrp`, `ln`, `find`,
123+
`grep`, `rg`, `sed`, `awk`, `tar`, `curl`, `wget`, `scp`/`rsync`,
124+
`cd`/`chdir`/`pushd`/`popd`/`push-location`/`set-location`
125+
- Default rule (all non-flag positionals → paths) for other FileVerbs
126+
- `LooksLikePath` fallback for non-FileVerbs
127+
- Flag-with-value path classification: `git -C` is path; `curl -d`
128+
is not; `docker -v` value is single literal IsPath=false
129+
(interpretation #8)
130+
- [x] 4.6 `BashCommandParser` updates:
131+
- Flag-with-value-aware verb-chain probe so `git -C /repo log`
132+
produces `Verb.Tokens=["git", "log"]` per SPEC §12
133+
- Resolver wired into Arg + Redirect building
134+
- `Redirect.Target = Resolved` when resolvable; `IsDynamicSkip=true`
135+
when not
136+
- [x] 4.7 SPEC.md §7 already updated in PR 3 with FlagsWithValue compat
137+
note + PR 4 follow-up. PR 4 SPEC.md updates: §8 step 4/6 overlap
138+
resolved (Glob ≠ DynamicSkip distinction). To be applied during
139+
commit.
140+
- [ ] 4.8 File 2 GitHub issues: tar action-flag awareness; docker -v
141+
colon-split + Windows drive-letter handling. (Tracked locally;
142+
filing in PR 5/6 when the issues are easier to reference real
143+
corpus repros.)
144+
- [x] 4.9 BashResolverTests (34) + BashPerVerbRulesTests (34) +
145+
BashCommandParserTests refresh (9 new) + 12 corpus entries
146+
refreshed + 20 new corpus entries (10 dynamic-skip + 10 per-verb)
147+
- [x] 4.10 **296/296 tests passing**; clean build; PublicApiSnapshotTests
148+
still green (no API surface change)
149+
150+
### PR 4 follow-ups (tracked for PR 5)
151+
152+
- `Segment.FromSubshell` flag plumbed in PR 4 but unused — PR 5 hooks it
153+
into IsSubshell + attribution-stack push/pop.
154+
- cd-attribution propagation: cleanest approach is constructing new
155+
`BashParserOptions{ WorkingDirectory = /target }` for clauses
156+
following `cd /target`. PR 5 wires this.
157+
- Locked interpretation #6 (cd $VAR propagation): when cd target is
158+
DynamicSkip, subsequent clauses' relative-path args need to be
159+
flagged as such. Mechanism (without adding to public BashParserOptions
160+
surface): internal context state piggy-backed on the parsing pipeline.
124161

125162
## 5. PR 5 — cd attribution + subshells + bash -c (interpretations #4, #5, #6)
126163

0 commit comments

Comments
 (0)