Public/Support/Helper/Limit-InstalledModule.ps1
# Module: TeamsFunctions # Function: ModuleManagement # Author: Barbara Forbes, David Eberhardt # Updated: 17-SEP 2022 # Status: Live function Limit-InstalledModule { <# .SYNOPSIS Removes older versions of installed modules. .DESCRIPTION Removes older versions of the discovered modules keeping them tidy and making space on your disk. .PARAMETER Name List of modules to limit version to .PARAMETER Exclude List of modules to exclude from limiting. .PARAMETER Repository String. If not provided, targets the PowerShell gallery (PsGallery) EXPERIMENTAL. Untested behaviour may occur for custom repositories Please use "Get-InstalledModule | Where Repository -eq 'MyRepo' | Update-Module" as an alternative .PARAMETER Force Switch. Allows removal of Modules that were excluded because they are known to cause instabilities. This does not impact any Module specified with Exclude. Please see notes below. Handle with Care! .EXAMPLE Limit-InstalledModule Uninstalls all versions except the most recent version for ALL Modules found in Repository PsGallery This may not work for Modules in the AllUsers Scope if the Cmdlet is not run with Administrative rights. .EXAMPLE Limit-InstalledModule [-Name] ImportExcel,AzureAD Uninstalls all versions except the most recent version for Modules ImportExcel and AzureAD .EXAMPLE Limit-InstalledModule -Exclude MySpecialModule -Repository MyRepo Uninstalls all versions except the most recent version of all Modules found installed from the Repository 'MyRepo', except MySpecialModule and the modules known to cause instabilities. Please see notes below. Limitations may apply .EXAMPLE Limit-InstalledModule -Exclude MicrosoftTeams -Force Uninstalls all versions except the most recent version of all Modules found in PsGallery, except MicrosoftTeams This will also target Modules known to cause instabilities. Please see notes below. Handle with Care! .INPUTS System.String .OUTPUTS System.Void .NOTES Inspired by Barbara Forbes (@ba4bes,https://4bes.nl) 'Update-EveryModule', just separated out into two scripts. This is splitting Update-EveryModule into Limit-InstalledModule: Removing old versions except the latest Update-InstalledModule: Updating modules with options Sensitive Modules or ones known to cause instabilities have been excluded by default: Az, PsReadline, PowerShellGet Override with Force to also target these protected modules (This does not impact the use of the Exclude parameter). Repositories other than PsGallery could not be tested. .COMPONENT SupportingFunction .FUNCTIONALITY Removes old Module Versions retaining the most recent one installed .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Limit-InstalledModule.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Update-InstalledModule.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_ModuleManagement.md .LINK https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/ #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [Alias('lim')] [OutputType([System.Void])] param ( [Parameter(HelpMessage = 'Modules will be updated')] [System.Collections.Generic.List[object]]$Name = @(), [Parameter(HelpMessage = 'Excluded Modules will not be removed')] [System.Collections.Generic.List[object]]$Exclude = @(), [Parameter()] [String]$Repository = 'PsGallery', [Parameter(HelpMessage = 'Overrides protection for Modules known to cause instabilities')] [switch]$Force ) begin { Show-FunctionStatus -Level Live Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)" # Setting Preference Variables according to Upstream settings if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' } if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' } #region Helper functions # Testing for Administrator function Test-Administrator { $user = [Security.Principal.WindowsIdentity]::GetCurrent(); (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } function Test-ModuleScopeIsMachine ([object]$Module) { $AllUserScopeLocations = ([Environment]::GetEnvironmentVariable('PSModulePath', 'Machine')) -split ';' $AllUserScopeLocations | ForEach-Object { $AllTests = ($_ -in $($Module.InstalledLocation | Split-Path -Parent | Split-Path -Parent)) if ( $AllTests ) { $Module } } } #endregion #region Defining Scope - Query & Filter # Preparing Query $GetInstalledModuleParams = @{ ErrorAction = 'Stop' } if ( $PSBoundParameters.ContainsKey('Name') ) { Write-Verbose -Message "Querying Modules with Name(s): $($Name -join ', ')" $GetInstalledModuleParams.Name = @($Name) } else { Write-Verbose -Message 'Querying all Modules' } # Querying installed Modules $CurrentModules = Get-InstalledModule @GetInstalledModuleParams | Where-Object Repository -EQ $Repository # Filter Excludes $ProtectedModules = @('Az', 'Az.*', 'PSReadLine', 'PowerShellGet') if ( -not $Force ) { $ProtectedModules | ForEach-Object { if ( !($_ -in $Exclude) ) { $Exclude.Add($_) } } $CurrentModules = $CurrentModules | Where-Object Name -NotMatch "^$($ProtectedModules -join '|')" } if ( $PSBoundParameters.ContainsKey('Exclude') ) { $CurrentModules = $CurrentModules | Where-Object Name -NotMatch "^$($Exclude -join '|')" } #endregion #region Confirm scope $Count = $(if ($CurrentModules.GetType().BaseType.Name -eq 'Array') { $CurrentModules.Count } else { 1 }) Write-Verbose -Message "Modules found to process: $Count" if ( $Force.IsPresent -or $($Exclude.Count -gt $ProtectedModules.Count + 1) -or $Count -gt 3) { $go = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Continue' $goWithConfirm = New-Object System.Management.Automation.Host.ChoiceDescription '&Confirm', 'Continue with Confirm' $abort = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'Abort' $options = [System.Management.Automation.Host.ChoiceDescription[]]($go, $goWithConfirm, $abort) $title = 'Multiple Modules discovered!' $message = "Detected $Count Modules to process, excluding: $($Exclude -join ', ' ) | Proceed?" $result = $host.ui.PromptForChoice($title, $message, $options, 0) switch ($result) { 0 { } 1 { $ConfirmRequired = $true } 2 { break } } } #endregion } process { Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)" $CurrentModules | ForEach-Object { Write-Verbose "$($_.Name) - Parsing System for older versions of Module" if ( $ProtectedModules -contains $_.Name ) { Write-Warning -Message "$($_.Name) - Removing versions of this Module can lead to instability. Handle with care!" $ConfirmRequired = $true } # Preparing Splatting parameters $ModuleParams = @{ ErrorAction = 'Stop' } if ( $PSBoundParameters.ContainsKey('WhatIf') ) { $ModuleParams.WhatIf = $True } if ( $ConfirmRequired -or $PSBoundParameters.ContainsKey('Confirm') ) { $ModuleParams.Confirm = $True } # Querying Versions to remove $VersionsToRemove = Get-InstalledModule -Name $_.Name -AllVersions | Where-Object Version -NE $_.Version if ( $VersionsToRemove ) { $RemoveCount = $(if ($VersionsToRemove.GetType().BaseType.Name -eq 'Array') { $VersionsToRemove.Count } else { 1 }) Write-Verbose "$($_.Name) - $RemoveCount old Versions found, attempting uninstall" $VersionsToRemove | ForEach-Object { try { $Name = $_.Name $OldVersion = $_.Version Write-Verbose -Message "$($_.Name) - Unloading Module" Remove-Module -Name $Name -Force -ErrorAction Ignore -Verbose:$False Write-Verbose -Message "$($_.Name) - Uninstalling Version $($_.Version)" $_ | Uninstall-Module -Force @ModuleParams Write-Information "INFO: $($_.Name) - Module Version $($_.Version) removed" } catch { if ( $_.Exception.Message.Contains('Administrator rights are required to uninstall from that folder') ) { Write-Warning -Message "$Name - Cannot remove Module from the 'AllUsers' Scope. Please run this script as Administrator to do so" } else { Write-Error "$Name - Uninstalling module version $OldVersion failed: $_" } } } Write-Verbose -Message "$($_.Name) - Module limited to Version $($_.Version)" } else { Write-Verbose -Message "$($_.Name) - Module limited to Version $($_.Version) (No older Version(s) discovered)" } } } #process end { Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)" } #end } # Limit-InstalledModule |