From 40e9501f42a3861f739ea1a5be3741405d0c9b3b Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 16 Oct 2026 12:56:18 +0800 Subject: [PATCH 1/4] fix(env): preserve PowerShell argument separators --- .../assert-arguments.ps1 | 45 ++++++++++++++++ .../command_env_powershell/assert.ps1 | 1 + .../command_env_powershell/print-args.cjs | 1 + .../shell_integration_cwd_templates.md | 54 +++++++++++++++++++ .../vp_global_cli/src/commands/env/setup.rs | 54 +++++++++++++++++++ 5 files changed, 155 insertions(+) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/print-args.cjs diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 new file mode 100644 index 0000000000..88e18f882e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 @@ -0,0 +1,45 @@ +# The PowerShell script binder consumes a bare -- before populating $args. +# Exercise the generated wrapper with a real native child, without a registry. +function Assert-Arguments($actual, $expected) { + if ($LASTEXITCODE -ne 0 -or ($actual -join "`n") -ne $expected) { + throw "Expected arguments $expected, got $actual (exit $LASTEXITCODE)" + } +} + +Assert-Arguments (vp env exec node print-args.cjs -- --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs '--' --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs "--" --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs '--' -- --template react-ts) '["--","--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs -- -- --template react-ts) '["--","--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs --) '["--"]' +Assert-Arguments (vp env exec node print-args.cjs 'value with spaces' -- --template 'react ts') '["value with spaces","--","--template","react ts"]' + +$prefix = @('env', 'exec', 'node', 'print-args.cjs') +$suffix = @('--template', 'react-ts') +$allArguments = $prefix + @('--') + $suffix +Assert-Arguments (vp @prefix -- --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs -- @suffix) '["--","--template","react-ts"]' +Assert-Arguments (vp @prefix -- @suffix) '["--","--template","react-ts"]' +Assert-Arguments (vp @allArguments) '["--","--template","react-ts"]' +$emptyArguments = @() +Assert-Arguments (vp @prefix @emptyArguments -- @emptyArguments @suffix) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs @('before', 'separator') -- @('after', 'separator')) '["before","separator","--","after","separator"]' + +# The caller AST must identify the invocation inside a function or on a later line. +function Invoke-ArgumentCheck { + Assert-Arguments (vp @prefix -- @suffix) '["--","--template","react-ts"]' +} +Invoke-ArgumentCheck +Assert-Arguments (& vp env exec node print-args.cjs ` + -- ` + --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs first -- second | ForEach-Object { $_ }) '["first","--","second"]' + +# Recovering the separator must not evaluate argument expressions a second time. +$script:argumentEvaluations = 0 +Assert-Arguments (vp env exec node print-args.cjs -- $($script:argumentEvaluations++; 'value with spaces')) '["--","value with spaces"]' +if ($script:argumentEvaluations -ne 1) { throw 'An argument expression ran more than once' } + +Assert-Arguments (vp env exec node print-args.cjs 'text -- with spaces' --template react-ts) '["text -- with spaces","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs `-- --template react-ts) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs --template react-ts) '["--template","react-ts"]' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert.ps1 b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert.ps1 index af730148d3..f8c5b1cdff 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert.ps1 +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert.ps1 @@ -117,6 +117,7 @@ if ($env:VP_NODE_VERSION -ne "22.18.0") { throw "file-based VP_NODE_VERSION mismatch: expected 22.18.0, got $env:VP_NODE_VERSION" } +. (Join-Path $PSScriptRoot "assert-arguments.ps1") . (Join-Path $PSScriptRoot "assert-status.ps1") Write-Output "PowerShell environment checks passed" diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/print-args.cjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/print-args.cjs new file mode 100644 index 0000000000..74e2668db0 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/print-args.cjs @@ -0,0 +1 @@ +console.log(JSON.stringify(process.argv.slice(2))); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 0365e9964b..2eee997af4 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -344,6 +344,60 @@ PowerShell wrapper keeps global -C before env use and propagates the native exit ``` # Vite+ PowerShell wrapper (https://viteplus.dev) $__vp_bin = '/home/bin' +# PowerShell consumes the first bare -- when calling a script. Recover its +# position from the caller's AST, without evaluating argument expressions again. +# The call stack also works on Windows PowerShell 5.1, which has no Statement. +$__vp_stack = @(Get-PSCallStack) +$__vp_command = $null +if ($__vp_stack.Count -gt 1) { + $__vp_ast = $__vp_stack[1].InvocationInfo.MyCommand.ScriptBlock.Ast + $__vp_line = $MyInvocation.ScriptLineNumber + $__vp_column = $MyInvocation.OffsetInLine + if ($__vp_ast) { + $__vp_command = $__vp_ast.Find({ param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.Extent.StartLineNumber -eq $__vp_line -and $node.Extent.StartColumnNumber -eq $__vp_column + }, $true) + } +} +if ($__vp_command) { + $__vp_elements = $__vp_command.CommandElements + for ($__vp_i = 1; $__vp_i -lt $__vp_elements.Count; $__vp_i++) { + if ($__vp_elements[$__vp_i].Extent.Text -ceq '--') { + $__vp_index = 0 + for ($__vp_j = 1; $__vp_j -lt $__vp_i; $__vp_j++) { + $__vp_element = $__vp_elements[$__vp_j] + if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { + # Count from the end when the prefix has a variable length. + $__vp_index = $args.Count + for ($__vp_k = $__vp_i + 1; $__vp_k -lt $__vp_elements.Count; $__vp_k++) { + $__vp_tail = $__vp_elements[$__vp_k] + if ($__vp_tail -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_tail.Splatted) { + $__vp_value = $ExecutionContext.SessionState.PSVariable.GetValue($__vp_tail.VariablePath.UserPath) + if ($__vp_value -is [System.Collections.IDictionary]) { + $__vp_index -= 2 * $__vp_value.Count + } else { + $__vp_index -= @($__vp_value).Count + } + } else { + $__vp_index-- + } + } + break + } + $__vp_index++ + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { + $__vp_index++ + } + } + $__vp_arguments = [System.Collections.Generic.List[object]]::new() + $__vp_arguments.AddRange($args) + $__vp_arguments.Insert($__vp_index, '--') + $args = $__vp_arguments.ToArray() + break + } + } +} $__vp_command_index = 0 if ($args.Count -ge 1) { if ($args[0] -eq "-C") { diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index c271d0de3a..7952026256 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1097,6 +1097,60 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp // PowerShell consuming common parameters such as -Verbose or its -V alias. const VP_WRAPPER_PS1: &str = r#"# Vite+ PowerShell wrapper (https://viteplus.dev) $__vp_bin = '__VP_BIN_WIN__' +# PowerShell consumes the first bare -- when calling a script. Recover its +# position from the caller's AST, without evaluating argument expressions again. +# The call stack also works on Windows PowerShell 5.1, which has no Statement. +$__vp_stack = @(Get-PSCallStack) +$__vp_command = $null +if ($__vp_stack.Count -gt 1) { + $__vp_ast = $__vp_stack[1].InvocationInfo.MyCommand.ScriptBlock.Ast + $__vp_line = $MyInvocation.ScriptLineNumber + $__vp_column = $MyInvocation.OffsetInLine + if ($__vp_ast) { + $__vp_command = $__vp_ast.Find({ param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.Extent.StartLineNumber -eq $__vp_line -and $node.Extent.StartColumnNumber -eq $__vp_column + }, $true) + } +} +if ($__vp_command) { + $__vp_elements = $__vp_command.CommandElements + for ($__vp_i = 1; $__vp_i -lt $__vp_elements.Count; $__vp_i++) { + if ($__vp_elements[$__vp_i].Extent.Text -ceq '--') { + $__vp_index = 0 + for ($__vp_j = 1; $__vp_j -lt $__vp_i; $__vp_j++) { + $__vp_element = $__vp_elements[$__vp_j] + if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { + # Count from the end when the prefix has a variable length. + $__vp_index = $args.Count + for ($__vp_k = $__vp_i + 1; $__vp_k -lt $__vp_elements.Count; $__vp_k++) { + $__vp_tail = $__vp_elements[$__vp_k] + if ($__vp_tail -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_tail.Splatted) { + $__vp_value = $ExecutionContext.SessionState.PSVariable.GetValue($__vp_tail.VariablePath.UserPath) + if ($__vp_value -is [System.Collections.IDictionary]) { + $__vp_index -= 2 * $__vp_value.Count + } else { + $__vp_index -= @($__vp_value).Count + } + } else { + $__vp_index-- + } + } + break + } + $__vp_index++ + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { + $__vp_index++ + } + } + $__vp_arguments = [System.Collections.Generic.List[object]]::new() + $__vp_arguments.AddRange($args) + $__vp_arguments.Insert($__vp_index, '--') + $args = $__vp_arguments.ToArray() + break + } + } +} $__vp_command_index = 0 if ($args.Count -ge 1) { if ($args[0] -eq "-C") { From b41b52b00517bc878537d268b2520fb1a9683334 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Tue, 20 Oct 2026 14:54:46 +0800 Subject: [PATCH 2/4] fix(env): simplify PowerShell separator recovery --- .../assert-arguments.ps1 | 25 ++++++++-- .../shell_integration_cwd_templates.md | 47 +++++++------------ .../vp_global_cli/src/commands/env/setup.rs | 47 +++++++------------ 3 files changed, 54 insertions(+), 65 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 index 88e18f882e..ec37c2d579 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 @@ -17,23 +17,38 @@ Assert-Arguments (vp env exec node print-args.cjs 'value with spaces' -- --templ $prefix = @('env', 'exec', 'node', 'print-args.cjs') $suffix = @('--template', 'react-ts') $allArguments = $prefix + @('--') + $suffix -Assert-Arguments (vp @prefix -- --template react-ts) '["--","--template","react-ts"]' Assert-Arguments (vp env exec node print-args.cjs -- @suffix) '["--","--template","react-ts"]' -Assert-Arguments (vp @prefix -- @suffix) '["--","--template","react-ts"]' Assert-Arguments (vp @allArguments) '["--","--template","react-ts"]' -$emptyArguments = @() -Assert-Arguments (vp @prefix @emptyArguments -- @emptyArguments @suffix) '["--","--template","react-ts"]' +Assert-Arguments (vp @prefix '--' @suffix) '["--","--template","react-ts"]' Assert-Arguments (vp env exec node print-args.cjs @('before', 'separator') -- @('after', 'separator')) '["before","separator","--","after","separator"]' +# A splat before -- makes its position unknowable from syntax alone. Preserve +# PowerShell's bound arguments in that case instead of guessing an insertion point. +Assert-Arguments (vp @prefix -- --template react-ts) '["--template","react-ts"]' +Assert-Arguments (vp @prefix -- @suffix) '["--template","react-ts"]' +$emptyArguments = @() +Assert-Arguments (vp @prefix @emptyArguments -- @emptyArguments @suffix) '["--template","react-ts"]' +$script:scopedPrefix = $prefix +$script:scopedSuffix = $suffix +Assert-Arguments (vp @script:scopedPrefix -- @script:scopedSuffix) '["--template","react-ts"]' +Assert-Arguments (vp @script:scopedPrefix '--' @script:scopedSuffix) '["--","--template","react-ts"]' +Assert-Arguments (vp env exec node print-args.cjs -- @script:scopedSuffix) '["--","--template","react-ts"]' + +# Argument expressions can change a splatted variable after it has been expanded. +Assert-Arguments (vp @prefix -- @script:scopedSuffix $($script:scopedSuffix = @(); 'last')) '["--template","react-ts","last"]' + # The caller AST must identify the invocation inside a function or on a later line. function Invoke-ArgumentCheck { - Assert-Arguments (vp @prefix -- @suffix) '["--","--template","react-ts"]' + Assert-Arguments (vp env exec node print-args.cjs -- @suffix) '["--","--template","react-ts"]' } Invoke-ArgumentCheck Assert-Arguments (& vp env exec node print-args.cjs ` -- ` --template react-ts) '["--","--template","react-ts"]' Assert-Arguments (vp env exec node print-args.cjs first -- second | ForEach-Object { $_ }) '["first","--","second"]' +$first = vp env exec node print-args.cjs one -- two; $second = vp env exec node print-args.cjs three -- four +Assert-Arguments $first '["one","--","two"]' +Assert-Arguments $second '["three","--","four"]' # Recovering the separator must not evaluate argument expressions a second time. $script:argumentEvaluations = 0 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 2eee997af4..575f4cc86d 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -361,41 +361,28 @@ if ($__vp_stack.Count -gt 1) { } } if ($__vp_command) { + $__vp_index = 0 $__vp_elements = $__vp_command.CommandElements for ($__vp_i = 1; $__vp_i -lt $__vp_elements.Count; $__vp_i++) { - if ($__vp_elements[$__vp_i].Extent.Text -ceq '--') { - $__vp_index = 0 - for ($__vp_j = 1; $__vp_j -lt $__vp_i; $__vp_j++) { - $__vp_element = $__vp_elements[$__vp_j] - if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { - # Count from the end when the prefix has a variable length. - $__vp_index = $args.Count - for ($__vp_k = $__vp_i + 1; $__vp_k -lt $__vp_elements.Count; $__vp_k++) { - $__vp_tail = $__vp_elements[$__vp_k] - if ($__vp_tail -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_tail.Splatted) { - $__vp_value = $ExecutionContext.SessionState.PSVariable.GetValue($__vp_tail.VariablePath.UserPath) - if ($__vp_value -is [System.Collections.IDictionary]) { - $__vp_index -= 2 * $__vp_value.Count - } else { - $__vp_index -= @($__vp_value).Count - } - } else { - $__vp_index-- - } - } - break - } - $__vp_index++ - if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { - $__vp_index++ - } + $__vp_element = $__vp_elements[$__vp_i] + # A preceding splat has an unknown length. Leave $args unchanged rather + # than reading caller variables, which may have changed or belong to another scope. + if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { + break + } + if ($__vp_element.Extent.Text -ceq '--') { + if ($__vp_index -le $args.Count) { + $__vp_arguments = [System.Collections.Generic.List[object]]::new() + $__vp_arguments.AddRange($args) + $__vp_arguments.Insert($__vp_index, '--') + $args = $__vp_arguments.ToArray() } - $__vp_arguments = [System.Collections.Generic.List[object]]::new() - $__vp_arguments.AddRange($args) - $__vp_arguments.Insert($__vp_index, '--') - $args = $__vp_arguments.ToArray() break } + $__vp_index++ + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { + $__vp_index++ + } } } $__vp_command_index = 0 diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 7952026256..033a45a329 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1114,41 +1114,28 @@ if ($__vp_stack.Count -gt 1) { } } if ($__vp_command) { + $__vp_index = 0 $__vp_elements = $__vp_command.CommandElements for ($__vp_i = 1; $__vp_i -lt $__vp_elements.Count; $__vp_i++) { - if ($__vp_elements[$__vp_i].Extent.Text -ceq '--') { - $__vp_index = 0 - for ($__vp_j = 1; $__vp_j -lt $__vp_i; $__vp_j++) { - $__vp_element = $__vp_elements[$__vp_j] - if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { - # Count from the end when the prefix has a variable length. - $__vp_index = $args.Count - for ($__vp_k = $__vp_i + 1; $__vp_k -lt $__vp_elements.Count; $__vp_k++) { - $__vp_tail = $__vp_elements[$__vp_k] - if ($__vp_tail -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_tail.Splatted) { - $__vp_value = $ExecutionContext.SessionState.PSVariable.GetValue($__vp_tail.VariablePath.UserPath) - if ($__vp_value -is [System.Collections.IDictionary]) { - $__vp_index -= 2 * $__vp_value.Count - } else { - $__vp_index -= @($__vp_value).Count - } - } else { - $__vp_index-- - } - } - break - } - $__vp_index++ - if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { - $__vp_index++ - } + $__vp_element = $__vp_elements[$__vp_i] + # A preceding splat has an unknown length. Leave $args unchanged rather + # than reading caller variables, which may have changed or belong to another scope. + if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { + break + } + if ($__vp_element.Extent.Text -ceq '--') { + if ($__vp_index -le $args.Count) { + $__vp_arguments = [System.Collections.Generic.List[object]]::new() + $__vp_arguments.AddRange($args) + $__vp_arguments.Insert($__vp_index, '--') + $args = $__vp_arguments.ToArray() } - $__vp_arguments = [System.Collections.Generic.List[object]]::new() - $__vp_arguments.AddRange($args) - $__vp_arguments.Insert($__vp_index, '--') - $args = $__vp_arguments.ToArray() break } + $__vp_index++ + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Argument) { + $__vp_index++ + } } } $__vp_command_index = 0 From 117f16b0f22e09157e63212d5eacae299f5735ef Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Tue, 20 Oct 2026 15:06:14 +0800 Subject: [PATCH 3/4] docs(env): reference mise separator recovery --- crates/vp_global_cli/src/commands/env/setup.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 033a45a329..4de834f312 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1095,6 +1095,8 @@ Register-ArgumentCompleter -Native -CommandName vpr -ScriptBlock $__vpr_comp // Keep the wrapper outside bin so it cannot shadow the native executable. // Unlike an advanced function, this script forwards all arguments without // PowerShell consuming common parameters such as -Verbose or its -V alias. +// Bare -- recovery is inspired by mise's __mise_restore_double_dash: +// https://github.com/jdx/mise/blob/34ec0439c868379be87a28591f50d41f84fb8d02/src/shell/pwsh.rs#L45-L103 const VP_WRAPPER_PS1: &str = r#"# Vite+ PowerShell wrapper (https://viteplus.dev) $__vp_bin = '__VP_BIN_WIN__' # PowerShell consumes the first bare -- when calling a script. Recover its From 30e40d4aaeac6729a37b916a307a61ee867f44ea Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Tue, 20 Oct 2026 23:35:38 +0800 Subject: [PATCH 4/4] fix(env): preserve PowerShell stop-parsing arguments --- .../command_env_powershell/assert-arguments.ps1 | 15 +++++++++++++++ .../snapshots/shell_integration_cwd_templates.md | 3 ++- crates/vp_global_cli/src/commands/env/setup.rs | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 index ec37c2d579..cbf5a0cac0 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_powershell/assert-arguments.ps1 @@ -14,6 +14,21 @@ Assert-Arguments (vp env exec node print-args.cjs -- -- --template react-ts) '[" Assert-Arguments (vp env exec node print-args.cjs --) '["--"]' Assert-Arguments (vp env exec node print-args.cjs 'value with spaces' -- --template 'react ts') '["value with spaces","--","--template","react ts"]' +# The stop-parsing token preserves its literal remainder without consuming --. +# Capture on a separate line because --% makes the rest of the line literal. +$literalArguments = vp env exec node print-args.cjs --% -- +Assert-Arguments $literalArguments '["--"]' +$literalArguments = vp env exec node print-args.cjs --% -- -- +Assert-Arguments $literalArguments '["--","--"]' +$literalArguments = vp env exec node print-args.cjs -- --% -- +Assert-Arguments $literalArguments '["--","--"]' + +# A quoted --% allows recovery; escaping its first dash still stops parsing. +$literalArguments = vp env exec node print-args.cjs '--%' -- after +Assert-Arguments $literalArguments '["--","after"]' +$literalArguments = vp env exec node print-args.cjs `--% -- +Assert-Arguments $literalArguments '["--"]' + $prefix = @('env', 'exec', 'node', 'print-args.cjs') $suffix = @('--template', 'react-ts') $allArguments = $prefix + @('--') + $suffix diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md index 575f4cc86d..390ff6fe9f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/shell_integration_cwd_templates/snapshots/shell_integration_cwd_templates.md @@ -370,7 +370,8 @@ if ($__vp_command) { if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { break } - if ($__vp_element.Extent.Text -ceq '--') { + # Only a parameter AST can be consumed. After --%, the separator is literal. + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Extent.Text -ceq '--') { if ($__vp_index -le $args.Count) { $__vp_arguments = [System.Collections.Generic.List[object]]::new() $__vp_arguments.AddRange($args) diff --git a/crates/vp_global_cli/src/commands/env/setup.rs b/crates/vp_global_cli/src/commands/env/setup.rs index 4de834f312..0c21031342 100644 --- a/crates/vp_global_cli/src/commands/env/setup.rs +++ b/crates/vp_global_cli/src/commands/env/setup.rs @@ -1125,7 +1125,8 @@ if ($__vp_command) { if ($__vp_element -is [System.Management.Automation.Language.VariableExpressionAst] -and $__vp_element.Splatted) { break } - if ($__vp_element.Extent.Text -ceq '--') { + # Only a parameter AST can be consumed. After --%, the separator is literal. + if ($__vp_element -is [System.Management.Automation.Language.CommandParameterAst] -and $__vp_element.Extent.Text -ceq '--') { if ($__vp_index -le $args.Count) { $__vp_arguments = [System.Collections.Generic.List[object]]::new() $__vp_arguments.AddRange($args)