Automate the discovery of mandatory parameters

Sometimes, when trying out a cmdlet I rarely use, I get that :

Missing Mandatory parameter

This means I forgot to enter a parameter which is mandatory for this cmdlet. PowerShell is very forgiving and asks me nicely to enter a value for this parameter.

You see, learning PowerShell is not about rote knowledge of every single cmdlets. We, IT pros, have other things to do with our time than memorizing thousands of cmdlets and parameters. Thankfully, PowerShell has been designed to be highly discoverable. There are plenty of tools built-in to PowerShell which allows to discover cmdlets and their parameters.

Let’s explore some these tools and see how we can automate the discovery of mandatory parameters.

PS C:\> $CmdString = "Register-ScheduledJob"
PS C:\> Get-Command $CmdString | Select-Object -ExpandProperty Parameters

Key                                                Value
---                                                -----
FilePath                                           System.Management.Automation.ParameterMetadata
ScriptBlock                                        System.Management.Automation.ParameterMetadata
Name                                               System.Management.Automation.ParameterMetadata
Trigger                                            System.Management.Automation.ParameterMetadata
InitializationScript                               System.Management.Automation.ParameterMetadata
RunAs32                                            System.Management.Automation.ParameterMetadata
Credential                                         System.Management.Automation.ParameterMetadata
Authentication                                     System.Management.Automation.ParameterMetadata
ScheduledJobOption                                 System.Management.Automation.ParameterMetadata
ArgumentList                                       System.Management.Automation.ParameterMetadata
MaxResultCount                                     System.Management.Automation.ParameterMetadata
RunNow                                             System.Management.Automation.ParameterMetadata
Verbose                                            System.Management.Automation.ParameterMetadata
Debug                                              System.Management.Automation.ParameterMetadata
ErrorAction                                        System.Management.Automation.ParameterMetadata
WarningAction                                      System.Management.Automation.ParameterMetadata
ErrorVariable                                      System.Management.Automation.ParameterMetadata
WarningVariable                                    System.Management.Automation.ParameterMetadata
OutVariable                                        System.Management.Automation.ParameterMetadata
OutBuffer                                          System.Management.Automation.ParameterMetadata
PipelineVariable                                   System.Management.Automation.ParameterMetadata
WhatIf                                             System.Management.Automation.ParameterMetadata
Confirm                                            System.Management.Automation.ParameterMetadata

That was easy.
But hold on, what I want is only the mandatory parameters.
Also, I want everything I need to know to test a parameter and how it may work (or not work) with other parameters : its parameter set, the data type it expects, its position if it is positional, whether it accepts pipeline input and whether it accepts wildcards.

Let’s start with the parameter sets. If you don’t know what parameter sets are, essentially, they are a way to exclude 2 parameters of a cmdlet from each other to make sure that these 2 parameters won’t be used at the same time.


PS C:\> $CmdData = Get-Command $CmdString
PS C:\> $CmdData.ParameterSets.name
ScriptBlock
FilePath

Here, we see that the cmdlet Register-ScheduledJob has 2 parameter sets : ScriptBlock and FilePath. Parameter(s) which are in one parameter set but not in any other set are what I call “exclusive parameters”. Exclusive parameters cannot be used in the same command as any other exclusive parameter from another set.

Here is how to identify these “exclusive” parameters :

Compare Parameter Sets

Aha.
The name of the parameter sets correspond to the exclusive parameter in that set. Many cmdlets parameter sets are designed that way.
So, if we use the -FilePath parameter, this puts the cmdlet Register-ScheduledJob in the FilePath mode, which prevents us from using the -ScriptBlock parameter. And vice versa.

Now, we select only the mandatory parameters :

Foreach ($ParameterSet in $CmdData.ParameterSets) {
    $MandatoryParams = $ParameterSet.Parameters | Where-Object {$_.IsMandatory }
}

But, it turns out that the Name parameter is displayed twice. This is because it is mandatory in both parameter sets. We don’t want duplicate parameters, so let’s do it another way.

The nested property called “Attributes” have some juicy bits for us :

Parameter attributes

So here is how we filter only mandatory parameters :

$MandatoryParameters = $CmdData.Parameters.Values | Where { $_.Attributes.Mandatory -eq $True }

Then, adding the parameter position, accepted data type, and parameter set is pretty simple because these are properties of our current objects, or of the nested Attributes property. Let’s build a custom object from that :

Foreach ( $MandatoryParameter in $MandatoryParameters ) {

    $Props = [ordered]@{'Name'=$MandatoryParameter.Name
                    'Parameter Set'=$MandatoryParameter.Attributes.ParameterSetName
                    'Position'=$MandatoryParameter.Attributes.Position
                    'Data Type'=$MandatoryParameter.ParameterType
                    }

    $Obj = New-Object -TypeName psobject -Property $Props
    $Obj
}

Now, there are 2 more properties we want to add to our parameter objects : whether they accept input from the pipeline and whether they accept wildcards. To this end, we are going to use another invaluable discoverability tool : Get-Help.

PS C:\> Get-Help $CmdString -Parameter $MandatoryParameters[0].Name

-FilePath <String>
    Specifies a script that the scheduled job runs. Enter the path to a .ps1 file on the local computer. To
    specify default values for the script parameters, use the ArgumentList parameter. Every
    Register-ScheduledJob command must use either the ScriptBlock or FilePath parameters.

    Required?                    true
    Position?                    2
    Default value                None
    Accept pipeline input?       false
    Accept wildcard characters?  false

It seems we have what we need here, but is the property regarding pipeline input really named “Accept pipeline input?” ? Is the property regarding the wildcard characters really named “Accept wildcard characters?” ?

No, this is the result of the default formatting view for MamlCommandHelpInfo#parameter type. We override the default formatting to get the actual property names :

PS C:\> Get-Help $CmdString -Parameter $MandatoryParameters[0].Name | Format-List

description    : {@{Text=Specifies a script that the scheduled job runs. Enter the path to a .ps1 file on the
                 local computer. To specify default values for the script parameters, use the ArgumentList
                 parameter. Every Register-ScheduledJob command must use either the ScriptBlock or FilePath
                 parameters.}}
defaultValue   : None
parameterValue : String
name           : FilePath
type           : @{name=String; uri=}
required       : true
variableLength : false
globbing       : false
pipelineInput  : false
position       : 2
aliases        :


So the properties we are interested in are named : “pipelineInput” and “globbing”. Let’s add them to our custom object :

Foreach ( $MandatoryParameter in $MandatoryParameters ) {

    $ParameterHelp = Get-Help $CmdString -Parameter $MandatoryParameter.Name

    $Props = [ordered]@{'Name'=$MandatoryParameter.Name
                    'Parameter Set'=$MandatoryParameter.Attributes.ParameterSetName
                    'Position'=$MandatoryParameter.Attributes.Position
                    'Data Type'=$MandatoryParameter.ParameterType
                    'Pipeline Input'=$ParameterHelp.pipelineInput
                    'Accepts Wildcards'=$ParameterHelp.globbing
                    }

    $Obj = New-Object -TypeName psobject -Property $Props
    $Obj
}

As a bonus, we can make this work not just for cmdlets and functions, but for aliases as well. Aliases have a property named “Definition” which provides the name of the command the alias points to. So, if the user inputs an alias, we can use this to resolve the alias to the actual cmdlet or function, like so :

If ($CmdData.CommandType -eq "Alias") {
    $CmdData = Get-Command (Get-Alias $CmdString).Definition
}

Putting it all together :

The end result is a function using the techniques explained above :

function Get-MandatoryParameters {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({ Get-Command $_ -ErrorAction SilentlyContinue })]        
        [string]$CmdString
    )

    $CmdData = Get-Command $CmdString

    # If the $CmdString provided by the user is an alias, resolve to the cmdlet name
    If ($CmdData.CommandType -eq "Alias") {
        $CmdData = Get-Command (Get-Alias $CmdString).Definition
    }

    $MandatoryParameters = $CmdData.Parameters.Values | Where { $_.Attributes.Mandatory -eq $True }

    Foreach ( $MandatoryParameter in $MandatoryParameters ) {

        $ParameterHelp = Get-Help $CmdString -Parameter $MandatoryParameter.Name

        $Props = [ordered]@{'Name'=$MandatoryParameter.Name
                        'Parameter Set'=$MandatoryParameter.Attributes.ParameterSetName
                        'Position'=$MandatoryParameter.Attributes.Position
                        'Data Type'=$MandatoryParameter.ParameterType
                        'Pipeline Input'=$ParameterHelp.pipelineInput
                        'Accepts Wildcards'=$ParameterHelp.globbing
                        }

        $Obj = New-Object -TypeName psobject -Property $Props
        $Obj
    }
}

Here is what the output of this function looks like :

Get-MandatoryParameters Output

Leave a Reply

Your email address will not be published. Required fields are marked *