Home > Net >  Making at least one of many parameters required in PowerShell
Making at least one of many parameters required in PowerShell

Time:03-23

In PowerShell, I want to write a function, that accepts different options as parameters. It is OK, if it receives more than one parameter, but it has to receive at least one parameter. I want to enforce it through the parameter definition and not through code afterwards. I can get it to work with the following code:

function Set-Option {

    Param(
        [Parameter(Mandatory, ParameterSetName="AtLeastOption1")]
        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption2")]
        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption3")]
        $Option1,

        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption1")]
        [Parameter(Mandatory, ParameterSetName="AtLeastOption2")]
        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption3")]
        $Option2,

        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption1")]
        [Parameter(Mandatory=$false, ParameterSetName="AtLeastOption2")]
        [Parameter(Mandatory, ParameterSetName="AtLeastOption3")]
        $Option3
    )

    # Do stuff, but don't evaluate the plausibility of the given parameters here
}

But as you can see, it scales badly. For each additional option, I have to add a line to all other options. Can this be done in a more efficient and a more maintainable way?

As I already said, I don't want to check the parameters in the code, e. g. through evaluating $PSBoundParameters. I want it to happen in the parameter definition for auto-doc reasons.


If you need a real world example, have a look at Set-DhcpServerv4OptionValue which accepts many different options (-DnsDomain, -DnsServer, -Router, ...), where it is OK to have them all, but it makes no sense to have none.

CodePudding user response:

The following isn't a great solution - and depending on what you mean by auto-doc, it may not work for you - but it scales well, as you'll only ever need one additional parameter set:

function Set-Option {

  [CmdletBinding(DefaultParameterSetName='Fail')]
  Param(
      [Parameter(ParameterSetName='AtLeastOne')]
      $Option1,

      [Parameter(ParameterSetName='AtLeastOne')]
      $Option2,

      [Parameter(ParameterSetName='AtLeastOne')]
      $Option3,

      # Note: All that 'DontShow' does is to exclude the param. from tab completion.
      [Parameter(ParameterSetName='Fail', DontShow)] 
      ${-} = $(throw "Please specify at least one option.")
  )

  # Do stuff, but don't evaluate the plausibility of the given parameters here
}
  • All real parameters are optional and belong to the same parameter set that is not the default.

  • The purpose of dummy parameter ${-}, which is the only one in the default parameter set, is solely to throw an error via its default value.

    • Due to its irregular name, you actually cannot pass an explicit value to it (which is desirable here, because it is purely auxiliary and not meant for direct use): you'd have to use -- <value>, but -- has special meaning to the parameter binder (deactivates named parameter binding for the subsequent arguments).

    • Unfortunately, property DontShow (e.g. [Parameter(DontShow)]) only hides the parameter from tab-completion, not also from the syntax diagrams.

      • GitHub issue #7868 proposes introducing a way to hide (obsolete) parameters from the syntax diagram.

Thus, unfortunately, the dummy parameter set and its parameter appear in the syntax diagram, so that Set-Option -? shows the following:

SYNTAX
    Set-Option [-- <Object>] [<CommonParameters>]

    Set-Option [-Option1 <Object>] [-Option2 <Object>] [-Option3 <Object>] [<CommonParameters>]

Note that syntax diagrams lack a notation for your desired logic.

CodePudding user response:

This is a solution using DynamicParam to automatically generate the same parameter sets that you have created manually. Despite not being a "code-free" solution, it still shows the expected syntax diagram (when called like Set-Option -?), because PowerShell gets all necessary information from the DynamicParam block.

First we define a reusable helper function to be able to write a DRY DynamicParam block:

Function Add-ParamGroupAtLeastOne {
    <#
    .SYNOPSIS
        Define a group of parameters from which at least one must be passed.
    #>
    Param(
        [Parameter(Mandatory)] [Management.Automation.RuntimeDefinedParameterDictionary] $Params,
        [Parameter(Mandatory)] [Collections.Specialized.IOrderedDictionary] $ParamDefinitions
    )

    foreach( $paramDef in $ParamDefinitions.GetEnumerator() ) {

        $attributes = [Collections.ObjectModel.Collection[Attribute]]::new()

        # Generate parameter sets for one parameter
        foreach( $groupItem in $ParamDefinitions.Keys ) {
            $attr = [Management.Automation.ParameterAttribute]@{
                Mandatory = $paramDef.Key -eq $groupItem
                ParameterSetName = "AtLeastOne$groupItem"
            }
            if( $paramDef.Value.HelpMessage ) {
                $attr.HelpMessage = $paramDef.Value.HelpMessage
            }
            
            $attributes.Add( $attr )
        }
    
        # Add one parameter
        $Params.Add( $paramDef.Key, [Management.Automation.RuntimeDefinedParameter]::new( $paramDef.Key, $paramDef.Value.Type, $attributes ))         
    }
}

The Set-Option function can now be written like this:

Function Set-Option {
    [CmdletBinding()]
    Param()  # Still required

    DynamicParam {
        $parameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new()

        Add-ParamGroupAtLeastOne -Params $parameters -ParamDefinitions ([ordered] @{ 
            Option1 = @{ Type = 'string'; HelpMessage = 'the 1st option' }
            Option2 = @{ Type = 'int';    HelpMessage = 'the 2nd option' }
            Option3 = @{ Type = 'bool';   HelpMessage = 'the 3rd option' }
        })

        $parameters
    }    

    process {
        # Do stuff
    }
}

Set-Option -? outputs this syntax diagram, as expected:

SYNTAX
    Set-Option -Option1 <string> [-Option2 <int>] [-Option3 <bool>] [<CommonParameters>]

    Set-Option -Option2 <int> [-Option1 <string>] [-Option3 <bool>] [<CommonParameters>]

    Set-Option -Option3 <bool> [-Option1 <string>] [-Option2 <int>] [<CommonParameters>]

If you want to add more parameter attributes, have a look at the ParameterAttribute class and add the desired attributes in the function Add-ParamGroupAtLeastOne as I have done exemplary for HelpMessage.

CodePudding user response:

If the parameters are all switches (i.e., you specify them as -Option1 rather than -Option1 SomeValue), include a test at the beginning of the actual code that checks that they're not ALL false, and if they are, reject the invocation. If they're value parameters (i.e., -Option1 SomeValue), you'll have to test each of them against $null, and if they're all $null, reject the invocation.

function Set-Option {
   param (
      [switch]$Option1,
      [switch]$Option2,
      ...
   )

   if (!($Option1 -or $Option2 -or ...)) {
      # reject the invocation and abort
   }
   ...
}
  • Related