Scripts/Uninstall-ObsoleteModule.ps1

#Requires -Version 3.0

[CmdletBinding(SupportsShouldProcess)]
Param(
    [String[]]$Name,

    [ValidateRange(1, 100)]
    [Int]$ProgressParentId
)

# Check required modules are present:
# - PowerShellGet: Included with PowerShell 5.0+ otherwise must be installed
$RequiredModules = @('PowerShellGet')
foreach ($Module in $RequiredModules) {
    Write-Verbose -Message ('Checking module is available: {0}' -f $Module)
    if (!(Get-Module -Name $Module -ListAvailable)) {
        throw ('Required module not available: {0}' -f $Module)
    }
}

$GetParams = @{ }
if ($PSBoundParameters.ContainsKey('Name')) {
    $GetParams['Name'] = $Name
}

$WriteProgressParams = @{ }

if ($PSBoundParameters.ContainsKey('ProgressParentId')) {
    $WriteProgressParams['ParentId'] = $ProgressParentId
    $WriteProgressParams['Id'] = $ProgressParentId + 1
}

if ($Name) {
    $WriteProgressParams['Activity'] = 'Uninstalling obsolete PowerShell modules (Filter: {0})' -f $Name
} else {
    $WriteProgressParams['Activity'] = 'Uninstalling obsolete PowerShell modules'
}

Write-Progress @WriteProgressParams -CurrentOperation 'Enumerating installed modules' -PercentComplete 0
$InstalledModules = Get-InstalledModule -Verbose:$false @GetParams

Write-Progress @WriteProgressParams -CurrentOperation 'Enumerating available modules' -PercentComplete 5
$AvailableModules = Get-Module -ListAvailable -Verbose:$false @GetParams

for ($ModuleIdx = 0; $ModuleIdx -lt $InstalledModules.Count; $ModuleIdx++) {
    $Module = $InstalledModules[$ModuleIdx]

    # Try to avoid subsequent call to Get-InstalledModule as it's *very* slow
    #
    # Unfortunately, we can't rely on "Get-Module -ListAvailable" due to a bug
    # in older PowerShell releases which results in modules with certain names
    # not being returned if they haven't been imported into the session.
    #
    # See: https://github.com/PowerShell/PowerShell/pull/8777
    [PSModuleInfo[]]$MatchingModules = $AvailableModules | Where-Object Name -EQ $Module.Name
    if ($MatchingModules -and $MatchingModules.Count -eq 1) {
        continue
    }

    [PSCustomObject[]]$AllVersions = Get-InstalledModule -AllVersions -Name $Module.Name
    if ($AllVersions.Count -gt 1) {
        $PercentComplete = ($ModuleIdx + 1) / $InstalledModules.Count * 90
        $CurrentOperation = 'Uninstalling {0} version(s): {1}' -f $Module.Name, [String]::Join(', ', $AllVersions.Version -ne $Module.Version)
        Write-Progress @WriteProgressParams -CurrentOperation $CurrentOperation -PercentComplete $PercentComplete

        if ($PSCmdlet.ShouldProcess($Module.Name, 'Uninstall obsolete versions')) {
            $ObsoleteModules = $AllVersions | Where-Object Version -NE $Module.Version
            foreach ($ObsoleteModule in $ObsoleteModules) {
                try {
                    $ObsoleteModule | Uninstall-Module -ErrorAction Stop
                } catch {
                    switch -Regex ($PSItem.FullyQualifiedErrorId) {
                        '^AdminPrivilegesRequiredForUninstall,' {
                            Write-Warning -Message ('Unable to uninstall module as Administrator rights are required: {0} v{1}' -f $ObsoleteModule.Name, $ObsoleteModule.Version)
                        }

                        # Uninstall-Module prints its own warning
                        '^ModuleIsInUse,' { }

                        '^UnableToUninstallAsOtherModulesNeedThisModule,' {
                            Write-Warning -Message ('Unable to uninstall module due to presence of dependent modules: {0} v{1}' -f $ObsoleteModule.Name, $ObsoleteModule.Version)
                        }

                        Default { throw }
                    }
                }
            }
        }
    }
}

Write-Progress @WriteProgressParams -Completed