Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
January 24, 2025

Module and Package Management Tools

When I started my IT career, we didn't have to worry much about the pace of change. Operating systems were released on a 3 to 5-year cycle, and software updates were released at a leisurely pace. However, in the DevOps-oriented world today, the pace of change is much faster. This is especially true in the PowerShell world, where new modules and updates are released frequently. I thought I would share some tooling I've created to help me keep up with module and package updates. Don't assume these solutions will work for you; take them as inspiration to create your own solutions.

Module Management Tools

My PowerShell world is primarily PowerShell 7. I have some modules that I use in both Windows PowerShell and PowerShell 7 that are installed in Windows PowerShell. But they are not my focus. I want to manage newer modules installed in PowerShell 7. I am also using the Microsoft.PowerShell.PSResourceGet module to help me manage modules. This module is available in the PowerShell Gallery and is the replacement for the now legacy, PowerShellGet module. The commands in the PSResourceGet module are similar to those in PowerShellGet and query the same repository, but the commands are faster.

One List to Rule Them All

I have a list of modules that I use in my PowerShell 7 environment. These are modules that I want to keep up-to-date and ensure that they are installed on my desktop and laptop. I keep the file in my Scripts folder which has a symbolic link to my OneDrive folder. This way, I can access the file from any of my computers.

# My PowerShell 7 modules
# C:\scripts\MyPS7Modules.txt
# These modules were, or should be, installed using PSResourceGet

Microsoft.PowerShell.ConsoleGuiTools
Microsoft.PowerShell.Crescendo
Microsoft.Powershell.PSResourceGet
Microsoft.WinGet.Client
Microsoft.PowerShell.WhatsNew
PSFunctionTools
PSWorkItem
PSReminderLite
PSReadLine
psedit
PSBluesky
PowerShellRun
SecretManagement.1Password
PwshSpectreConsole
PSDupes
Terminal-Icons
TerminalGuiDesigner
ThreadJob

I can use this list to install or update the modules on my desktop or laptop. I can also use it to check the status of the modules.

Get-PSModuleStatus

I wrote a PowerShell tool based on commands in the PSResourceGet module to help me manage my modules. The tool is called Get-PSModuleStatus.

Function Get-PSModuleStatus {
    [CmdletBinding(DefaultParameterSetName = 'File')]
    [OutputType('myPSResource')]
    Param (
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'Name',
            HelpMessage = 'The name of the module to check. The module should already be installed.'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('Name')]
        [string[]]$ModuleName,

        [Parameter(
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'Name',
            HelpMessage = 'The module installation scope'
        )]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string]$Scope = 'CurrentUser',

        [Parameter(
            ParameterSetName = 'File',
            HelpMessage = 'The path to the file containing the module names'
        )]
        [ValidateScript({ Test-Path $_ }, ErrorMessage = 'Cannot find the file {0}')]
        [string]$Path = 'C:\scripts\MyPS7Modules.txt'
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN  ] Starting $($MyInvocation.MyCommand)"
        Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN  ] Running under PowerShell version $($PSVersionTable.PSVersion)"
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Detected parameter set $($PSCmdlet.ParameterSetName)"
        if ($PSCmdlet.ParameterSetName -eq 'File') {
            #filter out commented and blank lines
            $ModuleName = Get-Content -Path $path | Where-Object { $_ -NotMatch '^#' -AND $_ -match '\w+' }
        }

        If ($ModuleName) {
            Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Processing $($ModuleName.Count) modules"

            $ModuleName | ForEach-Object {
                $Name = $_
                $hash = [ordered]@{
                    PSTypeName = 'myPSResource'
                    Name       = $Name
                    Version    = $Null
                    Path       = $Null
                    Installed  = $Null
                    Scope      = $Scope
                    Available  = $false
                    Update     = $false
                }
                $get = Try {
                    Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Getting PSResource $Name"
                    Get-InstalledPSResource -Name $Name -Scope $Scope -ErrorAction Stop -Verbose:$False
                } #Try
                Catch {
                    #Search for module if using a list
                    if ($PSCmdlet.ParameterSetName -eq "File") {
                        Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Testing $Name in AllUsers scope"
                        Try {
                            #suppressing Verbose output from the PSResourceGet commands
                            Get-InstalledPSResource -Name $Name -Scope AllUsers -ErrorAction Stop -Verbose:$False
                            $hash.Scope = 'AllUsers'
                        }
                        Catch {
                            Write-Warning "Cannot find $Name in either scope"
                            #write a placeholder to the pipeline
                            @{
                                Version           = $null
                                InstalledDate     = $null
                                InstalledLocation = $null
                            }
                            $hash.update = $False
                        }
                    }
                } #outer Catch

                if ($get) {
                    #get the most recent version
                    $current = $get | Sort-Object -Property Version | Select-Object -Last 1

                    #update the hash table
                    $hash.Version = $current.Version
                    $hash.Path = $Current.InstalledLocation
                    $hash.Installed = $current.InstalledDate

                    #get online version
                    $online = Find-PSResource -Name $Name -Type Module -Verbose:$False
                    $hash.Available = $online.Version

                    if ($hash.Version) {
                        $hash.Update = $online.Version -gt $hash.Version
                    }
                    #write a custom object to the pipeline
                    [PSCustomObject]$hash
                } #if $get
            } #foreach
        } #if module name
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeOfDay) END    ] Ending $($MyInvocation.MyCommand)"
    } #end
} #end function

The PSResourceGet module manages both modules and installed scripts. They are referred to as resources. Since my function is targeted at modules, I've given it a more targeted name.

The function will accept input from the pipeline or a file. You can see that I have set the module list as the default and made the File parameter set the default. I am bending best practices a bit here. The function should accept pipeline input like this:

Get-Content C:\scripts\MyPS7Modules.txt | Get-PSModuleStatus

But that is too much typing for me when I know I will always be using the same source file.

Getting module status
figure 1

Or I can get a single module status like this:

PS C:\> Get-PSModuleStatus PSBluesky -Scope AllUsers

Name      Scope    Version Available Update
----      -----    ------- --------- ------
PSBluesky AllUsers 2.1.0   2.4.0     True
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.