|
| 1 | +function Get-ModuleImportCandidate { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Returns module information for the specific instance of a module that Import-Module would load. |
| 5 | +
|
| 6 | + .DESCRIPTION |
| 7 | + Get-ModuleImportCandidate is a cross-platform function that reliably determines which module version would be |
| 8 | + selected by Import-Module when multiple versions of the same module are available in multiple installation scopes. |
| 9 | +
|
| 10 | + When importing modules, PSModulePath is the primary factor in determining which module version is loaded, |
| 11 | + and the order of the paths in PSModulePath is important. The CurrentUser paths generally appear first in PSModulePath, |
| 12 | + followed by the AllUsers scope paths. The function takes into account the following rules: |
| 13 | +
|
| 14 | + Location takes precedence over version: |
| 15 | + - A lower version in a higher-priority location will be loaded before a higher version in a lower-priority location. |
| 16 | + - Within a location, higher versions are loaded first. |
| 17 | +
|
| 18 | + .PARAMETER Name |
| 19 | + The name of the module[s] to check. This can be a single module name or an array of module names. |
| 20 | +
|
| 21 | + .EXAMPLE |
| 22 | + Get-ModuleImportCandidate -Name 'Az.Accounts' |
| 23 | +
|
| 24 | + Returns a PSModuleInfo object for the version of the 'Az.Accounts' module that would be imported by Import-Module. |
| 25 | +
|
| 26 | + .EXAMPLE |
| 27 | + Get-ModuleImportCandidate -Name 'Pester','Maester' |
| 28 | +
|
| 29 | + Returns PSModuleInfo objects for the versions of the 'Pester' and 'Maester' modules that would be imported by Import-Module. |
| 30 | +
|
| 31 | + .EXAMPLE |
| 32 | + 'Az.Accounts','ExchangeOnlineManagement','Microsoft.Graph.Authentication','MicrosoftTeams' | Get-ModuleImportCandidate |
| 33 | +
|
| 34 | + Returns PSModuleInfo objects for the specified modules that would be imported by Import-Module. |
| 35 | +
|
| 36 | + .NOTES |
| 37 | + Author: Sam Erde |
| 38 | + Version: 1.0.0 |
| 39 | + Date: 2025-06-05 |
| 40 | + #> |
| 41 | + |
| 42 | + [CmdletBinding()] |
| 43 | + param( |
| 44 | + # The name of the module[s] to check. This can be a single module name or an array of module names. |
| 45 | + [Parameter( |
| 46 | + Position = 0, |
| 47 | + ValueFromPipeline, |
| 48 | + ValueFromPipelineByPropertyName, |
| 49 | + HelpMessage = 'Enter a module name or a list of names. Wildcards are allowed.' |
| 50 | + )] |
| 51 | + [string[]]$Name = @( |
| 52 | + 'Az.Accounts', |
| 53 | + 'ExchangeOnlineManagement', |
| 54 | + 'Microsoft.Graph.Authentication', |
| 55 | + 'MicrosoftTeams' |
| 56 | + ) |
| 57 | + ) |
| 58 | + |
| 59 | + begin { |
| 60 | + # Get PSModulePath entries in order (defaults to the 'Process' environment variable target which includes users and computer values, in that order). |
| 61 | + $PSModulePathEntries = $env:PSModulePath -split [System.IO.Path]::PathSeparator | |
| 62 | + # Filter out empty entries and resolve the full path to account for symbolic links or variables. |
| 63 | + Where-Object { $_ } | ForEach-Object { [System.IO.Path]::GetFullPath($_) } |
| 64 | + |
| 65 | + } # end begin block |
| 66 | + |
| 67 | + process { |
| 68 | + # Process each module name (handles both array input and pipeline input) |
| 69 | + foreach ($ModuleName in $Name) { |
| 70 | + # Get all available modules with this name |
| 71 | + $AllModules = Get-Module -Name $ModuleName -ListAvailable |
| 72 | + |
| 73 | + # Skip and warn if no modules were found for this name. |
| 74 | + if (-not $AllModules) { |
| 75 | + Write-Warning "No module named '$ModuleName' found in PSModulePath." |
| 76 | + continue |
| 77 | + } |
| 78 | + |
| 79 | + # Use a script block to group modules by their base path and find the highest version in each grouped location. |
| 80 | + $ModulesByLocation = $AllModules | Group-Object { |
| 81 | + |
| 82 | + # Get the module's root directory (usually Modules folder) |
| 83 | + $ModulePath = [System.IO.Path]::GetFullPath($_.ModuleBase) |
| 84 | + |
| 85 | + # Find which PSModulePath entry this module's path begins with. |
| 86 | + foreach ($PathEntry in $PSModulePathEntries) { |
| 87 | + # Normalize the path to account for symbolic links or variables within the environment variable. |
| 88 | + $NormalizedPathEntry = [System.IO.Path]::GetFullPath($PathEntry) |
| 89 | + if ($ModulePath.StartsWith($NormalizedPathEntry, [System.StringComparison]::OrdinalIgnoreCase)) { |
| 90 | + # If the module path starts with this PSModulePath entry, return it as the grouping key. |
| 91 | + return $NormalizedPathEntry |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + } # end of Group-Object script block |
| 96 | + |
| 97 | + # Find the highest version module from the first location in PSModulePath order. |
| 98 | + # Initialize the variable at its max value to ensure it is always greater than any valid index. |
| 99 | + $BestLocationIndex = [int]::MaxValue |
| 100 | + $CandidateModule = $null |
| 101 | + |
| 102 | + foreach ($LocationGroup in $ModulesByLocation) { |
| 103 | + # Get the highest version module from this location |
| 104 | + $HighestVersionInLocation = $LocationGroup.Group | Sort-Object Version -Descending | Select-Object -First 1 |
| 105 | + |
| 106 | + # Find this location's index in PSModulePath (initialize at max value as best practice). |
| 107 | + $LocationIndex = [int]::MaxValue |
| 108 | + $LocationPath = $LocationGroup.Name |
| 109 | + |
| 110 | + for ($i = 0; $i -lt $PSModulePathEntries.Count; $i++) { |
| 111 | + # Perform a case-insensitive comparison to find the index of this location in PSModulePath. |
| 112 | + if ($LocationPath.StartsWith($PSModulePathEntries[$i], [System.StringComparison]::OrdinalIgnoreCase)) { |
| 113 | + $LocationIndex = $i |
| 114 | + break |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + # Use this module if it's from an earlier location in PSModulePath. |
| 119 | + if ($LocationIndex -lt $BestLocationIndex) { |
| 120 | + $BestLocationIndex = $LocationIndex |
| 121 | + $CandidateModule = $HighestVersionInLocation |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + # Output the candidate module for this module name |
| 126 | + $CandidateModule |
| 127 | + } |
| 128 | + } # end process block |
| 129 | + |
| 130 | +} |
0 commit comments