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
}
...
}