Invoke-ModuleUpdate.ps1

function Invoke-ModuleUpdate {
    <#
    .SYNOPSIS
    Update one, several or all installed modules if an update is available from a repository location.
 
    .DESCRIPTION
    Invoke-ModuleUpdate for installed modules that have a repository location, for example from PowerShell Gallery.
    The function, without the Update parameter, returns the current and the latest version available for each installed module with a repository location.
    If there is any existing installed versions for each module that current version number will be displayed under Multiple versions.
    When the Update parameter is issued the function will update the named modules.
 
    The script is based on the "Check-ModuleUpdate.ps1" from Jeffery Hicks* to check for available updates for installed PowerShell modules.
    *Credit: http://jdhitsolutions.com/blog/powershell/5441/check-for-module-updates/
 
    .PARAMETER Name
    Specifies names or name patterns of modules that this function gets. Wildcard characters are permitted.
 
    .PARAMETER Update
    Switch parameter to invoke the 'Update-Module' cmdlet for the targeted modules. The default behavior without this switch is that the function will only list the current and available versions.
 
    .PARAMETER Force
    Switch parameter forces the update of each specified module, regardless of the current version of the module installed. Using the 'Force' parameter without using 'Update' parameter does not perform anything extra.
 
    .EXAMPLE
    Invoke-ModuleUpdate
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    SpeculationControl 1.0.0 1.0.8 False
    AzureAD 2.0.0.131 2.0.1.10 {2.0.0.115}
    AzureADPreview 2.0.0.154 2.0.1.11 {2.0.0.137}
    ISESteroids 2.7.1.7 2.7.1.7 {2.6.3.30}
    MicrosoftTeams 0.9.1 0.9.3 False
    NTFSSecurity 4.2.3 4.2.3 False
    Office365Connect 1.5.0 1.5.0 False
    ... ... ... ...
 
    This example returns the current and the latest version available for all installed modules that have a repository location.
 
    .EXAMPLE
    Invoke-ModuleUpdate -Update
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    SpeculationControl 1.0.8 1.0.8 {1.0.0}
    AzureAD 2.0.1.10 2.0.1.10 {2.0.0.131, 2.0.0.115}
    AzureADPreview 2.0.1.11 2.0.1.11 {2.0.0.137, 2.0.0.154}
    ISESteroids 2.7.1.7 2.7.1.7 {2.6.3.30}
    MicrosoftTeams 0.9.3 0.9.3 {0.9.1}
    NTFSSecurity 4.2.3 4.2.3 False
    Office365Connect 1.5.0 1.5.0 False
    ... ... ... ...
 
    This example installs the latest version available for all installed modules that have a repository location.
 
    .EXAMPLE
    Invoke-ModuleUpdate -Name 'AzureAD', 'PSScriptAnalyzer' -Update -Force
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    AzureAD 2.0.1.10 2.0.1.10 {2.0.0.131, 2.0.0.115}
    PSScriptAnalyzer 1.17.1 1.17.1 {1.17.0}
 
    This example will force install the latest version available for the AzureAD and PSScriptAnalyzer modules.
 
    .NOTES
    Created by: Philip Haglund
    Organization: Omnicit AB
    Filename: Invoke-ModuleUpdate.ps1
    Version: 1.0.0
    Requirements: Powershell 4.0
    #>

    [CmdletBinding(
        DefaultParameterSetName = 'NoUpdate',
        SupportsShouldProcess = $true,
        HelpUri = 'https://github.com/Omnicit/Omnicit'
    )]
    param (
        # Specifies names or name patterns of modules that this cmdlet gets. Wildcard characters are permitted.
        [Parameter(
            ParameterSetName = 'NoUpdate',
            ValueFromPipeline = $true,
            Position = 0
        )]
        [Parameter(
            ParameterSetName = 'Update',
            ValueFromPipeline = $true,
            Position = 0
        )]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name = '*',

        # Switch parameter to invoke the 'Update-Module' cmdlet for the targeted modules. The default behavior without this switch is that the function will only list the current and available versions.
        [Parameter(
            ParameterSetName = 'Update',
            Position = 1
        )]
        [switch]$Update,

        # Switch parameter forces the update of each specified module, regardless of the current version of the module installed. Using the 'Force' parameter without using 'Update' parameter does not perform anything extra.
        [Parameter(
            ParameterSetName = 'Update',
            Position = 2
        )]
        [switch]$Force
    )

    begin {
        if ((-not $PSBoundParameters.ContainsKey('Update')) -and $PSBoundParameters.ContainsKey('Force')) {
            Write-Verbose -Message 'Using the "Force" parameter without using "Update" parameter does not perform anything extra.'
        }

        try {
            [array]$Modules = (Get-Module -Name $Name -ListAvailable -ErrorAction Stop -Verbose:$false).Where{$null -ne $_.RepositorySourceLocation}

            # Group all modules to exclude multiple versions.
            [array]$Modules = $Modules | Group-Object -Property Name
            [int]$TotalCount = $Modules.Count

            switch ($Update) {
                $true {
                    [string]$Status = 'Updating module'
                }
                Default {
                    [string]$Status = 'Looking for the latest version for module'
                }
            }
        }
        catch {
            [Exception]$Ex = New-Object -TypeName System.Exception -ArgumentList (' {0} {1}' -f 'Unable to get module information. Error: ', $_.Exception.Message)
            [Management.Automation.ErrorCategory]$Category = [Management.Automation.ErrorCategory]::InvalidResult
            [Management.Automation.ErrorRecord]$ErrRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $Ex, 'ModuleError', $Category, $_.InvocationInfo
            $PSCmdLet.WriteError($ErrRecord)
            break
        }

        try {
            # To speed up the 'Find-Module' cmdlet and not query all existing repositories, save all existing repositories.
            [PSCustomObject]$Repositories = Get-PSRepository -ErrorAction Stop

            if ($PSCmdLet.ParameterSetName -eq 'Update' -and $Repositories.InstallationPolicy -contains 'Untrusted' -and $TotalCount -gt 1) {
                Write-Verbose -Message 'One or more repositories have the InstallationPolicy set to Untrusted.'
                Write-Verbose -Message 'The function will temporary set all repositories to Trusted to avoid continues prompts of "Set-PSRepository" and revert back after finished updating.'
                $RepositoryChanged = $true
                foreach ($Repository in $Repositories) {
                    Set-PSRepository -Name $Repository.Name -InstallationPolicy Trusted -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose:$false
                }
            }
        }
        catch {
            [Exception]$Ex = New-Object -TypeName System.Exception -ArgumentList (' {0} {1}' -f 'Unable to get repository information. Error: ', $_.Exception.Message)
            [Management.Automation.ErrorCategory]$Category = [Management.Automation.ErrorCategory]::InvalidResult
            [Management.Automation.ErrorRecord]$ErrRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $Ex, 'ModuleError', $Category, $_.InvocationInfo
            $PSCmdLet.WriteError($ErrRecord)
            break
        }

    }
    process {
        foreach ($Group in $Modules) {
            [int]$PercentComplete = ' {0:N0}' -f (($Modules.IndexOf($Group) / $TotalCount) * 100)
            Write-Progress -Activity (' {0} {1}' -f $Status, $Group.Group[0].Name) -Status (' {0}% Complete:' -f $PercentComplete) -PercentComplete $PercentComplete

            if ($PSCmdlet.ShouldProcess(('{0}' -f $Group.Group[0].Name), $MyInvocation.MyCommand.Name)) {
                $MultipleVersions = @()
                switch ($Group.Count) {
                    ( {$PSITem -gt 1}) {
                        [string[]]$MultipleVersions = $Group.Group.Version[1..($Group.Group.Version.Length)]
                        [PSModuleInfo]$Module = (($Group).Group | Sort-Object -Property Version -Descending)[0]
                    }
                    Default {
                        $MultipleVersions = $null
                        [PSModuleInfo]$Module = $Group.Group[0]
                    }
                }
                try {
                    if ($Repository = ($Repositories.Where{[string]$_.SourceLocation -eq [string]$Module.RepositorySourceLocation}).Name) {
                        $FindModule = @{
                            Repository  = $Repository
                            ErrorAction = 'Stop'
                        }
                    }
                    else {
                        $FindModule = @{
                            ErrorAction = 'Stop'
                        }
                    }
                    [PSCustomObject]$Online = Find-Module -Name $Module.Name @FindModule
                }
                catch {
                    Write-Warning -Message ('Unable to find module {0}. Error: {1}' -f $Module.Name, $_.Exception.Message)
                    continue
                }

                [version]$CurrentVersion = $Module.Version
                if ($PSBoundParameters.ContainsKey('Update')) {
                    if ([version]$Online.Version -gt [version]$Module.Version) {
                        try {
                            Update-Module -Name $Module.Name -Force:$PSBoundParameters['Force'] -ErrorAction Stop
                            [version]$CurrentVersion = $Online.Version
                            $MultipleVersions += $Module.Version
                        }
                        catch {
                            Write-Warning -Message ('Unable to update module. Error: {0}' -f $_.Exception.Message)
                            [version]$CurrentVersion = $Module.Version
                        }
                    }
                    else {
                        [version]$CurrentVersion = $Online.Version
                    }
                }

                # Output result to pipeline
                [PSCustomObject]@{
                    'PSTypeName'        = 'Omnicit.Invoke.ModuleUpdate'
                    'Name'              = [string]$Module.Name
                    'Current Version'   = $CurrentVersion
                    'Online Version'    = $Online.Version
                    'Multiple Versions' = $MultipleVersions
                }
            }
        }
    }
    end {
        try {
            if ($RepositoryChanged) {
                Write-Verbose -Message 'Reverting back installation policies for repositories.'
                foreach ($Repository in $Repositories) {
                    Set-PSRepository -Name $Repository.Name -InstallationPolicy $Repository.InstallationPolicy -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose:$false
                }
            }
        }
        catch {
            [Exception]$Ex = New-Object -TypeName System.Exception -ArgumentList (' {0} {1}' -f 'Unable to get repository information. Error: ', $_.Exception.Message)
            [Management.Automation.ErrorCategory]$Category = [Management.Automation.ErrorCategory]::InvalidResult
            [Management.Automation.ErrorRecord]$ErrRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $Ex, 'ModuleError', $Category, $_.InvocationInfo
            $PSCmdLet.WriteError($ErrRecord)
            break
        }
    }
}