Skip to content

Instantly share code, notes, and snippets.

@alkampfergit
Last active December 8, 2024 12:57
Show Gist options
  • Save alkampfergit/19f89c1a93cc1e7b9ec9bf501f2b9134 to your computer and use it in GitHub Desktop.
Save alkampfergit/19f89c1a93cc1e7b9ec9bf501f2b9134 to your computer and use it in GitHub Desktop.
Winget upgrade output parsed into a real Powershell Object
class Software {
[string]$Name
[string]$Id
[string]$Version
[string]$AvailableVersion
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$upgradeResult = winget upgrade | Out-String
$lines = $upgradeResult.Split([Environment]::NewLine)
# Find the line that starts with Name, it contains the header
$fl = 0
while (-not $lines[$fl].StartsWith("Name"))
{
$fl++
}
# Line $i has the header, we can find char where we find ID and Version
$idStart = $lines[$fl].IndexOf("Id")
$versionStart = $lines[$fl].IndexOf("Version")
$availableStart = $lines[$fl].IndexOf("Available")
$sourceStart = $lines[$fl].IndexOf("Source")
# Now cycle in real package and split accordingly
$upgradeList = @()
For ($i = $fl + 1; $i -le $lines.Length; $i++)
{
$line = $lines[$i]
if ($line.Length -gt ($availableStart + 1) -and -not $line.StartsWith('-'))
{
$name = $line.Substring(0, $idStart).TrimEnd()
$id = $line.Substring($idStart, $versionStart - $idStart).TrimEnd()
$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
}
}
$upgradeList | Get-Member
$upgradeList | Format-Table
@ionred
Copy link

ionred commented Jul 4, 2023

Might I suggest adding

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

near the top to account for issues that may occur when running this in PS5.1? An example of before and after running that command, if using the default output encoding of ascii:
image
image

@alkampfergit
Copy link
Author

Done!

@Warrentheo
Copy link

Warrentheo commented Sep 13, 2023

My output includes the following line at the end:
2 package(s) have version numbers that cannot be determined. Use --include-unknown to see all results.
and it doesn't get handled correctly,

For ($i = $fl + 1; $i -le $lines.Length; $i++)
{
$line = $lines[$i]
if ($line.Length -gt ($availableStart + 1) -and -not $line.StartsWith('-') -and -not $line.Contains("package(s) have version numbers"))
{

Here is the suggested edit that just ignores these lines

@houtianze
Copy link

houtianze commented Jan 14, 2024

Thanks for the usefl script. I made some changes:

  1. Added --include-unknown to the winget upgrade command to avoid the issue mentioned below
  2. Becasue some of the software installed on my system has non-ascii name, I worked around the quirks
  3. Made this code snippet to a function. (This is just for my use case)
function Get-Winget-Upgradables
{

  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  $upgradeResult = winget upgrade --include-unknown | Out-String

  $lines = $upgradeResult.Split([Environment]::NewLine)


  # Find the line that starts with Name, it contains the header
  $fl = 0
  while (-not $lines[$fl].StartsWith("Name"))
  {
    $fl++
  }

  # Line $i has the header, we can find char where we find ID and Version
  $idStart = $lines[$fl].IndexOf("Id")
  $versionStart = $lines[$fl].IndexOf("Version")
  $availableStart = $lines[$fl].IndexOf("Available")
  $sourceStart = $lines[$fl].IndexOf("Source")

  # Now cycle in real package and split accordingly
  $upgradeList = @()
  For ($i = $fl + 1; $i -le $lines.Length; $i++) 
  {
    $line = $lines[$i]
    if ($line.Length -gt ($sourceStart + 1) -and -not $line.StartsWith('-'))
    {
      # when the name of the software is non-ascii, rune counting becomes messy.
      $nameWithSpaceEnding = $line.Substring(0, $idStart)
      $nameWithSpaceEndingByteCount = [System.Text.Encoding]::UTF8.GetByteCount($nameWithSpaceEnding)
      $nameOffset = ($nameWithSpaceEndingByteCount - $nameWithSpaceEnding.Length) / 2;
      $name = $line.Substring(0, $idStart - $nameOffset).Trim()
      # need to Trim() instead of TrimEnd() because somehow when run with | Out-String,
      # the winget output of name becomse ellipsized... Trim() would work around this.
      $id = $line.Substring($idStart - $nameOffset, $versionStart - $idStart).Trim()
      $version = $line.Substring($versionStart - $nameOffset, $availableStart - $versionStart).Trim()
      $available = $line.Substring($availableStart - $nameOffset, $sourceStart - $availableStart).Trim()
      $software = [WingetSoftware]::new()
      $software.Name = $name;
      $software.Id = $id;
      $software.Version = $version
      $software.AvailableVersion = $available;

      $upgradeList += $software
    }
  }

  return $upgradeList;
}

@F4Jonatas
Copy link

F4Jonatas commented Nov 20, 2024

@houtianze I tried to run your function, but it generates an error.

Line |
 1   |    while (-not $lines[$fl].StartsWith("Name"))
     |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     |     You cannot call a method on a null-valued expression.

That's why I created a function that creates a array with an object for each value.
Of course, it can be improved, but we'll leave that for later...

function Get-Winget-Upgradables {
	$apps  = @()
	$start = $false

	# Remove unnecessary first lines
	winget upgrade --accept-source-agreements --include-unknown | foreach-object {
		if ( $psitem -match '^([-]+)$' ) {
			$start = $true
		}
		elseif ( $start -eq $true ) {
			$apps += $psitem
		}
	}

	# Remove the last line
	$apps = $apps[ 0..( $apps.length - 2 ) ]


	# Loop the array and create an object for any value
	$index = 0
	$apps.foreach({
		$pattern = "^(.+\u2026?)\s+([\u2026\.\w\+]+)\s+([\.\d]+)\s+([\.\d]+)\s+([\w]+)$"
		$apps[ $index ] = @{
			'name'      = ( $apps[ $index ] -replace $pattern, '$1' ) -replace '\s+$', ''
			'id'        = $apps[ $index ] -replace $pattern, '$2'
			'version'   = $apps[ $index ] -replace $pattern, '$3'
			'available' = $apps[ $index ] -replace $pattern, '$4'
			'source'    = $apps[ $index ] -replace $pattern, '$5'
		}

		$index += 1
	})

	return $apps
}

@houtianze
Copy link

@F4Jonatas Thanks for the feedback. I tried again and it works on my computer, my guess is that your system is set to some non-English language that shows a localized string of Name and that's why the function failed to locate the line and the index ran out of bound. Locating using ---... is a better option in this case.

@JoeyNice
Copy link

JoeyNice commented Dec 8, 2024

In addition to the language setting, it also makes a difference whether you use Powershell, Powershell ISE or Visual Studio Code.

This has a particular effect on the truncation of text. Sometimes it is a character string like  and sometimes it is a special character that looks like 3 dots (Unicode ellipsis [char]0x2026).

I encountered this problem in a similar project.

https://gist.github.com/nouseforname/a8b7ebcb9d0c05e380c7e3c81c300923?permalink_comment_id=5232687#gistcomment-5232687

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment