Search code examples
powershellparameters

Problems with interconnected parameters in PowerShell Param block


I'm having troubles wrapping my head around the code logic needed to get my Param block functioning as I want in PowerShell 5.1. A very simplified (but usable for my request) script with the Param block is here and this works exactly as I want:

[CmdletBinding(DefaultParameterSetName = 'All')]

Param (
    [Parameter(Position=0,Mandatory=$True)]
    [String]$Name,
    
    [Parameter(ParameterSetName = 'All')]
    [Switch]$All,
    
    [Parameter(ParameterSetName = 'Individual')]
    [Switch]$P1,
    
    [Parameter(ParameterSetName = 'Individual')]
    [Switch]$P2
    
    )

If ($All -Or ($P1.ToBool() + $P2.ToBool() -eq 0)) {
    $All = $True
    $P1 = $True
    $P2 = $True
    }

"`$Name is $Name"
"`$All is $All"
"`$P1 is $P1"
"`$P2 is $P2"

The auto-completion of parameters when running this script above works as intended. If I use the "-All" switch, then -P# are not available. If I use -P# switche, -All is not available. If I omit the -Name, then it prompts me to put in a name. If I use the -All switch, I want all of the individual -P# options to later be set to $True. If I use no switches at all, it prompts me for a Name then sets all options to $True.

The first problem is that when using DefaultParameterSetName = 'All' (which I had to do in order to make the script work without any switches on the command line), then the $All variable is NOT actually being set to $True when it is not present on the command line. I had to make the "If" block in order to overcome that behavior. This makes the next problem come up because the actual script I'm trying to use this in will have fifteen or more -P# switches. That will make the "If" test more complex and ugly.

Is there a better way I can do this? Maybe something in the layout of my Parameter Sets? I could even eliminate the "-All" switch entirely if there's an easier way to evaluate that none of the -P# switches are used. Is there an easier way to add up the boolean value of all Parameters named P#? I've stumbled across the $MyInvocation variable and $MyInvocation.MyCommand.Parameters seems promising but I'm not sure exactly how to process that either.

Update after answer found: Here is my new, simplified working code sample which I arrived at thanks to all the suggestions here. The "-All" Switch was unnecessary. I decided to go with the Get-Variable method here for now due to its simplicity and scalability, but it does require a common prefix on the Switch variables. The If() block will remain the same no matter how many variables are used.

Param (
    [Parameter(Position=0,Mandatory=$True)]
    [String]$Name,
    [Alias('Blue')]
    [Switch]$OptionBlue,
    [Alias('Red')]
    [Switch]$OptionRed,
    [Alias('Yellow')]
    [Switch]$OptionYellow
    )

$AllOptions = Get-Variable -Name 'Option*'
If (-Not $AllOptions.Value.Contains($True)) {
    "None of the Option Switches were used, setting all Option Variables to $True"
    ForEach ($Option In $AllOptions) {$Option.Value = $True}
    }

"`$Name:          $Name"
"`$OptionBlue:    $OptionBlue"
"`$OptionRed:     $OptionRed"
"`$OptionYellow:  $OptionYellow"

and here's how it works:

PS C:\Scripts\PowerShell> .\Test-Params.ps1

cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
None of the Option Switches were used, setting all Option Variables to True
$Name:          Testing
$OptionBlue:    True
$OptionRed:     True
$OptionYellow:  True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -OptionRed
$Name:          Testing
$OptionBlue:    False
$OptionRed:     True
$OptionYellow:  False
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -Blue -Yellow
$Name:          Testing
$OptionBlue:    True
$OptionRed:     False
$OptionYellow:  True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Yellow -OptionRed

cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
$Name:          Testing
$OptionBlue:    False
$OptionRed:     True
$OptionYellow:  True

Final Update

I figured out the way to avoid any issue where the script's switch parameters might match an already existing variable in the scope. Pulling the script's parameters that match the proper prefix/suffix used in the script's parameter names using $MyInvocation and then passing those specific names to Get-Variable avoids the issue. The switches also work correctly both when they are not present, or if they are explicitly set to $False. If more switches are needed, simply add them in the Param block with the proper prefix/suffix in the name and an alias for the simpler version. I think this bit of the code is bulletproof now...

    Param (
        [Parameter(Position=0,Mandatory=$True)]
        [String]$Name,
        [Alias('Blue')]
        [Switch]$OptionBlue,
        [Alias('Red')]
        [Switch]$OptionRed,
        [Alias('Yellow')]
        [Switch]$OptionYellow
        )
        
    $OptionNotAnOption = $True
    
    $OptionSwitches = ForEach ($Option In ($MyInvocation.MyCommand.Parameters.Keys | Where {$_ -Like "Option*"})) {Get-Variable $Option}
    
    If ($OptionSwitches.Value.IsPresent -NotContains $True) {
        "All options are `$False or not present: Enabling all options"
        ForEach ($Switch In $OptionSwitches) {
            Get-Variable -Name ($Switch.Name) | Set-Variable -Value $True
            }
        }
    
    "`$Name:               $Name"
    "`$OptionBlue:         $OptionBlue"
    "`$OptionRed:          $OptionRed"
    "`$OptionYellow:       $OptionYellow"
    "`$OptionNotAnOption:  $OptionNotAnOption"


Solution

  • You can use the $PSBoundParameters "automatic variable" to access the parameters specified in the call to the script / function and alter the function's behaviour accordingly.

    For example:

    function Invoke-MyFunction
    {
        param
        (
            [string] $Name,
            [switch] $P1,
            [switch] $P2
        )
    
        # how many $Pn parameters were specified in the call to the function?
        $count = @( $PSBoundParameters.GetEnumerator()
            | where-object { $_.Key.StartsWith("P") }
        ).Length;
    
        # if *none* specified then enable *all*
        $all = $count -eq 0;
        if( $all )
        {
            $P1 = $true;
            $P2 = $true;
        }
    
        "`$Name is $Name"
        "`$all is $all"
        "`$P1 is $P1"
        "`$P2 is $P2"
    
    }
    

    And some tests:

    PS> Invoke-MyFunction
    $Name is
    $all is True
    $P1 is True
    $P2 is True
    
    PS> Invoke-MyFunction -Name "aaa"
    $Name is aaa
    $all is True
    $P1 is True
    $P2 is True
    
    PS> Invoke-MyFunction -Name "aaa" -P1
    $Name is aaa
    $all is False
    $P1 is True
    $P2 is False
    
    PS> Invoke-MyFunction -Name "aaa" -P2
    $Name is aaa
    $all is False
    $P1 is False
    $P2 is True
    
    PS>  Invoke-MyFunction -Name "aaa" -P1 -P2
    $Name is aaa
    $all is False
    $P1 is True
    $P2 is True
    

    but watch out because the way I've evaluated $count means that specifying -P1:$false or -P2:$false makes $all = $false:

    PS> Invoke-MyFunction -Name "aaa" -P1:$false
    $Name is aaa
    $all is False
    $P1 is False
    $P2 is False
    

    so you might need to refine the expression to suit whatever you want to happen in this edge case...


    Update

    If your parameter names don't follow a simple pattern you can do something like this instead:

    $names = @( "SomeParam", "AnotherParam", "Param3" );
    $count = @( $PSBoundParameters.GetEnumerator()
        | where-object { $_.Key -in $names }
    ).Length;