Private/Get-WingetUpgradeTable.ps1
|
function Get-WingetUpgradeTable { <# .SYNOPSIS Parses the table winget prints for available upgrades. .DESCRIPTION Returns one object per row, with whatever the columns held. The columns are found from the header's word positions rather than by name, because winget localises its headers -- "Name" and "Id" are English, and the table is not. The row of dashes under the header is the anchor. It is the one line in winget's output that is the same in every language. Ids arrive truncated when the console is narrower than the table, and that is left alone rather than repaired. This exists to compare one listing against another, and two listings truncated the same way compare correctly. An Id that reads "Microsoft.VisualStudio..." is also still recognisable to the person reading it, which is the other half of the job. Returns nothing when the text holds no table, which is the ordinary case when everything is up to date. .PARAMETER Text winget's output, as captured. .EXAMPLE Get-WingetUpgradeTable -Text (winget upgrade --include-unknown | Out-String) #> [CmdletBinding()] [OutputType([pscustomobject])] param( [AllowNull()] [AllowEmptyString()] [string] $Text ) if (-not $Text) { return } $lines = $Text -split '\r?\n' # The separator is a run of dashes on its own. Anything shorter than a few # characters is not it -- some locales underline other things. $separator = -1 for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^-{8,}\s*$') { $separator = $i; break } } if ($separator -lt 1) { return } $header = $lines[$separator - 1] # Column starts: every position where a non-space follows a space, plus the # first character. Fixed-width, so a row is sliced at the same offsets. $starts = [System.Collections.Generic.List[int]]::new() for ($i = 0; $i -lt $header.Length; $i++) { if ($header[$i] -ne ' ' -and ($i -eq 0 -or $header[$i - 1] -eq ' ')) { $starts.Add($i) } } if ($starts.Count -lt 2) { return } foreach ($line in $lines[($separator + 1)..($lines.Count - 1)]) { # The table ends at the first blank line. What follows is winget's own # summary, which is localised and is not a row. if (-not $line -or -not $line.Trim()) { break } # A row shorter than the second column start is not a row. if ($line.Length -le $starts[1]) { continue } $fields = for ($c = 0; $c -lt $starts.Count; $c++) { $from = $starts[$c] if ($from -ge $line.Length) { '' ; continue } $to = if ($c + 1 -lt $starts.Count) { [Math]::Min($starts[$c + 1], $line.Length) } else { $line.Length } $line.Substring($from, $to - $from).Trim() } $fields = @($fields) [pscustomobject]@{ Name = $fields[0] Id = if ($fields.Count -gt 1) { $fields[1] } else { '' } Version = if ($fields.Count -gt 2) { $fields[2] } else { '' } Available = if ($fields.Count -gt 3) { $fields[3] } else { '' } } } } |