Public/Find-DLLInPSModulePath.ps1
|
function Find-DLLInPSModulePath { <# .SYNOPSIS Find DLL files in module paths, filtered by product metadata. .DESCRIPTION Searches PowerShell module paths for DLL files and returns rich objects that include both file metadata and module path context. By default, searches all valid paths from PSModulePath. Scope filtering is cross-platform and classifies each path as CurrentUser, AllUsers, or Unknown based on common PowerShell module roots for Windows, Linux, and macOS. .PARAMETER ProductName The product name to search for in DLL ProductName properties. Supports wildcards. Defaults to 'Microsoft Identity'. .PARAMETER FileName The file name pattern to search for. Supports wildcards. Defaults to '*.dll' to search all DLL files. Use a specific pattern like 'Microsoft.IdentityModel*.dll' to narrow the search. .PARAMETER NewestVersion If specified, only the newest version of each matching DLL will be returned. .PARAMETER Path Locations to search for DLL files. Defaults to all valid directories from PSModulePath. .PARAMETER ExcludeDirectories Directory names to exclude from recursive inspection. .PARAMETER Scope Limits paths to CurrentUser, AllUsers, or Both. Unknown path classifications are included only when Scope is Both. .EXAMPLE Find-DLLInPSModulePath -ProductName "Microsoft Identity" Find all DLLs with 'Microsoft Identity' in their ProductName property within installed PowerShell module locations. .EXAMPLE Find-DLLInPSModulePath -FileName "Microsoft.IdentityModel*.dll" Find all DLL files matching the pattern 'Microsoft.IdentityModel*.dll' that also have 'Microsoft Identity' in their ProductName. .INPUTS None. This function does not accept pipeline input. .OUTPUTS DLLPickle.ModuleDllInfo Rich result objects with file metadata and path classification details. #> [CmdletBinding()] [OutputType('DLLPickle.ModuleDllInfo')] param ( # The product name to search for in DLL ProductName properties. Supports wildcards. Defaults to 'Microsoft Identity'. [Parameter()] [ValidateNotNullOrEmpty()] [string]$ProductName = 'Microsoft Identity', # The file name pattern to search for. Supports wildcards. Defaults to '*.dll'. [Parameter()] [ValidateNotNullOrEmpty()] [string]$FileName = '*.dll', # Locations to search for DLLs. Defaults to all valid directories in the PSModulePath environment variable. [Parameter()] [string[]]$Path = @( $env:PSModulePath -split [System.IO.Path]::PathSeparator | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container -ErrorAction SilentlyContinue) } ), # Directories to exclude from inspection so the process goes faster. [Parameter()] [string[]]$ExcludeDirectories = @('en-US', '.git'), # The module installation scope to search. Valid options are AllUsers, CurrentUser, or Both (default). [Parameter()] [ValidateSet('CurrentUser', 'AllUsers', 'Both')] [string]$Scope = 'Both', # If specified, only the newest version of each matching DLL will be returned. [switch]$NewestVersion ) $NormalizePath = { param ([string]$InputPath) if ([string]::IsNullOrWhiteSpace($InputPath)) { return $null } return $InputPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) } $CurrentUserRoots = @( (Join-Path -Path $HOME -ChildPath 'Documents\PowerShell\Modules') (Join-Path -Path $HOME -ChildPath 'Documents\WindowsPowerShell\Modules') (Join-Path -Path $HOME -ChildPath '.local/share/powershell/Modules') ) | Where-Object { $_ } $AllUsersRoots = @( (Join-Path -Path $PSHOME -ChildPath 'Modules') (Join-Path -Path '/usr/local/share' -ChildPath 'powershell/Modules') (Join-Path -Path '/usr/share' -ChildPath 'powershell/Modules') ) | Where-Object { $_ } if ($env:ProgramFiles) { $AllUsersRoots += Join-Path -Path $env:ProgramFiles -ChildPath 'PowerShell\Modules' } if ($env:ProgramFiles -and $PSVersionTable.PSVersion.Major -lt 6) { $AllUsersRoots += Join-Path -Path $env:ProgramFiles -ChildPath 'WindowsPowerShell\Modules' } if (${env:ProgramFiles(x86)}) { $AllUsersRoots += Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'PowerShell\Modules' } $CurrentUserRoots = @($CurrentUserRoots | ForEach-Object { & $NormalizePath $_ } | Where-Object { $_ } | Select-Object -Unique) $AllUsersRoots = @($AllUsersRoots | ForEach-Object { & $NormalizePath $_ } | Where-Object { $_ } | Select-Object -Unique) $KnownModuleBaseRoots = @($CurrentUserRoots + $AllUsersRoots) $GetPathScope = { param ([string]$PathItem) $NormalizedPath = & $NormalizePath $PathItem if (-not $NormalizedPath) { return 'Unknown' } foreach ($CurrentUserRoot in $CurrentUserRoots) { if ($NormalizedPath.StartsWith($CurrentUserRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return 'CurrentUser' } } foreach ($AllUsersRoot in $AllUsersRoots) { if ($NormalizedPath.StartsWith($AllUsersRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return 'AllUsers' } } return 'Unknown' } $GetModuleRoot = { param ( [string]$FilePath, [string[]]$SearchRoots ) $NormalizedFilePath = & $NormalizePath $FilePath if (-not $NormalizedFilePath) { return $null } $MatchedRoot = $null foreach ($RootPath in $SearchRoots) { $NormalizedRootPath = & $NormalizePath $RootPath if (-not $NormalizedRootPath) { continue } if ($NormalizedFilePath.StartsWith($NormalizedRootPath, [System.StringComparison]::OrdinalIgnoreCase)) { if (-not $MatchedRoot -or $NormalizedRootPath.Length -gt $MatchedRoot.Length) { $MatchedRoot = $NormalizedRootPath } } } if (-not $MatchedRoot) { return $null } $RelativePath = $NormalizedFilePath.Substring($MatchedRoot.Length).TrimStart([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) if ([string]::IsNullOrWhiteSpace($RelativePath)) { return $MatchedRoot } # If the matched root is already a module path supplied by the caller (not a known module base), # treat it as the module root directly instead of appending child segments. if ($KnownModuleBaseRoots -notcontains $MatchedRoot) { return $MatchedRoot } $FirstPathSegment = ($RelativePath -split '[\\/]', 2)[0] if ([string]::IsNullOrWhiteSpace($FirstPathSegment)) { return $MatchedRoot } return Join-Path -Path $MatchedRoot -ChildPath $FirstPathSegment } $ValidPaths = [System.Collections.Generic.List[string]]::new() foreach ($PathItem in $Path) { if (Test-Path -LiteralPath $PathItem -PathType Container) { [void]$ValidPaths.Add($PathItem) } else { Write-Warning "Path does not exist or is not accessible: $PathItem" } } if ($ValidPaths.Count -eq 0) { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new('No valid module paths were provided.'), 'NoValidModulePaths', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Path ) $PSCmdlet.WriteError($ErrorRecord) return } $ScopedPaths = [System.Collections.Generic.List[object]]::new() if ($Scope -eq 'CurrentUser') { foreach ($PathItem in $ValidPaths) { $PathScope = & $GetPathScope $PathItem if ($PathScope -eq 'CurrentUser') { [void]$ScopedPaths.Add([PSCustomObject]@{ Path = $PathItem; Scope = $PathScope }) } } } elseif ($Scope -eq 'AllUsers') { foreach ($PathItem in $ValidPaths) { $PathScope = & $GetPathScope $PathItem if ($PathScope -eq 'AllUsers') { [void]$ScopedPaths.Add([PSCustomObject]@{ Path = $PathItem; Scope = $PathScope }) } } } else { foreach ($PathItem in $ValidPaths) { [void]$ScopedPaths.Add([PSCustomObject]@{ Path = $PathItem; Scope = (& $GetPathScope $PathItem) }) } } if ($ScopedPaths.Count -eq 0) { $ErrorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Exception]::new("Scope '$Scope' produced no valid paths."), 'ScopePathsNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Scope ) $PSCmdlet.WriteError($ErrorRecord) return } $ScopedPathValues = @($ScopedPaths | Select-Object -ExpandProperty Path -Unique) Write-Verbose "Enumerating DLLs matching file pattern '$FileName' with ProductName containing '$ProductName' under:`n - $($ScopedPathValues -join "`n - ")" $ProductNamePattern = if ($ProductName -match '[\*\?\[]') { $ProductName } else { "*$ProductName*" } $Results = @( Get-ChildItem -Path $ScopedPathValues -Filter $FileName -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Directory.Name -notin $ExcludeDirectories } | ForEach-Object { $VersionInfo = $_.VersionInfo if ($null -eq $VersionInfo) { Write-Verbose "Skipping '$($_.FullName)' because VersionInfo metadata is missing." return } if ($VersionInfo.ProductName -like $ProductNamePattern) { $DirectoryName = if ($_.DirectoryName) { $_.DirectoryName } else { Split-Path -Path $_.FullName -Parent } $ModuleRoot = & $GetModuleRoot $_.FullName $ScopedPathValues if (-not $ModuleRoot -and $_.Directory -and $_.Directory.Parent) { $ModuleRoot = $_.Directory.Parent.FullName } $PathScope = (& $GetPathScope $DirectoryName) [PSCustomObject]@{ PSTypeName = 'DLLPickle.ModuleDllInfo' FileName = $_.Name FullName = $_.FullName Directory = $DirectoryName ModuleRoot = $ModuleRoot PathScope = $PathScope ProductName = $VersionInfo.ProductName ProductVersion = $VersionInfo.ProductVersion InternalName = $VersionInfo.InternalName OriginalFilename = $VersionInfo.OriginalFilename FileVersion = $VersionInfo.FileVersion VersionInfo = $VersionInfo } } } ) if ($Results.Count -eq 0) { Write-Warning "No DLLs found matching file pattern '$FileName' with ProductName containing '*$ProductName*'." } if ($NewestVersion) { $Results = @( $Results | Group-Object -Property OriginalFilename | ForEach-Object { $_.Group | Sort-Object -Property @{ Expression = { try { [version]$_.FileVersion } catch { [version]'0.0.0.0' } } } -Descending | Select-Object -First 1 } | Sort-Object -Property InternalName ) } $Results | Sort-Object -Property InternalName } |