From fb6673b2f126e8e440f67586d62ce32f4f044011 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Sat, 8 Aug 2026 05:29:12 +0000 Subject: [PATCH] Analyze remote Invoke-Command regions --- IMPLEMENTATION_PLAN.md | 10 +- SPEC.POWERSHELL.md | 16 +- .../v0-3-structured-shell-analysis/design.md | 12 + .../specs/bounded-shell-analysis/spec.md | 35 +- .../Pwsh/Parsing/PwshForEachValueAnalysis.cs | 73 ++ .../PwshExecutionRegionBindingCatalog.cs | 442 +++++++++- ...e_command_remote_single_target_region.json | 369 ++++++++ ..._v03_invoke_command_remote_job_region.json | 385 ++++++++ ...ke_command_remote_disconnected_region.json | 385 ++++++++ ...nvoke_command_remote_multiple_targets.json | 385 ++++++++ ...ke_command_remote_inline_target_array.json | 374 ++++++++ ...invoke_command_remote_dynamic_session.json | 369 ++++++++ ..._invoke_command_remote_state_isolated.json | 827 ++++++++++++++++++ ...ke_command_remote_nested_comma_scalar.json | 369 ++++++++ ...e_command_remote_escaped_comma_scalar.json | 369 ++++++++ ...ommand_remote_positional_target_array.json | 369 ++++++++ .../PwshExecutionRegionBindingCatalogTests.cs | 138 ++- .../PwshExecutionRegionStructuralTests.cs | 87 +- .../Parsing/ShellValueOracleTests.cs | 85 ++ tools/PwshCorpusTool/CorpusManifest.cs | 30 + 20 files changed, 5077 insertions(+), 52 deletions(-) create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/384_v03_invoke_command_remote_single_target_region.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/385_v03_invoke_command_remote_job_region.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/386_v03_invoke_command_remote_disconnected_region.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/387_v03_invoke_command_remote_multiple_targets.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/388_v03_invoke_command_remote_inline_target_array.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/389_v03_invoke_command_remote_dynamic_session.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/390_v03_invoke_command_remote_state_isolated.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/391_v03_invoke_command_remote_nested_comma_scalar.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/392_v03_invoke_command_remote_escaped_comma_scalar.json create mode 100644 tests/ShellSyntaxTree.Tests/Corpus/powershell/393_v03_invoke_command_remote_positional_target_array.json diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index c0545d2..28411b7 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -350,7 +350,11 @@ priorities. delivered for ordinary, adjacent, quoted, here-string, redirect, standalone, call-operator, dynamic-identity, and host-wrapper positions, with current-scope state propagation and bounded expression rejection pinned - by the 383-entry executable corpus. + by the 393-entry executable corpus. Remote/session/SSH/VM/container + `Invoke-Command` now starts from arbitrary child state, isolates all exit + effects, publishes synchronous/once only for one proved target, and + publishes concurrent timing for multiple targets or enabled asynchronous + switches while keeping dynamic cardinality fail closed. - [ ] Deliver typed PowerShell script-block execution regions before calling tasks 7.5-7.7 complete. The corrected contract adds an execution-region syntax node with independent origin, phase, timing, and cardinality rather than a @@ -443,8 +447,8 @@ priorities. mutator such as `Set-Alias` being rebound to `Set-Item` before a later authored `Set-Alias Env:...` invocation. The generator-owned executable corpus now supports per-entry initial- - state mode and includes the promoted Parallel design case. Pinned child - runspace jobs and remote/session invocation remain before deferred + state mode and includes the promoted Parallel and remote/session cases. + Pinned child runspace jobs remain before deferred breakpoint/event/completion actions; then unknown receiver and nested/adversarial matrices. Preserve script blocks proved to be data as opaque values, expose ambiguous bodies with incomplete diff --git a/SPEC.POWERSHELL.md b/SPEC.POWERSHELL.md index f59906d..45b2220 100644 --- a/SPEC.POWERSHELL.md +++ b/SPEC.POWERSHELL.md @@ -532,7 +532,7 @@ The version-pinned PowerShell 7 catalog covers: | `ForEach-Object -Parallel` | Process | Concurrent | OncePerInputObject | child runspace; runspace-local exit isolated; process-wide effects conservative | | `Where-Object -FilterScript` | Filter | Synchronous | OncePerInputObject | current runspace | | in-process `Invoke-Command -ScriptBlock` | Main | Synchronous | Once | child scope unless `-NoNewScope`; shared location | -| remote/session/SSH/VM/container `Invoke-Command` | Main | proved from complete parameter set | Unknown unless targets are proved | remote/child state; exit isolated | +| remote/session/SSH/VM/container `Invoke-Command` | Main | Synchronous for one proved target; Concurrent for multiple targets or enabled `-AsJob` / `-InDisconnectedSession`; otherwise Unknown | Once for one proved target; otherwise Unknown | arbitrary remote state; exit isolated | | `Measure-Command -Expression`, `Trace-Command -Expression` | Main | Synchronous | Once | current scope and location | | `Start-Job -InitializationScript` | Initialization | Concurrent | Once | child process before Main | | `Start-Job -ScriptBlock` | Main | Concurrent | Once | child process; exit isolated | @@ -540,6 +540,20 @@ The version-pinned PowerShell 7 catalog covers: | `Set-PSBreakpoint -Action`, event `-Action` | Action | Deferred | ZeroOrMore | trigger-time state Unknown without proof | | `Register-ArgumentCompleter -ScriptBlock` | Completion | Deferred | ZeroOrMore | completion-time state Unknown without proof | +Remote `Invoke-Command` bodies begin with Unknown working directory, bindings, +aliases, functions, modules, profiles, and command resolution. Local parser +state is not an inheritance proof for a remote host or persistent session, and +remote exit state never flows into the invoking host continuation. A complete +literal, quoted, URI, GUID, or hashtable target proves one activation. A +complete top-level comma-separated target list, whether named, inline, or +positional, proves concurrent scheduling but maps to public cardinality +`Unknown`, because the public enum intentionally has no once-per-target value. +Quoted, backtick-escaped, or nested commas remain part of one target value. A +dynamic target or session collection leaves both +timing and cardinality `Unknown` unless an enabled `-AsJob` or +`-InDisconnectedSession` independently proves concurrent scheduling. Explicit +`:$false` switch values do not prove concurrency. + Parallel child runspaces do not inherit ordinary caller variables or aliases, and their runspace-local variable and location exit state does not flow into the host continuation. They do share process-wide state such as the environment diff --git a/openspec/changes/v0-3-structured-shell-analysis/design.md b/openspec/changes/v0-3-structured-shell-analysis/design.md index 32a7527..30685a4 100644 --- a/openspec/changes/v0-3-structured-shell-analysis/design.md +++ b/openspec/changes/v0-3-structured-shell-analysis/design.md @@ -372,6 +372,7 @@ PowerShell 7.6.4 probes demonstrate that these are independent dimensions: | `ForEach-Object { ... }` | shared current runspace | shared | synchronous / once per input | | `Where-Object { ... }` | shared current runspace | shared | synchronous / once per input | | local `Invoke-Command { ... }` | child scope unless `-NoNewScope` | shared | synchronous / once | +| remote/session/SSH/VM/container `Invoke-Command { ... }` | arbitrary remote state; no local binding proof | arbitrary remote location; exit isolated | one target synchronous/once; multiple or async concurrent; otherwise unknown | | `Measure-Command { ... }` / `Trace-Command -Expression { ... }` | shared current scope | shared | synchronous / once | | `Start-Job { ... }` | child process state | inherited initial location; exit isolated | concurrent / once | | `ForEach-Object -Parallel { ... }` | child runspace state; process-wide effects may escape | inherited initial location; runspace-local exit isolated | concurrent / once per input | @@ -399,6 +400,17 @@ wildcards, and set overflow collapse to all unproved command names. This prevents both an alias to `Set-Item` and a rebound `Set-Alias` mutator from hiding an environment-provider write without tainting unrelated names. +Remote `Invoke-Command` does not reuse local cwd, binding, alias, function, +module, profile, or command-resolution facts. The remote body is always +authorization-visible but incomplete under that arbitrary initial state, and +its exit state never affects the invoking host continuation. A complete single +target proves synchronous/once execution. A complete top-level multiple-target +list, an enabled `-AsJob`, or an enabled `-InDisconnectedSession` proves +concurrent execution; quoted, escaped, or nested commas remain scalar, and +multiple or dynamic targets retain cardinality `Unknown`. A dynamic +target also retains timing `Unknown` unless one of those enabled switches proves +concurrency. An explicit false switch value does not change scheduling facts. + `SimpleCommandSyntax.ExecutionRegions` preserves authored script-block order. The analyzer separately applies semantic phase order. PowerShell's binder can assign Begin, Process, and End roles to multiple `ForEach-Object` blocks even 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 c9990b0..26db4c6 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 @@ -653,14 +653,45 @@ execute synchronously in the current scope. In-process `Invoke-Command` without `-NoNewScope` isolates ordinary assignment while sharing location; `-NoNewScope` shares supported state. The in-process parameter set does not support `-AsJob` and is always synchronous/once. Remote/session/SSH/VM/container -targets, multiple targets, and remote `-AsJob` SHALL retain only facts proved -from the complete parameter set. +`Invoke-Command` SHALL start from Unknown remote cwd, bindings, aliases, +functions, modules, profiles, and command resolution, and SHALL NOT flow remote +exit state into the invoking host. One complete proved target SHALL publish +Synchronous timing and Once cardinality. Multiple complete proved targets SHALL +publish Concurrent timing and Unknown cardinality. Named, inline, and positional +target arrays SHALL bind consistently. Only a top-level unescaped comma SHALL +separate targets; quoted, backtick-escaped, and structurally nested commas SHALL +remain part of one target. An enabled remote `-AsJob` +or `-InDisconnectedSession` SHALL publish Concurrent timing independently of +target cardinality. A dynamic target or session collection SHALL otherwise +retain Unknown timing and Unknown cardinality. Explicit false-valued switches +SHALL NOT prove concurrency. #### Scenario: In-process Invoke-Command does not invent AsJob semantics - **WHEN** PowerShell parses `Invoke-Command -ScriptBlock { Get-Date }` - **THEN** the region is synchronous and activates once - **THEN** analysis does not model `-AsJob` as an in-process option +#### Scenario: One remote target has an isolated synchronous region +- **WHEN** isolated-mode PowerShell parses `Invoke-Command -ComputerName server -ScriptBlock { Get-Item child.txt }; Get-Item host.txt` +- **THEN** the region timing is Synchronous and its cardinality is Once +- **THEN** the body working directory and mutable state are Unknown and incomplete +- **THEN** the following host command retains its exact local state + +#### Scenario: Remote asynchronous switches prove only concurrency +- **WHEN** PowerShell parses a remote invocation with enabled `-AsJob` or `-InDisconnectedSession` +- **THEN** the execution region timing is Concurrent +- **THEN** target cardinality is proved independently +- **THEN** an explicit `:$false` value does not change synchronous single-target scheduling + +#### Scenario: Multiple and dynamic remote targets remain bounded +- **WHEN** PowerShell parses a complete two-computer target list +- **THEN** the execution region timing is Concurrent and cardinality is Unknown +- **WHEN** the comma is instead quoted, backtick-escaped, or nested in one hashtable +- **THEN** it does not prove multiple targets +- **WHEN** PowerShell instead parses a runtime `-Session $session` target +- **THEN** timing and cardinality are Unknown unless an enabled asynchronous switch independently proves Concurrent timing +- **THEN** every remote body command remains visible and incomplete + #### Scenario: Direct invocation origin survives without source text - **WHEN** a consumer receives direct call and dot-source execution-region nodes - **THEN** their origins are `DirectCall` and `DotSource` respectively diff --git a/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs b/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs index 05c4686..7093423 100644 --- a/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs +++ b/src/ShellSyntaxTree/Internal/Pwsh/Parsing/PwshForEachValueAnalysis.cs @@ -2018,6 +2018,14 @@ flow.OnFailure is AnalysisContext failure flow); } + if (binding.ParameterSet == PwshExecutionRegionParameterSet.InvokeRemote) + { + return AnalyzeRemoteInvokeCommand( + regions, + receiverInput, + flow); + } + if (binding.ParameterSet == PwshExecutionRegionParameterSet.NewModuleScriptBlock) { var bodyInput = PwshPersistentStateMutation.HasVariableWritingArgument( @@ -2288,6 +2296,37 @@ private PwshFlowResult AnalyzeInProcessInvokeCommand( : fallback; } + private PwshFlowResult AnalyzeRemoteInvokeCommand( + IReadOnlyList regions, + AnalysisContext receiverInput, + PwshFlowResult hostFlow) + { + var executionRegionEffectCount = _executionRegionEffectCount; + var nonRegionStateMutationCount = _nonRegionStateMutationCount; + var locationStateMutationCount = _locationStateMutationCount; + var childScopeEscapeRiskCount = _childScopeEscapeRiskCount; + var childRunspaceProcessEscapeRiskCount = + _childRunspaceProcessEscapeRiskCount; + try + { + TryAnalyzeRegionSequence( + regions, + receiverInput.CreateRemoteInput(), + out _); + } + finally + { + _executionRegionEffectCount = executionRegionEffectCount + regions.Count; + _nonRegionStateMutationCount = nonRegionStateMutationCount; + _locationStateMutationCount = locationStateMutationCount; + _childScopeEscapeRiskCount = childScopeEscapeRiskCount; + _childRunspaceProcessEscapeRiskCount = + childRunspaceProcessEscapeRiskCount; + } + + return hostFlow; + } + private PwshFlowResult AnalyzePipelineCallbackRegions( PwshExecutionRegionBindingResult binding, IReadOnlyList regions, @@ -2521,6 +2560,8 @@ private static bool IsSupportedExecutionRegionReceiver( AllBindingsAreCompleteWithTiming( binding.Bindings, ExecutionRegionTiming.Concurrent), + PwshExecutionRegionParameterSet.InvokeRemote => + AllBindingsAreComplete(binding.Bindings), PwshExecutionRegionParameterSet.MeasureExpression or PwshExecutionRegionParameterSet.TraceExpression or PwshExecutionRegionParameterSet.InvokeInProcess or @@ -2534,6 +2575,25 @@ PwshExecutionRegionParameterSet.ForEachScriptBlock or }; } + private static bool AllBindingsAreComplete( + IReadOnlyList bindings) + { + if (bindings.Count == 0) + { + return false; + } + + for (var index = 0; index < bindings.Count; index++) + { + if (!bindings[index].IsComplete) + { + return false; + } + } + + return true; + } + private static bool AllBindingsAreCompleteWithTiming( IReadOnlyList bindings, ExecutionRegionTiming timing) @@ -4380,6 +4440,19 @@ internal AnalysisContext CreateChildProcessInput() => ProcessWideStateInvalidated, Array.Empty()); + internal AnalysisContext CreateRemoteInput() => + // The target host or persistent session can have arbitrary cwd, + // variables, aliases, functions, modules, and profiles. Its exit + // state is isolated from the invoking host. + new( + workingDirectory: null, + canPromote: false, + commandResolutionInvalidated: true, + allRunspaceCommandResolutionMayReachProcessMutation: false, + Array.Empty(), + processWideStateInvalidated: false, + Array.Empty()); + internal AnalysisContext CreateChildRunspaceInput() => // Caller aliases and ordinary variables do not initialize a Parallel // child runspace; explicit $using: values remain conservatively unknown. diff --git a/src/ShellSyntaxTree/Internal/Pwsh/Verbs/PwshExecutionRegionBindingCatalog.cs b/src/ShellSyntaxTree/Internal/Pwsh/Verbs/PwshExecutionRegionBindingCatalog.cs index 295d63e..8dc8c2d 100644 --- a/src/ShellSyntaxTree/Internal/Pwsh/Verbs/PwshExecutionRegionBindingCatalog.cs +++ b/src/ShellSyntaxTree/Internal/Pwsh/Verbs/PwshExecutionRegionBindingCatalog.cs @@ -553,14 +553,24 @@ private static PwshExecutionRegionParameterSet BindInvokeCommand( var set = remote ? PwshExecutionRegionParameterSet.InvokeRemote : PwshExecutionRegionParameterSet.InvokeInProcess; + var remoteTargetCardinality = remote + ? GetRemoteTargetCardinality(arguments, compatibleSets) + : RemoteTargetCardinality.Unknown; + var timing = remote + ? GetRemoteTiming(arguments, remoteTargetCardinality) + : ExecutionRegionTiming.Synchronous; + var cardinality = remoteTargetCardinality == RemoteTargetCardinality.Single + ? ExecutionRegionCardinality.Once + : remote + ? ExecutionRegionCardinality.Unknown + : ExecutionRegionCardinality.Once; var named = AddNamed( arguments, bindings, "ScriptBlock", ExecutionRegionPhase.Main, - remote ? ExecutionRegionTiming.Unknown : ExecutionRegionTiming.Synchronous, - remote ? ExecutionRegionCardinality.Unknown : ExecutionRegionCardinality.Once, - isComplete: !remote); + timing, + cardinality); if (named > 0) { return set; @@ -575,14 +585,230 @@ private static PwshExecutionRegionParameterSet BindInvokeCommand( boundBlock.ElementIndex, "ScriptBlock", ExecutionRegionPhase.Main, - remote ? ExecutionRegionTiming.Unknown : ExecutionRegionTiming.Synchronous, - remote ? ExecutionRegionCardinality.Unknown : ExecutionRegionCardinality.Once, - isComplete: !remote)); + timing, + cardinality)); } return set; } + private static ExecutionRegionTiming GetRemoteTiming( + BoundArguments arguments, + RemoteTargetCardinality targetCardinality) + { + if (arguments.IsSwitchEnabled("AsJob") || + arguments.IsSwitchEnabled("InDisconnectedSession") || + targetCardinality == RemoteTargetCardinality.Multiple) + { + return ExecutionRegionTiming.Concurrent; + } + + return targetCardinality == RemoteTargetCardinality.Single + ? ExecutionRegionTiming.Synchronous + : ExecutionRegionTiming.Unknown; + } + + private static RemoteTargetCardinality GetRemoteTargetCardinality( + BoundArguments arguments, + IReadOnlyList compatibleSets) + { + string? targetParameter = null; + ParameterSetDefinition? selectedSet = null; + foreach (var compatibleSet in compatibleSets) + { + var candidate = RemoteTargetParameterFor(compatibleSet.Name); + if (candidate is null || + targetParameter is not null && !targetParameter.Equals( + candidate, + StringComparison.OrdinalIgnoreCase)) + { + return RemoteTargetCardinality.Unknown; + } + + targetParameter = candidate; + selectedSet = compatibleSet; + } + + if (targetParameter is null || selectedSet is null) + { + return RemoteTargetCardinality.Unknown; + } + + var targets = arguments.ArgumentsBoundTo(targetParameter, selectedSet); + if (targets.Count == 0) + { + return RemoteTargetCardinality.Unknown; + } + + if (targets.Count > 1 || targets.Any(TargetHasMultipleValues)) + { + return RemoteTargetCardinality.Multiple; + } + + return IsProvedScalarTarget(targets[0], targetParameter) + ? RemoteTargetCardinality.Single + : RemoteTargetCardinality.Unknown; + } + + private static bool TargetHasMultipleValues(BoundArgument target) + { + if (target.HasTrailingComma) + { + return true; + } + + var raw = target.RawValue.Trim(); + if (raw.Length == 0 || IsQuotedScalar(raw)) + { + return false; + } + + if (raw.StartsWith("@(", StringComparison.Ordinal) && + raw.EndsWith(")", StringComparison.Ordinal)) + { + raw = raw.Substring(2, raw.Length - 3); + } + + return ContainsTopLevelComma(raw); + } + + private static bool ContainsTopLevelComma(string raw) + { + var parenthesisDepth = 0; + var braceDepth = 0; + var bracketDepth = 0; + var quote = '\0'; + for (var index = 0; index < raw.Length; index++) + { + var current = raw[index]; + if (quote != '\0') + { + if (quote == '\'' && current == '\'' && + index + 1 < raw.Length && raw[index + 1] == '\'') + { + index++; + continue; + } + + if (quote == '"' && current == '`' && index + 1 < raw.Length) + { + index++; + continue; + } + + if (current == quote) + { + quote = '\0'; + } + + continue; + } + + if (current == '`' && index + 1 < raw.Length) + { + index++; + continue; + } + + if (current is '\'' or '"') + { + quote = current; + continue; + } + + switch (current) + { + case '(': + parenthesisDepth++; + break; + case ')': + if (parenthesisDepth > 0) + { + parenthesisDepth--; + } + + break; + case '{': + braceDepth++; + break; + case '}': + if (braceDepth > 0) + { + braceDepth--; + } + + break; + case '[': + bracketDepth++; + break; + case ']': + if (bracketDepth > 0) + { + bracketDepth--; + } + + break; + case ',' when parenthesisDepth == 0 && + braceDepth == 0 && bracketDepth == 0: + return true; + } + } + + return false; + } + + private static bool IsProvedScalarTarget( + BoundArgument target, + string targetParameter) + { + if (target.HasTrailingComma || TargetHasMultipleValues(target)) + { + return false; + } + + var raw = target.RawValue.Trim(); + if (IsQuotedScalar(raw)) + { + return true; + } + + if (targetParameter.Equals("SSHConnection", StringComparison.OrdinalIgnoreCase)) + { + return raw.StartsWith("@{", StringComparison.Ordinal) && + raw.EndsWith("}", StringComparison.Ordinal); + } + + if (targetParameter.Equals("ConnectionUri", StringComparison.OrdinalIgnoreCase) || + targetParameter.Equals("VMId", StringComparison.OrdinalIgnoreCase)) + { + // Compatible parameter-set selection already proved the scalar + // URI or GUID conversion; their punctuation can be DynamicSkip + // in the general shell argument classifier. + return true; + } + + return target.Kind is ArgKind.Literal or ArgKind.Glob or ArgKind.Tilde; + } + + private static bool IsQuotedScalar(string raw) => + raw.Length >= 2 && + raw[0] is '\'' or '"' && + raw[raw.Length - 1] == raw[0]; + + private static string? RemoteTargetParameterFor(string parameterSetName) => + parameterSetName switch + { + "Session" => "Session", + "ComputerName" => "ComputerName", + "Uri" => "ConnectionUri", + "VMId" => "VMId", + "VMName" => "VMName", + "SSHHost" => "HostName", + "ContainerId" => "ContainerId", + "SSHHostHashParam" => "SSHConnection", + _ => null, + }; + private static PwshExecutionRegionParameterSet BindTraceCommand( BoundArguments arguments, List bindings) @@ -869,6 +1095,8 @@ private static BoundArguments BindArguments( result.NamedArguments.Add(new BoundArgument( index, -1, true, resolution.CanonicalName, HasTrailingComma(element), parameter.InlineValue!, + InlineRawValue(element.Raw), + element.Kind, element.Value.Length - parameter.InlineValue!.Length)); if (HasTrailingComma(element) && !AcceptsScriptBlockArray(resolution.CanonicalName!)) @@ -881,10 +1109,28 @@ private static BoundArguments BindArguments( if (parameter.HasInlineValue) { + var hasTrailingComma = HasTrailingComma(element); result.NamedArguments.Add(new BoundArgument( - index, -1, false, resolution.CanonicalName, false, + index, -1, false, resolution.CanonicalName, hasTrailingComma, parameter.InlineValue!, + InlineRawValue(element.Raw), + element.Kind, element.Value.Length - parameter.InlineValue!.Length)); + if (hasTrailingComma && + AcceptsArgumentArray(resolution.CanonicalName!)) + { + ConsumeNamedArrayContinuation( + elements, + index, + resolution.CanonicalName!, + consumed, + result); + } + else if (hasTrailingComma) + { + result.HasInvalidScalarScriptBlockArray = true; + } + continue; } @@ -909,9 +1155,21 @@ private static BoundArguments BindArguments( result.NamedArguments.Add(new BoundArgument( valueIndex, -1, IsScriptBlock(elements[valueIndex]), resolution.CanonicalName, HasTrailingComma(elements[valueIndex]), - elements[valueIndex].Value)); - if (HasTrailingComma(elements[valueIndex]) - && !AcceptsScriptBlockArray(resolution.CanonicalName!)) + elements[valueIndex].Value, + elements[valueIndex].Raw, + elements[valueIndex].Kind)); + if (HasTrailingComma(elements[valueIndex]) && + AcceptsArgumentArray(resolution.CanonicalName!)) + { + ConsumeNamedArrayContinuation( + elements, + valueIndex, + resolution.CanonicalName!, + consumed, + result); + } + else if (HasTrailingComma(elements[valueIndex]) && + !AcceptsScriptBlockArray(resolution.CanonicalName!)) { result.HasInvalidScalarScriptBlockArray = true; } @@ -921,13 +1179,45 @@ private static BoundArguments BindArguments( result.PositionalArguments.Add(new BoundArgument( index, positionalIndex, IsScriptBlock(element), null, - HasTrailingComma(element), element.Value)); + HasTrailingComma(element), element.Value, element.Raw, element.Kind)); positionalIndex++; } return result; } + private static void ConsumeNamedArrayContinuation( + IReadOnlyList elements, + int firstValueIndex, + string parameterName, + HashSet consumed, + BoundArguments result) + { + var valueIndex = firstValueIndex; + while (HasTrailingComma(elements[valueIndex])) + { + if (!TryFindNextArgument(elements, valueIndex + 1, out var nextIndex) || + elements[nextIndex].IsFlag) + { + result.HasAmbiguousScriptBlockBinding = true; + return; + } + + consumed.Add(nextIndex); + var next = elements[nextIndex]; + result.NamedArguments.Add(new BoundArgument( + nextIndex, + -1, + IsScriptBlock(next), + parameterName, + HasTrailingComma(next), + next.Value, + next.Raw, + next.Kind)); + valueIndex = nextIndex; + } + } + private static bool TryFindNextArgument( IReadOnlyList elements, int start, @@ -972,6 +1262,22 @@ private static bool AcceptsScriptBlockArray(string parameterName) => string.Equals(parameterName, "Process", StringComparison.OrdinalIgnoreCase) || string.Equals(parameterName, "RemainingScripts", StringComparison.OrdinalIgnoreCase); + private static bool AcceptsArgumentArray(string parameterName) => + parameterName.Equals("ComputerName", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("ConnectionUri", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("Session", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("VMId", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("VMName", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("HostName", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("ContainerId", StringComparison.OrdinalIgnoreCase) || + parameterName.Equals("SSHConnection", StringComparison.OrdinalIgnoreCase); + + private static string InlineRawValue(string raw) + { + var separator = raw.IndexOf(':'); + return separator < 0 ? raw : raw.Substring(separator + 1); + } + private static bool HasTrailingComma(ClauseElement element) => element.Raw.TrimEnd().EndsWith(",", StringComparison.Ordinal); @@ -1027,36 +1333,42 @@ private static bool CanConvert( string parameterName) { var valueKind = ValueKindFor(parameterName); + var value = argument.HasTrailingComma + ? argument.Value.TrimEnd().TrimEnd(',') + : argument.Value; switch (valueKind) { case PwshParameterValueKind.String: return !argument.IsScriptBlock - && IsStringLiteral(parameterName, argument.Value); + && IsStringLiteral(parameterName, value); case PwshParameterValueKind.Object: return !string.Equals( - argument.Value, + value, "$null", StringComparison.OrdinalIgnoreCase); case PwshParameterValueKind.ScriptBlock: return argument.IsScriptBlock; case PwshParameterValueKind.Switch: - return IsBooleanLiteral(parameterName, argument.Value); + return IsBooleanLiteral(parameterName, value); case PwshParameterValueKind.Int32: - return IsInt32Literal(parameterName, argument.Value); + return IsInt32Literal(parameterName, value); case PwshParameterValueKind.Enum: - return IsEnumLiteral(parameterName, argument.Value); + return IsEnumLiteral(parameterName, value); case PwshParameterValueKind.Uri: - return Uri.TryCreate(argument.Value, UriKind.Absolute, out _); + return Uri.TryCreate(value, UriKind.Absolute, out _); case PwshParameterValueKind.Guid: - return System.Guid.TryParse(argument.Value, out _); + return System.Guid.TryParse(value, out _); case PwshParameterValueKind.Version: - return string.Equals(argument.Value, "5.1", StringComparison.Ordinal); + return string.Equals(value, "5.1", StringComparison.Ordinal); case PwshParameterValueKind.Hashtable: - return argument.Value.StartsWith("@{", StringComparison.Ordinal) - && argument.Value.EndsWith("}", StringComparison.Ordinal) - && argument.Value.Substring(2, argument.Value.Length - 3) + return value.StartsWith("@{", StringComparison.Ordinal) + && value.EndsWith("}", StringComparison.Ordinal) + && value.Substring(2, value.Length - 3) .Trim().Length > 0; case PwshParameterValueKind.RuntimeObject: + return !argument.IsScriptBlock && + argument.Kind is ArgKind.EnvVar or ArgKind.DynamicSkip && + !string.Equals(value, "$null", StringComparison.OrdinalIgnoreCase); default: return false; } @@ -1459,45 +1771,69 @@ private static IReadOnlyList InvokeParameterSets() => ne Set("Session", "Session,ThrottleLimit,AsJob,HideComputerName,JobName,ScriptBlock,RemoteDebug," + "InputObject,ArgumentList", - "ScriptBlock", new[] { Pos("Session"), Pos("ScriptBlock", true) }, + "ScriptBlock", new[] + { + Pos("Session", acceptsCommaContinuation: true), + Pos("ScriptBlock", true), + }, selectionRequiredAny: "Session", remote: true), Set("FilePathRunspace", "Session,ThrottleLimit,AsJob,HideComputerName,JobName,FilePath,RemoteDebug," + "InputObject,ArgumentList", - "FilePath", new[] { Pos("Session"), Pos("FilePath") }, + "FilePath", new[] + { + Pos("Session", acceptsCommaContinuation: true), Pos("FilePath"), + }, selectionRequiredAny: "Session", remote: true), Set("ComputerName", "ComputerName,Credential,Port,UseSSL,ConfigurationName,ApplicationName," + "ThrottleLimit,AsJob,InDisconnectedSession,SessionName,HideComputerName," + "JobName,ScriptBlock,SessionOption,Authentication,EnableNetworkAccess," + "RemoteDebug,InputObject,ArgumentList,CertificateThumbprint", - "ScriptBlock", new[] { Pos("ComputerName"), Pos("ScriptBlock", true) }, + "ScriptBlock", new[] + { + Pos("ComputerName", acceptsCommaContinuation: true), + Pos("ScriptBlock", true), + }, selectionRequiredAny: "ComputerName", remote: true), Set("FilePathComputerName", "ComputerName,Credential,Port,UseSSL,ConfigurationName,ApplicationName," + "ThrottleLimit,AsJob,InDisconnectedSession,SessionName,HideComputerName," + "JobName,FilePath,SessionOption,Authentication,EnableNetworkAccess,RemoteDebug," + "InputObject,ArgumentList", - "FilePath", new[] { Pos("ComputerName"), Pos("FilePath") }, + "FilePath", new[] + { + Pos("ComputerName", acceptsCommaContinuation: true), Pos("FilePath"), + }, selectionRequiredAny: "ComputerName", remote: true), Set("Uri", "Credential,ConfigurationName,ThrottleLimit,ConnectionUri,AsJob," + "InDisconnectedSession,HideComputerName,JobName,ScriptBlock,AllowRedirection," + "SessionOption,Authentication,EnableNetworkAccess,RemoteDebug,InputObject," + "ArgumentList,CertificateThumbprint", - "ScriptBlock", new[] { Pos("ConnectionUri"), Pos("ScriptBlock", true) }, + "ScriptBlock", new[] + { + Pos("ConnectionUri", acceptsCommaContinuation: true), + Pos("ScriptBlock", true), + }, selectionRequiredAny: "ConnectionUri", remote: true), Set("FilePathUri", "Credential,ConfigurationName,ThrottleLimit,ConnectionUri,AsJob," + "InDisconnectedSession,HideComputerName,JobName,FilePath,AllowRedirection," + "SessionOption,Authentication,EnableNetworkAccess,RemoteDebug,InputObject," + "ArgumentList", - "FilePath", new[] { Pos("ConnectionUri"), Pos("FilePath") }, + "FilePath", new[] + { + Pos("ConnectionUri", acceptsCommaContinuation: true), Pos("FilePath"), + }, selectionRequiredAny: "ConnectionUri", remote: true), Set("VMId", "Credential,ConfigurationName,ThrottleLimit,AsJob,HideComputerName,ScriptBlock," + "RemoteDebug,InputObject,ArgumentList,VMId", - "Credential,ScriptBlock,VMId", new[] { Pos("VMId"), Pos("ScriptBlock", true) }, + "Credential,ScriptBlock,VMId", new[] + { + Pos("VMId", acceptsCommaContinuation: true), Pos("ScriptBlock", true), + }, remote: true), Set("VMName", "Credential,ConfigurationName,ThrottleLimit,AsJob,HideComputerName,ScriptBlock," + @@ -1519,7 +1855,10 @@ private static IReadOnlyList InvokeParameterSets() => ne Set("FilePathVMId", "Credential,ConfigurationName,ThrottleLimit,AsJob,HideComputerName,FilePath," + "RemoteDebug,InputObject,ArgumentList,VMId", - "Credential,FilePath,VMId", new[] { Pos("VMId"), Pos("FilePath") }, remote: true), + "Credential,FilePath,VMId", new[] + { + Pos("VMId", acceptsCommaContinuation: true), Pos("FilePath"), + }, remote: true), Set("FilePathVMName", "Credential,ConfigurationName,ThrottleLimit,AsJob,HideComputerName,FilePath," + "RemoteDebug,InputObject,ArgumentList,VMName", @@ -1553,7 +1892,9 @@ private static ParameterSetDefinition Set( private static PositionalParameter Pos( string name, bool acceptsScriptBlock = false, - bool multiple = false) => new(name, acceptsScriptBlock, multiple); + bool multiple = false, + bool acceptsCommaContinuation = false) => + new(name, acceptsScriptBlock, multiple, acceptsCommaContinuation); private readonly record struct ParsedParameter( string Name, @@ -1570,6 +1911,8 @@ private readonly record struct BoundArgument( string? ParameterName, bool HasTrailingComma, string Value, + string RawValue, + ArgKind Kind, int ValueOffset = 0); private sealed class BoundArguments @@ -1695,6 +2038,25 @@ internal IEnumerable PositionalScriptBlocks() => .Select(binding => (BoundArgument?)binding.Argument) .FirstOrDefault(); } + + internal IReadOnlyList ArgumentsBoundTo( + string parameterName, + ParameterSetDefinition parameterSet) + { + var result = NamedArguments + .Where(argument => string.Equals( + argument.ParameterName, + parameterName, + StringComparison.OrdinalIgnoreCase)) + .ToList(); + result.AddRange(parameterSet.BindPositionals(this) + .Where(binding => string.Equals( + binding.ParameterName, + parameterName, + StringComparison.OrdinalIgnoreCase)) + .Select(binding => binding.Argument)); + return result; + } } private sealed class CommandEntry @@ -1854,14 +2216,18 @@ private bool TryBindPositionals( var positional = _positionals[slot]; if (argument.IsScriptBlock != positional.AcceptsScriptBlock || !CanConvert(argument, positional.Name) - || (argument.HasTrailingComma && !positional.AcceptsMultiple)) + || (argument.HasTrailingComma && + !positional.AcceptsMultiple && + !positional.AcceptsCommaContinuation)) { bindings = Array.Empty(); return false; } result.Add(new PositionalBinding(argument, positional.Name)); - if (!positional.AcceptsMultiple) + if (!positional.AcceptsMultiple && + !(positional.AcceptsCommaContinuation && + argument.HasTrailingComma)) { slot++; } @@ -1887,7 +2253,8 @@ private bool TryBindPositionals( private readonly record struct PositionalParameter( string Name, bool AcceptsScriptBlock, - bool AcceptsMultiple); + bool AcceptsMultiple, + bool AcceptsCommaContinuation); private readonly record struct PositionalBinding( BoundArgument Argument, @@ -1897,4 +2264,11 @@ private readonly record struct ParameterResolution( string? CanonicalName, bool IsSwitch, bool IsKnown); + + private enum RemoteTargetCardinality + { + Unknown, + Single, + Multiple, + } } diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/384_v03_invoke_command_remote_single_target_region.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/384_v03_invoke_command_remote_single_target_region.json new file mode 100644 index 0000000..c29775c --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/384_v03_invoke_command_remote_single_target_region.json @@ -0,0 +1,369 @@ +{ + "name": "V03 invoke command remote single target region", + "input": "Invoke-Command -ComputerName server -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName", + "value": "-ComputerName", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 13, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "server", + "value": "server", + "role": "Argument", + "sourceStart": 29, + "sourceLength": 6, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 36, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 49, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 51, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 60, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 73, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 82, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 90, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 71, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 49, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Synchronous", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 50, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 51, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 73, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 49, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 50, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 90 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 90 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "One proved literal remote target executes synchronously once, while the remote body starts from unknown state and cannot taint the host continuation." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/385_v03_invoke_command_remote_job_region.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/385_v03_invoke_command_remote_job_region.json new file mode 100644 index 0000000..a3ed994 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/385_v03_invoke_command_remote_job_region.json @@ -0,0 +1,385 @@ +{ + "name": "V03 invoke command remote job region", + "input": "Invoke-Command -ComputerName server -AsJob -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-AsJob", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName", + "value": "-ComputerName", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 13, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "server", + "value": "server", + "role": "Argument", + "sourceStart": 29, + "sourceLength": 6, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-AsJob", + "value": "-AsJob", + "role": "Argument", + "sourceStart": 36, + "sourceLength": 6, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 43, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 56, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 58, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 67, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 80, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 89, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 97, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 78, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 56, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 5, + "executionPhase": "Main", + "executionTiming": "Concurrent", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 57, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 58, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 80, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 56, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 57, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 97 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 97 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "An enabled remote AsJob switch proves concurrent scheduling without exporting child state." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/386_v03_invoke_command_remote_disconnected_region.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/386_v03_invoke_command_remote_disconnected_region.json new file mode 100644 index 0000000..3f75c3f --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/386_v03_invoke_command_remote_disconnected_region.json @@ -0,0 +1,385 @@ +{ + "name": "V03 invoke command remote disconnected region", + "input": "Invoke-Command -ComputerName server -InDisconnectedSession -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-InDisconnectedSession", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName", + "value": "-ComputerName", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 13, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "server", + "value": "server", + "role": "Argument", + "sourceStart": 29, + "sourceLength": 6, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-InDisconnectedSession", + "value": "-InDisconnectedSession", + "role": "Argument", + "sourceStart": 36, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 59, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 72, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 74, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 83, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 96, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 105, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 113, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 94, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 72, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 5, + "executionPhase": "Main", + "executionTiming": "Concurrent", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 74, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 96, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 72, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 113 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 113 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "A disconnected remote invocation is concurrent even though its single target remains cardinality Once." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/387_v03_invoke_command_remote_multiple_targets.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/387_v03_invoke_command_remote_multiple_targets.json new file mode 100644 index 0000000..56ad03c --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/387_v03_invoke_command_remote_multiple_targets.json @@ -0,0 +1,385 @@ +{ + "name": "V03 invoke command remote multiple targets", + "input": "Invoke-Command -ComputerName server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server1,", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "server2", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName", + "value": "-ComputerName", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 13, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "server1,", + "value": "server1,", + "role": "Argument", + "sourceStart": 29, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + }, + { + "raw": "server2", + "value": "server2", + "role": "Argument", + "sourceStart": 38, + "sourceLength": 7, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 46, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 59, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 61, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 70, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 83, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 92, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 81, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 5, + "executionPhase": "Main", + "executionTiming": "Concurrent", + "executionCardinality": "Unknown" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 61, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 83, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Multiple proved targets execute concurrently, but the public activation cardinality remains Unknown because it has no once-per-target discriminator." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/388_v03_invoke_command_remote_inline_target_array.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/388_v03_invoke_command_remote_inline_target_array.json new file mode 100644 index 0000000..c4a639f --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/388_v03_invoke_command_remote_inline_target_array.json @@ -0,0 +1,374 @@ +{ + "name": "V03 invoke command remote inline target array", + "input": "Invoke-Command -ComputerName:server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server1,", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "server2", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName:server1,", + "value": "-ComputerName:server1,", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": true, + "isPath": false + }, + { + "raw": "server2", + "value": "server2", + "role": "Argument", + "sourceStart": 38, + "sourceLength": 7, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 46, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 59, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 61, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 70, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 83, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 92, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 81, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Concurrent", + "executionCardinality": "Unknown" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 61, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 83, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "PowerShell binds a comma continuation after an inline-colon first value into the same remote target array." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/389_v03_invoke_command_remote_dynamic_session.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/389_v03_invoke_command_remote_dynamic_session.json new file mode 100644 index 0000000..6fcc2f5 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/389_v03_invoke_command_remote_dynamic_session.json @@ -0,0 +1,369 @@ +{ + "name": "V03 invoke command remote dynamic session", + "input": "Invoke-Command -Session $session -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-Session", + "kind": "Literal", + "isPath": false + }, + { + "raw": "$session", + "kind": "EnvVar", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-Session", + "value": "-Session", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "$session", + "value": "$session", + "role": "Argument", + "sourceStart": 24, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "EnvVar", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 33, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 46, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 48, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 57, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 70, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 79, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 87, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 68, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 46, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Unknown", + "executionCardinality": "Unknown" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 47, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 48, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 70, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 46, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 47, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 87 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 87 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "A runtime session collection keeps timing and cardinality Unknown while retaining the isolated remote execution boundary." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/390_v03_invoke_command_remote_state_isolated.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/390_v03_invoke_command_remote_state_isolated.json new file mode 100644 index 0000000..5730cd3 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/390_v03_invoke_command_remote_state_isolated.json @@ -0,0 +1,827 @@ +{ + "name": "V03 invoke command remote state isolated", + "input": "Invoke-Command -ConnectionUri https://example.invalid/wsman -ScriptBlock { Set-Location /tmp; Set-Alias Measure-Command Write-Output; Set-Item Env:SST_REMOTE_CHILD child }; Get-Item host.txt; Measure-Command { Get-Date }", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ConnectionUri", + "kind": "Literal", + "isPath": false + }, + { + "raw": "https://example.invalid/wsman", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Set-Location /tmp; Set-Alias Measure-Command Write-Output; Set-Item Env:SST_REMOTE_CHILD child }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ConnectionUri", + "value": "-ConnectionUri", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 14, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "https://example.invalid/wsman", + "value": "https://example.invalid/wsman", + "role": "Argument", + "sourceStart": 30, + "sourceLength": 29, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 60, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Set-Location /tmp; Set-Alias Measure-Command Write-Output; Set-Item Env:SST_REMOTE_CHILD child }", + "value": "{ Set-Location /tmp; Set-Alias Measure-Command Write-Output; Set-Item Env:SST_REMOTE_CHILD child }", + "role": "Argument", + "sourceStart": 73, + "sourceLength": 98, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Set-Location" + ], + "args": [ + { + "raw": "/tmp", + "kind": "Literal", + "isPath": true, + "resolved": "/tmp" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Set-Location", + "value": "Set-Location", + "role": "Verb", + "sourceStart": 75, + "sourceLength": 12, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "/tmp", + "value": "/tmp", + "role": "Argument", + "sourceStart": 88, + "sourceLength": 4, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "/tmp" + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Set-Alias" + ], + "args": [ + { + "raw": "Measure-Command", + "kind": "Literal", + "isPath": false + }, + { + "raw": "Write-Output", + "kind": "Literal", + "isPath": false + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Set-Alias", + "value": "Set-Alias", + "role": "Verb", + "sourceStart": 94, + "sourceLength": 9, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "Measure-Command", + "value": "Measure-Command", + "role": "Argument", + "sourceStart": 104, + "sourceLength": 15, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "Write-Output", + "value": "Write-Output", + "role": "Argument", + "sourceStart": 120, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Set-Item" + ], + "args": [ + { + "raw": "Env:SST_REMOTE_CHILD", + "kind": "Literal", + "isPath": false + }, + { + "raw": "child", + "kind": "Literal", + "isPath": false + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Set-Item", + "value": "Set-Item", + "role": "Verb", + "sourceStart": 134, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "Env:SST_REMOTE_CHILD", + "value": "Env:SST_REMOTE_CHILD", + "role": "Argument", + "sourceStart": 143, + "sourceLength": 20, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child", + "value": "child", + "role": "Argument", + "sourceStart": 164, + "sourceLength": 5, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + }, + { + "raw": "C:/work", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work", + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 173, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 182, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Measure-Command" + ], + "args": [ + { + "raw": "{ Get-Date }", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "C:/work", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work", + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Measure-Command", + "value": "Measure-Command", + "role": "Verb", + "sourceStart": 192, + "sourceLength": 15, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "{ Get-Date }", + "value": "{ Get-Date }", + "role": "Argument", + "sourceStart": 208, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Date" + ], + "args": [ + { + "raw": "C:/work", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work", + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Date", + "value": "Get-Date", + "role": "Verb", + "sourceStart": 210, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 220, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 171, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 98, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Synchronous", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 74, + "sourceLength": 96, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 75, + "sourceLength": 94, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 5, + "region": "Statement", + "childIndex": 0, + "sourceStart": 75, + "sourceLength": 17, + "clauseIndex": 1, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "SimpleCommand", + "parentIndex": 5, + "region": "Statement", + "childIndex": 1, + "sourceStart": 94, + "sourceLength": 38, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + }, + { + "kind": "SimpleCommand", + "parentIndex": 5, + "region": "Statement", + "childIndex": 2, + "sourceStart": 134, + "sourceLength": 35, + "clauseIndex": 3, + "groupKind": null, + "listOperator": "Sequence" + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 173, + "sourceLength": 17, + "clauseIndex": 4, + "groupKind": null, + "listOperator": "Sequence" + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 2, + "sourceStart": 192, + "sourceLength": 28, + "clauseIndex": 5, + "groupKind": null, + "listOperator": "Sequence" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 10, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 208, + "sourceLength": 12, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 1, + "executionPhase": "Main", + "executionTiming": "Synchronous", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 11, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 209, + "sourceLength": 10, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 12, + "region": "Statement", + "childIndex": 0, + "sourceStart": 210, + "sourceLength": 8, + "clauseIndex": 6, + "groupKind": null, + "listOperator": null + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 98 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 74, + "sourceLength": 96 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 75, + "sourceLength": 94 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 98 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 74, + "sourceLength": 96 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 75, + "sourceLength": 94 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 3, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 73, + "sourceLength": 98 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 74, + "sourceLength": 96 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 2, + "sourceStart": 75, + "sourceLength": 94 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 4, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 220 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 5, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 2, + "sourceStart": 0, + "sourceLength": 220 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 6, + "immediateRole": "ExecutionRegion", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 2, + "sourceStart": 0, + "sourceLength": 220 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 208, + "sourceLength": 12 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 209, + "sourceLength": 10 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "Remote cwd, aliases, environment, and command-resolution state neither inherit local proofs nor flow into the host continuation." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/391_v03_invoke_command_remote_nested_comma_scalar.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/391_v03_invoke_command_remote_nested_comma_scalar.json new file mode 100644 index 0000000..9d053ba --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/391_v03_invoke_command_remote_nested_comma_scalar.json @@ -0,0 +1,369 @@ +{ + "name": "V03 invoke command remote nested comma scalar", + "input": "Invoke-Command -SSHConnection @{HostName=\u0027server,corp\u0027} -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-SSHConnection", + "kind": "Literal", + "isPath": false + }, + { + "raw": "@{HostName=\u0027server,corp\u0027}", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-SSHConnection", + "value": "-SSHConnection", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 14, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "@{HostName=\u0027server,corp\u0027}", + "value": "@{HostName=\u0027server,corp\u0027}", + "role": "Argument", + "sourceStart": 30, + "sourceLength": 25, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 56, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 69, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 71, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 80, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 93, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 102, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 110, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 91, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 69, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Synchronous", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 70, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 71, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 93, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 69, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 70, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 110 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 110 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "A comma nested inside one literal SSH connection hashtable is not a target-array separator." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/392_v03_invoke_command_remote_escaped_comma_scalar.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/392_v03_invoke_command_remote_escaped_comma_scalar.json new file mode 100644 index 0000000..94a90f6 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/392_v03_invoke_command_remote_escaped_comma_scalar.json @@ -0,0 +1,369 @@ +{ + "name": "V03 invoke command remote escaped comma scalar", + "input": "Invoke-Command -ComputerName server1\u0060,server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "-ComputerName", + "kind": "Literal", + "isPath": false + }, + { + "raw": "server1\u0060,server2", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ComputerName", + "value": "-ComputerName", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 13, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "server1\u0060,server2", + "value": "server1,server2", + "role": "Argument", + "sourceStart": 29, + "sourceLength": 16, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 46, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 59, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 61, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 70, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 83, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 92, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 81, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Synchronous", + "executionCardinality": "Once" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 61, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 83, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 59, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 60, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 100 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 100 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "A backtick-escaped comma belongs to one computer-name value and does not prove concurrent targets." +} diff --git a/tests/ShellSyntaxTree.Tests/Corpus/powershell/393_v03_invoke_command_remote_positional_target_array.json b/tests/ShellSyntaxTree.Tests/Corpus/powershell/393_v03_invoke_command_remote_positional_target_array.json new file mode 100644 index 0000000..a194141 --- /dev/null +++ b/tests/ShellSyntaxTree.Tests/Corpus/powershell/393_v03_invoke_command_remote_positional_target_array.json @@ -0,0 +1,369 @@ +{ + "name": "V03 invoke command remote positional target array", + "input": "Invoke-Command server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "powerShellInitialStateMode": "IsolatedNonInteractiveNoProfile", + "expected": { + "isUnparseable": false, + "clauses": [ + { + "operator": "None", + "verb": [ + "Invoke-Command" + ], + "args": [ + { + "raw": "server1,", + "kind": "DynamicSkip", + "isPath": false + }, + { + "raw": "server2", + "kind": "Literal", + "isPath": false + }, + { + "raw": "-ScriptBlock", + "kind": "Literal", + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "kind": "DynamicSkip", + "isPath": false + } + ], + "redirects": [], + "elements": [ + { + "raw": "Invoke-Command", + "value": "Invoke-Command", + "role": "Verb", + "sourceStart": 0, + "sourceLength": 14, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "server1,", + "value": "server1,", + "role": "Argument", + "sourceStart": 15, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + }, + { + "raw": "server2", + "value": "server2", + "role": "Argument", + "sourceStart": 24, + "sourceLength": 7, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "-ScriptBlock", + "value": "-ScriptBlock", + "role": "Argument", + "sourceStart": 32, + "sourceLength": 12, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": true, + "isPath": false + }, + { + "raw": "{ Get-Item child.txt }", + "value": "{ Get-Item child.txt }", + "role": "Argument", + "sourceStart": 45, + "sourceLength": 22, + "precedingVerbElementCount": 1, + "kind": "DynamicSkip", + "isFlag": false, + "isPath": false + } + ] + }, + { + "operator": "None", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "child.txt", + "kind": "Literal", + "isPath": true + }, + { + "raw": "\u003Cdynamic-cwd\u003E", + "kind": "DynamicSkip", + "isPath": false, + "isCwdAttribution": true + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 47, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "child.txt", + "value": "child.txt", + "role": "Argument", + "sourceStart": 56, + "sourceLength": 9, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true + } + ] + }, + { + "operator": "Sequence", + "verb": [ + "Get-Item" + ], + "args": [ + { + "raw": "host.txt", + "kind": "Literal", + "isPath": true, + "resolved": "C:/work/host.txt" + } + ], + "redirects": [], + "elements": [ + { + "raw": "Get-Item", + "value": "Get-Item", + "role": "Verb", + "sourceStart": 69, + "sourceLength": 8, + "precedingVerbElementCount": 0, + "kind": "Literal", + "isFlag": false, + "isPath": false + }, + { + "raw": "host.txt", + "value": "host.txt", + "role": "Argument", + "sourceStart": 78, + "sourceLength": 8, + "precedingVerbElementCount": 1, + "kind": "Literal", + "isFlag": false, + "isPath": true, + "resolved": "C:/work/host.txt" + } + ] + } + ], + "syntax": [ + { + "kind": "Block", + "parentIndex": null, + "region": "Unknown", + "childIndex": null, + "sourceStart": 0, + "sourceLength": 86, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "CommandList", + "parentIndex": 0, + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 67, + "clauseIndex": 0, + "groupKind": null, + "listOperator": "None" + }, + { + "kind": "ExecutionRegion", + "parentIndex": 2, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 45, + "sourceLength": 22, + "clauseIndex": null, + "groupKind": null, + "listOperator": null, + "executionOrigin": "CommandArgument", + "hostClauseElementIndex": 4, + "executionPhase": "Main", + "executionTiming": "Concurrent", + "executionCardinality": "Unknown" + }, + { + "kind": "Block", + "parentIndex": 3, + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 46, + "sourceLength": 20, + "clauseIndex": null, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 4, + "region": "Statement", + "childIndex": 0, + "sourceStart": 47, + "sourceLength": 18, + "clauseIndex": 1, + "groupKind": null, + "listOperator": null + }, + { + "kind": "SimpleCommand", + "parentIndex": 1, + "region": "Statement", + "childIndex": 1, + "sourceStart": 69, + "sourceLength": 17, + "clauseIndex": 2, + "groupKind": null, + "listOperator": "Sequence" + } + ], + "commands": [ + { + "clauseIndex": 0, + "immediateRole": "Ordinary", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 1, + "immediateRole": "ExecutionRegion", + "isComplete": false, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86 + }, + { + "ancestorKind": "ExecutionRegion", + "region": "ExecutionRegion", + "childIndex": 0, + "sourceStart": 45, + "sourceLength": 22 + }, + { + "ancestorKind": "Block", + "region": "Statement", + "childIndex": 0, + "sourceStart": 46, + "sourceLength": 20 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Unknown", + "values": [], + "pattern": null, + "coveringDirectory": null + } + }, + { + "clauseIndex": 2, + "immediateRole": "Ordinary", + "isComplete": true, + "ancestry": [ + { + "ancestorKind": "Block", + "region": "Root", + "childIndex": 0, + "sourceStart": 0, + "sourceLength": 86 + }, + { + "ancestorKind": "CommandList", + "region": "Statement", + "childIndex": 1, + "sourceStart": 0, + "sourceLength": 86 + } + ], + "effectiveArguments": [], + "workingDirectory": { + "kind": "Exact", + "values": [ + "C:/work" + ], + "pattern": null, + "coveringDirectory": null + } + } + ] + }, + "notes": "PowerShell binds a positional comma continuation into the ComputerName target array before the script block." +} diff --git a/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionBindingCatalogTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionBindingCatalogTests.cs index 6cf201d..fd6995a 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionBindingCatalogTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionBindingCatalogTests.cs @@ -318,28 +318,141 @@ public void Invoke_command_distinguishes_in_process_and_remote_parameter_sets() "Invoke-Command -NoNewScope:1 { Get-Date }").HasNoNewScope); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, remote.ParameterSet); var remoteBinding = Assert.Single(remote.Bindings); - Assert.Equal(ExecutionRegionTiming.Unknown, remoteBinding.Timing); - Assert.Equal(ExecutionRegionCardinality.Unknown, remoteBinding.Cardinality); - Assert.False(remoteBinding.IsComplete); + Assert.Equal(ExecutionRegionTiming.Concurrent, remoteBinding.Timing); + Assert.Equal(ExecutionRegionCardinality.Once, remoteBinding.Cardinality); + Assert.True(remoteBinding.IsComplete); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, inlineRemote.ParameterSet); - Assert.False(Assert.Single(inlineRemote.Bindings).IsComplete); + Assert.True(Assert.Single(inlineRemote.Bindings).IsComplete); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, aliasRemote.ParameterSet); - Assert.False(Assert.Single(aliasRemote.Bindings).IsComplete); + Assert.True(Assert.Single(aliasRemote.Bindings).IsComplete); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, positionalRemote.ParameterSet); - Assert.False(Assert.Single(positionalRemote.Bindings).IsComplete); + Assert.True(Assert.Single(positionalRemote.Bindings).IsComplete); } [Theory] [InlineData("Invoke-Command server -ScriptBlock { Get-Date }")] [InlineData("Invoke-Command -Command { Get-Date } server")] - public void Mixed_named_and_positional_remote_targets_cannot_be_proved_local(string source) + public void Mixed_named_and_positional_remote_targets_bind_as_single_remote_targets( + string source) { var result = Bind(source); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, result.ParameterSet); var binding = Assert.Single(result.Bindings); - Assert.Equal(ExecutionRegionTiming.Unknown, binding.Timing); - Assert.False(binding.IsComplete); + Assert.Equal(ExecutionRegionTiming.Synchronous, binding.Timing); + Assert.Equal(ExecutionRegionCardinality.Once, binding.Cardinality); + Assert.True(binding.IsComplete); + } + + [Theory] + [InlineData( + "Invoke-Command -ComputerName server -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server -AsJob -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server -InDisconnectedSession " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server -AsJob:$false " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server -InDisconnectedSession:$false " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName 'server1,server2' " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server1`,server2 " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once)] + [InlineData( + "Invoke-Command -ComputerName server1,server2 -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command -ComputerName server1, server2 -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command server1, server2 -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command -ComputerName:server1, server2 " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command -ComputerName $servers -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Unknown, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command -Session $session -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Unknown, + ExecutionRegionCardinality.Unknown)] + [InlineData( + "Invoke-Command -Session $session -AsJob -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown)] + public void Remote_targets_publish_only_proved_scheduling_facts( + string source, + ExecutionRegionTiming expectedTiming, + ExecutionRegionCardinality expectedCardinality) + { + var result = Bind(source); + + Assert.Equal(PwshExecutionRegionBindingStatus.ProvedExecution, result.Status); + Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, result.ParameterSet); + var binding = Assert.Single(result.Bindings); + Assert.Equal(expectedTiming, binding.Timing); + Assert.Equal(expectedCardinality, binding.Cardinality); + Assert.True(binding.IsComplete); + } + + [Theory] + [InlineData( + "Invoke-Command -ConnectionUri https://example.invalid/wsman " + + "-ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -HostName example.invalid -ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -VMId 8a9d9e75-0ec0-4e0d-948a-a0d876ccf995 " + + "-Credential $credential -ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -VMName vm01 -Credential $credential " + + "-ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -ContainerId container01 -ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -SSHConnection @{HostName='example.invalid'} " + + "-ScriptBlock { Get-Date }")] + [InlineData( + "Invoke-Command -SSHConnection @{HostName='server,corp'} " + + "-ScriptBlock { Get-Date }")] + public void Remote_target_families_share_the_isolated_single_target_contract( + string source) + { + var result = Bind(source); + + Assert.Equal(PwshExecutionRegionBindingStatus.ProvedExecution, result.Status); + Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, result.ParameterSet); + var binding = Assert.Single(result.Bindings); + Assert.Equal(ExecutionRegionTiming.Synchronous, binding.Timing); + Assert.Equal(ExecutionRegionCardinality.Once, binding.Cardinality); + Assert.True(binding.IsComplete); } [Fact] @@ -463,7 +576,7 @@ public void Proved_literal_value_conversions_preserve_execution_binding(string s } [Fact] - public void Ssh_transport_true_value_preserves_remote_incomplete_binding() + public void Ssh_transport_true_value_preserves_remote_binding() { var result = Bind( "Invoke-Command -HostName example.invalid -ScriptBlock { Get-Date } " + @@ -471,7 +584,10 @@ public void Ssh_transport_true_value_preserves_remote_incomplete_binding() Assert.Equal(PwshExecutionRegionBindingStatus.ProvedExecution, result.Status); Assert.Equal(PwshExecutionRegionParameterSet.InvokeRemote, result.ParameterSet); - Assert.False(Assert.Single(result.Bindings).IsComplete); + var binding = Assert.Single(result.Bindings); + Assert.Equal(ExecutionRegionTiming.Synchronous, binding.Timing); + Assert.Equal(ExecutionRegionCardinality.Once, binding.Cardinality); + Assert.True(binding.IsComplete); } [Fact] diff --git a/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionStructuralTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionStructuralTests.cs index 00400b2..67fe7e8 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionStructuralTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/PwshExecutionRegionStructuralTests.cs @@ -112,7 +112,6 @@ public void In_process_invoke_command_explicit_false_no_new_scope_isolates_state } [Theory] - [InlineData("Invoke-Command -ComputerName server -ScriptBlock { Get-Date }")] [InlineData("Invoke-Command -AsJob -ScriptBlock { Get-Date }")] [InlineData("Invoke-Command -NoNewScope:$scope -ScriptBlock { Get-Date }")] public void Unproved_invoke_command_shapes_remain_unknown(string source) @@ -127,6 +126,92 @@ public void Unproved_invoke_command_shapes_remain_unknown(string source) Assert.All(result.Commands, command => Assert.False(command.IsComplete)); } + [Theory] + [InlineData( + "Invoke-Command -ComputerName server -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Synchronous, + ExecutionRegionCardinality.Once, + true)] + [InlineData( + "Invoke-Command -ComputerName server -AsJob -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Once, + true)] + [InlineData( + "Invoke-Command -ComputerName server1,server2 " + + "-ScriptBlock { Get-Date }", + ExecutionRegionTiming.Concurrent, + ExecutionRegionCardinality.Unknown, + false)] + [InlineData( + "Invoke-Command -Session $session -ScriptBlock { Get-Date }", + ExecutionRegionTiming.Unknown, + ExecutionRegionCardinality.Unknown, + false)] + public void Remote_invoke_command_publishes_proved_region_facts( + string source, + ExecutionRegionTiming expectedTiming, + ExecutionRegionCardinality expectedCardinality, + bool expectedHostComplete) + { + var result = ParseIsolated(source); + + var host = Assert.IsType(Assert.Single(result.Syntax.Statements)); + var region = Assert.Single(host.ExecutionRegions); + Assert.Equal(ExecutionRegionPhase.Main, region.Phase); + Assert.Equal(expectedTiming, region.Timing); + Assert.Equal(expectedCardinality, region.Cardinality); + Assert.Equal(expectedHostComplete, result.Commands[0].IsComplete); + var body = result.Commands[1]; + Assert.Equal("Get-Date", body.Clause.Verb.Tokens[0]); + Assert.False(body.IsComplete); + Assert.Equal(ShellValueDomainKind.Unknown, body.WorkingDirectory.Kind); + } + + [Fact] + public void Remote_invoke_command_isolates_child_state_from_host_continuation() + { + var result = ParseIsolated( + "Invoke-Command -ComputerName server -ScriptBlock { " + + "Set-Location /tmp; " + + "Set-Alias Measure-Command Write-Output; " + + "Set-Item Env:SST_REMOTE_CHILD child }; " + + "Get-Item host.txt; Measure-Command { Get-Date }"); + + var continuation = result.Commands + .Where(command => command.Clause.Verb.Tokens[0] is + "Get-Item" or "Measure-Command") + .TakeLast(2) + .ToArray(); + Assert.Equal( + new[] { "C:/work" }, + continuation[0].WorkingDirectory.Values); + Assert.All(continuation, command => Assert.True(command.IsComplete)); + } + + [Fact] + public void Remote_invoke_command_does_not_inherit_or_export_bindings() + { + var result = ParseIsolated( + "foreach ($x in 'outer') { }; " + + "Invoke-Command -ComputerName server -ScriptBlock { " + + "Write-Output $x; foreach ($x in 'inner') { } }; " + + "Write-Output $x"); + + var writes = result.Commands + .Where(command => command.Clause.Verb.Tokens[0] == "Write-Output") + .ToArray(); + Assert.Equal(2, writes.Length); + Assert.Equal( + ShellValueDomainKind.Unknown, + Assert.Single(writes[0].EffectiveArguments).Value.Kind); + Assert.False(writes[0].IsComplete); + Assert.Equal( + new[] { "outer" }, + Assert.Single(writes[1].EffectiveArguments).Value.Values); + Assert.True(writes[1].IsComplete); + } + [Fact] public void In_process_invoke_command_child_scope_isolates_alias_mutation() { diff --git a/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs b/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs index 1ee569c..c5f0638 100644 --- a/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs +++ b/tests/ShellSyntaxTree.Tests/Parsing/ShellValueOracleTests.cs @@ -1082,6 +1082,91 @@ public void PowerShell_pipeline_callbacks_share_state_and_use_semantic_phases() Lines(output)); } + [Fact] + public void PowerShell_invoke_command_remote_parameter_metadata_matches_catalog() + { + if (!IsAvailable("pwsh")) + { + return; + } + + var output = Run( + "pwsh", + "-NoProfile", + "-NonInteractive", + "-Command", + "$command=Get-Command Invoke-Command; " + + "\"default=<$($command.DefaultParameterSet)>\"; " + + "foreach($name in 'ComputerName','Session','ConnectionUri','VMId'," + + "'VMName','HostName','ContainerId','SSHConnection'){ " + + "\"$name=<$($command.Parameters[$name].ParameterType.FullName)>\" }; " + + "\"as-job-in-process=<$([bool]($command.ParameterSets | " + + "Where-Object Name -eq 'InProcess' | Where-Object { " + + "$_.Parameters.Name -contains 'AsJob' }))>\"; " + + "\"disconnected-session-set=<$([bool]($command.ParameterSets | " + + "Where-Object Name -eq 'Session' | Where-Object { " + + "$_.Parameters.Name -contains 'InDisconnectedSession' }))>\""); + + Assert.Equal( + new[] + { + "default=", + "ComputerName=", + "Session=", + "ConnectionUri=", + "VMId=", + "VMName=", + "HostName=", + "ContainerId=", + "SSHConnection=", + "as-job-in-process=", + "disconnected-session-set=", + }, + Lines(output)); + } + + [Fact] + public void PowerShell_parameter_arrays_respect_comma_boundaries() + { + if (!IsAvailable("pwsh")) + { + return; + } + + var output = Run( + "pwsh", + "-NoProfile", + "-NonInteractive", + "-Command", + "function Test-Computer { param(" + + "[Parameter(Position=0)][string[]]$ComputerName," + + "[Parameter(Position=1)][scriptblock]$ScriptBlock) " + + "\"inline-count=<$($ComputerName.Count)>\" }; " + + "function Test-Positional { param(" + + "[Parameter(Position=0)][string[]]$ComputerName," + + "[Parameter(Position=1)][scriptblock]$ScriptBlock) " + + "\"positional-count=<$($ComputerName.Count)>\" }; " + + "function Test-Escaped { param([string[]]$ComputerName) " + + "\"escaped-count=<$($ComputerName.Count)>\" }; " + + "function Test-Ssh { param([hashtable[]]$SSHConnection) " + + "\"ssh-count=<$($SSHConnection.Count)>\" }; " + + "Test-Computer -ComputerName:server1, server2 " + + "-ScriptBlock { Get-Date }; " + + "Test-Positional server1, server2 -ScriptBlock { Get-Date }; " + + "Test-Escaped -ComputerName server1`,server2; " + + "Test-Ssh -SSHConnection @{HostName='server,corp'}"); + + Assert.Equal( + new[] + { + "inline-count=<2>", + "positional-count=<2>", + "escaped-count=<1>", + "ssh-count=<1>", + }, + Lines(output)); + } + [Fact] public void PowerShell_synchronous_regions_observe_scope_and_pipeline_stage_effects() { diff --git a/tools/PwshCorpusTool/CorpusManifest.cs b/tools/PwshCorpusTool/CorpusManifest.cs index 891b297..129ef94 100644 --- a/tools/PwshCorpusTool/CorpusManifest.cs +++ b/tools/PwshCorpusTool/CorpusManifest.cs @@ -991,5 +991,35 @@ private static string NestIex(string inner, int depth) VIE("v03_foreach_object_parallel_provider_name_colon_precision", "1 | ForEach-Object -Parallel { Set-Item Alias:::sstProviderSet Set-Item; sstProviderSet Env:SST_PARALLEL_PROVIDER_COLON_91F62F1F child }; Get-Item host.txt", "Only the provider-qualification colon is removed; an additional authored colon remains part of the alias name and does not taint the unprefixed invocation."), + VIE("v03_invoke_command_remote_single_target_region", + "Invoke-Command -ComputerName server -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "One proved literal remote target executes synchronously once, while the remote body starts from unknown state and cannot taint the host continuation."), + VIE("v03_invoke_command_remote_job_region", + "Invoke-Command -ComputerName server -AsJob -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "An enabled remote AsJob switch proves concurrent scheduling without exporting child state."), + VIE("v03_invoke_command_remote_disconnected_region", + "Invoke-Command -ComputerName server -InDisconnectedSession -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "A disconnected remote invocation is concurrent even though its single target remains cardinality Once."), + VIE("v03_invoke_command_remote_multiple_targets", + "Invoke-Command -ComputerName server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "Multiple proved targets execute concurrently, but the public activation cardinality remains Unknown because it has no once-per-target discriminator."), + VIE("v03_invoke_command_remote_inline_target_array", + "Invoke-Command -ComputerName:server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "PowerShell binds a comma continuation after an inline-colon first value into the same remote target array."), + VIE("v03_invoke_command_remote_dynamic_session", + "Invoke-Command -Session $session -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "A runtime session collection keeps timing and cardinality Unknown while retaining the isolated remote execution boundary."), + VIE("v03_invoke_command_remote_state_isolated", + "Invoke-Command -ConnectionUri https://example.invalid/wsman -ScriptBlock { Set-Location /tmp; Set-Alias Measure-Command Write-Output; Set-Item Env:SST_REMOTE_CHILD child }; Get-Item host.txt; Measure-Command { Get-Date }", + "Remote cwd, aliases, environment, and command-resolution state neither inherit local proofs nor flow into the host continuation."), + VIE("v03_invoke_command_remote_nested_comma_scalar", + "Invoke-Command -SSHConnection @{HostName='server,corp'} -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "A comma nested inside one literal SSH connection hashtable is not a target-array separator."), + VIE("v03_invoke_command_remote_escaped_comma_scalar", + "Invoke-Command -ComputerName server1`,server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "A backtick-escaped comma belongs to one computer-name value and does not prove concurrent targets."), + VIE("v03_invoke_command_remote_positional_target_array", + "Invoke-Command server1, server2 -ScriptBlock { Get-Item child.txt }; Get-Item host.txt", + "PowerShell binds a positional comma continuation into the ComputerName target array before the script block."), }; }