Commands/Aspects/Module-Aspects.ps.ps1

Aspect function ModuleExtensionType {
    <#
    .SYNOPSIS
        Outputs a module's extension types
    .DESCRIPTION
        Outputs the extension types defined in a module's manifest.
    .EXAMPLE
        # Outputs a PSObject with information about extension command types.
        
        # The two primary pieces of information are the `.Name` and `.Pattern`.
        Aspect.ModuleExtensionType -Module PipeScript # Should -BeOfType ([PSObject])
    #>

    [Alias('Aspect.ModuleCommandTypes','Aspect.ModuleCommandType','Aspect.ModuleExtensionTypes')]
    param(
    # The name of a module, or a module info object.
    [vbn(Mandatory)]
    [ValidateTypes(TypeName={[string],[Management.Automation.PSModuleInfo]})]
    $Module
    )

    begin {
        $ExtensionCollectionNames = 
            "Extension", "Command", "Cmdlet", "Function", "Alias", "Script", "Application", "File","Configuration"
        $ExtensionCollectionNames = @($ExtensionCollectionNames -replace '.+$','${0}Type') + @($ExtensionCollectionNames -replace '.+$','${0}Types')
    }

    process {
        #region Resolve Module Info
        if ($Module -is [string]) {
            $Module = Get-Module $Module
        }
        $ModuleInfo = $module                
        return if -not $ModuleInfo
        #endregion Resolve Module Info

        #region Check Cache and Hopefully Return
        if (-not $script:ModuleExtensionTypeCache) {
            $script:ModuleExtensionTypeCache = @{}
        }
        
        if ($script:ModuleExtensionTypeCache[$ModuleInfo]) {
            return $script:ModuleExtensionTypeCache[$ModuleInfo]
        }
        #endregion Check Cache and Hopefully Return

        #region Find Extension Types
        $modulePrivateData  = $ModuleInfo.PrivateData

        $SortedExtensionTypes = [Ordered]@{}
        foreach ($TypeOfExtensionCollection in $ExtensionCollectionNames) {
            $moduleExtensionTypes = 
                if ($modulePrivateData.$TypeOfExtensionCollection) {
                    $modulePrivateData.$TypeOfExtensionCollection
                } elseif ($modulePrivateData.PSData.$TypeOfExtensionCollection) {
                    $modulePrivateData.PSData.$TypeOfExtensionCollection
                } else {
                    $null
                }

            continue if -not $moduleExtensionTypes

            foreach ($commandType in @($ModuleExtensionTypes.GetEnumerator() | Sort-Object Key)) {
                if ($commandType.Value -is [Collections.IDictionary]) {
                    if (-not $commandType.Value.Name) {
                        $commandType.Value["Name"] = $commandType.Key
                    }
                    if (-not $commandType.Value.PSTypeName) {
                        $commandType.Value["PSTypeName"] = "$($module.Name).ExtensionCommandType"
                    }
                    $SortedExtensionTypes[$commandType.Name] = $commandType.Value
                } else {
                    $SortedExtensionTypes[$commandType.Name] = [Ordered]@{
                        PSTypeName = "$($module.Name).ExtensionCommandType"
                        Name    = $commandType.Key
                        Pattern = $commandType.Value
                    }
                }
                if ($TypeOfExtensionCollection -notmatch '(?>Extension|Command|Cmdlet)') {
                    $SortedExtensionTypes[$commandType.Name].CommandType = $TypeOfExtensionCollection -replace 'Type(?:s)?$'
                } elseif ($TypeOfExtensionCollection -match 'Cmdlet') {
                    $SortedExtensionTypes[$commandType.Name].CommandType = "(?>Alias|Function|Filter|Cmdlet)"
                }
            }
        }
        
        $SortedExtensionTypes.PSTypeName="$($Module.Name).ExtensionCommandTypes"
        
        $script:ModuleExtensionTypeCache[$ModuleInfo] = [PSCustomObject]$SortedExtensionTypes
        $script:ModuleExtensionTypeCache[$ModuleInfo]
        #endregion Find Extension Types

    }    
}

Aspect function ModuleExtensionPattern {
    <#
    .SYNOPSIS
        Outputs a module's extension pattern
    .DESCRIPTION
        Outputs a regular expression that will match any possible pattern.
    .EXAMPLE
        Aspect.ModuleCommandPattern -Module PipeScript # Should -BeOfType ([Regex])
    #>

    [Alias('Aspect.ModuleCommandPattern')]
    param(
    # The name of a module, or a module info object.
    [vbn(Mandatory)]
    [ValidateTypes(TypeName={[string],[Management.Automation.PSModuleInfo]})]
    $Module,

    # The suffix to apply to each named capture.
    # Defaults to '_Command'
    [vbn()]
    [string]
    $Suffix = '_Command',

    # The prefix to apply to each named capture.
    [vbn()]
    [string]
    $Prefix
    )

    process {
        if ($Module -is [string]) {
            $Module = Get-Module $Module
        }
        $ModuleInfo = $module


        #region Search for Module Extension Types
        return if -not $ModuleInfo
        $ModuleExtensionTypes = Aspect.ModuleExtensionTypes -Module $moduleInfo

        
        return if -not $ModuleExtensionTypes
            
        # With some clever understanding of Regular expressions, we can make match any/all of our potential command types.
        # Essentially: Regular Expressions can look ahead (matching without changing the position), and be optional.
        # So we can say "any/all" by making a series of optional lookaheads.
        
        # We'll go thru each pattern in order
        $combinedRegex = @(foreach ($categoryExtensionTypeInfo in @($ModuleExtensionTypes.psobject.properties)) {
            $categoryPattern = $categoryExtensionTypeInfo.Value.Pattern                
            # ( and skip anyone that does not have a pattern)
            continue if -not $categoryPattern

            '(?=' + # Start a lookahead
                '.{0,}' + # match any or no characters
                # followed by the command pattern
                "(?<$Prefix$($categoryExtensionTypeInfo.Name -replace '\p{P}', '_')$Suffix>$categoryPattern)" +
                ')?' # made optional
        }) -join [Environment]::NewLine

        # Now that we've combined the whole thing, make it a Regex and output it.
        [Regex]::new("$combinedRegex", 'IgnoreCase,IgnorePatternWhitespace','00:00:01')
    }
}

Aspect function ModuleExtensionCommand {
    <#
    .SYNOPSIS
        Returns a module's extended commands
    .DESCRIPTION
        Returns the commands or scripts in a module that match the module command pattern.

        Each returned script will be decorated with the typename(s) that match,
        so that the extended commands can be augmented by the extended types system.
    .LINK
        Aspect.ModuleCommandPattern
    .EXAMPLE
        Aspect.ModuleExtensionCommand -Module PipeScript # Should -BeOfType ([Management.Automation.CommandInfo])
    #>

    [Alias('Aspect.ModuleExtendedCommand')]
    param(
    # The name of a module, or a module info object.
    [vbn(Mandatory)]
    [ValidateTypes(TypeName={[string],[Management.Automation.PSModuleInfo]})]
    $Module,
    
    # A list of commands.
    # If this is provided, each command that is a valid extension will be returned.
    [vbn()]
    [Management.Automation.CommandInfo[]]
    $Commands,

    # The suffix to apply to each named capture.
    # Defaults to '_Command'
    [vbn()]
    [string]
    $Suffix = '_Command',

    # The prefix to apply to each named capture.
    [vbn()]
    [string]
    $Prefix,

    # The file path(s). If provided, will look for commands within these paths.
    [vbn()]
    [Alias('Fullname')]    
    $FilePath,

    # The PowerShell command type. If this is provided, will only get commands of this type.
    [vbn()]
    [Management.Automation.CommandTypes]
    $CommandType,

    # The base PSTypeName(s).
    # If provided, any commands that match the pattern will apply these typenames, too.
    [string[]]
    $PSTypeName
    )

    process {        
        if ($Module -is [string]) {
            $Module = Get-Module $Module
        }
        $ModuleInfo = $module

        return if -not $ModuleInfo
        
        $ModuleCommandPattern = Aspect.ModuleExtensionPattern $ModuleInfo -Prefix $prefix -Suffix $Suffix
        $ModuleCommandTypes   = Aspect.ModuleExtensionType $ModuleInfo
        
        $commands    =
            @(
            if ($PSBoundParameters['Commands']) {
                $commands
            }
            elseif ($PSBoundParameters['FilePath']) {
                if (-not $commandType) {
                    $commandType = 'Application,ExternalScript'
                }
                foreach ($file in Get-ChildItem -File -Path $PSBoundParameters['FilePath'] -Recurse) {
                    $ExecutionContext.SessionState.InvokeCommand.GetCommand($file.FullName, $commandType)
                }
            } else {
                if (-not $CommandType) {
                    $commandType = 'Function,Alias,Filter,Cmdlet'
                }
                $ExecutionContext.SessionState.InvokeCommand.GetCommands('*', $commandType, $true)
            })

        :nextCommand foreach ($cmd in $commands) {            
            $matched = $ModuleCommandPattern.Match("$cmd")
            if (-not $matched.Success) { continue }
            $NamedGroupMatch = $false
            :nextCommandType foreach ($group in $matched.Groups) {
                if (-not $group.Success) { continue }
                if ($null -ne ($group.Name -as [int])) { continue }                
                $CommandTypeName     = $group.Name.Replace('_','.')
                $ThisCommandsType    = $ModuleCommandTypes.($group.Name -replace "^$prefix" -replace "$suffix$")
                if ($ThisCommandsType) {
                    $ThisTypeFilter = @($ThisCommandsType.CommandType,$ThisCommandsType.CommandTypes -ne $null)[0]
                    if ($ThisTypeFilter -and ($cmd.CommandType -notmatch $ThisTypeFilter)) {
                        continue nextCommandType
                    }
                    $ThisExcludeFilter = @($ThisCommandsType.ExcludeCommandType,$ThisCommandsType.ExcludeCommandTypes -ne $null)[0]
                    if ($ThisExcludeFilter -and ($cmd.CommandType -match $ThisExcludeFilter)) {
                        continue nextCommandType
                    }
                }
                $NamedGroupMatch     = $true
                if ($PSTypeName) {
                    foreach ($psuedoType in $PSTypeName) {
                        if ($cmd.pstypenames -notcontains $psuedoType) {
                            $cmd.pstypenames.insert(0, $psuedoType)        
                        }
                    }
                }
                if ($cmd.pstypenames -notcontains $CommandTypeName) {
                    $cmd.pstypenames.insert(0, $CommandTypeName)
                }
            }
            if ($NamedGroupMatch) {
                $cmd
            }
        }
    }
}