diff --git a/docs/src/content/docs/guides/automation.mdx b/docs/src/content/docs/guides/automation.mdx index 0047c013..38681440 100644 --- a/docs/src/content/docs/guides/automation.mdx +++ b/docs/src/content/docs/guides/automation.mdx @@ -33,7 +33,9 @@ To create your own config file: Local configuration paths are used exactly as provided when importing or exporting. Square brackets in folder or file names, such as `C:\Configs\setup[1].json`, are treated as part of the name rather than wildcard patterns. -Once you have exported a config, launch WinUtil with it using this command: +Export also copies a command to the clipboard that launches WinUtil with the saved configuration. The copied command preserves special characters in the path, including apostrophes, dollar signs, and backticks. + +You can also launch WinUtil with an exported config using this command: ```powershell & ([ScriptBlock]::Create((irm "https://christitus.com/win"))) -Config "C:\Path\To\Config.json" ``` diff --git a/functions/public/Invoke-WPFImpex.ps1 b/functions/public/Invoke-WPFImpex.ps1 index f000c459..cdf26c7a 100644 --- a/functions/public/Invoke-WPFImpex.ps1 +++ b/functions/public/Invoke-WPFImpex.ps1 @@ -60,7 +60,9 @@ function Invoke-WPFImpex { } $jsonFile = $allConfs | ConvertTo-Json $jsonFile | Out-File -LiteralPath $Config -Force - "iex ""& { `$(irm https://christitus.com/win) } -Config '$Config'""" | Set-Clipboard + # Keep the path literal when the copied command is pasted into PowerShell. + $escapedConfig = [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Config) + "& ([ScriptBlock]::Create((Invoke-RestMethod 'https://christitus.com/win'))) -Config '$escapedConfig'" | Set-Clipboard } } catch { Write-Error "An error occurred while exporting: $_" diff --git a/pester/impex-command.Tests.ps1 b/pester/impex-command.Tests.ps1 new file mode 100644 index 00000000..f0508f7f --- /dev/null +++ b/pester/impex-command.Tests.ps1 @@ -0,0 +1,44 @@ +BeforeAll { + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + . (Join-Path $repoRoot "functions\public\Invoke-WPFImpex.ps1") +} + +Describe "Exported configuration clipboard command" { + BeforeEach { + $script:sync = [Hashtable]::Synchronized(@{ + selectedApps = @("WPFInstallGit") + selectedTweaks = @() + selectedToggles = @() + selectedFeatures = @() + selectedAppx = @() + }) + $script:copiedCommand = $null + Mock Set-Clipboard { $script:copiedCommand = $Value } + # Execute only a harmless replacement that echoes the received path. + Mock Invoke-RestMethod { 'param([string]$Config) $Config' } + } + + AfterEach { + Remove-Variable -Name sync, copiedCommand -Scope Script -ErrorAction SilentlyContinue + } + + It "preserves the configuration path for when the copied command runs" -ForEach @( + @{ FileName = 'my settings.json' } + @{ FileName = "O'Brien.json" } + @{ FileName = ('O{0}Brien.json' -f [char]0x2019) } + @{ FileName = '$HOME.json' } + @{ FileName = 'back`tick.json' } + @{ FileName = '$(1+2).json' } + ) { + $configPath = Join-Path $TestDrive $FileName + Invoke-WPFImpex -type "export" -Config $configPath + + $script:copiedCommand | Should -Not -BeNullOrEmpty + $result = & ([scriptblock]::Create($script:copiedCommand)) + + $result | Should -Be $configPath + Should -Invoke Invoke-RestMethod -Times 1 -Exactly -ParameterFilter { + $Uri -eq 'https://christitus.com/win' + } + } +}