PowerShell version of Rich Trouton’s “Listing and downloading available macOS installers…” script

The original shell version is here: https://derflounder.wordpress.com/2023/06/30/listing-and-downloading-available-macos-installers-using-apples-softwareupdate-tool/#more-12084. The basics are simple: use the sofwareupdate command to display a list of available updates for a given Mac and let the user pick the one they want to download. This doesn’t install it, just downloads them.

Because I have an upcoming presentation at MacAdmins 2023 on PowerShell on the Mac, I wanted to do a PowerShell version of Rich’s shells script, mostly to see how it would work out. Turns out it worked pretty well, once I got past one hump, namely being able to create an array of items that allows for duplicates. Doing that with a hashtable isn’t possible and most of the workarounds are tedious as hell, so I took advantage of a custom feature in PowerShell: I created a custom class.

The entire script is at: https://github.com/johncwelch/List-and-Download-available-macOS-Updates, but we’ll do the tour here.

The first line is pretty basic, it sets up the command we’ll use to actually download the installer by version number, so that when we run the command, all we have to do is tack on the version:

$getFullInstallerByVersion = "/usr/sbin/softwareupdate --fetch-full-installer --full-installer-version " 

The next fourteen lines are where we define our custom class, which is an object with three string parameters: an index, a title, and a version:

class OSUpdate {
     [string]$index
     [string]$title
     [string]$version;

     OSUpdate(
          [string]$index,
          [string]$title,
          [string]$version
     ){
          $this.index = $index
          $this.title = $title
          $this.version = $version
     }
}

By doing this, we avoid having to work around hashtable issues or what have you, and we can take advantage of PowerShell’s object syntax.

Next we set up our two arraylists (think NSMutableArray), the first will hold the raw output from the software update command to list the available updates, the second will be an arraylist of our custom objects:

[System.Collections.ArrayList]$softareUpdateArrayList=@()
[System.Collections.ArrayList]$availableOSUpdates=@() 

Now we run the command to get the full list of installers, and shove it into the first arraylist:

$softareUpdateArrayList = /usr/sbin/softwareupdate --list-full-installers 

Then we remove the first two lines, because they’re not useful for our needs. It’s not the most elegant thing, but it works, and only two lines:

$softareUpdateArrayList.RemoveAt(0) 
$softareUpdateArrayList.RemoveAt(0)  

Now we iterate through that array and for each item we:

  • shove the index of the current entry into $theIndex as a string
  • remove the first two characters in the string (* ) via Substring(2)
  • we split each entry into a separate string array on the comma via Split(“,”) which creates a 5-item string array for the current entry
  • We take the first item of that string array, $OSVersionStringArray[0], and split it on the colon, which gives us another string array ($OSTitleTemp) with two items: the word “Title” and the actual name of the OS version with a leading space via Split(“:”)
  • We take the second item, the actual name, strip off the leading space with Substring(1) and put that into $OSTitle
  • We repeat the process for the title with the second item of the main array in our loop, $OSVersionStringArray[1], and shove just the version number into $OSVersion
  • We create a new OSUpdate Object called “$OSUpdateItem” consisting of the index ($theIndex), the title, $OSTitle, and the version ($OSVersion) of the current item
  • Finally we add that into the $availableOSUpdates arraylist, piping the output to Out-Null so you avoid PowerShell’s love of counting off iterations through an array
foreach ($installer in $softareUpdateArrayList  ) {
     #get the index of the item
     [string]$theIndex = $softareUpdateArrayList.IndexOf($installer)
     #remove the *<space> chars in each line
     $installer = $installer.Substring(2)
     #split on the comma to create a string array 
     $OSVersionStringArray = $installer.Split(",")
     
     #get just the title from the string array
     $OSTitleTemp = $OSVersionStringArray[0].Split(":")
     #grab just the title and delete the leading space
     $OSTitle =  $OSTitleTemp[1].Substring(1)

     #get just the version number from the string array
     $OSVersionTemp = $OSVersionStringArray[1].Split(":")
     #grab just the version and delete the leading space
     $OSVersion = $OSVersionTemp[1].Substring(1)
     
     #create new OSUpdate Object
     $OSUpdateItem = @([OSUpdate]::new($theIndex,$OSTitle ,$OSVersion))

     #add the item to the arraylist of updates, suppress index output
     $availableOSUpdates.Add($OSUpdateItem)|Out-Null
} 

Now, we list out the available updates and ask the user to enter the index of the desired update. We pipe the Write-Host lines to Out-Host, because otherwise, Read-Host will suppress all of it until you enter an index. That you can’t see because Read-Host is suppressing it. Because we’re using Out-Host, we specify newlines with `n in the Write-Host lines:

Write-Host "The available updates for this Mac are:`n" | Out-Host 

This next line sets up the headers for the list, colors them green via -ForegroundColor Green, (kind of a tradition in PowerShell when you’re displaying array contents with content names), and use escape characters to underline the words in the line. `e[4m starts the underlining, and `e[24m ends it:

Write-Host "`e[4mIndex`tTitle`t`tVersion`e[24m`n" -ForegroundColor Green| Out-Host 

Next, iterate through the arraylist of available updates, with each entry as a tab-delimited (`t) string:

foreach ($update in $availableOSUpdates) {
     $update.index + "`t" + $update.title + "`t" + $update.version | Out-Host  
} 

The Read-Host line to get the index number as an int:

[Int32]$desiredUpdate = Read-Host "Enter the index of the update you want to download" 

Get the object data from the array for that index:

$updateToFetch = $availableOSUpdates[$desiredUpdate] 

And finally, tell the user we’re downloading the installer, build the full command, and run the command via Invoke-Expression:

Write-Host "Downloading Installer"
#build the command
$theSoftwareUpdateCommand = $getFullInstallerByVersion + $updateToFetch.version
#run the command via Invoke-Expression
Invoke-Expression $theSoftwareUpdateCommand 

That’s the script, and it’s only about ten lines longer than Rich’s shell script, but thanks to PowerShell understanding more than “everything is text”, it’s a bit more readable, and we don’t need to do as much cat/tail/awk/build/grep, etc. If we want to say, add the size parameter, modifying the script for that would be pretty easy, since we’re not slashing strings but manipulating objects and arrays.