diff --git a/functions/private/Invoke-WinUtilCurrentSystem.ps1 b/functions/private/Invoke-WinUtilCurrentSystem.ps1 index ac4cfc9d..cb23f552 100644 --- a/functions/private/Invoke-WinUtilCurrentSystem.ps1 +++ b/functions/private/Invoke-WinUtilCurrentSystem.ps1 @@ -52,14 +52,24 @@ Function Invoke-WinUtilCurrentSystem { } $installedProgramText = $installedProgramOutput -join "`n" + # winget pads each column to its widest value plus one space, so the row with the widest Name or Id + # has only one space next to its Id. Take the Id column position from the header above the dashes. + $idColumnStart = 0 + if ($installedProgramText -match '(?m)^(\S+[^\S\r\n]+)\S[^\r\n]*\r?\n-{3,}') { + $idColumnStart = $Matches[1].Length + } + $sync.configs.applicationsHashtable.GetEnumerator() | ForEach-Object { $packageId = (($_.Value.winget -split ";")[-1] -replace "^msstore:", "").Trim() if ([string]::IsNullOrWhiteSpace($packageId) -or $packageId -eq "na") { return } - $packagePattern = "(?im)[^\S\r\n]{2,}$([regex]::Escape($packageId))(?=[^\S\r\n]{2,}|$)" - if ($installedProgramText -match $packagePattern) { + $escapedPackageId = [regex]::Escape($packageId) + $packagePattern = "(?im)[^\S\r\n]{2,}$escapedPackageId(?=[^\S\r\n]{2,}|$)" + # Skip rows whose Name has combining marks or emoji: they are more characters than columns and shift the offset + $idColumnPattern = "(?im)(?<=^[^\r\n\p{M}\p{Cs}\p{Cf}]{$($idColumnStart - 1)}[^\S\r\n])$escapedPackageId(?=[^\S\r\n]|$)" + if ($installedProgramText -match $packagePattern -or ($idColumnStart -and $installedProgramText -match $idColumnPattern)) { Write-Output $_.Key } } diff --git a/pester/system-helpers.Tests.ps1 b/pester/system-helpers.Tests.ps1 index 5b5d8457..933d6803 100644 --- a/pester/system-helpers.Tests.ps1 +++ b/pester/system-helpers.Tests.ps1 @@ -103,6 +103,69 @@ Describe "Invoke-WinUtilCurrentSystem installed apps" { $script:wingetArguments | Should -Be @("list", "--accept-source-agreements", "--disable-interactivity") } + It "matches a package whose name is the widest in the winget list" { + Mock winget { + $global:LASTEXITCODE = 0 + @( + "Name Id Version Source", + "--------------------------------------------------------------------------------------------------------------", + "Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247 Microsoft.VCRedist.2015+.x64 14.51.36247.0 winget", + "Git Git.Git 2.0 winget" + ) + } + $script:sync.configs.applicationsHashtable["WPFInstallvc2015_64"] = [pscustomobject]@{ winget = "Microsoft.VCRedist.2015+.x64"; choco = "vcredist2015" } + + $result = @(Invoke-WinUtilCurrentSystem -CheckBox "winget") + + $result | Should -HaveCount 2 + $result | Should -Contain "WPFInstallvc2015_64" + $result | Should -Contain "WPFInstallGit" + $result | Should -Not -Contain "WPFInstallMissing" + } + + It "matches the widest ID when its version has only one preceding space ()" -TestCases @( + @{ Mode = "winget" } + @{ Mode = "choco" } + ) { + param($Mode) + Mock winget { + $global:LASTEXITCODE = 0 + @( + "Name Id Version Source", + "------------------------------------", + "Git Git.Git 2.0 winget", + "ChatGPT 9NT1R1C2HH7J 1.0 msstore" + ) + } + + $result = @(Invoke-WinUtilCurrentSystem -CheckBox $Mode) + + $result | Should -HaveCount 2 + $result | Should -Contain "WPFInstallGit" + $result | Should -Contain "WPFInstallChatGPT" + $result | Should -Not -Contain "WPFInstallMissing" + Should -Invoke -CommandName winget -Times 1 -Exactly + } + + It "matches IDs only in the Id column, not inside a package name" { + Mock winget { + $global:LASTEXITCODE = 0 + # "a" plus 16 combining accents is 17 characters but one column, which puts Git.Git at the Id column offset + $accentedName = "a" + ([string][char]0x0301 * 16) + " Git.Git" + @( + "Name Id Version Source", + "-------------------------------------------", + "Some Git.Git Tool Other.Tool 1.0 winget", + "$accentedName Third.Tool 1.0 winget" + ) + } + $script:sync.configs.applicationsHashtable["WPFInstallOther"] = [pscustomobject]@{ winget = "Other.Tool"; choco = "na" } + + $result = @(Invoke-WinUtilCurrentSystem -CheckBox "winget") + + $result | Should -Be @("WPFInstallOther") + } + It "fails promptly when Winget cannot list applications" { Mock winget { $global:LASTEXITCODE = 1