Commands/Aspects/DynamicParameter-Aspects.ps.ps1

Aspect function DynamicParameter {
    <#
    .SYNOPSIS
        Dynamic Parameter Aspect
    .DESCRIPTION
        The Dynamic Parameter Aspect is used to add dynamic parameters, well, dynamically.

        It can create dynamic parameters from one or more input objects or scripts.
    .EXAMPLE
        Get-Command Get-Command |
            Aspect.DynamicParameter
    .EXAMPLE
        Get-Command Get-Process |
            Aspect.DynamicParameter -IncludeParameter Name # Select -Expand Keys | Should -Be Name
    #>

    [Alias('Aspect.DynamicParameters')]
    param(
    # The InputObject.
    # This can be anything, but will be ignored unless it is a `[ScriptBlock]` or `[Management.Automation.CommandInfo]`.
    [vfp()]
    $InputObject,

    # The name of the parameter set the dynamic parameters will be placed into.
    [string]
    $ParameterSetName,

    # The positional offset. If this is provided, all positional parameters will be shifted by this number.
    # For example, if -PositionOffset is 1, the first parameter would become the second parameter (and so on)
    [int]
    $PositionOffset,

    # If set, will make all dynamic parameters non-mandatory.
    [switch]
    $NoMandatory,

    # If provided, will check that dynamic parameters are valid for a given command.
    # If the [Management.Automation.CmdletAttribute]
    [string[]]
    $commandList,

    # If provided, will include only these parameters from the input.
    [string[]]
    $IncludeParameter,

    # If provided, will exclude these parameters from the input.
    [string[]]
    $ExcludeParameter,

    # If provided, will make a blank parameter for every -PositionOffset.
    # This is so, presumably, whatever has already been provided in these positions will bind correctly.
    # The name of this parameter, by default, will be "ArgumentN" (for example, Argument1)
    [switch]
    $BlankParameter,

    # The name of the blank parameter.
    # If there is a -PositionOffset, this will make a blank parameter by this name for the position.
    [string[]]
    $BlankParameterName = "Argument"
    )

    begin {
        $inputQueue = [Collections.Queue]::new()
    }
    process {
        $inputQueue.Enqueue($InputObject)
    }

    end {        
        $DynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new()

        if ($PositionOffset -and 
            ($BlankParameter -or $PSBoundParameters['BlankParameterName'])) {
            for ($pos =0; $pos -lt $PositionOffset; $pos++) {
                $paramName = $BlankParameterName[$pos]
                if (-not $paramName) {
                    $paramName = "$($BlankParameterName[-1])$pos"
                }                
                $DynamicParameters.Add($paramName, 
                    [Management.Automation.RuntimeDefinedParameter]::new(
                        $paramName,
                        [PSObject], 
                        @(
                            $paramAttr = [Management.Automation.ParameterAttribute]::new()
                            $paramAttr.Position = $pos
                            $paramAttr
                        )
                    )
                )
            }
        }
        while ($inputQueue.Count) {
            $InputObject = $inputQueue.Dequeue()

            $inputCmdMetaData = 
                if ($inputObject -is [Management.Automation.CommandInfo]) {
                    [Management.Automation.CommandMetaData]$InputObject
                }
                elseif ($inputObject -is [scriptblock]) {
                    $function:TempFunction = $InputObject
                    [Management.Automation.CommandMetaData]$ExecutionContext.SessionState.InvokeCommand.GetCommand('TempFunction','Function')
                }

            continue if -not $inputCmdMetaData
                                        
            :nextDynamicParameter foreach ($paramName in $inputCmdMetaData.Parameters.Keys) {

                if ($ExcludeParameter) {
                    foreach ($exclude in $ExcludeParameter) {
                        if ($paramName -like $exclude) { continue nextDynamicParameter}
                    }
                }
                if ($IncludeParameter) {
                    $shouldInclude = 
                        foreach ($include in $IncludeParameter) {
                            if ($paramName -like $include) { $true;break}
                        }
                    if (-not $shouldInclude) { continue nextDynamicParameter }
                }

                $attrList = [Collections.Generic.List[Attribute]]::new()
                $validCommandNames = @()
                foreach ($attr in $inputCmdMetaData.Parameters[$paramName].attributes) {
                    if ($attr -isnot [Management.Automation.ParameterAttribute]) {
                        # we can passthru any non-parameter attributes
                        $attrList.Add($attr)
                        # (`[Management.Automation.CmdletAttribute]` is special, as it indicates if the parameter applies to a command)
                        if ($attr -is [Management.Automation.CmdletAttribute] -and $commandList) {
                            $validCommandNames += (
                                ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s')
                            ) -replace '^\-' -replace '\-$'
                        }
                    } else {
                        # but parameter attributes need to copied.
                        $attrCopy = [Management.Automation.ParameterAttribute]::new()
                        # (Side note: without a .Clone, copying is tedious.)
                        foreach ($prop in $attrCopy.GetType().GetProperties('Instance,Public')) {
                            if (-not $prop.CanWrite) { continue }
                            if ($null -ne $attr.($prop.Name)) {
                                $attrCopy.($prop.Name) = $attr.($prop.Name)
                            }
                        }

                        $attrCopy.ParameterSetName =
                            if ($ParameterSetName) {
                                $ParameterSetName
                            }
                            else {
                                $defaultParamSetName = $inputCmdMetaData.DefaultParameterSetName
                                if ($attrCopy.ParameterSetName -ne '__AllParameterSets') {
                                    $attrCopy.ParameterSetName
                                }
                                elseif ($defaultParamSetName) {
                                    $defaultParamSetName
                                }
                                elseif ($this -is [Management.Automation.FunctionInfo]) {
                                    $this.Name
                                } elseif ($this -is [Management.Automation.ExternalScriptInfo]) {
                                    $this.Source
                                }
                            }

                        if ($NoMandatory -and $attrCopy.Mandatory) {
                            $attrCopy.Mandatory = $false
                        }

                        if ($PositionOffset -and $attr.Position -ge 0) {
                            $attrCopy.Position += $PositionOffset
                        }
                        $attrList.Add($attrCopy)
                    }
                }

                if ($commandList -and $validCommandNames) {
                    :CheckCommandValidity do {
                        foreach ($vc in $validCommandNames) {
                            if ($commandList -match $vc) { break CheckCommandValidity }
                        }
                        continue nextDynamicParameter
                    } while ($false)
                }
                
                if ($DynamicParameters.ContainsKey($paramName)) {                    
                    $DynamicParameters[$paramName].ParameterType = [PSObject]                    
                    foreach ($attr in $attrList) {                        
                        $DynamicParameters[$paramName].Attributes.Add($attr)
                    }
                } else {
                    $DynamicParameters.Add($paramName, [Management.Automation.RuntimeDefinedParameter]::new(
                        $inputCmdMetaData.Parameters[$paramName].Name,
                        $inputCmdMetaData.Parameters[$paramName].ParameterType,
                        $attrList
                    ))
                }
            }
        }
        $DynamicParameters
    }
}