diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 2354ffc..ce4c0e8 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -259,6 +259,16 @@ priorities. The PowerShell manifest owns all 422 entries and round-trips them exactly, including case-specific isolated-state inputs. Explicit false/null assertions remain opt-in and generator-preserved. +- [x] Promote the remaining 21 stable Bash design cases into the executable + corpus with complete compatibility, syntax, occurrence, value, ancestry, + redirect, and completeness assertions. Nine compatibility-only entries + now carry the v0.3 projections and entries 281-292 cover the inputs that + had no exact executable-corpus case. The three Bash future-scope design + cases remain non-gating. Promotion also reconciled the unquoted wildcard + redirect story with the fail-closed completeness contract, publishes + sparse exact/unknown effective-value overlays, and pins quoted, escaped, + and continued tilde-prefix behavior against Bash. The PowerShell promotion + half keeps OpenSpec task 1.10 open. - [x] Deliver the first Bash `$()` substitution slice for supported simple-command arguments and redirect targets. Direct tests and corpus entries pin multiple and nested ordering, exact ancestry/spans, isolated diff --git a/SPEC.md b/SPEC.md index d0cdddc..6afa8f7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -971,6 +971,11 @@ occurrence. Bash `HereString` data uses `Target`, includes the shell's trailing newline in an exact value, and is not path-relevant. PowerShell here-strings remain ordinary value tokens rather than redirect operations. +A Bash file redirect whose expansion cannot prove exactly one target has an +`Unknown` target and `IsComplete=false`; its containing command occurrence is +also incomplete. In particular, an unquoted wildcard target is not completed +by enumerating the parser process's filesystem. + The public records define an in-memory typed API, not a stable polymorphic JSON wire format. Their generated equality, hashing, and `ToString()` behavior is part of the normal record shape. Consumers that persist parser results own a @@ -1859,7 +1864,9 @@ a normalized absolute path. Resolution order: stays literal — `$HOME` is not expanded inside single quotes. 1. **Tilde expansion.** `~` → `BashParserOptions.HomeDirectory`. - `~/foo` → `/foo`. `~user` not supported → `DynamicSkip`. + `~/foo` → `/foo`. The complete tilde prefix must be unquoted; + quoted or escaped slash spellings remain literal, while backslash-newline + is removed before this test. `~user` not supported → `DynamicSkip`. 2. **Env-var substitution.** `$VAR` and `${VAR}` are **not expanded** even if the value is in `Environment`. We treat any env var reference diff --git a/openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md b/openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md index ff6a0e0..b37bf06 100644 --- a/openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md +++ b/openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md @@ -28,6 +28,12 @@ retain their existing meanings. - **THEN** the compatibility argument is a literal path resolved as `/$HOME` - **THEN** it is not `DynamicSkip` and is not resolved as the configured home directory +#### Scenario: Bash tilde expansion retains prefix provenance +- **WHEN** Bash parses `~/x` or a backslash-newline continuation between `~` and `/x` +- **THEN** the unquoted tilde prefix expands from the configured home directory +- **WHEN** the slash or an empty intervening fragment is quoted or escaped +- **THEN** the decoded `~/x` remains a literal path resolved under the configured cwd + #### Scenario: PowerShell escaped variable is a literal path component - **WHEN** PowerShell parses ``Get-Content `$HOME`` with an exact working directory - **THEN** the shell value is the literal `$HOME` @@ -123,6 +129,7 @@ retain their existing meanings. #### Scenario: Bash redirect wildcard cardinality is quote-sensitive - **WHEN** Bash parses unquoted `> *.txt` - **THEN** the target is unknown without filesystem enumeration because expansion may produce zero, one, or multiple paths +- **THEN** the redirect and containing command occurrence remain incomplete - **WHEN** Bash parses quoted `> "*.txt"` - **THEN** the target is the exact literal filename `*.txt` diff --git a/src/ShellSyntaxTree/Internal/Bash/Lexing/BashLexer.cs b/src/ShellSyntaxTree/Internal/Bash/Lexing/BashLexer.cs index edea34f..243aa50 100644 --- a/src/ShellSyntaxTree/Internal/Bash/Lexing/BashLexer.cs +++ b/src/ShellSyntaxTree/Internal/Bash/Lexing/BashLexer.cs @@ -573,11 +573,20 @@ private static int ReadSingleQuoted( // src[i] = closing ' // Strip the delimiters from the value per SPEC §5. var inner = src.Slice(start + 1, i - start - 1).ToString(); + var boundary = new ShellValueBuilder(); + boundary.AppendBoundary(start + 1); + if (inner.Length > 0) + { + boundary.AppendLiteral(inner, start + 1, i - start - 1); + } + + var resolverValue = boundary.Build(); + tokens.Add(new BashToken( BashTokenKind.QuotedString, inner, null, start, (i - start) + 1, null) { IsSingleQuoted = true, - ResolverValue = ShellValue.Literal(inner, start + 1, i - start - 1), + ResolverValue = resolverValue, }); return i + 1; } diff --git a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs index 22efc4d..562d3b8 100644 --- a/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs +++ b/src/ShellSyntaxTree/Internal/Bash/Parsing/BashAbstractStateAnalyzer.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Text; using ShellSyntaxTree.Internal.Resolving; namespace ShellSyntaxTree.Internal.Bash.Parsing; @@ -467,11 +468,27 @@ private void RecordEffectiveArguments( var evaluator = input.Bindings; foreach (var provenance in sourceFacts.ValueProvenance) { - if (!evaluator.TryAnalyzeEffectiveValue(provenance.Value, out var domain)) + var hasStateDependentValue = evaluator.TryAnalyzeEffectiveValue( + provenance.Value, + out var domain); + if (!hasStateDependentValue && + !RequiresIndependentEffectiveValue(simple.Clause, provenance)) { continue; } + if (!hasStateDependentValue) + { + if (TryAnalyzeParserKnownValue(provenance.Value, out var knownValue)) + { + domain = knownValue; + } + else + { + domain = evaluator.AnalyzeWordForTransfer(provenance.Value); + } + } + accumulated ??= GetEffectiveArguments(simple.Clause); if (accumulated.TryGetValue(provenance.ClauseElementIndex, out var prior)) { @@ -485,6 +502,156 @@ private void RecordEffectiveArguments( } } + private static bool RequiresIndependentEffectiveValue( + Clause clause, + ShellValueElementProvenance provenance) + { + if (provenance.ClauseElementIndex < 0 || + provenance.ClauseElementIndex >= clause.Elements.Count) + { + return false; + } + + var nonEmptyLiteralFragments = 0; + var hasLiteralLexicalTransform = false; + foreach (var fragment in provenance.Value.Fragments) + { + if (fragment.Kind != ShellValueFragmentKind.Literal) + { + return true; + } + + if (fragment.Value.Length == 0) + { + continue; + } + + nonEmptyLiteralFragments++; + if (fragment.SourceLength != fragment.Value.Length) + { + hasLiteralLexicalTransform = true; + } + } + + // Clause.Elements already carries ordinary authored literals. Keep the + // overlay for values whose shell decoding or shell-specific path + // spelling gives a policy consumer additional information. + var element = clause.Elements[provenance.ClauseElementIndex]; + if ((element.IsPath || element.IsFlag) && + (hasLiteralLexicalTransform || nonEmptyLiteralFragments > 1)) + { + return true; + } + + return element.IsPath && HasProviderQualifier(provenance.Value.Decoded); + } + + private bool TryAnalyzeParserKnownValue( + ShellValue value, + out ShellValueDomain domain) + { + var homeDirectory = BashResolver.GetHomeDirectory(_options); + var composed = new StringBuilder(value.Decoded.Length); + for (var fragmentIndex = 0; + fragmentIndex < value.Fragments.Count; + fragmentIndex++) + { + var fragment = value.Fragments[fragmentIndex]; + if (fragment.Kind == ShellValueFragmentKind.Literal) + { + composed.Append(fragment.Value); + continue; + } + + if (fragment.Kind != ShellValueFragmentKind.Expansion || + fragment.Expansion is not ShellExpansionReference expansion) + { + domain = ShellValueDomain.Unknown; + return false; + } + + if (expansion.Kind == ShellExpansionKind.Glob && + (fragment.AllowedTransforms & ShellLexicalTransform.Glob) == 0) + { + composed.Append(fragment.Value); + continue; + } + + if (expansion.Kind == ShellExpansionKind.Tilde) + { + var tildeKind = BashResolver.ClassifyTildeExpansion(value, fragmentIndex); + if (tildeKind == BashTildeExpansionKind.Literal) + { + composed.Append(fragment.Value); + continue; + } + + if (tildeKind == BashTildeExpansionKind.Unknown || + homeDirectory.Length == 0) + { + domain = ShellValueDomain.Unknown; + return false; + } + + composed.Append(homeDirectory); + continue; + } + + if (expansion.Kind != ShellExpansionKind.Variable || + !string.Equals(expansion.Name, "HOME", StringComparison.Ordinal) || + fragment.Cardinality != ShellValueCardinality.ExactlyOne || + (fragment.AllowedTransforms & ShellLexicalTransform.Variable) == 0 || + homeDirectory.Length == 0 || + ((fragment.AllowedTransforms & ShellLexicalTransform.FieldSplit) != 0 && + ContainsFieldSplitOrGlobCharacter(homeDirectory))) + { + domain = ShellValueDomain.Unknown; + return false; + } + + composed.Append(homeDirectory); + } + + domain = new ShellValueDomain + { + Kind = ShellValueDomainKind.Exact, + Values = new[] { composed.ToString() }, + }; + return true; + } + + private static bool ContainsFieldSplitOrGlobCharacter(string value) + { + foreach (var character in value) + { + if (char.IsWhiteSpace(character) || character is '*' or '?' or '[') + { + return true; + } + } + + return false; + } + + private static bool HasProviderQualifier(string value) + { + var separator = value.IndexOf("::", StringComparison.Ordinal); + if (separator <= 0) + { + return false; + } + + for (var index = 0; index < separator; index++) + { + if (!char.IsLetterOrDigit(value[index]) && value[index] != '-') + { + return false; + } + } + + return true; + } + private void RecordUnvisitedBindingArguments( ShellBlockSyntax block, string bindingName) diff --git a/src/ShellSyntaxTree/Internal/Resolving/BashResolver.cs b/src/ShellSyntaxTree/Internal/Resolving/BashResolver.cs index 44be178..b731229 100644 --- a/src/ShellSyntaxTree/Internal/Resolving/BashResolver.cs +++ b/src/ShellSyntaxTree/Internal/Resolving/BashResolver.cs @@ -9,6 +9,13 @@ namespace ShellSyntaxTree.Internal.Resolving; +internal enum BashTildeExpansionKind +{ + Unknown, + Literal, + Home, +} + /// /// Path-token resolver for the bash parser. Implements SPEC §8 — tilde /// expansion, the lone $HOME expansion, filesystem:: prefix @@ -295,25 +302,21 @@ internal static (ArgKind Kind, string? Resolved, bool IsPath) Resolve( break; case ShellExpansionKind.Tilde: - // An empty quoted fragment before '~' is still an authored - // word prefix and suppresses Bash tilde expansion. - if (fragmentIndex != 0 - || (fragment.AllowedTransforms & ShellLexicalTransform.Tilde) == 0) + var tildeKind = ClassifyTildeExpansion(value, fragmentIndex); + if (tildeKind == BashTildeExpansionKind.Literal) { composed.Append(fragment.Value); break; } - if (value.Decoded.Length > 1 - && value.Decoded[1] != '/' - && value.Decoded[1] != '\\') + if (tildeKind == BashTildeExpansionKind.Unknown) { return treatAsPath ? (ArgKind.DynamicSkip, null, false) : (ArgKind.Tilde, null, false); } - composed.Append(GetHomeDirectory(options).TrimEnd('/', '\\')); + composed.Append(GetHomeDirectory(options)); hadHomeExpansion = true; break; @@ -439,9 +442,67 @@ internal static bool LooksLikePath(string token) return false; } + internal static BashTildeExpansionKind ClassifyTildeExpansion( + ShellValue value, + int fragmentIndex) + { + var fragment = value.Fragments[fragmentIndex]; + if (fragmentIndex != 0 || + (fragment.AllowedTransforms & ShellLexicalTransform.Tilde) == 0) + { + return BashTildeExpansionKind.Literal; + } + + if (value.Decoded.Length == 1) + { + return value.Fragments.Count == 1 + ? BashTildeExpansionKind.Home + : BashTildeExpansionKind.Literal; + } + + if (value.Decoded[1] != '/') + { + if (value.Fragments.Count > 1) + { + var prefix = value.Fragments[1]; + var hasQuoteBoundary = prefix.Kind == ShellValueFragmentKind.Literal && + prefix.Value.Length == 0; + var hasEscapedPrefix = prefix.Kind == ShellValueFragmentKind.Literal && + prefix.Value.Length > 0 && + prefix.SourceLength is not null && + prefix.SourceLength != prefix.Value.Length; + if (hasQuoteBoundary || hasEscapedPrefix) + { + return BashTildeExpansionKind.Literal; + } + } + + return BashTildeExpansionKind.Unknown; + } + + if (value.Fragments.Count <= 1 || + fragment.SourceStart is null || + fragment.SourceLength is null) + { + return BashTildeExpansionKind.Unknown; + } + + var delimiter = value.Fragments[1]; + // A source gap without a quote boundary is a removed line + // continuation. Bash removes it before testing the unquoted slash. + var isUnquotedSlash = delimiter.Kind == ShellValueFragmentKind.Literal && + delimiter.Value.Length > 0 && + delimiter.Value[0] == '/' && + delimiter.SourceStart >= fragment.SourceStart + fragment.SourceLength && + delimiter.SourceLength == delimiter.Value.Length; + return isUnquotedSlash + ? BashTildeExpansionKind.Home + : BashTildeExpansionKind.Literal; + } + // ---------------------------------------------------------------- helpers - private static string GetHomeDirectory(BashParserOptions options) + internal static string GetHomeDirectory(BashParserOptions options) { if (!string.IsNullOrEmpty(options.HomeDirectory)) { diff --git a/src/ShellSyntaxTree/Internal/Resolving/ShellValue.cs b/src/ShellSyntaxTree/Internal/Resolving/ShellValue.cs index 5ed197f..fc4bbeb 100644 --- a/src/ShellSyntaxTree/Internal/Resolving/ShellValue.cs +++ b/src/ShellSyntaxTree/Internal/Resolving/ShellValue.cs @@ -405,6 +405,10 @@ internal void Append(ShellValueFragment fragment) && previous.SourceLength is not null && fragment.SourceStart == previous.SourceStart + previous.SourceLength; var bothUnmapped = previous.SourceStart is null && fragment.SourceStart is null; + // Keep escape-collapsed text separate from source-exact neighbors; + // later shell analysis must know which character was quoted. + var bothSourceExact = previous.SourceLength == previous.Value.Length && + fragment.SourceLength == fragment.Value.Length; if (previous.Value.Length > 0 && fragment.Value.Length > 0 && previous.Kind == ShellValueFragmentKind.Literal @@ -413,7 +417,7 @@ internal void Append(ShellValueFragment fragment) && previous.Expansion == fragment.Expansion && previous.Cardinality == fragment.Cardinality && previous.OpaqueCause == fragment.OpaqueCause - && (sourceIsContiguous || bothUnmapped)) + && ((sourceIsContiguous && bothSourceExact) || bothUnmapped)) { _fragments[_fragments.Count - 1] = previous with { diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/166_curl_data_mixed_literal_dynamic.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/166_curl_data_mixed_literal_dynamic.json index 1e75479..4a30323 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/166_curl_data_mixed_literal_dynamic.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/166_curl_data_mixed_literal_dynamic.json @@ -6,11 +6,31 @@ "clauses": [ { "operator": "None", - "verb": ["curl"], + "verb": [ + "curl" + ], "args": [ - { "raw": "--data", "kind": "Literal", "isPath": false, "isFlag": true }, - { "raw": "'@$HOME'\".json\"", "kind": "Literal", "isPath": true, "isFlag": false, "resolved": "/work/$HOME.json" }, - { "raw": "https://example.invalid/api", "kind": "Literal", "isPath": false, "isFlag": false } + { + "raw": "--data", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": true + }, + { + "raw": "'@$HOME'\".json\"", + "kind": "Literal", + "isPath": true, + "resolved": "/work/$HOME.json", + "isFlag": false + }, + { + "raw": "https://example.invalid/api", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } ], "redirects": [], "elements": [ @@ -48,11 +68,70 @@ "isFlag": false, "isPath": false } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 55, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 55, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 55 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "--data=@$HOME.json" + ], + "pattern": null, + "coveringDirectory": null + } + } ], - "isSubshell": false, - "isCommandStringWrapped": false + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } } ] }, - "notes": "All fragments are literal, so the curl @ prefix is removed without reinterpreting the quoted $HOME text." + "notes": "All fragments are literal, so the curl @ prefix is removed without reinterpreting the quoted $HOME text. Promotes stable design case bash-static-mixed-quote-native-fragment with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/168_escaped_home_literal_path.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/168_escaped_home_literal_path.json index 11d7865..d1c2738 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/168_escaped_home_literal_path.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/168_escaped_home_literal_path.json @@ -3,12 +3,110 @@ "input": "cat \\$HOME", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["cat"], - "args": [{ "raw": "\\$HOME", "kind": "Literal", "isPath": true, "resolved": "/work/$HOME" }], - "redirects": [] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\\$HOME", + "kind": "Literal", + "isPath": true, + "resolved": "/work/$HOME", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\\$HOME", + "value": "$HOME", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 6, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/$HOME" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 10, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 10, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 10 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "$HOME" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] }, - "notes": "The escape removes variable-transform eligibility without erasing the authored path value." + "notes": "The escape removes variable-transform eligibility without erasing the authored path value. Promotes stable design case bash-escaped-home-literal-path with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/170_adjacent_escaped_redirect_target.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/170_adjacent_escaped_redirect_target.json index f973043..23efa8a 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/170_adjacent_escaped_redirect_target.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/170_adjacent_escaped_redirect_target.json @@ -3,12 +3,127 @@ "input": "echo ok > \\$HOME\".txt\"", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["echo", "ok"], - "args": [], - "redirects": [{ "direction": "Out", "target": "/work/$HOME.txt", "isDynamicSkip": false }] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "echo", + "ok" + ], + "args": [], + "redirects": [ + { + "direction": "Out", + "target": "/work/$HOME.txt", + "isDynamicSkip": false + } + ], + "elements": [ + { + "raw": "echo", + "value": "echo", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "ok", + "value": "ok", + "role": "Verb", + "sourceStart": 5, + "sourceLength": 2, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "> \\$HOME\".txt\"", + "value": "$HOME.txt", + "role": "Redirect", + "sourceStart": 8, + "sourceLength": 14, + "precedingVerbElementCount": 2, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/$HOME.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 22, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 22 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + }, + "redirects": [ + { + "redirectIndex": 0, + "sourceKind": "Default", + "sourceDescriptor": null, + "operation": "FileOutput", + "targetDescriptor": null, + "target": { + "kind": "Exact", + "values": [ + "/work/$HOME.txt" + ], + "pattern": null, + "coveringDirectory": null + }, + "isPathRelevant": true, + "isComplete": true + } + ] + } + ] }, - "notes": "The complete adjacent target run is aggregated before redirect resolution." + "notes": "The complete adjacent target run is aggregated before redirect resolution. Promotes stable design case bash-adjacent-redirect-target-fragments with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/171_runtime_special_parameter_path.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/171_runtime_special_parameter_path.json index 8615d92..c846915 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/171_runtime_special_parameter_path.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/171_runtime_special_parameter_path.json @@ -3,12 +3,107 @@ "input": "cat \"$?\"", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["cat"], - "args": [{ "raw": "\"$?\"", "kind": "DynamicSkip", "isPath": false, "resolved": "__NULL__" }], - "redirects": [] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"$?\"", + "kind": "DynamicSkip", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"$?\"", + "value": "$?", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] }, - "notes": "The parser retains the special-parameter identity but cannot prove its runtime value." + "notes": "The parser retains the special-parameter identity but cannot prove its runtime value. Promotes stable design case bash-runtime-special-parameter-path with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/172_quoted_wildcard_redirect.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/172_quoted_wildcard_redirect.json index 13a32b6..7c389c0 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/172_quoted_wildcard_redirect.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/172_quoted_wildcard_redirect.json @@ -3,12 +3,127 @@ "input": "echo ok > \"*.txt\"", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["echo", "ok"], - "args": [], - "redirects": [{ "direction": "Out", "target": "/work/*.txt", "isDynamicSkip": false }] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "echo", + "ok" + ], + "args": [], + "redirects": [ + { + "direction": "Out", + "target": "/work/*.txt", + "isDynamicSkip": false + } + ], + "elements": [ + { + "raw": "echo", + "value": "echo", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "ok", + "value": "ok", + "role": "Verb", + "sourceStart": 5, + "sourceLength": 2, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "> \"*.txt\"", + "value": "*.txt", + "role": "Redirect", + "sourceStart": 8, + "sourceLength": 9, + "precedingVerbElementCount": 2, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/*.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 17, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 17, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 17 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + }, + "redirects": [ + { + "redirectIndex": 0, + "sourceKind": "Default", + "sourceDescriptor": null, + "operation": "FileOutput", + "targetDescriptor": null, + "target": { + "kind": "Exact", + "values": [ + "/work/*.txt" + ], + "pattern": null, + "coveringDirectory": null + }, + "isPathRelevant": true, + "isComplete": true + } + ] + } + ] }, - "notes": "Quote provenance suppresses Bash pathname expansion for the redirect word." + "notes": "Quote provenance suppresses Bash pathname expansion for the redirect word. Promotes stable design case bash-quoted-wildcard-redirect with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/173_unquoted_wildcard_redirect.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/173_unquoted_wildcard_redirect.json index 4162ed8..d3cd03c 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/173_unquoted_wildcard_redirect.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/173_unquoted_wildcard_redirect.json @@ -3,12 +3,124 @@ "input": "echo ok > *.txt", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["echo", "ok"], - "args": [], - "redirects": [{ "direction": "Out", "target": "*.txt", "isDynamicSkip": true }] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "echo", + "ok" + ], + "args": [], + "redirects": [ + { + "direction": "Out", + "target": "*.txt", + "isDynamicSkip": true + } + ], + "elements": [ + { + "raw": "echo", + "value": "echo", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "ok", + "value": "ok", + "role": "Verb", + "sourceStart": 5, + "sourceLength": 2, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "> *.txt", + "value": "*.txt", + "role": "Redirect", + "sourceStart": 8, + "sourceLength": 7, + "precedingVerbElementCount": 2, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 15, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 15, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 15 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + }, + "redirects": [ + { + "redirectIndex": 0, + "sourceKind": "Default", + "sourceDescriptor": null, + "operation": "FileOutput", + "targetDescriptor": null, + "target": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + }, + "isPathRelevant": true, + "isComplete": false + } + ] + } + ] }, - "notes": "Without filesystem enumeration the parser cannot prove Bash produces exactly one redirect target." + "notes": "Without filesystem enumeration the parser cannot prove Bash produces exactly one redirect target. Promotes stable design case bash-unquoted-wildcard-redirect with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/183_escaped_open_brace_literal_path.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/183_escaped_open_brace_literal_path.json index 3b33209..032714d 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/183_escaped_open_brace_literal_path.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/183_escaped_open_brace_literal_path.json @@ -3,12 +3,110 @@ "input": "cat \"\\${HOME\"", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["cat"], - "args": [{ "raw": "\"\\${HOME\"", "kind": "Literal", "isPath": true, "resolved": "/work/${HOME" }], - "redirects": [] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"\\${HOME\"", + "kind": "Literal", + "isPath": true, + "resolved": "/work/${HOME", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"\\${HOME\"", + "value": "${HOME", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/${HOME" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 13, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 13, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 13 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "${HOME" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] }, - "notes": "Escaping the dollar prevents interpolation; the unmatched-looking brace is literal shell data." + "notes": "Escaping the dollar prevents interpolation; the unmatched-looking brace is literal shell data. Promotes stable design case bash-escaped-open-brace-literal-path with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/185_provider_looking_unquoted_path.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/185_provider_looking_unquoted_path.json index 72792ef..b49205a 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/185_provider_looking_unquoted_path.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/185_provider_looking_unquoted_path.json @@ -3,12 +3,110 @@ "input": "cat filesystem::/safe", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["cat"], - "args": [{ "raw": "filesystem::/safe", "kind": "Literal", "isPath": true, "resolved": "/work/filesystem::/safe" }], - "redirects": [] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "filesystem::/safe", + "kind": "Literal", + "isPath": true, + "resolved": "/work/filesystem::/safe", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "filesystem::/safe", + "value": "filesystem::/safe", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 17, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/filesystem::/safe" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 21, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 21, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 21 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "filesystem::/safe" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] }, - "notes": "Bash does not apply PowerShell provider semantics to authored argument text." + "notes": "Bash does not apply PowerShell provider semantics to authored argument text. Promotes stable design case bash-provider-looking-unquoted-path with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/186_provider_looking_quoted_path.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/186_provider_looking_quoted_path.json index d2f6336..ec049d6 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/186_provider_looking_quoted_path.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/186_provider_looking_quoted_path.json @@ -3,12 +3,110 @@ "input": "cat \"filesystem::/safe\"", "expected": { "isUnparseable": false, - "clauses": [{ - "operator": "None", - "verb": ["cat"], - "args": [{ "raw": "\"filesystem::/safe\"", "kind": "Literal", "isPath": true, "resolved": "/work/filesystem::/safe" }], - "redirects": [] - }] + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"filesystem::/safe\"", + "kind": "Literal", + "isPath": true, + "resolved": "/work/filesystem::/safe", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"filesystem::/safe\"", + "value": "filesystem::/safe", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 19, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/filesystem::/safe" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 23, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 23, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 23 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "filesystem::/safe" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] }, - "notes": "Quote removal does not introduce foreign provider semantics into Bash." + "notes": "Quote removal does not introduce foreign provider semantics into Bash. Promotes stable design case bash-provider-looking-quoted-path with complete v0.3 syntax, occurrence, value, and completeness assertions." } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json index 30215d8..c0a8108 100644 --- a/tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json @@ -58,7 +58,17 @@ { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 49 }, { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 29 } ], - "effectiveArguments": [], + "effectiveArguments": [ + { + "clauseElementIndex": 2, + "value": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + } + ], "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } } ] diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/281_v03_design_literal_substitution_spellings.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/281_v03_design_literal_substitution_spellings.json new file mode 100644 index 0000000..a22bc62 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/281_v03_design_literal_substitution_spellings.json @@ -0,0 +1,135 @@ +{ + "name": "v0.3 Bash design promotion: Quoted and escaped substitution-looking text does not execute", + "input": "printf '%s %s' '$(whoami)' \"\\$(id)\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "printf" + ], + "args": [ + { + "raw": "'%s %s'", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + }, + { + "raw": "'$(whoami)'", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + }, + { + "raw": "\"\\$(id)\"", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "printf", + "value": "printf", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 6, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "'%s %s'", + "value": "%s %s", + "role": "Argument", + "sourceStart": 7, + "sourceLength": 7, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "'$(whoami)'", + "value": "$(whoami)", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 11, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"\\$(id)\"", + "value": "$(id)", + "role": "Argument", + "sourceStart": 27, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 35, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 35, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 35 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-literal-substitution-spellings into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/282_v03_design_for_empty_iterable_preserves_cwd.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/282_v03_design_for_empty_iterable_preserves_cwd.json new file mode 100644 index 0000000..a509525 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/282_v03_design_for_empty_iterable_preserves_cwd.json @@ -0,0 +1,240 @@ +{ + "name": "v0.3 Bash design promotion: A proved empty loop has no state transition", + "input": "for f in; do cd /tmp; done; pwd", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cd" + ], + "args": [ + { + "raw": "/tmp", + "kind": "Literal", + "isPath": true, + "resolved": "/tmp", + "isFlag": false + }, + { + "raw": "", + "kind": "DynamicSkip", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "cd", + "value": "cd", + "role": "Verb", + "sourceStart": 13, + "sourceLength": 2, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "/tmp", + "value": "/tmp", + "role": "Argument", + "sourceStart": 16, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/tmp" + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "pwd" + ], + "args": [], + "redirects": [], + "elements": [ + { + "raw": "pwd", + "value": "pwd", + "role": "Verb", + "sourceStart": 28, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 31, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 31, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "ForEach", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 26, + "clauseIndex": null, + "groupKind": null, + "listOperator": "None", + "bindingName": "f", + "bindingRaw": "f", + "bindingSourceStart": 4, + "bindingSourceLength": 1, + "iterableRaw": "", + "iterableSourceStart": 8, + "iterableSourceLength": 0 + }, + { + "kind": "Block", + "parentIndex": 2, + "region": "Iterator", + "childIndex": null, + "sourceStart": 8, + "sourceLength": 0, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "Block", + "parentIndex": 2, + "region": "LoopBody", + "childIndex": null, + "sourceStart": 12, + "sourceLength": 10, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 13, + "sourceLength": 7, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 28, + "sourceLength": 3, + "clauseIndex": 1, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "LoopBody", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 31 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 31 + }, + { + "ancestorKind": "ForEach", + "region": "LoopBody", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 26 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 12, + "sourceLength": 10 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 31 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 31 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-for-empty-iterable-preserves-cwd into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/283_v03_design_decoded_wrapper_loop_binding.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/283_v03_design_decoded_wrapper_loop_binding.json new file mode 100644 index 0000000..462ab6c --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/283_v03_design_decoded_wrapper_loop_binding.json @@ -0,0 +1,218 @@ +{ + "name": "v0.3 Bash design promotion: A decoded child process does not receive an unexported loop binding", + "input": "for f in a; do bash -c 'printf \"%s\" \"$f\"'; done", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "printf" + ], + "args": [ + { + "raw": "\"%s\"", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + }, + { + "raw": "\"$f\"", + "kind": "EnvVar", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "printf", + "value": "printf", + "role": "Verb", + "sourceStart": null, + "sourceLength": null, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"%s\"", + "value": "%s", + "role": "Argument", + "sourceStart": null, + "sourceLength": null, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"$f\"", + "value": "$f", + "role": "Argument", + "sourceStart": null, + "sourceLength": null, + "precedingVerbElementCount": 1, + "kind": "EnvVar", + "isFlag": false, + "isPath": false + } + ], + "isCommandStringWrapped": true + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 47, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "ForEach", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 47, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "bindingName": "f", + "bindingRaw": "f", + "bindingSourceStart": 4, + "bindingSourceLength": 1, + "iterableRaw": "a", + "iterableSourceStart": 9, + "iterableSourceLength": 1 + }, + { + "kind": "Block", + "parentIndex": 1, + "region": "Iterator", + "childIndex": null, + "sourceStart": 9, + "sourceLength": 1, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "Block", + "parentIndex": 1, + "region": "LoopBody", + "childIndex": null, + "sourceStart": 14, + "sourceLength": 29, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "Group", + "parentIndex": 3, + "region": "Statement", + "childIndex": 0, + "sourceStart": 15, + "sourceLength": 26, + "clauseIndex": null, + "groupKind": "IsolatedScope", + "listOperator": null + }, + { + "kind": "Block", + "parentIndex": 4, + "region": "GroupBody", + "childIndex": null, + "sourceStart": null, + "sourceLength": null, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 5, + "region": "Statement", + "childIndex": 0, + "sourceStart": null, + "sourceLength": null, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "LoopBody", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 47 + }, + { + "ancestorKind": "ForEach", + "region": "LoopBody", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 47 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 14, + "sourceLength": 29 + }, + { + "ancestorKind": "Group", + "region": "GroupBody", + "childIndex": null, + "sourceStart": 15, + "sourceLength": 26 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": null, + "sourceLength": null + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 2, + "value": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-decoded-wrapper-does-not-inherit-loop-binding into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/284_v03_design_subshell_inherits_loop_binding.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/284_v03_design_subshell_inherits_loop_binding.json new file mode 100644 index 0000000..8cdc6a8 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/284_v03_design_subshell_inherits_loop_binding.json @@ -0,0 +1,220 @@ +{ + "name": "v0.3 Bash design promotion: A parenthesized subshell inherits shell bindings while isolating exit state", + "input": "for f in a; do (printf \"%s\" \"$f\"); done", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "printf" + ], + "args": [ + { + "raw": "\"%s\"", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + }, + { + "raw": "\"$f\"", + "kind": "EnvVar", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "printf", + "value": "printf", + "role": "Verb", + "sourceStart": 16, + "sourceLength": 6, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"%s\"", + "value": "%s", + "role": "Argument", + "sourceStart": 23, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"$f\"", + "value": "$f", + "role": "Argument", + "sourceStart": 28, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "EnvVar", + "isFlag": false, + "isPath": false + } + ], + "isSubshell": true + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 39, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "ForEach", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 39, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "bindingName": "f", + "bindingRaw": "f", + "bindingSourceStart": 4, + "bindingSourceLength": 1, + "iterableRaw": "a", + "iterableSourceStart": 9, + "iterableSourceLength": 1 + }, + { + "kind": "Block", + "parentIndex": 1, + "region": "Iterator", + "childIndex": null, + "sourceStart": 9, + "sourceLength": 1, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "Block", + "parentIndex": 1, + "region": "LoopBody", + "childIndex": null, + "sourceStart": 14, + "sourceLength": 21, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "Group", + "parentIndex": 3, + "region": "Statement", + "childIndex": 0, + "sourceStart": 15, + "sourceLength": 18, + "clauseIndex": null, + "groupKind": "IsolatedScope", + "listOperator": null + }, + { + "kind": "Block", + "parentIndex": 4, + "region": "GroupBody", + "childIndex": null, + "sourceStart": 16, + "sourceLength": 16, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 5, + "region": "Statement", + "childIndex": 0, + "sourceStart": 16, + "sourceLength": 16, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "LoopBody", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 39 + }, + { + "ancestorKind": "ForEach", + "region": "LoopBody", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 39 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 14, + "sourceLength": 21 + }, + { + "ancestorKind": "Group", + "region": "GroupBody", + "childIndex": null, + "sourceStart": 15, + "sourceLength": 18 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 16, + "sourceLength": 16 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 2, + "value": { + "kind": "Exact", + "values": [ + "a" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-parenthesized-subshell-inherits-loop-binding into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/285_v03_design_contextual_for_argument.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/285_v03_design_contextual_for_argument.json new file mode 100644 index 0000000..048d5b4 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/285_v03_design_contextual_for_argument.json @@ -0,0 +1,99 @@ +{ + "name": "v0.3 Bash design promotion: Control keyword outside command position", + "input": "echo for", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "echo" + ], + "args": [ + { + "raw": "for", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "echo", + "value": "echo", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "for", + "value": "for", + "role": "Argument", + "sourceStart": 5, + "sourceLength": 3, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-contextual-for-argument into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/286_v03_design_here_string_literal_data.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/286_v03_design_here_string_literal_data.json new file mode 100644 index 0000000..cbe40c1 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/286_v03_design_here_string_literal_data.json @@ -0,0 +1,116 @@ +{ + "name": "v0.3 Bash design promotion: Static here-string data does not force a raw-command approval prompt", + "input": "cat <<< \"hello\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [], + "redirects": [ + { + "direction": "In", + "target": "hello", + "isDynamicSkip": false + } + ], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "<<< \"hello\"", + "value": "hello", + "role": "Redirect", + "sourceStart": 4, + "sourceLength": 11, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 15, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 15, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 15 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + }, + "redirects": [ + { + "redirectIndex": 0, + "sourceKind": "Default", + "sourceDescriptor": null, + "operation": "HereString", + "targetDescriptor": null, + "target": { + "kind": "Exact", + "values": [ + "hello\n" + ], + "pattern": null, + "coveringDirectory": null + }, + "isPathRelevant": false, + "isComplete": true + } + ] + } + ] + }, + "notes": "Promotes stable design case bash-here-string-literal-data into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/287_v03_design_here_string_dynamic_data.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/287_v03_design_here_string_dynamic_data.json new file mode 100644 index 0000000..add2ef2 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/287_v03_design_here_string_dynamic_data.json @@ -0,0 +1,114 @@ +{ + "name": "v0.3 Bash design promotion: Unknown stdin data does not make an otherwise complete redirect structurally incomplete", + "input": "cat <<< \"$value\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [], + "redirects": [ + { + "direction": "In", + "target": "\"$value\"", + "isDynamicSkip": true + } + ], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "<<< \"$value\"", + "value": "$value", + "role": "Redirect", + "sourceStart": 4, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "EnvVar", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 16, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 16, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 16 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + }, + "redirects": [ + { + "redirectIndex": 0, + "sourceKind": "Default", + "sourceDescriptor": null, + "operation": "HereString", + "targetDescriptor": null, + "target": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + }, + "isPathRelevant": false, + "isComplete": true + } + ] + } + ] + }, + "notes": "Promotes stable design case bash-here-string-dynamic-data into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/288_v03_design_escaped_home_adjacent_fragment.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/288_v03_design_escaped_home_adjacent_fragment.json new file mode 100644 index 0000000..f015b4e --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/288_v03_design_escaped_home_adjacent_fragment.json @@ -0,0 +1,137 @@ +{ + "name": "v0.3 Bash design promotion: Escaped inline prefix composes with a quoted suffix", + "input": "curl --data=@\\$HOME\".json\" https://example.invalid/api", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "curl" + ], + "args": [ + { + "raw": "--data", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": true + }, + { + "raw": "@\\$HOME\".json\"", + "kind": "Literal", + "isPath": true, + "resolved": "/work/$HOME.json", + "isFlag": false + }, + { + "raw": "https://example.invalid/api", + "kind": "Literal", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "curl", + "value": "curl", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "--data=@\\$HOME\".json\"", + "value": "--data=@$HOME.json", + "role": "Argument", + "sourceStart": 5, + "sourceLength": 21, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": true, + "resolved": "/work/$HOME.json" + }, + { + "raw": "https://example.invalid/api", + "value": "https://example.invalid/api", + "role": "Argument", + "sourceStart": 27, + "sourceLength": 27, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 54, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 54, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 54 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "--data=@$HOME.json" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-escaped-home-adjacent-native-fragment into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/289_v03_design_double_quoted_escaped_home.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/289_v03_design_double_quoted_escaped_home.json new file mode 100644 index 0000000..6acbfe6 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/289_v03_design_double_quoted_escaped_home.json @@ -0,0 +1,112 @@ +{ + "name": "v0.3 Bash design promotion: Escape provenance survives within one double-quoted token", + "input": "cat \"\\$HOME.txt\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"\\$HOME.txt\"", + "kind": "Literal", + "isPath": true, + "resolved": "/work/$HOME.txt", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"\\$HOME.txt\"", + "value": "$HOME.txt", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/work/$HOME.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 16, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 16, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 16 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "$HOME.txt" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-double-quoted-escaped-home-path into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/290_v03_design_double_quoted_literal_expandable.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/290_v03_design_double_quoted_literal_expandable.json new file mode 100644 index 0000000..dfae576 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/290_v03_design_double_quoted_literal_expandable.json @@ -0,0 +1,111 @@ +{ + "name": "v0.3 Bash design promotion: Literal and expandable regions compose within one token", + "input": "echo \"\\${HOME}-$HOME\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "echo" + ], + "args": [ + { + "raw": "\"\\${HOME}-$HOME\"", + "kind": "Tilde", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "echo", + "value": "echo", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 4, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"\\${HOME}-$HOME\"", + "value": "${HOME}-$HOME", + "role": "Argument", + "sourceStart": 5, + "sourceLength": 16, + "precedingVerbElementCount": 1, + "kind": "Tilde", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 21, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 21, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 21 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Exact", + "values": [ + "${HOME}-/home/test" + ], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-double-quoted-literal-expandable-composition into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/291_v03_design_runtime_positional_parameter.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/291_v03_design_runtime_positional_parameter.json new file mode 100644 index 0000000..8f3e4b7 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/291_v03_design_runtime_positional_parameter.json @@ -0,0 +1,109 @@ +{ + "name": "v0.3 Bash design promotion: Runtime positional parameters cannot become literal paths", + "input": "cat \"$1\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"$1\"", + "kind": "DynamicSkip", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"$1\"", + "value": "$1", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-runtime-positional-parameter-path into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/292_v03_design_runtime_argument_vector.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/292_v03_design_runtime_argument_vector.json new file mode 100644 index 0000000..97a4408 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/292_v03_design_runtime_argument_vector.json @@ -0,0 +1,109 @@ +{ + "name": "v0.3 Bash design promotion: Quoted dollar-at can produce zero or multiple arguments", + "input": "cat \"$@\"", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "cat" + ], + "args": [ + { + "raw": "\"$@\"", + "kind": "DynamicSkip", + "isPath": false, + "resolved": "__NULL__", + "isFlag": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "cat", + "value": "cat", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 3, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "\"$@\"", + "value": "$@", + "role": "Argument", + "sourceStart": 4, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8, + "clauseIndex": 0, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 8 + } + ], + "effectiveArguments": [ + { + "clauseElementIndex": 1, + "value": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + } + ], + "workingDirectory": { + "kind": "Exact", + "values": [ + "/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Promotes stable design case bash-runtime-argument-vector-boundary into the executable corpus with complete compatibility, syntax, occurrence, value, and completeness assertions." +} diff --git a/tests/ShellSyntaxTree.Tests/DesignCorpus/v0.3/bash.json b/tests/ShellSyntaxTree.Tests/DesignCorpus/v0.3/bash.json index 8a2356a..cbcc3fd 100644 --- a/tests/ShellSyntaxTree.Tests/DesignCorpus/v0.3/bash.json +++ b/tests/ShellSyntaxTree.Tests/DesignCorpus/v0.3/bash.json @@ -1441,12 +1441,12 @@ "authoredVerb": "echo", "immediateRole": "Ordinary", "ancestry": ["root"], - "isComplete": true, + "isComplete": false, "redirects": [{ "operation": "FileOutput", "target": { "sourceElement": "*.txt", "kind": "Unknown", "isPolicySensitive": true }, "isPathRelevant": true, - "isComplete": true + "isComplete": false }] }], "compatibilityClause": { diff --git a/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs index ee1d030..1e88248 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs @@ -448,7 +448,9 @@ public void Iterator_substitution_precedes_body_and_does_not_see_new_binding() Assert.False(result.IsUnparseable, result.UnparseableReason); Assert.Equal(new[] { "printf", "rm" }, result.Commands.Select(CommandVerb)); Assert.Equal(CommandOccurrenceRole.Substitution, result.Commands[0].ImmediateRole); - Assert.Empty(result.Commands[0].EffectiveArguments); + Assert.Equal( + ShellValueDomainKind.Unknown, + Assert.Single(result.Commands[0].EffectiveArguments).Value.Kind); Assert.Equal(CommandOccurrenceRole.LoopBody, result.Commands[1].ImmediateRole); Assert.Equal( ShellValueDomainKind.Unknown, @@ -934,7 +936,9 @@ public void Static_bash_c_does_not_inherit_unexported_loop_binding() var result = Parse("for f in a b; do bash -c 'echo \"$f\"'; done"); Assert.False(result.IsUnparseable, result.UnparseableReason); - Assert.Empty(Assert.Single(result.Commands).EffectiveArguments); + Assert.Equal( + ShellValueDomainKind.Unknown, + Assert.Single(Assert.Single(result.Commands).EffectiveArguments).Value.Kind); } [Fact] diff --git a/tests/ShellSyntaxTree.Tests/Parsing/BashStructuralProjectionTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/BashStructuralProjectionTests.cs index dac9315..b47f277 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/BashStructuralProjectionTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/BashStructuralProjectionTests.cs @@ -1559,6 +1559,119 @@ public void Ordinary_variable_value_does_not_make_structure_incomplete() Assert.True(Assert.Single(result.Commands).IsComplete); } + [Fact] + public void Policy_relevant_static_path_publishes_exact_effective_value() + { + var command = Assert.Single(Parse("cat \\$HOME").Commands); + + var effective = Assert.Single(command.EffectiveArguments); + Assert.Equal(1, effective.ClauseElementIndex); + Assert.Equal(ShellValueDomainKind.Exact, effective.Value.Kind); + Assert.Equal("$HOME", Assert.Single(effective.Value.Values)); + } + + [Fact] + public void Runtime_parameter_publishes_unknown_effective_value() + { + var command = Assert.Single(Parse("cat \"$?\"").Commands); + + var effective = Assert.Single(command.EffectiveArguments); + Assert.Equal(1, effective.ClauseElementIndex); + Assert.Equal(ShellValueDomainKind.Unknown, effective.Value.Kind); + Assert.True(command.IsComplete); + } + + [Fact] + public void Plain_literal_non_path_argument_does_not_add_effective_overlay() + { + var command = Assert.Single(Parse("printf '%s' value").Commands); + + Assert.Empty(command.EffectiveArguments); + } + + [Theory] + [InlineData("cat ~/x")] + [InlineData("cat ~\\\n/x")] + [InlineData("cat ~\\\r\n/x")] + [InlineData("cat ~/\\x")] + [InlineData("cat ~/\"x\"")] + public void Proved_home_path_does_not_publish_unknown_overlay(string source) + { + var result = Parse(source); + var argument = Assert.Single(Assert.Single(result.Clauses).Args); + var command = Assert.Single(result.Commands); + + Assert.Equal(ArgKind.Tilde, argument.Kind); + Assert.Equal("/home/test/x", argument.Resolved); + var effective = Assert.Single(command.EffectiveArguments); + Assert.Equal(ShellValueDomainKind.Exact, effective.Value.Kind); + Assert.Equal("/home/test/x", Assert.Single(effective.Value.Values)); + } + + [Theory] + [InlineData("cat ~\\/x", "~/x", "/work/~/x")] + [InlineData("cat ~\"/x\"", "~/x", "/work/~/x")] + [InlineData("cat ~'/x'", "~/x", "/work/~/x")] + [InlineData("cat ~''", "~", "/work/~")] + [InlineData("cat ~''/x", "~/x", "/work/~/x")] + [InlineData("cat ~\"x\"", "~x", "/work/~x")] + [InlineData("cat ~'x'", "~x", "/work/~x")] + [InlineData("cat ~\\x", "~x", "/work/~x")] + [InlineData("cat ~\"root\"/x", "~root/x", "/work/~root/x")] + [InlineData("cat ~\\\n\"x\"", "~x", "/work/~x")] + public void Quoted_or_escaped_tilde_prefix_remains_literal( + string source, + string effectiveValue, + string resolvedPath) + { + var result = Parse(source); + var argument = Assert.Single(Assert.Single(result.Clauses).Args); + var command = Assert.Single(result.Commands); + + Assert.Equal(ArgKind.Literal, argument.Kind); + Assert.Equal(resolvedPath, argument.Resolved); + var effective = Assert.Single(command.EffectiveArguments); + Assert.Equal(ShellValueDomainKind.Exact, effective.Value.Kind); + Assert.Equal(effectiveValue, Assert.Single(effective.Value.Values)); + } + + [Theory] + [InlineData("cat ~root/x")] + [InlineData("cat ~\\\nroot/x")] + public void Unquoted_named_tilde_prefix_remains_unknown(string source) + { + var result = Parse(source); + var argument = Assert.Single(Assert.Single(result.Clauses).Args); + var effective = Assert.Single(Assert.Single(result.Commands).EffectiveArguments); + + Assert.Equal(ArgKind.DynamicSkip, argument.Kind); + Assert.Null(argument.Resolved); + Assert.Equal(ShellValueDomainKind.Unknown, effective.Value.Kind); + } + + [Theory] + [InlineData("/", "~", "/")] + [InlineData("/", "~/x", "//x")] + [InlineData("/home/test/", "~", "/home/test/")] + [InlineData("/home/test/", "~/x", "/home/test//x")] + public void Effective_tilde_value_preserves_configured_home_bytes( + string homeDirectory, + string authoredValue, + string effectiveValue) + { + var parser = new BashParser(new BashParserOptions + { + HomeDirectory = homeDirectory, + WorkingDirectory = "/work", + InitialStateMode = BashInitialStateMode.IsolatedNonInteractive, + }); + var command = Assert.Single(parser.Parse("cat " + authoredValue).Commands); + + var effective = Assert.Single(command.EffectiveArguments); + Assert.Equal(ShellValueDomainKind.Exact, effective.Value.Kind); + Assert.Equal(effectiveValue, Assert.Single(effective.Value.Values)); + } + private static ParsedCommand Parse(string input) { var parser = new BashParser(new BashParserOptions diff --git a/tools/PwshCorpusTool/Program.cs b/tools/PwshCorpusTool/Program.cs index 0b18c1c..9644ef2 100644 --- a/tools/PwshCorpusTool/Program.cs +++ b/tools/PwshCorpusTool/Program.cs @@ -147,7 +147,7 @@ int CheckBash(string command) parsed, "ad-hoc check", outOfScope: false, - includeElements: false, + includeElements: true, includeStructure: true, includeOptionalAssertions: true, includeV03Assertions: true));