public/Resolve-DynamicFunctionDefinition.ps1
$code = @' using System; using System.Collections.Generic; using System.Management.Automation; public class DynamicAttribute : System.Attribute { private ScriptBlock sb; public DynamicAttribute(ScriptBlock condition) { sb = condition; } } '@ $null = Add-Type -TypeDefinition $code *>&1 function Resolve-DynamicFunctionDefinition { <# .SYNOPSIS Generates a scriptblock defining an interpreted function based on the provided function info .DESCRIPTION Module adds a new attribute named [Dynamic()] that can be used to turn static parameters into dynamic parameters. The attribute definition will be defined above a standard parameter definition and must contain a scriptblock that evaluates truthiness. Resolve-DynamicFunctionDefinition will then interpret the [Dynamic(...)] attributes into standard PowerShell dynamic parameter declarations and return back a scriptblock with the new function definition. Example Dynamic Parameter Declaration: [Dynamic({$OtherParameter -match "(Value1|Value2)"})] [Parameter(Mandatory)] [string]$Name .EXAMPLE Resolve-DynamicFunctionDefinition -FunctionInfo Value Evaluates function definition for dynamic parameters and returns a new scriptblock containing a function definition to properly handle the defined dynamic parameters .NOTES Inspired in large part by Dr. Tobias Weltner (https://github.com/TobiasPSP/) and his amazing work at https://powershell.one/ #> [CmdletBinding()] param ( # # A scriptblock with a param() block. Assign the attribute [Dynamic()] to all parameters that you want to convert to a dynamic parameter. # [Parameter(Mandatory, ValueFromPipeline)] # [ScriptBlock]$ScriptBlock, # Name of the function to be created. Can be anything, should adhere to common Verb-Noun syntax. [Parameter(Mandatory)] [System.Management.Automation.FunctionInfo]$FunctionInfo ) begin { # common parameters $commonParameters = @( 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable' ) } process { try { # collect generated code: [System.Text.StringBuilder]$result = '' # store parameter default values: $defaultValues = @{} # store list of dynamic parameters: $dynParamList = [System.Collections.ObjectModel.Collection[string]]::new() # store list of static parameters: $paramList = [System.Collections.ObjectModel.Collection[string]]::new() # store list of pipeline-aware parameters: $pipelineAttribs = 'ValueFromPipeline', 'ValueFromPipelineByPropertyName' $pipelineParamList = [System.Collections.ObjectModel.Collection[string]]::new() # store list of standard parameters: $standardParamList = [System.Collections.ObjectModel.Collection[string]]::new() $null = $result.AppendLine("function $($FunctionInfo.Name)") $null = $result.AppendLine('{') if (-not $FunctionInfo.CmdletBinding) { Write-Warning "$($FunctionInfo.Name) is not an advanced function, using original function content" $null = $result.AppendLine($FunctionInfo.ScriptBlock) $null = $result.Append('}') return [ScriptBlock]::Create($result) } # extract the content of the param() block from the submitted scriptblock: $param = $FunctionInfo.ScriptBlock.ast.Body.ParamBlock[0] # add attributes foreach ($_ in $param.Attributes) { $null = $result.AppendLine(' ' + $_.Extent.Text) } $null = $result.AppendLine(@' param ( ##StaticParams## ) dynamicparam { # create container for all dynamically created parameters: $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() '@) $param.Parameters | ForEach-Object { $parameter = $_ # check to see if this parameter was decorated with a [Dynamic()] attribute: $dynamicAttribute = $parameter.Attributes.Where{ $_.TypeName.FullName -eq 'Dynamic' } | Select-Object -First 1 # if so, add to dynamic parameters: if ($dynamicAttribute) { $condition = $dynamicAttribute.PositionalArguments[0].Extent.Text -replace '^{' -replace '}$' $name = $parameter.Name.VariablePath.UserPath $dynParamList.Add($name) $null = $result.AppendLine() $null = $result.AppendLine(' <#') $null = $result.AppendLine(" region Start Parameter -${name} ####") $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') $null = $result.AppendLine() if ($condition) { $null = $result.AppendLine(" if ($condition) {") } $null = $result.AppendLine(" # create container storing all attributes for parameter -$name") $null = $result.AppendLine(' $attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()') $null = $result.AppendLine('') $conflicts = $commonParameters -like "$name*" if ($conflicts.Count -gt 0) { throw ('Parameter -{0} conflicts with built-in parameters {1}. Rename -{0}.' -f $name, ('-' + ($conflicts -join ', -'))) } $defaultValue = $parameter.DefaultValue.Extent.Text $theType = 'Object' $hasParameterAttribute = $false $parameter.Attributes | ForEach-Object { $attribute = $_ switch ($attribute.GetType().FullName) { 'System.Management.Automation.Language.TypeConstraintAst' { $theType = $attribute.TypeName.FullName } 'System.Management.Automation.Language.AttributeAst' { $typeName = $attribute.TypeName.FullName if ($typename -ne 'Dynamic') { if (!$hasParameterAttribute -and $typename -eq 'Parameter') { $hasParameterAttribute = $true } [string]$positionals = $attribute.PositionalArguments.Extent.Text -join ',' $null = $result.AppendLine((' # Define attribute [{0}()]:' -f $attribute.TypeName.FullName)) $null = $result.AppendLine((' $attrib = [{0}]::new({1})' -f $attribute.TypeName.FullName, $positionals)) $attribute.NamedArguments | ForEach-Object { $namedAttributeExpression = $_.ToString() if ($_.ExpressionOmitted) { $namedAttributeExpression += '=$true' } $null = $result.AppendLine((' $attrib.{0}' -f $namedAttributeExpression)) # if parameter is pipeline-aware, remember it: if ($_.ArgumentName -in $pipelineAttribs -and $pipelineParamList.Contains($Name) -eq $false) { $pipelineParamList.Add($name) } else { $standardParamList.Add($name) } } $null = $result.AppendLine(' $attributeCollection.Add($attrib)') $null = $result.AppendLine('') } } default { Write-Warning "Unexpected Type: $_" } } } if (!$hasParameterAttribute) { $null = $result.AppendLine(' # Define attribute [Parameter()]:') $null = $result.AppendLine(' $attrib = [Parameter]::new()') $null = $result.AppendLine(' $attributeCollection.Add($attrib)') $null = $result.AppendLine('') } $null = $result.AppendLine(' # compose dynamic parameter:') $null = $result.AppendLine((' $dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new({0},{1},$attributeCollection)' -f "'$Name'", "[$theType]")) if ($theType -eq 'Object') { Write-Warning ('Parameter -{0} currently is of type [Object]. Using an appropriate type constraint like [string], [int], [datetime] etc in your parameter definition is recommended.' -f $Name) } # store parameter default value: if ($null -ne $defaultValue) { $defaultValues[$name] = $defaultValue } $null = $result.AppendLine() $null = $result.AppendLine(' # add parameter to parameter collection:') $null = $result.AppendLine((' $paramDictionary.Add({0},$dynParam)' -f "'$Name'")) if ($condition) { $null = $result.AppendLine(' }') } $null = $result.AppendLine() $null = $result.AppendLine(' <#') $null = $result.AppendLine(" endregion End Parameter -${name} ####") $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') $null = $result.AppendLine() } # else, add to static parameters else { $paramList.Add($parameter.Extent.Text) } } # determine the maximum parameter name length for formatting purposes: $longest = 0 foreach ($_ in $dynParamList) { $longest = [Math]::Max($longest, $_.Length) } $null = $result.AppendLine(@' # return dynamic parameter collection: $paramDictionary } begin { '@) $beginBlock = $FunctionInfo.ScriptBlock.Ast.Body.BeginBlock.extent.Text $beginBlockContent = $beginBlock -replace "^begin\s{\s?`n?" -replace '.*}$' if ($standardParamList.Count) { $null = $result.AppendLine(' <#') $null = $result.AppendLine(' region initialize variables for dynamic parameters') $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') $null = $result.AppendLine() foreach ($varName in $dynParamList) { $null = $result.AppendLine((' if($PSBoundParameters.ContainsKey(''{0}'')) {{ ${0} = $PSBoundParameters[''{0}''] }}' -f $varName)) if ($defaultValues.ContainsKey($varName)) { $null = $result.AppendLine((' else {{ ${0} = {1} }}' -f $varName, $defaultValues[$varName])) } else { $null = $result.AppendLine((' else {{ ${0} = $null}}' -f $varName)) } $null = $result.AppendLine() } $null = $result.AppendLine(' <#') $null = $result.AppendLine(' endregion initialize variables for dynamic parameters') $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') if (-not [string]::IsNullOrWhiteSpace($beginBlockContent)) { $null = $result.AppendLine() } } if (-not [string]::IsNullOrWhiteSpace($beginBlockContent)) { $null = $result.AppendLine(@" $beginBlockContent "@) } $null = $result.AppendLine(@" } "@) $processBlock = $FunctionInfo.ScriptBlock.Ast.Body.ProcessBlock.extent.Text $processBlockContent = $processBlock -replace "^process\s{\s?`n?" -replace '.*}$' if (-not [string]::IsNullOrWhiteSpace($processBlock) -or $pipelineParamList.Count) { $null = $result.AppendLine(' process {') if ($pipelineParamList.Count) { $null = $result.AppendLine(' <#') $null = $result.AppendLine(' region update variables for pipeline-aware parameters:') $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') $null = $result.AppendLine() foreach ($varName in $pipelineParamList) { $null = $result.AppendLine((' if ($PSBoundParameters.ContainsKey(''{0}'')) {{ ${0} = $PSBoundParameters[''{0}''] }}' -f $varName)) $null = $result.AppendLine() } $null = $result.AppendLine(' <#') $null = $result.AppendLine(' endregion update variables for pipeline-aware parameters') $null = $result.AppendLine(' created programmatically via Resolve-DynamicFunctionDefinition') $null = $result.AppendLine(' #>') if (-not [string]::IsNullOrWhiteSpace($processBlockContent)) { $null = $result.AppendLine() } } $null = $result.AppendLine("$processBlockContent }") } $endBlock = $FunctionInfo.ScriptBlock.Ast.Body.EndBlock.extent $processBlockContent = $endBlock -replace '^end\s{\s?`n?' -replace '.*}$' if (-not [string]::IsNullOrWhiteSpace($endBlock)) { $null = $result.AppendLine($endBlock) } $null = $result.AppendLine('}') # insert list of static parameters (if any present) # into param() inside the composed code: # turn array of parameters in comma-separated list: $staticParams = $paramList -join ", `r`n`r`n " # replace placeholder in the result with the static parameter list: $null = $result.Replace('##StaticParams##', $staticParams) # return composed script code, this will validate the compiled code return [ScriptBlock]::Create($result) } catch { $PSCmdlet.ThrowTerminatingError($_) } } } |