From 70097af1874f77eee7ddb097a607b6e45cd56c0d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 27 Aug 2026 18:56:14 -0700 Subject: [PATCH 1/3] Forward the Original Invocation's Flags in the Download Remedy Show-DownloadAndRunRemedy printed a fixed 'powershell -File menu.ps1' regardless of what the original invocation was called with, so a user told to download and run the file after a piped-in or no-console run lost -DryRun (or, on bootstrap.ps1, the chosen action) and got the full default behavior instead. Reuses the existing Get-ForwardedArgument helper to append the original bound parameters to the printed command, in both menu.ps1 and bootstrap.ps1. --- host-setup/bootstrap.ps1 | 5 ++++- host-setup/menu.ps1 | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/host-setup/bootstrap.ps1 b/host-setup/bootstrap.ps1 index ca99c2ef..764bf772 100644 --- a/host-setup/bootstrap.ps1 +++ b/host-setup/bootstrap.ps1 @@ -385,7 +385,10 @@ function Resolve-Mode { function Show-DownloadAndRunRemedy { info ' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12' info " Invoke-WebRequest -UseBasicParsing -Uri https://raw.githubusercontent.com/$script:REPO/$script:DEFAULT_REF/host-setup/bootstrap.ps1 -OutFile bootstrap.ps1" - info ' powershell -ExecutionPolicy Bypass -File bootstrap.ps1' + # Reuses Get-ForwardedArgument rather than a second implementation: the original invocation's own action and flags belong on the re-run this remedy is telling the caller to make. + $forwarded = (Get-ForwardedArgument) -join ' ' + $suffix = if ($forwarded) { " $forwarded" } else { '' } + info " powershell -ExecutionPolicy Bypass -File bootstrap.ps1$suffix" } function main { diff --git a/host-setup/menu.ps1 b/host-setup/menu.ps1 index 93330615..f87d3fa1 100644 --- a/host-setup/menu.ps1 +++ b/host-setup/menu.ps1 @@ -514,7 +514,10 @@ function Resolve-Directory { function Show-DownloadAndRunRemedy { info ' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12' info " Invoke-WebRequest -UseBasicParsing -Uri https://raw.githubusercontent.com/$script:HUB_REPO/$script:DEFAULT_REF/host-setup/menu.ps1 -OutFile menu.ps1" - info ' powershell -ExecutionPolicy Bypass -File menu.ps1' + # Reuses Get-ForwardedArgument rather than a second implementation: the original invocation's own flags (-DryRun among them) belong on the re-run this remedy is telling the caller to make. + $forwarded = (Get-ForwardedArgument) -join ' ' + $suffix = if ($forwarded) { " $forwarded" } else { '' } + info " powershell -ExecutionPolicy Bypass -File menu.ps1$suffix" } function main { From 442cd6f7b45f506baa348d0df00895ed430a9c14 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 27 Aug 2026 19:09:04 -0700 Subject: [PATCH 2/3] Quote Forwarded Values in the Printed Download Remedy Get-ForwardedArgument's array is safe for splatting into a real process invocation, but joining its raw tokens with a plain space for display loses argument boundaries: a value carrying a space (-Dir 'C:\Work Area') printed as -Dir C:\Work Area, and a value carrying a single quote could be reinterpreted as syntax when pasted. Format-ForwardedArgumentForDisplay renders the same array as one pasteable command-line string instead, single-quoting every value and doubling an embedded quote. Verified by feeding the printed remedy back through a real PowerShell param() block and confirming both a spaced path and an embedded quote round-trip. --- host-setup/bootstrap.ps1 | 12 ++++++++++-- host-setup/menu.ps1 | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/host-setup/bootstrap.ps1 b/host-setup/bootstrap.ps1 index 764bf772..1fff0744 100644 --- a/host-setup/bootstrap.ps1 +++ b/host-setup/bootstrap.ps1 @@ -171,6 +171,14 @@ function Get-ForwardedArgument { return , $forward } +# The same array Get-ForwardedArgument returns, rendered as one pasteable command-line string rather than split into argv elements: a flag name is never quoted, and a value is always single-quoted with its own embedded quotes doubled, since a bare value carrying a space or a PowerShell metacharacter would otherwise split apart or be reinterpreted as syntax the moment a person pastes it. +function Format-ForwardedArgumentForDisplay { + $parts = foreach ($token in (Get-ForwardedArgument)) { + if ($token -match '^-[A-Za-z]+$') { $token } else { "'" + ($token -replace "'", "''") + "'" } + } + return ($parts -join ' ') +} + function Invoke-PwshHandoff { $pwshPath = Resolve-Pwsh if (-not $pwshPath) { $pwshPath = Install-Pwsh } @@ -385,8 +393,8 @@ function Resolve-Mode { function Show-DownloadAndRunRemedy { info ' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12' info " Invoke-WebRequest -UseBasicParsing -Uri https://raw.githubusercontent.com/$script:REPO/$script:DEFAULT_REF/host-setup/bootstrap.ps1 -OutFile bootstrap.ps1" - # Reuses Get-ForwardedArgument rather than a second implementation: the original invocation's own action and flags belong on the re-run this remedy is telling the caller to make. - $forwarded = (Get-ForwardedArgument) -join ' ' + # Reuses Format-ForwardedArgumentForDisplay rather than a second implementation: the original invocation's own action and flags belong on the re-run this remedy is telling the caller to make. + $forwarded = Format-ForwardedArgumentForDisplay $suffix = if ($forwarded) { " $forwarded" } else { '' } info " powershell -ExecutionPolicy Bypass -File bootstrap.ps1$suffix" } diff --git a/host-setup/menu.ps1 b/host-setup/menu.ps1 index f87d3fa1..11368dee 100644 --- a/host-setup/menu.ps1 +++ b/host-setup/menu.ps1 @@ -126,6 +126,14 @@ function Get-ForwardedArgument { return , $forward } +# The same array Get-ForwardedArgument returns, rendered as one pasteable command-line string rather than split into argv elements: a flag name is never quoted, and a value is always single-quoted with its own embedded quotes doubled, since a bare value carrying a space or a PowerShell metacharacter would otherwise split apart or be reinterpreted as syntax the moment a person pastes it. +function Format-ForwardedArgumentForDisplay { + $parts = foreach ($token in (Get-ForwardedArgument)) { + if ($token -match '^-[A-Za-z]+$') { $token } else { "'" + ($token -replace "'", "''") + "'" } + } + return ($parts -join ' ') +} + function Invoke-PwshHandoff { $pwshPath = Resolve-Pwsh if (-not $pwshPath) { $pwshPath = Install-Pwsh } @@ -514,8 +522,8 @@ function Resolve-Directory { function Show-DownloadAndRunRemedy { info ' [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12' info " Invoke-WebRequest -UseBasicParsing -Uri https://raw.githubusercontent.com/$script:HUB_REPO/$script:DEFAULT_REF/host-setup/menu.ps1 -OutFile menu.ps1" - # Reuses Get-ForwardedArgument rather than a second implementation: the original invocation's own flags (-DryRun among them) belong on the re-run this remedy is telling the caller to make. - $forwarded = (Get-ForwardedArgument) -join ' ' + # Reuses Format-ForwardedArgumentForDisplay rather than a second implementation: the original invocation's own flags (-DryRun among them) belong on the re-run this remedy is telling the caller to make. + $forwarded = Format-ForwardedArgumentForDisplay $suffix = if ($forwarded) { " $forwarded" } else { '' } info " powershell -ExecutionPolicy Bypass -File menu.ps1$suffix" } From fab628603774fd56c2c28f34cf8b5ab9b1bdf5f4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 27 Aug 2026 19:21:24 -0700 Subject: [PATCH 3/3] Classify Forwarded Values by Type, Not by Shape Format-ForwardedArgumentForDisplay guessed whether a token was a flag name or a value by matching it against /^-[A-Za-z]+$/. A value that happens to look like a flag (-Ref '-Yes') matched that pattern too, so it printed unquoted (-Ref -Yes) and would bind -Yes as its own switch when pasted, exactly the class of bug the quoting fix in the prior commit was meant to close. Rewritten to classify directly from SCRIPT_BOUND_PARAMETERS's own [switch] type instead of guessing from a value's shape. Verified against the reported repro (-Ref '-Yes' now prints and round-trips correctly) and both prior cases (a spaced path, an embedded quote). --- host-setup/bootstrap.ps1 | 13 ++++++++++--- host-setup/menu.ps1 | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/host-setup/bootstrap.ps1 b/host-setup/bootstrap.ps1 index 1fff0744..1d6e938a 100644 --- a/host-setup/bootstrap.ps1 +++ b/host-setup/bootstrap.ps1 @@ -171,10 +171,17 @@ function Get-ForwardedArgument { return , $forward } -# The same array Get-ForwardedArgument returns, rendered as one pasteable command-line string rather than split into argv elements: a flag name is never quoted, and a value is always single-quoted with its own embedded quotes doubled, since a bare value carrying a space or a PowerShell metacharacter would otherwise split apart or be reinterpreted as syntax the moment a person pastes it. +# The same parameters Get-ForwardedArgument forwards, rendered as one pasteable command-line string rather than split into argv elements. +# Classified by SCRIPT_BOUND_PARAMETERS's own type rather than by a value's shape: a value that happens to look like a flag (-Ref '-Yes') would otherwise print unquoted and bind as its own switch when pasted, which is exactly the bug guessing from shape produced here before. function Format-ForwardedArgumentForDisplay { - $parts = foreach ($token in (Get-ForwardedArgument)) { - if ($token -match '^-[A-Za-z]+$') { $token } else { "'" + ($token -replace "'", "''") + "'" } + $parts = foreach ($key in $script:SCRIPT_BOUND_PARAMETERS.Keys) { + $value = $script:SCRIPT_BOUND_PARAMETERS[$key] + if ($value -is [switch]) { + if ($value.IsPresent) { "-$key" } + } else { + "-$key" + "'" + ("$value" -replace "'", "''") + "'" + } } return ($parts -join ' ') } diff --git a/host-setup/menu.ps1 b/host-setup/menu.ps1 index 11368dee..4d4c14b8 100644 --- a/host-setup/menu.ps1 +++ b/host-setup/menu.ps1 @@ -126,10 +126,17 @@ function Get-ForwardedArgument { return , $forward } -# The same array Get-ForwardedArgument returns, rendered as one pasteable command-line string rather than split into argv elements: a flag name is never quoted, and a value is always single-quoted with its own embedded quotes doubled, since a bare value carrying a space or a PowerShell metacharacter would otherwise split apart or be reinterpreted as syntax the moment a person pastes it. +# The same parameters Get-ForwardedArgument forwards, rendered as one pasteable command-line string rather than split into argv elements. +# Classified by SCRIPT_BOUND_PARAMETERS's own type rather than by a value's shape: a value that happens to look like a flag (-Ref '-Yes') would otherwise print unquoted and bind as its own switch when pasted, which is exactly the bug guessing from shape produced here before. function Format-ForwardedArgumentForDisplay { - $parts = foreach ($token in (Get-ForwardedArgument)) { - if ($token -match '^-[A-Za-z]+$') { $token } else { "'" + ($token -replace "'", "''") + "'" } + $parts = foreach ($key in $script:SCRIPT_BOUND_PARAMETERS.Keys) { + $value = $script:SCRIPT_BOUND_PARAMETERS[$key] + if ($value -is [switch]) { + if ($value.IsPresent) { "-$key" } + } else { + "-$key" + "'" + ("$value" -replace "'", "''") + "'" + } } return ($parts -join ' ') }