Skip to content

Commit 74ffa25

Browse files
committed
Add Get-ModuleImportCandidate function to determine the appropriate module version for Import-Module
1 parent 3bbb7ec commit 74ffa25

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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

Comments
 (0)