3

Here are my parameters of one of the nested modules in my main module:

    Param(
        [Parameter(Mandatory = $false, ParameterSetName = "set1", Position = 0, ValueFromPipeline = $true)][switch]$Get_BlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set2", Position = 0, ValueFromPipeline = $true)][switch]$Get_DriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set3", Position = 0, ValueFromPipeline = $true)][switch]$Make_AllowMSFT_WithBlockRules,  
        [Parameter(Mandatory = $false, ParameterSetName = "set4", Position = 0, ValueFromPipeline = $true)][switch]$Deploy_LatestDriverBlockRules,                                                                                       
        [Parameter(Mandatory = $false, ParameterSetName = "set5", Position = 0, ValueFromPipeline = $true)][switch]$Set_AutoUpdateDriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set6", Position = 0, ValueFromPipeline = $true)][switch]$Prep_MSFTOnlyAudit,
        [Parameter(Mandatory = $false, ParameterSetName = "set7", Position = 0, ValueFromPipeline = $true)][switch]$Make_PolicyFromAuditLogs,  
        [Parameter(Mandatory = $false, ParameterSetName = "set8", Position = 0, ValueFromPipeline = $true)][switch]$Make_LightPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set9", Position = 0, ValueFromPipeline = $true)][switch]$Make_SuppPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set10", Position = 0, ValueFromPipeline = $true)][switch]$Make_DefaultWindows_WithBlockRules,
       
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$ScanLocation,
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$SuppPolicyName,
        [ValidatePattern('.*\.xml')][parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$PolicyPath,

        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]        
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [switch]$Deployit,

        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [switch]$TestMode,
        
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [switch]$RequireEVSigners,

        [Parameter(Mandatory = $false, ParameterSetName = "set7")][switch]$Debugmode,

        [ValidateSet([Levelz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string]$Levels,

        [ValidateSet([Fallbackz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string[]]$Fallbacks, 

        [ValidateRange(1024KB, [int64]::MaxValue)]
        [Parameter(Mandatory = $false, ParameterSetName = "set6")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]        
        [Int64]$LogSize,

        [Parameter(Mandatory = $false)][switch]$SkipVersionCheck    
    )

How can I prevent parameters that are not position 0 to appear in position 0?

I've tried adding other positions like 1,2 etc. to other parameters but that didn't work.

Only parameters with position 0 are the main ones, the rest only need to be used/suggested when a position 0 parameter is first selected by the user.

To clarify, I'm using this

Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete

and seeing parameters that don't belong to position 0 in the menu is just weird and I don't want them to appear there or be selectable.

enter image description here

the yellow underlines show parameters that don't make sense on their own if used in position 0.

Update, using Dynamic parameters, I did this but still the other yellow underlined parameters are showing up for position 0.

So maybe someone can post an answer showing how I can actually implement it in my function.

#requires -version 7.3.3
function New-WDACConfig {
    [CmdletBinding(
        DefaultParameterSetName = "set1",
        HelpURI = "https://github.com/HotCakeX/Harden-Windows-Security/wiki/WDACConfig",
        SupportsShouldProcess = $true,
        PositionalBinding = $false,
        ConfirmImpact = 'High'
    )]
    Param(
        [Parameter(Mandatory = $false, ParameterSetName = "set1")][switch]$Get_BlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set2")][switch]$Get_DriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set3")][switch]$Make_AllowMSFT_WithBlockRules,  
        [Parameter(Mandatory = $false, ParameterSetName = "set4")][switch]$Deploy_LatestDriverBlockRules,                                                                                       
        [Parameter(Mandatory = $false, ParameterSetName = "set5")][switch]$Set_AutoUpdateDriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set6")][switch]$Prep_MSFTOnlyAudit,
        [Parameter(Mandatory = $false, ParameterSetName = "set7")][switch]$Make_PolicyFromAuditLogs,  
        [Parameter(Mandatory = $false, ParameterSetName = "set8")][switch]$Make_LightPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set9")][switch]$Make_SuppPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set10")][switch]$Make_DefaultWindows_WithBlockRules,
       
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$ScanLocation,
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$SuppPolicyName,
        
        [ValidatePattern('.*\.xml')]
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)]
        [string]$PolicyPath,

        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]        
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [switch]$Deployit,

        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [switch]$TestMode,
        
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [switch]$RequireEVSigners,

        [Parameter(Mandatory = $false, ParameterSetName = "set7")][switch]$Debugmode,

        [ValidateSet([Levelz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string]$Levels,

        [ValidateSet([Fallbackz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string[]]$Fallbacks, 

        [ValidateRange(1024KB, [int64]::MaxValue)]
        [Parameter(Mandatory = $false, ParameterSetName = "set6")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]        
        [Int64]$LogSize,

        [Parameter(Mandatory = $false)][switch]$SkipVersionCheck
            
    )


   # parameter control for Make_AllowMSFT_WithBlockRules - Set3
   DynamicParam {
    if ($PSCmdlet.ParameterSetName -eq "Set3") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'RequireEVSigners', 'TestMode', 'Deployit') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set3" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [Int64], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }
 

    # parameter control for Prep_MSFTOnlyAudit - Set6

    if ($PSCmdlet.ParameterSetName -eq "Set6") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'LogSize') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set6" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [Int64], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }


    # parameter control for Make_PolicyFromAuditLogs - Set7

    if ($PSCmdlet.ParameterSetName -eq "Set7") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Deployit', 'TestMode', 'RequireEVSigners') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set7" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [switch], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }

    if ($PSCmdlet.ParameterSetName -eq "Set7") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Fallbacks', 'Levels') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set7" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [string], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }

    if ($PSCmdlet.ParameterSetName -eq "Set7") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'LogSize') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set7" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [Int64], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }


    # parameter control for Make_LightPolicy - Set8

    if ($PSCmdlet.ParameterSetName -eq "Set8") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Deployit', 'TestMode', 'RequireEVSigners') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set8" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [switch], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }


    # parameter control for Make_SuppPolicy - Set9

    if ($PSCmdlet.ParameterSetName -eq "Set9") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Fallbacks', 'Levels', 'Deployit', 'PolicyPath', 'SuppPolicyName', 'ScanLocation') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set9" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [switch], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }

    if ($PSCmdlet.ParameterSetName -eq "Set9") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Fallbacks', 'Levels', 'PolicyPath', 'SuppPolicyName', 'ScanLocation') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set9" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [string], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }

    if ($PSCmdlet.ParameterSetName -eq "Set9") {
        $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        foreach ($param in 'Deployit') {
            [Parameter[]] $paramAttribute = [Parameter]@{ ParameterSetName = "Set9" }
            $runtimeParam = [System.Management.Automation.RuntimeDefinedParameter]::new($param, [switch], $paramAttribute)
            $paramDictionary.Add($param, $runtimeParam)
        }
        return $paramDictionary
    }
}
}
}
SpyNet
  • 323
  • 8
  • 2
    Why do you care about the order or the parameters if you are referencing by NAME? If you want to have more than one name for a parameter use the ALIAS attribute. See : https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?force_isolation=true&view=powershell-7.3 – jdweng Apr 12 '23 at 12:40
  • I don't care about the order of the parameters I set to position 0, but I'm using `Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete` and the tab completion makes everything beautiful, and seeing non-position 0 parameters in it is just weird and doesn't make sense. For example, the `-RequireEVSigners` switch parameter makes no sense on its own but it's being suggested and is selectable in position 0. – SpyNet Apr 12 '23 at 12:42
  • 6
    Specifying position for a switch parameter makes 0 sense. What exactly do you think `Position = 0` means/does? – Mathias R. Jessen Apr 12 '23 at 12:54
  • Who cares what it look like as long as it works. – jdweng Apr 12 '23 at 13:02
  • 3
    `Position` is not there to configure tab completion and trying to use it like that doesn't work, as you've discovered. It's there to allow the user to specify the value without the name, which is not meaningful in this case and is necessarily ignored. You can use dynamic parameters and/or a custom argument completer if you want to specify advanced behavior, which is probably clearer in this case than trying to do it all with statically specified validate sets anyway. It looks like splitting things into different cmdlets rather than overloading one huge one might also not go amiss. – Jeroen Mostert Apr 12 '23 at 13:33
  • 1
    I may be getting confused by what you are trying to do. Are you wanting that first set of 10 switches to decide what the other switches are valid? For example, `$Make_LightPolicy` in position 0 will make it valid to optionally use `$Deployit`, `$TestMode`, and `$RequireEVSigners` - with `$SkipVersionCheck' valid for all parametersets? – Darin Apr 12 '23 at 13:57
  • 2
    it sounds like you're looking for a `dynamicparam` block. Having switches be only available when a specific parameter is in use. here is an example: https://stackoverflow.com/questions/74870393/issues-with-parameter-sets-in-a-function-not-working-as-expected/74870616#74870616 – Santiago Squarzon Apr 12 '23 at 14:04
  • 1
    @Darin Yes that's exactly what I'm trying to do – SpyNet Apr 12 '23 at 14:18
  • @SpyNet, I just realized your code has those top 10 [switches] to optionally come in on the pipeline: `ValueFromPipeline = $true`. Is that what you want? I would think you would want them to only be in position 0. – Darin Apr 12 '23 at 14:23
  • 2
    Re: your update, I don't recommend dynamic parameters for what you're trying to do. I think it'd be simpler to use basic parameter sets, by making some of your switches mandatory within specific sets (more info in my answer). – briantist Apr 12 '23 at 14:40
  • @Darin Yes I was trying to chain them with other cmdlets of the same function as some of the outputs are objects but I'll remove them for now to find a solution for this problem. – SpyNet Apr 12 '23 at 14:41
  • @SantiagoSquarzon I tried but still can't get it to work, even that other dynamic parameter in your other answer doesn't seem to be working when I try it. – SpyNet Apr 12 '23 at 15:31
  • 1
    the code from my answer works perfectly fine: https://i.imgur.com/Zy2uEnz.png perhaps if you can reduce the wall of code you have posted in your question it might be easier to understand what you're looking for. as of now, there is too much code to go through and too many parameter sets. I also agree with @briantist using `dynamicparams` doesn't a good UX and I wouldnt recommend using it – Santiago Squarzon Apr 12 '23 at 15:35

2 Answers2

3

When working with parameter sets, I find it's very helpful to use the Get-Help command on your function or script, because it shows you how PowerShell is interpreting your sets. That way you can compare it with how you want it to work.

So if I take your param block and put it in a function a like this:

function a { 
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false, ParameterSetName = "set1", Position = 0, ValueFromPipeline = $true)][switch]$Get_BlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set2", Position = 0, ValueFromPipeline = $true)][switch]$Get_DriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set3", Position = 0, ValueFromPipeline = $true)][switch]$Make_AllowMSFT_WithBlockRules,  
        [Parameter(Mandatory = $false, ParameterSetName = "set4", Position = 0, ValueFromPipeline = $true)][switch]$Deploy_LatestDriverBlockRules,                                                                                       
        [Parameter(Mandatory = $false, ParameterSetName = "set5", Position = 0, ValueFromPipeline = $true)][switch]$Set_AutoUpdateDriverBlockRules,
        [Parameter(Mandatory = $false, ParameterSetName = "set6", Position = 0, ValueFromPipeline = $true)][switch]$Prep_MSFTOnlyAudit,
        [Parameter(Mandatory = $false, ParameterSetName = "set7", Position = 0, ValueFromPipeline = $true)][switch]$Make_PolicyFromAuditLogs,  
        [Parameter(Mandatory = $false, ParameterSetName = "set8", Position = 0, ValueFromPipeline = $true)][switch]$Make_LightPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set9", Position = 0, ValueFromPipeline = $true)][switch]$Make_SuppPolicy,
        [Parameter(Mandatory = $false, ParameterSetName = "set10", Position = 0, ValueFromPipeline = $true)][switch]$Make_DefaultWindows_WithBlockRules,
       
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$ScanLocation,
        [parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$SuppPolicyName,
        [ValidatePattern('.*\.xml')][parameter(Mandatory = $true, ParameterSetName = "set9", ValueFromPipelineByPropertyName = $true)][string]$PolicyPath,

        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]        
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [switch]$Deployit,

        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [switch]$TestMode,
        
        [Parameter(Mandatory = $false, ParameterSetName = "set3")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]
        [Parameter(Mandatory = $false, ParameterSetName = "set8")]
        [switch]$RequireEVSigners,

        [Parameter(Mandatory = $false, ParameterSetName = "set7")][switch]$Debugmode,

        [ValidateSet([Levelz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string]$Levels,

        [ValidateSet([Fallbackz])]
        [parameter(Mandatory = $false, ParameterSetName = "set7")]
        [parameter(Mandatory = $false, ParameterSetName = "set9")]
        [string[]]$Fallbacks, 

        [ValidateRange(1024KB, [int64]::MaxValue)]
        [Parameter(Mandatory = $false, ParameterSetName = "set6")]
        [Parameter(Mandatory = $false, ParameterSetName = "set7")]        
        [Int64]$LogSize,

        [Parameter(Mandatory = $false)][switch]$SkipVersionCheck    
    )
}

Then we can Get-Help a, to show us:

NAME
    a
    
SYNTAX
    a [[-Get_BlockRules]] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Get_DriverBlockRules]] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Make_AllowMSFT_WithBlockRules]] [-Deployit] [-TestMode] [-RequireEVSigners] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Deploy_LatestDriverBlockRules]] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Set_AutoUpdateDriverBlockRules]] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Prep_MSFTOnlyAudit]] [-LogSize <long>] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Make_PolicyFromAuditLogs]] [-Deployit] [-TestMode] [-RequireEVSigners] [-Debugmode] [-Levels {}] [-Fallbacks {}] [-LogSize <long>] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Make_LightPolicy]] [-Deployit] [-TestMode] [-RequireEVSigners] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Make_SuppPolicy]] -ScanLocation <string> -SuppPolicyName <string> -PolicyPath <string> [-Deployit] [-Levels {}] [-Fallbacks {}] [-SkipVersionCheck]  [<CommonParameters>]
    
    a [[-Make_DefaultWindows_WithBlockRules]] [-SkipVersionCheck]  [<CommonParameters>]

For me, the first thing that stands out is that most of the parameters sets, I think all except set9, have no mandatory parameters. That means that calling this with no parameters set at all is valid, which may not be what you want.

I would also suggest giving your parameter sets meaningful names. They don't show up in the regular help output, but it can help you organize them.

The reason I mention all this is that it's probably affecting the thing you're really interested in, which is how things show up in the PSReadLine menu. I haven't used that menu option before, but the parameters you underlined in yellow are perfectly valid given the parameter sets you defined.

Position as set on a parameter is only used so that callers can supply the value without the name. But PowerShell otherwise doesn't care in what order the values were supplied.

What I think you want to do, is better define your parameter sets to include mandatory parameters, as sort of "anchor" parameters for the set, and ensure that parameters that cannot be used with the mandatory one(s) are not included in that set.

Since I don't know which combinations you have in mind, I can't really show an example with your existing params.

One trick I use often is to make [Switch] parameters mandatory in a given parameter set.

Usually, it doesn't make sense to make a [Switch] mandatory because that (usually) means it will always be $true, but when used with parameter sets, it can make a lot of sense as a way of "selecting" a particular set.

Here's a contrived example:

function z {
    [CmdletBinding(DefaultParameterSetName='normal')]
    param(
        # all parameter sets
        [Parameter(Mandatory)]
        [String]
        $Thing,

        [Parameter(Mandatory, ParameterSetName='mode2')]
        [Switch]
        $Mode2,

        # mode2 only, mandatory
        [Parameter(Mandatory, ParameterSetName='mode2')]
        [String]
        $Mode2FooBar,

        # mode2 only optional
        [Parameter(ParameterSetName='mode2')]
        [String]
        $Mode2ExtraData
    )
}

Now with Get-Help z:


NAME
    z
    
SYNTAX
    z -Thing <string>  [<CommonParameters>]
    
    z -Thing <string> -Mode2 -Mode2FooBar <string> [-Mode2ExtraData <string>]  [<CommonParameters>]

Hopefully this gives you some ideas!

briantist
  • 45,546
  • 6
  • 82
  • 127
  • Thank you, that is helpful but unfortunately I can't make any of the non-mandatory switch parameters mandatory, it'd break the logic – SpyNet Apr 12 '23 at 14:43
  • @SpyNet the key is to have them mandatory only in the parameter set where other parameters can only be used with that switch. They are optional in the sense that other parameters sets exist that don't include them. In my last example, `-Mode2` is still not "mandatory" for the `z` command as a whole, it's just that `-Mode2ExtraData` can't be used unless the switch is too, and `-Mode2FooBar` A) can't be used with the other parameter set and B) must be use used if `-Mode2` is used. Your use case might be even more complicated, but it should be possible with similar combinations. – briantist Apr 12 '23 at 17:00
  • Sorry but I really can't apply that on my module, I tried. I will try changing the names of the parameter sets as you suggested and if it won't look too messy then I'll keep it. This sub-module has less parameters and sets, maybe you could take a look and try to apply that trick on them: https://github.com/HotCakeX/Harden-Windows-Security/blob/main/WDACConfig/Edit-WDACConfig.psm1 if you just highlight a set's name in VS code then it will be easy to recognize which combination belongs to which set. If that won't work then I guess I'll just leave it be. thank you again – SpyNet Apr 12 '23 at 17:26
  • The combination of each cmdlet: https://github.com/HotCakeX/Harden-Windows-Security/wiki/WDACConfig#here-is-the-list-of-parameters-the-module-supports-with-the-syntaxes – SpyNet Apr 12 '23 at 17:31
  • Thanks @SpyNet I took a look at `Edit-WDACConfig`, the thing I'm not sure of is whether the current parameter sets do what they are supposed to do, the way you want them to. That is, does the `Get-Help` output look correct to you? If so, then I think the only thing needed is to change those `[Switch]` parameters to mandatory; since they are already only present in their "own" set, they should still work as intended. I can't say for sure if it will affect the PSReadLine menu the way you want, but I'm curious to see if it does. – briantist Apr 12 '23 at 19:19
  • Yes they are correct and setting for example `Debugmode` switch to mandatory won't hide it from being suggested for position 0. the problem is only position 0, the first parameter, once I select the first parameter then the rest of the parameters are suggested correctly based on their sets. For position 0, everything is suggested. – SpyNet Apr 12 '23 at 19:35
  • @SpyNet ah right I don't mean to set every `[Switch]` to mandatory, the ones I was looking at were the first four you have listed. I could have been reading the sets wrong, but it seemed like those were the "defining" switch parameters that kind of determined what could even be in a set. I wondered if those would change what is being displayed as part of your first choice. In the end, it may not be possible after all with parameter sets alone; dynamic params can do it, but for me personally it would not be worth it due to dynamic params being a pain to manage. – briantist Apr 12 '23 at 20:40
  • 2
    @SpyNet [mklement0's new answer](https://stackoverflow.com/a/75999573/3905079) is best (as usual), and confirms that what you want is not entirely possible with statically defined parameters alone – briantist Apr 12 '23 at 20:49
2

To complement briantist's helpful answer:

Context:

  • The concept of positional parameters doesn't apply to your use case, as it only applies when arguments (parameter values) are passed unnamed, i.e. not preceded by a target parameter name.

    • As an aside:

      • [switch] parameters shouldn't be declared as positional (even though you can technically get away with it) - switch parameters are expected to be passed as named arguments, with their name alone implying their value ($true), and their absence implying $false.

      • Similarly, binding [switch] parameters via the pipeline is highly unusual and users may not expect it. Switch parameters are usually understood to be a single flag per invocation, not one that may vary based on each pipeline input object.

      • Also, using _ in parameter names is unusual - consider using camel case instead; e.g., consider using $GetBlockRules (-GetBlockRules) instead of $Get_BlockRules (-Get_BlockRules).

  • Tab-completing parameter names always invariably offers you all of the statically declared parameters when completing the first argument, and - after having typed or completed at least one argument and tab-completing a new one after having typed only - - may expand or shrink the set of parameters offered:

    • in the presence of multiple parameter sets, the set of static parameters offered may shrink, if the argument(s) specified so imply parameter set(s) that support only a subset of the static parameters.

    • in the presence of dynamic parameters, they may surface or disappear, depending on the conditions under which they are designed to become available.

    • Note that if you type a prefix of the name of a static parameter (e.g., -fo), it will be offered for completion even if it doesn't belong to the parameter set(s) implied by arguments typed so far.

  • For discoverability and maintainability, it is best to make do with static parameters, as shown in briantist's answer, but it doesn't provide the step-by-step user guidance you're looking for.


Solution:

The only way to ensure that only the desired subset of parameters is shown when you initially type - and tab-complete is to:

  • Declare only that subset as static parameters, putting each parameter in its own parameter set (as you already do).

    • Note, however, that you can never prevent the common parameters (such as -Verbose from showing in the menu display; they show after the command-specific ones (they display is column-based, so read downward).
  • Declare all other parameters as dynamic ones, surfacing them as appropriate based on what initial parameter was bound.

    • In effect, this amounts to a manual re-implementation of the parameter-set logic for those parameters.

A minimal example:

[CmdletBinding(PositionalBinding = $false)]
param(
  # The only parameters to show initially.
  [Parameter(Mandatory, ParameterSetName = "set1")] [switch] $Get_BlockRules,
  [Parameter(Mandatory, ParameterSetName = "set2")] [switch] $Get_DriverBlockRules,
  [Parameter(Mandatory, ParameterSetName = "set3")] [switch] $Make_AllowMSFT_WithBlockRules

  # All other parameters are declared *dynamically*
)

dynamicParam {
  # Create a dynamic-parameters dictionary
  $dict = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
        
  # Define and add the -Deployit switch, which only belongs to 
  # parameter set "set3"
  $paramName = 'Deployit'
  $dict.Add(
    $paramName,
    [System.Management.Automation.RuntimeDefinedParameter]::new(
      $paramName,
      [switch],
      [System.Management.Automation.ParameterAttribute] @{
        ParameterSetName = 'set3'
      }
    )
  )

  # Define and add the -SkipVersionCheck switch, which belongs to *all*
  # parameter sets.
  $paramName = 'SkipVersionCheck'
  $dict.Add(
    $paramName,
    [System.Management.Automation.RuntimeDefinedParameter]::new(
      $paramName,
      [switch],
      [System.Management.Automation.ParameterAttribute] @{
        ParameterSetName = '__allParameterSets'
      }
    )
  )

  # Return the dictionary
  return $dict
}

process {
  # Print all parameters that were bound.
  $PSBoundParameters
}

Note:

  • The solution requires no Position properties.

  • All static parameters - the only ones to show initially - are declared as Mandatory, with no default parameter-set designation in the [CmdletBinding()] attribute.

    • This prevents all-parameter-sets dynamic parameters such as -SkipVersionCheck from surfacing prematurely, and also prevents argument-less invocation.
  • -SkipVersionCheck is offered once any one of the initial, static parameters has been completed.

  • -Deployit is only offered if the initially completed static parameter (switch) is -Make_AllowMSFT_WithBlockRules, because like the latter it belongs to parameter set set3 only.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you, I did everything you said and applied them on a very small sub-module but it's not working, could you take a look pls https://github.com/HotCakeX/Harden-Windows-Security/blob/main/WDACConfig/test/Confirm-WDACConfig.psm1 – SpyNet Apr 12 '23 at 21:25
  • 1
    @SpyNet, dynamic parameters are declared _instead_ of static ones, not in addition. Remove the _static_ `$OnlyBasePolicies`, `$OnlySupplementalPolicies` and `$SkipVersionCheck` declarations from your `param(...)` block. – mklement0 Apr 12 '23 at 21:32
  • 1
    Thank you, that's perfect, I will start implementing it on the rest of the modules. I'm really curios to know what the syntax for dynamic parameters is. I've seen many of them and each of them has a new syntax and I find it hard to understand or create them from scratch, can't find a pattern. could you point me to some documentation that shows all the syntaxes? thanks! – SpyNet Apr 12 '23 at 21:38
  • 1
    Glad to hear it worked. As for documentation: I suggest starting with [about_Functions_Advanced_Parameters#dynamic-parameters](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Functions_Advanced_Parameters#dynamic-parameters). – mklement0 Apr 12 '23 at 21:40
  • 2
    I think the only downside is that `get-help` won't show the dynamic parameters – SpyNet Apr 12 '23 at 21:45