From a5637dec0a532b22edca09abf7e1db0d307080c0 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Fri, 7 Aug 2026 11:47:10 +0000 Subject: [PATCH] Complete Bash loop coverage matrix --- IMPLEMENTATION_PLAN.md | 11 ++- .../v0-3-structured-shell-analysis/tasks.md | 10 ++- .../bash/251_v03_for_multiline_redirect.json | 68 ++++++++++++++++++ .../bash/252_v03_for_binding_redirect.json | 44 ++++++++++++ ...3_v03_for_indirect_expansion_rejected.json | 9 +++ ...4_v03_for_parameter_operator_rejected.json | 9 +++ .../bash/255_v03_for_body_substitution.json | 67 +++++++++++++++++ ...56_v03_for_transition_budget_rejected.json | 9 +++ .../Parsing/BashForInStructuralTests.cs | 58 +++++++++++++++ .../Parsing/ShellValueOracleTests.cs | 72 +++++++++++++++++++ 10 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/251_v03_for_multiline_redirect.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/252_v03_for_binding_redirect.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/253_v03_for_indirect_expansion_rejected.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/254_v03_for_parameter_operator_rejected.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/bash/256_v03_for_transition_budget_rejected.json diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 5df59a7..d519315 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -295,9 +295,14 @@ priorities. operands, recursive exact `command` / `builtin` dispatch, physical-path compatibility sanitation, and post-loop binding mutation are pinned by unit tests, native Bash oracles, the design corpus, and executable corpus. - All unmodeled mutations, dynamic dispatch, and control transfers remain - fail closed. Next add the Netclaw approval matrix before calling the Bash - consumer integration complete. + The coverage matrix now also pins empty and multiline loops, mixed + separators, pipelines, nested loops, wrapper scope, static and + binding-derived redirects, substitutions, option-shaped and unquoted + values, indirect and parameter-operator rejection, and every candidate + and transition cap. All unmodeled mutations, dynamic dispatch, control + transfers, and occurrence-specific redirect values remain fail closed. + Next add the Netclaw approval matrix before calling the Bash consumer + integration complete. - [ ] Complete PowerShell `$()` discovery in `foreach` expressions and add the Netclaw approval-matrix cases. The simple-command slice is delivered for ordinary, adjacent, quoted, here-string, redirect, standalone, diff --git a/openspec/changes/v0-3-structured-shell-analysis/tasks.md b/openspec/changes/v0-3-structured-shell-analysis/tasks.md index daa7dc9..2ee58f8 100644 --- a/openspec/changes/v0-3-structured-shell-analysis/tasks.md +++ b/openspec/changes/v0-3-structured-shell-analysis/tasks.md @@ -105,8 +105,14 @@ physical-path sanitation, and persistent post-loop binding protection are implemented. It still rejects every unmodeled shell-state mutation, control transfer, nested active-binding reuse, and dynamic dispatch. -- [ ] 6.6 Cover empty iterables, separators, multiline bodies, redirects, pipelines, nested loops, and wrapper boundaries. -- [ ] 6.7 Add adversarial cases for option injection, mutation, unquoted expansion, indirect expansion, substitutions, and cap overflow. +- [x] 6.6 Cover empty iterables, separators, multiline bodies, redirects, pipelines, nested loops, and wrapper boundaries. + - Direct tests, executable corpus, and native Bash oracles cover each + interaction. Redirect-bearing occurrences remain incomplete until the + separately tracked explicit redirect-analysis slice lands. +- [x] 6.7 Add adversarial cases for option injection, mutation, unquoted expansion, indirect expansion, substitutions, and cap overflow. + - The executable corpus includes indirect and parameter-operator rejection, + loop-body substitution, and atomic transition-budget overflow; native + oracles pin the shell semantics behind the conservative boundaries. - [ ] 6.8 Add sanitized Bash corpus entries and Netclaw allow/prompt/deny integration cases. ## 7. PowerShell Foreach Vertical Slice diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/251_v03_for_multiline_redirect.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/251_v03_for_multiline_redirect.json new file mode 100644 index 0000000..7cc4d63 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/251_v03_for_multiline_redirect.json @@ -0,0 +1,68 @@ +{ + "name": "v0.3 Bash multiline for-in body with redirect", + "input": "for f in a b\ndo\nprintf '%s\\n' \"$f\" > out.txt\necho done\ndone", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": ["printf"], + "args": [ + { "raw": "'%s\\n'", "kind": "Literal", "isPath": true, "resolved": "/work/%s/n", "isFlag": false }, + { "raw": "\"$f\"", "kind": "EnvVar", "isPath": false, "resolved": "__NULL__", "isFlag": false } + ], + "redirects": [ + { "direction": "Out", "target": "/work/out.txt", "isDynamicSkip": false } + ] + }, + { + "operator": "Sequence", + "verb": ["echo"], + "args": [ + { "raw": "done", "kind": "Literal", "isPath": false, "resolved": "__NULL__", "isFlag": false } + ], + "redirects": [] + } + ], + "syntax": [ + { "kind": "Block", "parentIndex": null, "region": "Unknown", "childIndex": null, "sourceStart": 0, "sourceLength": 59, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "ForEach", "parentIndex": 0, "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 59, "clauseIndex": null, "groupKind": null, "listOperator": null, "bindingName": "f", "bindingRaw": "f", "bindingSourceStart": 4, "bindingSourceLength": 1, "iterableRaw": "a b", "iterableSourceStart": 9, "iterableSourceLength": 3 }, + { "kind": "Block", "parentIndex": 1, "region": "Iterator", "childIndex": null, "sourceStart": 9, "sourceLength": 3, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "Block", "parentIndex": 1, "region": "LoopBody", "childIndex": null, "sourceStart": 15, "sourceLength": 40, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "CommandList", "parentIndex": 3, "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 38, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "SimpleCommand", "parentIndex": 4, "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 28, "clauseIndex": 0, "groupKind": null, "listOperator": "None" }, + { "kind": "SimpleCommand", "parentIndex": 4, "region": "Statement", "childIndex": 1, "sourceStart": 45, "sourceLength": 9, "clauseIndex": 1, "groupKind": null, "listOperator": "Sequence" } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "LoopBody", + "isComplete": false, + "ancestry": [ + { "ancestorKind": "Block", "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 59 }, + { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 59 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 15, "sourceLength": 40 }, + { "ancestorKind": "CommandList", "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 38 } + ], + "effectiveArguments": [ + { "clauseElementIndex": 2, "value": { "kind": "FiniteSet", "values": ["a", "b"], "pattern": null, "coveringDirectory": null } } + ], + "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } + }, + { + "clauseIndex": 1, + "immediateRole": "LoopBody", + "isComplete": true, + "ancestry": [ + { "ancestorKind": "Block", "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 59 }, + { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 59 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 15, "sourceLength": 40 }, + { "ancestorKind": "CommandList", "region": "Statement", "childIndex": 1, "sourceStart": 16, "sourceLength": 38 } + ], + "effectiveArguments": [], + "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } + } + ] + }, + "notes": "Pins newline list terminators and separators, a multiline loop body, finite loop binding analysis, and the compatibility redirect boundary. Redirect-bearing occurrences remain incomplete until explicit redirect analysis lands." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/252_v03_for_binding_redirect.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/252_v03_for_binding_redirect.json new file mode 100644 index 0000000..96b3d79 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/252_v03_for_binding_redirect.json @@ -0,0 +1,44 @@ +{ + "name": "v0.3 Bash loop binding in redirect target", + "input": "for f in a b; do printf '%s' \"$f\" > \"$f.out\"; 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": [ + { "direction": "Out", "target": "\"$f.out\"", "isDynamicSkip": true } + ] + } + ], + "syntax": [ + { "kind": "Block", "parentIndex": null, "region": "Unknown", "childIndex": null, "sourceStart": 0, "sourceLength": 50, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "ForEach", "parentIndex": 0, "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 50, "clauseIndex": null, "groupKind": null, "listOperator": null, "bindingName": "f", "bindingRaw": "f", "bindingSourceStart": 4, "bindingSourceLength": 1, "iterableRaw": "a b", "iterableSourceStart": 9, "iterableSourceLength": 3 }, + { "kind": "Block", "parentIndex": 1, "region": "Iterator", "childIndex": null, "sourceStart": 9, "sourceLength": 3, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "Block", "parentIndex": 1, "region": "LoopBody", "childIndex": null, "sourceStart": 16, "sourceLength": 30, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "SimpleCommand", "parentIndex": 3, "region": "Statement", "childIndex": 0, "sourceStart": 17, "sourceLength": 27, "clauseIndex": 0, "groupKind": null, "listOperator": null } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "LoopBody", + "isComplete": false, + "ancestry": [ + { "ancestorKind": "Block", "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 50 }, + { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 50 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 30 } + ], + "effectiveArguments": [ + { "clauseElementIndex": 2, "value": { "kind": "FiniteSet", "values": ["a", "b"], "pattern": null, "coveringDirectory": null } } + ], + "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } + } + ] + }, + "notes": "The argument binding is finite, but the compatibility redirect remains dynamic and the occurrence incomplete until explicit redirect analysis can publish occurrence-specific redirect values." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/253_v03_for_indirect_expansion_rejected.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/253_v03_for_indirect_expansion_rejected.json new file mode 100644 index 0000000..813a2d2 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/253_v03_for_indirect_expansion_rejected.json @@ -0,0 +1,9 @@ +{ + "name": "v0.3 Bash for-in indirect expansion rejected", + "input": "for f in name; do echo \"${!f}\"; done", + "expected": { + "isUnparseable": true, + "unparseableReasonContains": "complex parameter expansion is not supported in v0.3" + }, + "notes": "Indirect expansion can select ambient variable state, so the bounded loop analyzer rejects the entire executable region rather than guessing an effective value." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/254_v03_for_parameter_operator_rejected.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/254_v03_for_parameter_operator_rejected.json new file mode 100644 index 0000000..2d1704f --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/254_v03_for_parameter_operator_rejected.json @@ -0,0 +1,9 @@ +{ + "name": "v0.3 Bash for-in parameter operator rejected", + "input": "for f in value; do echo \"${f:-fallback}\"; done", + "expected": { + "isUnparseable": true, + "unparseableReasonContains": "complex parameter expansion is not supported in v0.3" + }, + "notes": "Ordinary parameter operators remain outside the bounded value grammar and fail atomically even when their apparent fallback text is static." +} 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 new file mode 100644 index 0000000..30215d8 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/255_v03_for_body_substitution.json @@ -0,0 +1,67 @@ +{ + "name": "v0.3 Bash for-in body substitution inherits binding", + "input": "for f in a b; do printf '%s' \"$(echo \"$f\")\"; done", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": ["echo"], + "args": [ + { "raw": "\"$f\"", "kind": "EnvVar", "isPath": false, "resolved": "__NULL__", "isFlag": false } + ], + "redirects": [] + }, + { + "operator": "None", + "verb": ["printf"], + "args": [ + { "raw": "'%s'", "kind": "Literal", "isPath": false, "resolved": "__NULL__", "isFlag": false }, + { "raw": "\"$(echo \"$f\")\"", "kind": "DynamicSkip", "isPath": false, "resolved": "__NULL__", "isFlag": false } + ], + "redirects": [] + } + ], + "syntax": [ + { "kind": "Block", "parentIndex": null, "region": "Unknown", "childIndex": null, "sourceStart": 0, "sourceLength": 49, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "ForEach", "parentIndex": 0, "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 49, "clauseIndex": null, "groupKind": null, "listOperator": null, "bindingName": "f", "bindingRaw": "f", "bindingSourceStart": 4, "bindingSourceLength": 1, "iterableRaw": "a b", "iterableSourceStart": 9, "iterableSourceLength": 3 }, + { "kind": "Block", "parentIndex": 1, "region": "Iterator", "childIndex": null, "sourceStart": 9, "sourceLength": 3, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "Block", "parentIndex": 1, "region": "LoopBody", "childIndex": null, "sourceStart": 16, "sourceLength": 29, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "SimpleCommand", "parentIndex": 3, "region": "Statement", "childIndex": 0, "sourceStart": 17, "sourceLength": 26, "clauseIndex": 1, "groupKind": null, "listOperator": null }, + { "kind": "CommandSubstitution", "parentIndex": 4, "region": "Substitution", "childIndex": 0, "sourceStart": 30, "sourceLength": 12, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "Block", "parentIndex": 5, "region": "Substitution", "childIndex": 0, "sourceStart": 32, "sourceLength": 9, "clauseIndex": null, "groupKind": null, "listOperator": null }, + { "kind": "SimpleCommand", "parentIndex": 6, "region": "Statement", "childIndex": 0, "sourceStart": 32, "sourceLength": 9, "clauseIndex": 0, "groupKind": null, "listOperator": null } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Substitution", + "isComplete": true, + "ancestry": [ + { "ancestorKind": "Block", "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 49 }, + { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 49 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 29 }, + { "ancestorKind": "CommandSubstitution", "region": "Substitution", "childIndex": 0, "sourceStart": 30, "sourceLength": 12 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 32, "sourceLength": 9 } + ], + "effectiveArguments": [ + { "clauseElementIndex": 1, "value": { "kind": "FiniteSet", "values": ["a", "b"], "pattern": null, "coveringDirectory": null } } + ], + "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } + }, + { + "clauseIndex": 1, + "immediateRole": "LoopBody", + "isComplete": true, + "ancestry": [ + { "ancestorKind": "Block", "region": "Root", "childIndex": 0, "sourceStart": 0, "sourceLength": 49 }, + { "ancestorKind": "ForEach", "region": "LoopBody", "childIndex": null, "sourceStart": 0, "sourceLength": 49 }, + { "ancestorKind": "Block", "region": "Statement", "childIndex": 0, "sourceStart": 16, "sourceLength": 29 } + ], + "effectiveArguments": [], + "workingDirectory": { "kind": "Exact", "values": ["/work"], "pattern": null, "coveringDirectory": null } + } + ] + }, + "notes": "The substitution command is visible in authored order, inherits the finite loop binding, and leaves the outer produced argument policy-sensitive." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/bash/256_v03_for_transition_budget_rejected.json b/tests/ShellSyntaxTree.Tests/Corpus/bash/256_v03_for_transition_budget_rejected.json new file mode 100644 index 0000000..adbeb86 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/bash/256_v03_for_transition_budget_rejected.json @@ -0,0 +1,9 @@ +{ + "name": "v0.3 Bash nested for-in transition budget overflow", + "input": "for a in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32; do for b in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32; do for c in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32; do :; done; done; done", + "expected": { + "isUnparseable": true, + "unparseableReasonContains": "Bash structural syntax exceeded limits or contained invalid parser-owned facts" + }, + "notes": "The concrete nested iteration product exceeds the 4096-transition analysis budget, so the result fails atomically with no partial projections." +} diff --git a/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs index a598070..f564553 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/BashForInStructuralTests.cs @@ -752,6 +752,51 @@ public void Pipelines_compose_with_loop_ancestry_without_synthetic_operators() frame.Region == CommandAncestryRegion.LoopBody); } + [Fact] + public void Newline_separators_and_redirects_compose_inside_a_multiline_loop_body() + { + const string source = """ + for f in a b + do + printf '%s\n' "$f" > out.txt + echo done + done + """; + + var result = Parse(source); + + Assert.False(result.IsUnparseable, result.UnparseableReason); + Assert.Equal(new[] { CompoundOperator.None, CompoundOperator.Sequence }, + result.Clauses.Select(clause => clause.Operator)); + var redirect = Assert.Single(result.Clauses[0].Redirects); + Assert.Equal(RedirectDirection.Out, redirect.Direction); + Assert.Equal("/work/out.txt", redirect.Target); + Assert.False(redirect.IsDynamicSkip); + Assert.False(result.Commands[0].IsComplete); + Assert.True(result.Commands[1].IsComplete); + Assert.All(result.Commands, command => + Assert.Equal(CommandOccurrenceRole.LoopBody, command.ImmediateRole)); + AssertDomain( + Assert.Single(result.Commands[0].EffectiveArguments).Value, + ShellValueDomainKind.FiniteSet, + "a", + "b"); + } + + [Fact] + public void Loop_binding_in_redirect_target_stays_policy_sensitive() + { + var result = Parse( + "for f in a b; do printf '%s' \"$f\" > \"$f.out\"; done"); + + Assert.False(result.IsUnparseable, result.UnparseableReason); + var command = Assert.Single(result.Commands); + var redirect = Assert.Single(command.Clause.Redirects); + Assert.Equal("\"$f.out\"", redirect.Target); + Assert.True(redirect.IsDynamicSkip); + Assert.False(command.IsComplete); + } + [Theory] [InlineData("for f in a; do unset f; done")] [InlineData("for f in a; do read f; done")] @@ -826,6 +871,19 @@ public void Hidden_execution_in_parameter_operator_fails_atomically() Assert.Empty(result.Clauses); } + [Theory] + [InlineData("for f in name; do echo \"${!f}\"; done")] + [InlineData("for f in value; do echo \"${f:-fallback}\"; done")] + public void Complex_parameter_expansion_in_loop_body_fails_atomically(string source) + { + var result = Parse(source); + + Assert.True(result.IsUnparseable); + Assert.Empty(result.Commands); + Assert.Empty(result.Clauses); + Assert.Contains("complex parameter expansion", result.UnparseableReason!); + } + [Fact] public void Contextual_words_remain_ordinary_arguments_outside_command_position() { diff --git a/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs index f82b8fd..a275d22 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs @@ -637,6 +637,78 @@ public void Bash_for_in_runtime_oracle_matches_word_formation_and_empty_values() Assert.Equal(new[] { "", "<>", "", "" }, Lines(output)); } + [Fact] + public void Bash_for_in_runtime_oracle_matches_multiline_control_and_nested_data_flow() + { + if (!IsNativeBashAvailable()) + { + return; + } + + var output = Run( + "bash", + "--noprofile", + "--norc", + "-c", + "root=$(mktemp -d); trap 'rm -rf \"$root\"' EXIT; " + + "count=0; for f in; do count=1; done; printf 'empty=<%s>\\n' \"$count\"; " + + "for f in a b\ndo\nprintf '%s\\n' \"$f\" > \"$root/$f.out\"\n" + + "printf 'pipe=<%s>\\n' \"$f\" | tr '[:lower:]' '[:upper:]'\ndone; " + + "cat \"$root/a.out\" \"$root/b.out\"; " + + "for d in x y; do for f in 1 2; do printf 'nested=<%s%s>\\n' \"$d\" \"$f\"; done; done"); + + Assert.Equal( + new[] + { + "empty=<0>", + "PIPE=", + "PIPE=", + "a", + "b", + "nested=", + "nested=", + "nested=", + "nested=", + }, + Lines(output)); + } + + [Fact] + public void Bash_for_in_runtime_oracle_matches_scope_expansion_and_option_values() + { + if (!IsNativeBashAvailable()) + { + return; + } + + var output = Run( + "bash", + "--noprofile", + "--norc", + "-c", + "unset f; secret=value; " + + "for f in 'a b'; do printf 'split=<%s>\\n' $f; done; " + + "for f in -rf safe; do printf 'argv=<%s>\\n' \"$f\"; done; " + + "for f in secret; do printf 'indirect=<%s>\\n' \"${!f}\"; " + + "printf 'substitution=<%s>\\n' \"$(printf '%s' \"$f\")\"; " + + "bash --noprofile --norc -c 'printf \"child=<%s>\\n\" \"$f\"'; " + + "printf 'parent=<%s>\\n' \"$f\"; done"); + + Assert.Equal( + new[] + { + "split=", + "split=", + "argv=<-rf>", + "argv=", + "indirect=", + "substitution=", + "child=<>", + "parent=", + }, + Lines(output)); + } + [Theory] [InlineData("for")] [InlineData("in")]