Private/Select-MatchingRule.ps1

function New-NamePattern {
    <#
    .SYNOPSIS
        Pre-compiles a case-insensitive name pattern (exact name or * ? wildcards).
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only creates an in-memory object.')]
    [CmdletBinding()]
    [OutputType([System.Management.Automation.WildcardPattern])]
    param(
        [Parameter(Mandatory)]
        [string]$Pattern
    )
    # WildcardPattern also handles exact names; escape [ and ] so they are matched literally.
    $escaped = $Pattern.Replace('`', '``').Replace('[', '`[').Replace(']', '`]')
    New-Object System.Management.Automation.WildcardPattern -ArgumentList $escaped, ([System.Management.Automation.WildcardOptions]::IgnoreCase)
}

function Test-NamePattern {
    <#
    .SYNOPSIS
        Case-insensitive name comparison that supports * and ? wildcards.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$Pattern
    )
    return (New-NamePattern -Pattern $Pattern).IsMatch($Name)
}

function New-RuleIndex {
    <#
    .SYNOPSIS
        Builds a lookup structure for name-based rules (Command or Module) so matching stays fast.
    .DESCRIPTION
        Exact names go into a case-insensitive dictionary (O(1) lookup). Wildcard rules are kept in a
        short list ordered from most to least specific (longest pattern first).
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only creates an in-memory object.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Rules
    )

    $exact = New-Object 'System.Collections.Generic.Dictionary[string,object]' ([System.StringComparer]::OrdinalIgnoreCase)
    $wildcards = New-Object System.Collections.Generic.List[object]
    foreach ($rule in $Rules) {
        if ($rule.IsWildcard) {
            $wildcards.Add($rule)
        }
        elseif (-not $exact.ContainsKey($rule.Pattern)) {
            $exact[$rule.Pattern] = $rule
        }
    }
    @{
        Exact     = $exact
        Wildcards = @($wildcards | Sort-Object -Property @{ Expression = { $_.Pattern.Length }; Descending = $true })
        Count     = $Rules.Count
    }
}

function Select-MatchingRule {
    <#
    .SYNOPSIS
        Returns the single most specific name-based rule (Command or Module) that matches a name.
    .DESCRIPTION
        An exact rule (e.g. 'Get-MsolUser') always wins over a wildcard catch-all (e.g. '*-Msol*').
        Between several wildcard rules, the longest pattern is considered the most specific.
        Names listed in a rule's 'exclude' never match that rule.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Index,

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Name
    )

    if ($Index.Count -eq 0 -or [string]::IsNullOrEmpty($Name)) {
        return $null
    }

    $rule = $null
    if ($Index.Exact.TryGetValue($Name, [ref]$rule) -and -not (Test-RuleExclusion -Rule $rule -Name $Name)) {
        return $rule
    }
    foreach ($candidate in $Index.Wildcards) {
        if ($candidate.Matcher.IsMatch($Name) -and -not (Test-RuleExclusion -Rule $candidate -Name $Name)) {
            return $candidate
        }
    }
    return $null
}

function Test-RuleExclusion {
    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory)]
        [object]$Rule,

        [Parameter(Mandatory)]
        [string]$Name
    )
    foreach ($matcher in $Rule.ExcludeMatchers) {
        if ($matcher.IsMatch($Name)) {
            return $true
        }
    }
    return $false
}