Help with Metaprogramming how to create a new Parameter from scratch

This topic contains 2 replies, has 2 voices, and was last updated by Profile photo of Peter Kriegel Peter Kriegel 3 years ago.

  • Author
    Posts
  • #16390
    Profile photo of Peter Kriegel
    Peter Kriegel
    Participant

    Hi PowerShell people!

    For metaprogramming and to create ProxyCommands (proxy functions) I had the need to create a Windows PowerShell Parameter programmatically from scratch. I even don't like to use Reflection to break into classes and steal and use forbidden stuff, that is subject to change.
    Even the serialization trick with Microsoft.PowerShell.DeserializingTypeConverter is the same dirty way to do the same on a different route.

    Here is my prototype of a solution. I am creating a Function with a Parameter as Text (function sourcecode). My first attempt was to do a “New-Item Function:\ -value {code}” into the function drive and then do a Get-Command to the new function to extract the Metadata. But this shows up, that the function was a dead sourcecode only horse. It was not got compiled. So I had to use Invoke-Expression to 'compile' the sourcecode of the function.

    Am i doing right to construct the Parametersets?
    Does every parameterset need even the other metadata (like Madatory=$True.Positio=3 ....)?

    See even discussion here: http://stackoverflow.com/questions/2466210/cannot-generate-parametersetmetadata-while-programmatically-creating-a-parameter

    Function New-Parameter {
        
        [CmdletBinding()]
        param(
            [Switch]$Mandatory,
            [UInt32]$Position,
            [Switch]$ValueFromPipeline,
            [Switch]$ValueFromPipelineByPropertyName,
            [Switch]$ValueFromRemainingArguments,
            [String]$HelpMessage,
    
            [Type]$Type=[Type]'System.Management.Automation.SwitchParameter',
            [Parameter(Mandatory=$True)]
            [String]$Name,
            [String]$DefaultValue,
    
            [Switch]$DontShow,
            
            [String[]]$ParameterSetName,
            [String[]]$Aliases,
            # if Metadata is present the result is an System.Management.Automation.ParameterMetadata object
            # If Metadata is absent the sourcecode for the Parameter is returned
            [Switch]$Metadata
        )
            
        $ParameterAttrib = [System.Collections.ArrayList]@()
    
        # using GUID to create an unique function Name
        $Guid = ([Guid]::NewGuid()).ToString()
    
        # using a StringBuilder to glue the sourcecode 
        $stringBuilder = New-Object System.Text.StringBuilder
    
            If($Metadata.IsPresent) {
    
            # Open the Function{} block
            [Void]$stringBuilder.Append("Function $Guid {`n")
        
            # add the [CmdletBinding()] attribute 
            [Void]$stringBuilder.Append("[CmdletBinding()]`n")
        
            # Open the Param() block
            [Void]$stringBuilder.Append("param(`n")
        } 
    
        # query if we have one or more ParameterSetName
        $ParmameterSetNameCount = 0
        If(-not [String]::IsNullOrEmpty($ParameterSetName)) {
            $ParmameterSetNameCount = @($ParameterSetName).Count
        } 
    
        # Open the [Parameter()] attribut
        [Void]$stringBuilder.Append('[Parameter(')
            
        If($Mandatory.IsPresent) {
            [Void]$ParameterAttrib.Add('Mandatory=$True')
        }
        If($Position) {
            [Void]$ParameterAttrib.Add("Position=$Position")
        }
        If($ParmameterSetNameCount -gt 0){
                # in the first full blown [Parameter()] attribut allways insert the first ParametersetName
                [Void]$ParameterAttrib.Add("ParameterSetName='$($ParameterSetName[0])'")  
        }
    
    
        If($ValueFromPipeline.IsPresent) {
            [Void]$ParameterAttrib.Add('ValueFromPipeline=$True')
        }
        If($ValueFromPipelineByPropertyName.IsPresent) {
            [Void]$ParameterAttrib.Add('ValueFromPipelineByPropertyName=$True')
        }
        If($ValueFromRemainingArguments.IsPresent) {
            [Void]$ParameterAttrib.Add('ValueFromRemainingArguments=$True')
        }
        If($DontShow.IsPresent) {
            If($PSVersionTable.PSVersion.Major -lt 4) {
                Write-Warning "The 'DontShow' attribute requires PowerShell 4.0 or above! `n Supressing the 'DontShow' attribute!"
            } Else {
                [Void]$ParameterAttrib.Add('DontShow')
            }
            
        }
        If(-not [String]::IsNullOrEmpty($HelpMessage)) {
            [Void]$ParameterAttrib.Add("HelpMessage='$HelpMessage'")
        }
        
        # generate comma separated list from array
        [Void]$stringBuilder.Append("$($ParameterAttrib -Join ',')")
            
        $ParameterAttrib.Clear()
    
        # close the [Parameter()] attribut
        [Void]$stringBuilder.Append(")]`n")
        $ParmameterSetLoopCounter++
      
        # If we have more then one ParametersetName
        IF($ParmameterSetNameCount -gt 1) {
            # add remaining parameterset names the parameter belongs to
            for ($i = 1; $i -lt $ParmameterSetNameCount; $i++) { 
                [Void]$stringBuilder.Append("[Parameter(ParameterSetName='$($ParameterSetName[$i])')]`n")  
            }  
        }
    
        # Create Alias Attribute from Aliases
        If(-not [String]::IsNullOrEmpty($Aliases)) {
            [Void]$stringBuilder.Append("[Alias('$($Aliases -join "','")')]`n")
        }
    
        # add Parameter Type
        [Void]$stringBuilder.Append("[$($Type.Fullname)]")
    
        # add the Parameter Name
        [Void]$stringBuilder.Append("`$$Name")
    
            If(-not [String]::IsNullOrEmpty($ParameterSetName)) {
            [Void]$stringBuilder.Append("=$DefaultValue")
            }
    
        If($Metadata.IsPresent) {
            # close the Param() block
            [Void]$stringBuilder.Append("`n)`n")
    
            # close the Function block
            [Void]$stringBuilder.Append("}`n")
        }
    
        # return the result
        If($Metadata.IsPresent) {
            # if we have to return a ParameterMetadata Object we create a temporary function
            # because you can instatiate a ParameterMetadata Object but most of the Properties are constrained to get only and not to set!
           
            # Create and 'compile' the function into the function: drive
            Invoke-Expression ($stringBuilder.ToString())
        
            # from the temporary function we query the the ParameterMetadata and
            # return theParameterMetadata Object
            (Get-Command -Name $Guid -CommandType Function).Parameters.$Name
    
            # remove the Function from Function: drive
            $Null = Remove-Item Function:\$Guid -Force
    
        } Else {
            # return the sourcecode of the Parameter
            Write-Output $stringBuilder.ToString()
        }
        
    }
    
    #Example calls:
    
    # without Parametersets
    New-Parameter -Name 'Param1' -Mandatory -Position 3 -ValueFromPipeline -ValueFromPipelineByPropertyName -ValueFromRemainingArguments -HelpMessage "Give me hope Joana!" -Type 'System.String' -Aliases 'Ali1','Ali2','Ali3' -DontShow -DefaultValue 34
    New-Parameter -Name 'Param1' -Mandatory -Position 3 -ValueFromPipeline -ValueFromPipelineByPropertyName -ValueFromRemainingArguments -HelpMessage "Give me hope Joana!" -Type 'System.String' -Aliases 'Ali1','Ali2','Ali3' -DontShow -DefaultValue 34 -Metadata
    
    # with Parametersets
    New-Parameter -Name 'Param1' -Mandatory -Position 3 -ValueFromPipeline -ValueFromPipelineByPropertyName -ValueFromRemainingArguments -HelpMessage "Give me hope Joana!" -Type 'System.String' -ParameterSetName 'Snover','Payette' -Aliases 'Ali1','Ali2','Ali3' -DontShow -DefaultValue 34
    New-Parameter -Name 'Param1' -Mandatory -Position 3 -ValueFromPipeline -ValueFromPipelineByPropertyName -ValueFromRemainingArguments -HelpMessage "Give me hope Joana!" -Type 'System.String' -ParameterSetName 'Snover','Payette' -Aliases 'Ali1','Ali2','Ali3' -DontShow -DefaultValue 34 -Metadata
    

    rant on:
    I was very, very angry that we can instantiate a Type of System.Management.Automation.ParameterMetadata but we cannot initialize it. Microsoft destroys much joy of the class library by contraining the classes by use of private or internal or sealed...... they use it to often, and without any thinkable reason. Even the Types System.Management.Automation.ParameterMetadata and System.Management.Automation.ParameterSetMetadata have identical Members, but no common parent Object. That is a very nuts library design!
    rant off:

  • #16420
    Profile photo of Don Jones
    Don Jones
    Keymaster

    You should consider providing that feedback to Microsoft in Connect, where they'll see it, if you haven't done so. Additionally, we don't tend to get as many dev-heavy folks here, at least not yet. If you don't get a response in a few days, consider posting elsewhere like StackOverflow – but if you get an answer, and don't min popping back here and linking to it, it'll help someone else in the future.

  • #16421
    Profile photo of Peter Kriegel
    Peter Kriegel
    Participant

    Hi Don!
    Thank you for your kind reply!

    I have investigate to that topic a bit further and I have tested with some multi parameterset parameters.
    This question can be closed.

    greets Peter

You must be logged in to reply to this topic.