-
-
Save inxomnyaa/0ee29d68a6b4c92ba53810828dbc0f4a to your computer and use it in GitHub Desktop.
# Based on https://www.codewrecks.com/post/general/winget-update-selective/ (gist at https://gist.github.com/alkampfergit/2f662c07df0ca379c8e8e65e588c687b) | |
# Adds a grid for selecting multiple apps | |
# Fixes packages with truncated names | |
# Adds a confirmation dialogue before actually upgrading | |
# Updated to use newer winget commands and improved error handling | |
# Improved Winget Selective Upgrade Script using Microsoft.WinGet.Client PowerShell Module | |
# This version uses structured PowerShell objects instead of parsing text output, which is way faster | |
class Software { | |
[string]$Name | |
[string]$Id | |
[string]$Version | |
[string]$AvailableVersion | |
} | |
# Check if Microsoft.WinGet.Client module is available | |
$useFallback = $false | |
if (-not (Get-Module -ListAvailable -Name Microsoft.WinGet.Client)) { | |
Write-Host "Microsoft.WinGet.Client module not found. Installing..." -ForegroundColor Yellow | |
try { | |
# Try with -AllowPrerelease first (newer PowerShellGet) | |
try { | |
Install-Module -Name Microsoft.WinGet.Client -Force -AllowPrerelease -Scope CurrentUser | |
} | |
catch { | |
# Fallback for older PowerShellGet versions | |
Write-Host "Trying without -AllowPrerelease parameter..." -ForegroundColor Yellow | |
Install-Module -Name Microsoft.WinGet.Client -Force -Scope CurrentUser | |
} | |
Write-Host "Module installed successfully!" -ForegroundColor Green | |
} | |
catch { | |
Write-Warning "Failed to install Microsoft.WinGet.Client module: $($_.Exception.Message)" | |
Write-Host "" | |
Write-Host "Manual installation options:" -ForegroundColor Cyan | |
Write-Host "1. Run PowerShell as Administrator and try: Install-Module -Name Microsoft.WinGet.Client -Force" -ForegroundColor White | |
Write-Host "2. Or install via winget: winget install Microsoft.PowerShell.PSResourceGet" -ForegroundColor White | |
Write-Host "3. Update PowerShellGet: Install-Module PowerShellGet -Force" -ForegroundColor White | |
Write-Host "" | |
Write-Host "Falling back to traditional winget parsing method..." -ForegroundColor Yellow | |
$useFallback = $true | |
} | |
} | |
# Import the module if available, otherwise use fallback | |
if (-not $useFallback) { | |
try { | |
Import-Module Microsoft.WinGet.Client -Force | |
Write-Host "Using Microsoft.WinGet.Client PowerShell module" -ForegroundColor Green | |
} | |
catch { | |
Write-Warning "Failed to import Microsoft.WinGet.Client module: $($_.Exception.Message)" | |
Write-Host "Using fallback parsing method..." -ForegroundColor Yellow | |
$useFallback = $true | |
} | |
} | |
Write-Host "Checking for available upgrades..." | |
# Try PowerShell module first if available | |
$upgradeList = @() | |
if (-not $useFallback) { | |
try { | |
# Get all installed packages that have upgrades available | |
$installedPackages = Get-WinGetPackage | Where-Object { | |
$_.IsUpdateAvailable -eq $true | |
} | |
Write-Host "PowerShell module found $($installedPackages.Count) packages with available updates" | |
# If we got very few results, suggest fallback for unknown versions | |
if ($installedPackages.Count -eq 0) { | |
Write-Host "No packages found via PowerShell module." -ForegroundColor Yellow | |
Write-Host "This might be due to packages with unknown versions not being detected." -ForegroundColor Yellow | |
Write-Host "Falling back to traditional parsing to include --include-unknown packages..." -ForegroundColor Yellow | |
$useFallback = $true | |
} else { | |
# Convert to our Software class for consistency | |
foreach ($pkg in $installedPackages) { | |
$software = [Software]::new() | |
$software.Name = $pkg.Name | |
$software.Id = $pkg.Id | |
$software.Version = if ($pkg.InstalledVersion) { $pkg.InstalledVersion } else { "Unknown" } | |
$software.AvailableVersion = if ($pkg.AvailableVersions -and $pkg.AvailableVersions.Count -gt 0) { | |
$pkg.AvailableVersions[0] | |
} else { | |
"Unknown" | |
} | |
$upgradeList += $software | |
} | |
} | |
} | |
catch { | |
Write-Warning "Failed to get package information via PowerShell module: $($_.Exception.Message)" | |
Write-Host "Falling back to traditional parsing method..." -ForegroundColor Yellow | |
$useFallback = $true | |
} | |
} | |
# If we're using fallback or PowerShell module found no packages, use traditional parsing | |
if ($useFallback) { | |
Write-Host "Using traditional winget parsing method (includes --include-unknown packages)..." | |
try { | |
$upgradeResult = winget list --upgrade-available --include-unknown | Out-String | |
} | |
catch { | |
Write-Error "Failed to get upgrade list from winget: $($_.Exception.Message)" | |
Read-Host -Prompt "Press any key to exit" | |
exit 1 | |
} | |
# Check if we have any content to process | |
if ([string]::IsNullOrWhiteSpace($upgradeResult)) { | |
Write-Host "No upgrade information available from winget." | |
Read-Host -Prompt "Press any key to exit" | |
exit 0 | |
} | |
$lines = $upgradeResult.Split([Environment]::NewLine) | |
# Find the line that starts with Name, it contains the header | |
$fl = 0 | |
while ($fl -lt $lines.Length -and -not $lines[$fl].StartsWith("Name")) | |
{ | |
$fl++ | |
} | |
# Check if we found a header line | |
if ($fl -ge $lines.Length) { | |
Write-Host "No packages available for upgrade or unexpected winget output format." | |
Read-Host -Prompt "Press any key to exit" | |
exit 0 | |
} | |
# Parse the header positions | |
$headerLine = $lines[$fl] | |
$idStart = $headerLine.IndexOf("Id") | |
$versionStart = $headerLine.IndexOf("Version") | |
$availableStart = $headerLine.IndexOf("Available") | |
$sourceStart = $headerLine.IndexOf("Source") | |
# Validate required columns | |
if ($idStart -eq -1 -or $versionStart -eq -1 -or $availableStart -eq -1) { | |
Write-Error "Could not find required columns in winget output" | |
Read-Host -Prompt "Press any key to exit" | |
exit 1 | |
} | |
if ($sourceStart -eq -1) { | |
$sourceStart = $headerLine.Length | |
} | |
# Parse packages from traditional output | |
For ($i = $fl + 1; $i -lt $lines.Length; $i++) | |
{ | |
$line = $lines[$i] | |
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith('-')) { | |
continue | |
} | |
if ($line.Length -gt $availableStart) | |
{ | |
try { | |
$name = $line.Substring(0, $idStart).TrimEnd() | |
$id = $line.Substring($idStart, $versionStart - $idStart).TrimEnd() | |
if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($id)) { | |
continue | |
} | |
$version = $line.Substring($versionStart, $availableStart - $versionStart).TrimEnd() | |
$available = $line.Substring($availableStart, $sourceStart - $availableStart).TrimEnd() | |
$software = [Software]::new() | |
$software.Name = $name | |
$software.Id = $id | |
$software.Version = $version | |
$software.AvailableVersion = $available | |
$upgradeList += $software | |
} | |
catch { | |
Write-Warning "Failed to parse line $i`: '$line'. Error: $($_.Exception.Message)" | |
continue | |
} | |
} | |
} | |
} | |
# Check if we have any packages to process | |
if (-not $upgradeList -or $upgradeList.Count -eq 0) { | |
Write-Host "No packages available for upgrade." -ForegroundColor Green | |
Read-Host -Prompt "Press any key to exit" | |
exit 0 | |
} | |
Write-Host "Found $($upgradeList.Count) packages available for upgrade:" -ForegroundColor Green | |
$upgradeList | Format-Table | |
Write-Host "Select packages to upgrade in the grid view..." | |
$toUpgrade = $upgradeList | Out-GridView -PassThru -Title "Select packages to upgrade" | |
if (-not $toUpgrade -or $toUpgrade.Count -eq 0) { | |
Write-Host "No packages selected for upgrade." | |
Read-Host -Prompt "Press any key to exit" | |
exit 0 | |
} | |
Write-Host "Selected $($toUpgrade.Count) packages for upgrade." | |
$upgraded = 0 | |
$failed = 0 | |
foreach ($package in $toUpgrade) | |
{ | |
# Confirmation dialogue | |
$title = "Upgrade $($package.Id)?" | |
$question = "Upgrade package '$($package.Name)' ($($package.Id)) from $($package.Version) to $($package.AvailableVersion)?" | |
$choices = '&Yes', '&No', '&All' | |
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 0) | |
if ($decision -eq 0 -or $decision -eq 2) { | |
Write-Host "Upgrading package $($package.Id)..." | |
try { | |
# Use --wait only for individual "Yes" selections, not for "All" | |
if ($decision -eq 0) { | |
& winget upgrade --include-unknown --wait --verbose $package.Id | |
} else { | |
& winget upgrade --include-unknown --verbose $package.Id | |
} | |
if($LASTEXITCODE -eq 0){ | |
Write-Host "Successfully upgraded $($package.Id)" -ForegroundColor Green | |
$upgraded++ | |
} | |
else{ | |
Write-Host "Failed to upgrade $($package.Id) (exit code: $LASTEXITCODE)" -ForegroundColor Red | |
$failed++ | |
} | |
} | |
catch { | |
Write-Host "Error upgrading $($package.Id): $($_.Exception.Message)" -ForegroundColor Red | |
$failed++ | |
} | |
# If "All" was selected, process remaining packages automatically | |
if ($decision -eq 2) { | |
# Get current package index to process remaining packages | |
$currentIndex = $toUpgrade.IndexOf($package) | |
for ($j = $currentIndex + 1; $j -lt $toUpgrade.Count; $j++) { | |
$remainingPackage = $toUpgrade[$j] | |
Write-Host "Auto-upgrading package $($remainingPackage.Id)..." | |
try { | |
& winget upgrade --include-unknown --verbose $remainingPackage.Id | |
if($LASTEXITCODE -eq 0){ | |
Write-Host "Successfully upgraded $($remainingPackage.Id)" -ForegroundColor Green | |
$upgraded++ | |
} | |
else{ | |
Write-Host "Failed to upgrade $($remainingPackage.Id) (exit code: $LASTEXITCODE)" -ForegroundColor Red | |
$failed++ | |
} | |
} | |
catch { | |
Write-Host "Error upgrading $($remainingPackage.Id): $($_.Exception.Message)" -ForegroundColor Red | |
$failed++ | |
} | |
} | |
break # Exit the main loop since we processed all remaining packages | |
} | |
} | |
else { | |
Write-Host "Skipped upgrade for package $($package.Id)" -ForegroundColor Yellow | |
} | |
} | |
Write-Host "" | |
Write-Host "Upgrade Summary:" -ForegroundColor Cyan | |
Write-Host " Successfully upgraded: $upgraded" -ForegroundColor Green | |
Write-Host " Failed: $failed" -ForegroundColor Red | |
Write-Host " Total selected: $($toUpgrade.Count)" -ForegroundColor White | |
Read-Host -Prompt "Press any key to exit" |
Suggest more error checking on the first parse of the winget output at line 40 to handle the case where there is nothing to process and the output doesn't contain an expected string which causes the script to spew several lines of error messages.
@chriscollingwood613 thanks for the suggestion, i implemented it now and switched to winget list --upgrade-available
. i am still working on some enhancements and speed improvements
based on this comment microsoft/winget-cli#4743 (comment) which suggests "If you use the PowerShell Module, Get-WinGetPackage will return structured data that includes all the information and can be converted to JSON using other inbuilt PowerShell functions." it seems right now the only option is to install a module, but i feel like this kind of is bloat, when it could just be untruncated. It is faster than parsing text though ^^"
BUT ive tested it just now, and my current version doesnt list the packages with unknown version 💀
Suggest more error checking on the first parse of the winget output at line 40 to handle the case where there is nothing to process and the output doesn't contain an expected string which causes the script to spew several lines of error messages.