Powershell: Advanced functions require specifying a default parameter set with two or more explicit parameter sets

Created on 21 Nov 2019  Â·  28Comments  Â·  Source: PowerShell/PowerShell

_Update_: Closed in favor of a new proposal at https://github.com/PowerShell/PowerShell/issues/11201#issuecomment-559491980.


Generally, if you invoke an advanced function / script with:

  • either: no arguments at all

  • or: only arguments that bind to parameters that aren't explicitly tagged as belonging to an explicit parameter set.

PowerShell makes the automatic __AllParameterSets parameter set the effective one ($PSCmdlet.ParameterSetName)

However, this only works if there are either _no_ explicitly defined parameter sets or there is at most _one_.

With two or more explicit parameter sets, you're suddenly forced to define a _default_ parameter set via [CmdletBinding(DefaultParameterSetName='...')], or else the invocations described above result in an ambiguous-or-incompatible-parameters-specified error.

This situational requirement is obscure, especially given that there's no _logical_ reason to enforce it.


_Update_: What I am suggesting comes down to this:

If an advanced function or script _doesn't explicitly declare a default parameter set_ via its [CmdletBinding()] attribute:

  • Always allow invocations _without arguments_ as well as invocations comprising _only parameters not explicitly assigned to any parameter set_ - irrespective of the presence of parameters declared as belonging to one or more explicitly named parameter sets and irrespective of whether any of these parameters are declared as _mandatory_ in their sets.

  • Make $PSCmdlet.ParameterSetName contain __AllParameterSets in that event.

    • Specifically, do not try to _infer_ a default parameter set from among the explicit ones for parameter-less or only all-parameter-sets parameter invocations.

Also see this alternative (more breaking) proposal, which instead proposes enforcing designating a default parameter set explicitly, at parse time.


See the example in this comment for how that could be useful in the real world.


Technically, this is a _breaking change_ in the following edge cases, both of which apply only if _no_ default parameter set is specified:

  • (a) If a function/script has only _one_ explicitly named parameter set, that parameter set currently becomes the effective one _by default_ in parameter-less invocations _if_ at least one parameter in that explicit set is declared as _mandatory_:

    • That is, currently the mandatory parameter is prompted for; aside from the fact that this behavior is questionable, there's a real bug associated with it - see #11201.
    • With the proposed change, this will no longer be the case (but arguably it should never have worked this way): a parameter-less invocation will succeed as-is, and $PSCmdlet.ParameterSetName will contain __AllParameterSets.
  • (b) If a function/script has _multiple_ explicitly named parameter sets only _one_ of which does _not_ comprise mandatory parameters, the latter currently becomes the effective one by default in a parameter-less invocation and in all-parameter-sets-only parameters invocations.

    • With the proposed change, this will no longer be the case (but arguably it should never have worked that way): such invocations will then make $PSCmdlet.ParameterSetName contain __AllParameterSets.

Example of (a):

function foo {
  [CmdletBinding()]
  param(
    [Parameter(ParameterSetName='ByBar', Mandatory)]
    $Bar
  )
  $PSCmdlet.ParameterSetName
}
# Currently: prompts for -Bar; 
# After change: allows parameter-less invocation
foo 

Example of (b):

function foo {
  [CmdletBinding()]
  param(
    [Parameter(ParameterSetName='ByBar', Mandatory)]
    $Bar,
    [Parameter(ParameterSetName='ByBaz')]  # non-mandatory
    $Baz,
    $Bam  # all-parameter-sets parameter
  )
  $PSCmdlet.ParameterSetName
}
# Currently: makes $PSCmdlet.ParameterSetName contain 'ByBaz'
# After change: makes $PSCmdlet.ParameterSetName contain '__AllParameterSets'
foo   # ditto for: foo -Bam arg

Steps to reproduce


# Advanced function with 1 explicit parameter set
function foo1 { [CmdletBinding()] param($bar1, [Parameter(ParameterSetName='bar2')] $bar2) 'foo1' }

# Advanced function with 2 explicit parameter sets
function foo2 { [CmdletBinding()] param($bar1, [Parameter(ParameterSetName='bar2')] $bar2, [Parameter(ParameterSetName='bar3')] $bar3) 'foo2' }

# Now invoke the functions without arguments, which should succeed in both cases, due to the
# presence of "untagged" parameter $bar1
{ foo1 } | Should -Not -Throw
{ foo2 } | Should -Not -Throw

Expected behavior

Both tests should succeed.

Actual behavior

The 2nd test fails:

Expected no exception to be thrown, but an exception 
"Parameter set cannot be resolved using the specified named parameters. 
One or more parameters issued cannot be used together or an insufficient number of parameters were provided." was thrown...

Environment data

PowerShell Core 7.0.0-preview.6
Issue-Question WG-Engine

Most helpful comment

Can we please agree drop the personal aspect of this argument? It hasn't furthered this discussion in any way.

To your actual point @jhoneill I will simply say that the binder is woefully inadequate in any but very simple circumstances. As soon as you have a number of sets with at least one mandatory parameter each, it is completely impossible to tell the binder that you want to create a set with no mandatory parameters at all.

For an exercise in frustration, I invite you to attempt to add a parameter set to Where-Object that takes no mandatory parameters. The binder alternates between other defined sets, but will completely refuse to recognise a set that doesn't contain any mandatory parameters.

If you can figure it out, let me know; I've been trying for _weeks_ to get that going so I can resolve a different issue.

The binder needs improvement.

That said, I do see potential for confusion with users here if we take this suggestion as it is. From what I've seen, at least, it seems quite common for cmdlets and functions to leave optional parameters in "all sets" when the design is based around a specific number of sets. I can see how this suggestion would tend to break that.

I'm not sure what a better solution would be on the whole, at least not without massively complicating the binder and/or the methods in which parameter sets must be defined by the author of a command.

Given that the binder is a sensitive area, I would be inclined to think that any changes to it would require at minimum a fairly specific and extensive RFC, so I don't think a great deal is likely to come from this issue itself. There are a lot of edge cases I'd like to fix in the binder, in truth, but I think this suggestion is a bit too likely to break existing cmdlet and function designs.

I would, however, definitely appreciate some way to indicate to the binder that I _do_ want __AllParameterSets to be a valid choice when working through binding logic. There are definitely cases where it would be significantly simpler to design parameter sets if that was an available option. It would probably have to be an option in the [Cmdlet()] and/or [CmdletBinding()] attribute properties somewhere, I would think.

All 28 comments

I've also run into cases where even specifying the default parameter set will not work, especially if you have a default parameter set with no mandatory parameters. It will revert to prompting for mandatory parameters from any other set.

What you are seeing is correct behaviour

[Parameter(ParameterSetName='bar2')] $bar2,
[Parameter(ParameterSetName='bar3')] $bar3) 

Says there are two parameter sets.
If -Bar2 is passed you are in set "bar2" ; if -Bar3 is passed you in are "bar3" However both parameters are optional so if neither is present you could still be in either of the two sets.
More experienced authors might write like this.

function foo2 { [CmdletBinding()] param($bar1, [Parameter(ParameterSetName='bar2')] $bar2, [Parameter(ParameterSetName='bar3',Mandatory)] $bar3) $PSCmdlet.ParameterSetName }

PowerShell can now infer that if -Bar3 is not passed, it can not be using set "bar3" and it must be in set bar2.

I _thought_ that the advice was 1 parameter in every non-default set should be mandatory which takes away the need to have a default.

There is a complication which I blogged here
https://jamesone111.wordpress.com/2016/11/30/powershell-piped-parameter-peculiarities-and-a-palliative-pattern/
from something I first hit in 2013.

James O'Neill's Blog
Writing some notes before sharing a PowerShell module,  I did a quick fact check and rediscovered a hiccup with piped parameters and (eventually) remembered writing a simplified script to show…

if neither is present you could still be in either of the two sets.

No, you should still be in the automatic __AllParameterSets set that the untagged parameter(s) belong to.

There's no reason to force you to artificially designate a default parameter set you're not actually using (which is what you need to do to make the problem go away: [CmdletBinding(DefaultParameterSet='IShouldNotNeedToDoThis')])

if neither is present you could still be in either of the two sets.

No, you should still be in the automatic __AllParameterSets set that the untagged parameter(s) belong to.

Actually no.
What should happen if I run this with no parameters ?

function test1 { 
[CmdletBinding()]
  param(
  [Parameter(Mandatory)] $first
  )
$PSCmdlet.ParameterSetName 
}

It knows what the default parameter set is, and that it is missing an mandatory parameter so it prompts for it. _It doesn't create an extra parameter set and say you can run without the parameter._
Let's try another one. What should happen here

function test2a { 
[CmdletBinding()]
  param(
  [Parameter(Mandatory,ParameterSetName="one")] $first 
  )
$PSCmdlet.ParameterSetName 
}

Should it prompt ? Or should it say "there must be two sets, so we're in __AllParameterSets don't prompt" (it prompts)

function test2b { 
[CmdletBinding()]
  param(
  [Parameter(Mandatory,ParameterSetName="one")] $first,
  [Parameter(Mandatory,ParameterSetName="Two")] $Second
  )
$PSCmdlet.ParameterSetName 
}

Should this create a third parameter set to run with no parameters ? Most people would look at this and say the command must run with one parameter or the other, but the function has no way to choose a set; and it would be better to set a default and prompt than get "parameter set cannot be resolved" but that is down to the programmer to write in the correct way.

Next we have only one mandatory parameter.

function test2c { 
[CmdletBinding()]
  param(
  [Parameter(Mandatory,ParameterSetName="one")] $first,
  [Parameter(ParameterSetName="Two")] $Second,
  [Parameter(ParameterSetName="Two")] $twentySecond
  )
$PSCmdlet.ParameterSetName 
}

We can tell from the absence of that parameter that we can't be using that set, and there is only one set left. All its parameters are optional. Using second or twentysecond prevents us using first, and vice versa. Again no extra parameter set is created. No mandatory parameter rules out set "one" so we must be in set two even if no optional parameters from that set are given. That's what I meant about not being able to infer anything from the absence of an optional parameter. Presence of mandatory or optional tells you that a set is selected, Absence of mandatory tells you it isn't selected. even without the optional parameter(s) a set might be the only one left.

With those behaviours it would be odd to say, for the one case where no default is given, and there are >= 2 named sets, and the set can't be determined from the parameters given (or mandatory parameters not given) then an extra parameter set should be added.

There's no reason to force you to artificially designate a default parameter set you're not actually using (which is what you need to do to make the problem go away: [CmdletBinding(DefaultParameterSet='IShouldNotNeedToDoThis')])

You're not forced to. In case you've given you can say either "bar1 must be present in set 1, but bar2 is optional in set 2". Only where you have a or b or c means set1 and d or e or f means set (i.e. you can't make a parameter all in all but one of the set) are forced to name a set for "neither".

You're missing the premise of this issue: that there are non-mandatory all-parameter-sets parameters.
They alone unambiguously imply the automatic __AllParameterSets parameter set, if either _no_ parameters are given or only said non-mandatory all-parameter-sets parameters are given.

If this works - and it does:

function foo2 { 
    [CmdletBinding(DefaultParameterSetName = 'PleaseDontMakeMeDoThisIBegOfYou')] 
    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3')] $bar3
    ) 
    'foo2' 
}

foo2 # OK

Then so should this - which doesn't:

function foo2 { 
    [CmdletBinding()] 
    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3')] $bar3
    ) 
    'foo2' 
}

foo2 # Kaboom!

Please focus on just this specific case in your reply.

@mklement0 please stop being obtuse. There is no reason why the second one should work.

The first one fixes an error in the second and where you have a requirement to call with neither parameter but haven't coded that. What you're saying is that PowerShell shouldn't need that fix, and should guess that there was a requirement to call with neither.

  • It is pointless defining one parameter set, because even the parameters no assigned to a set belong to it.
  • When you define two or more sets PowerShell needs a way to select a set.
  • If you want a an additional set which is "none of the combinations associated with a declared set" you have to declare that. PowerShell doesn't do it for you and it shouldn't guess you wanted.

You have shown that it is possible for someone who doesn't understand this to write something which a user can call in a way which makes it impossible for PowerShell to make a selection.

You're asking for a special case which is, if more than one set is made up entirely of non-mandatory parameters, PowerShell should create an additional parameter set. I think there should be a special case where 0/0 isn't a divide by zero error but I'm not going to get _that_. :-)

If you write

    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3')] $bar3
    ) 

it says "There are two parameter sets 'bar2 and 'bar3' , If the user doesn't specify a parameter for one or the other throw an error" .But

    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3',mandatory=true)] $bar3
    ) 

says "There are two parameter sets 'bar2 and 'bar3' , If the user doesn't specify a parameter for bar3 you can assume bar2" So does this

[CmdletBinding(DefaultParameterSetName = 'bar2')]
    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3')] $bar3
    ) 

and this

[CmdletBinding(DefaultParameterSetName = 'neither')]
    param(
        $bar1, 
        [Parameter(ParameterSetName = 'bar2')] $bar2, 
        [Parameter(ParameterSetName = 'bar3',mandatory=true)] $bar3
    ) 

Says there is a third set. and is equivalent to

[CmdletBinding()]
    param(
        [Parameter(ParameterSetName = 'neither')]  
        [Parameter(ParameterSetName = 'bar2')], 
        [Parameter(ParameterSetName = 'bar3')] 
        $bar1, 
        [Parameter(ParameterSetName = 'bar2',mandatory=true)] $bar2, 
        [Parameter(ParameterSetName = 'bar3',mandatory=true)] $bar3
    ) 

Write the correct one for the situation.

If you want PowerShell to make guesses. This

function get-thing { 
     param(
       $bar
        [Parameter(ParameterSetName = 'byName',Mandatory)] $Name, 
        [Parameter(ParameterSetName = 'byID',Mandatory)] $Id
    ) 
    $pscmdlet.ParameterSetName 
}

Says you must give a name or an ID. The function won't work otherwise.

function get-thing { 
     param(
       $bar
        [Parameter(ParameterSetName = 'byName',Mandatory)] $Name, 
    ) 
    $pscmdlet.ParameterSetName 
}

Should this prompt for name or use the "default" parameter set. ?
What it does is selects by name. But if $name isn't mandatory it selects _allParameterSets. That's the broken behaviour but it doesn't mater . No one should be writing a functions with a single one parameter set.

Demanding that everything else behaves like that case is muddled at best

On a quick meta note, @jhoneill:

please stop being obtuse.

Please remain from making such ad hominem statements and generally from striking a flippant tone in your comments. It adds nothing to the discussion and serves only to antagonize.

here you have a requirement to call with neither parameter but haven't coded that.

The point is that there shouldn't be a _need_ to code that, because the desired behavior is _implied_:

  • Whenever you have at least one _untagged_ parameter (not explicitly assigned to any parameter set), it is implicitly in the __AllParameterSets set.

  • PowerShell selects the __AllParameterSets parameter set - reflected in $PSCmdlet.ParameterSetName - _by default_, if you either specify no arguments or only arguments to parameters that are untagged. (If there are no tagged parameters, __AllParameterSets is by definition always the selected parameter set.)

That's how it works with untagged-parameters-only functions and those with only _one_ explicit parameter set (assigned to one or more additional parameters).

There is no logical reason not to extend that logic to _multiple_ explicit parameter sets: If you specify no parameters or only untagged ones, __AllParameterSets is implied.

It is pointless defining one parameter set, because even the parameters no assigned to a set belong to it.

Only one explicitly defined parameter set may be the rarer use case, but it's definitely not pointless.
For instance, say you have one untagged parameter, plus two others that must be specified _together_: you'll need to put those two in an explicit parameter set (but the untagged one can remain untagged).

Here's an example inspired by this real-world use case (not mine) that caused me to open this issue:

Consider function Write-Message, which should wrap Write-Host as follows:

  • Print a default message, if no arguments are passed at all.
  • If only a -Message argument is passed, print that message as-is.
  • If one of the mutually exclusive -AsError or -AsWarning switches is passed, print the (default or explicit) message in a switch-specific color.
function Write-Message {
  [CmdletBinding(DefaultParameterSetName = 'PleaseDontMakeMeDoThis')]
  param(
    [string] $Message = 'Completed.',

    [Parameter(ParameterSetName = 'err')] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn')] [switch] $AsWarning
  )

  $writeHostArgs = @{ Object = $Message }

  if ($AsError) { $writeHostArgs.ForegroundColor = 'Red' }
  elseif ($AsWarning) { $writeHostArgs.ForegroundColor = 'Yellow' }

  Write-Host @writeHostArgs

}

As you can see, the requirement to specify the PleaseDontMakeMeDoThis parameter set name is artificial - there's no reason for the code not to work without it, because the automatic __AllParameterSets default parameter set could just as well be used instead of the - otherwise unused - PleaseDontMakeMeDoThis explicit default parameter set.

Often you simply don't care or need to care about what $PSCmdlet.ParameterSetName contains.

On a quick meta note, @jhoneill:

please stop being obtuse.

Please remain from making such ad hominem statements and generally from striking a flippant tone in your comments. It adds nothing to the discussion and serves only to antagonize.

The case you are making here is ... whatever. You make plenty of comments this is only one where I think the line of reasoning amounts to "being obtuse". If that felt like a personal attack it wasn't intended that way, and I apologize. Though on the flip side others might call characterizing a comment on your reasoning as a personal attack as "antagonistic". So... shall we both don the metaphorical kid gloves and have a final try ?

here you have a requirement to call with neither parameter but haven't coded that.

The point is that there shouldn't be a _need_ to code that, because the desired behavior is _implied_:

No. There is a behavior which you have not coded, but which you would like the software to deduce that you want from something else which you are doing incorrectly.

  • Whenever you have at least one _untagged_ parameter (not explicitly assigned to any parameter set), it is implicitly in the __AllParameterSets set.
  • PowerShell selects the __AllParameterSets parameter set - reflected in $PSCmdlet.ParameterSetName - _by default_, if you either specify no arguments or only arguments to parameters that are untagged. (If there are no tagged parameters, __AllParameterSets is by definition always the selected parameter set.)

No. There is no __AllParameterSets set _as such_. That is a name which PowerShell uses when it has to create a fictious set, which only happens when there is one set or no sets defined. There is also a flag of the same name which "says this parameter is in all the sets".

Function Test1 {
[cmdletbinding()]
Param ($a,$b ) 
$PSCmdlet.ParameterSetName
}

(get-command test1).ParameterSets.name
__AllParameterSets


test1
__AllParameterSets

Test 1 doesn't have any parameter sets. Where PowerShell needs to give a name to the collection of parameters this "anonymous set" gets the label "__AllParameterSets"

Function Test2 {
[cmdletbinding()]
Param (
$a, [parameter(ParameterSetName="one")] $b
) 
$PSCmdlet.ParameterSetName

}
(get-command test2).ParameterSets.name
one

Test2 has a parameter set named one. It doesn't have a set named __AllParameterSets_
But if it is possible to select "set one", it must be possible to select "not set one".

test2
__AllParameterSets

Again that label is used for the "anonymous parameter set", but the next case has two named sets. There either you are in set one or you are in set2. There will never be an anonymous set once you get to two sets and beyond.

Function Test3 {
[cmdletbinding()]
Param (
$a, [parameter(ParameterSetName="one")] $b, [parameter(ParameterSetName="two")] $c
) 
$PSCmdlet.ParameterSetName
}
(get-command test3).ParameterSets.name

For instance, say you have one untagged parameter, plus two others that must be specified _together_: you'll need to put those two in an explicit parameter set (but the untagged one can remain untagged).

Doesn't work. If you have this

Function Test4 {
[cmdletbinding()]
Param (
$a, [parameter(ParameterSetName="one",Mandatory)] $b, [parameter(ParameterSetName="one",Mandatory)] $c

) 
$PSCmdlet.ParameterSetName

}

Running test4 with no parameters will prompt for B and C. You must create a default set to allow only -a (or for no parameters at all). Then C will be demanded if only B is supplied and vice versa.

Here's an example inspired by [this real-world use case]> Consider function Write-Message, which should wrap Write-Host as follows:

  • Print a default message, if no arguments are passed at all.
  • If only a -Message argument is passed, print that message as-is.
  • If one of the mutually exclusive -AsError or -AsWarning switches is passed, print the (default or explicit) message in a switch-specific color.
function Write-Message {
  [CmdletBinding(DefaultParameterSetName = 'PleaseDontMakeMeDoThis')]
  param(
    [string] $Message = 'Completed.',

    [Parameter(ParameterSetName = 'err')] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn')] [switch] $AsWarning
  )

  $writeHostArgs = @{ Object = $Message }

  if ($AsError) { $writeHostArgs.ForegroundColor = 'Red' }
  elseif ($AsWarning) { $writeHostArgs.ForegroundColor = 'Yellow' }

  Write-Host @writeHostArgs

}

As warning and as error are wrongly specified. What you have said is _you can be in Warn without AsWarning_, and _you can be in err without AsErr_ they should be

    [Parameter(ParameterSetName = 'err',mandatory)] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn'mandatory)] [switch] $AsWarning

Which says "You an only be in err if AsError is specified, and only be in warn if asWarning is specified"
Where a set has only one parameter that parameter should be mandatory.

This is like theGet-Somethingwhich must be given either -ID or -Name. If you want a third case you Should tell powershell there is a third set. But you can cheat, and let one case be the default and not have its parameter as mandatory. (provided you don't inspect the set name ... see below).

As you can see, the requirement to specify the PleaseDontMakeMeDoThis parameter set name is artificial - there's no reason for the code not to work without it, because the automatic __AllParameterSets default parameter set
It's _not a parameter set it is a label to use when PowerShell needs to invent a set name_. I think once you start thinking of it that way it becomes clear. There is no case where $PSCmdlet.ParameterSetName will return that as a set name, or the command object has that name when there are 2 or more sets.

What you're saying is that by wrongly coding parameters which are _required_ in order to select a set as _non-mandatory_ members of the set, you are sending a signal to PowerShell to assume you want it to create a third set. I'm failing to see a strong case for that.

Often you simply don't care or need to care about what $PSCmdlet.ParameterSetName contains.
I never use it in real code, I only look at what is in the values or PsboundParameters. I'm only using it in the examples to show what powershell selects.

@jhoneill:

On the meta issue:

Thanks for apologizing, but to be clear: "please stop being obtuse" refers to a person's behavior, not to a line of reasoning; it is the very embodiment of an ad hominem remark.


there is no __AllParameterSets set as such

For all intents and purposes, there is, as also demonstrated by your examples; most succinctly:

PS> & { [CmdletBinding()] param($foo) $PSCmdlet.ParameterSetName }
__AllParameterSets

That it would have been better to name this implicit, default parameter set Default I fully agree with.

As warning and as error are wrongly specified. What you have said is you can be in Warn without AsWarning, and you can be in err without AsErr they should be

Making these switches mandatory is neither a logical nor an actual requirement. Their mere presence - if specified - selects their specific parameter set.

If _neither_ is specified, the default parameter set applies - and there is no reason that that default parameter set _has_ to be given a name by the user, especially given that there is no strict reason to use it inside the function.

There will never be an anonymous set once you get to two sets and beyond.

There _currently isn't_, but there _should be_, as demonstrated.
There is no good reason to make the implicit __AllParameterSets parameter set go away, just
because there happens to be more than one explicit one.

@jhoneill:

On the meta issue:

Thanks for apologizing, but to be clear: "please stop being obtuse" refers to a person's behavior, not to a line of reasoning; it is the very embodiment of an ad hominem remark.

Ad hominem is "playing the man not the ball". Was the remark critical of what you are ? Because saying no speech or behaviour can ever be criticized would be absurd.

there is no __AllParameterSets set as such

For all intents and purposes, there is, as also demonstrated by your examples; most succinctly:

PS> & { [CmdletBinding()] param($foo) $PSCmdlet.ParameterSetName }
__AllParameterSets

That it would have been better to name this implicit, default parameter set Default I fully agree with.

As warning and as error are wrongly specified. What you have said is you can be in Warn without AsWarning, and you can be in err without AsErr they should be

Making these switches mandatory is neither a logical nor an actual requirement. Their mere presence - if specified - selects their specific parameter set.

As written it says "The parameter set can exist without this parameter". If there is only one parameter in a set it is mandatory but you can get away without saying so. Mostly things will work. Ditto not saying which set should be the default. But it's slippy and will create parameter combinations where PowerShell can't apply logic which will always be right

If _neither_ is specified, the default parameter set applies - and there is no reason that that default parameter set _has_ to be given a name by the user, especially given that there is no strict reason to use it inside the function.

NO ! in this

function Write-Message {
  [CmdletBinding()]
  param(
    [string] $Message = 'Completed.',

    [Parameter(ParameterSetName = 'err')] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn')] [switch] $AsWarning
  ) 
}

There are two parameter sets, there is no default set; Message belongs to both sets.

(Get-Command write-message).ParameterSets[0].Parameters.name
Message
AsError .... 

(Get-Command write-message).ParameterSets[1].Parameters.name
Message
AsWarning

There will never be an anonymous set once you get to two sets and beyond.

There _currently isn't_, but there _should be_, as demonstrated.
You've said you want it. That's not a demonstration that anything should be arranged in any particular way. What you've said is such a dreadful imposition to write code properly that Powershell should guess the author's intention rather than follow what they actually wrote. Most of the time that's a bad way to go

There is no good reason to make the implicit __AllParameterSets parameter set go away, just
because there happens to be more than one explicit one.

It's not "made to go away". It's not used as label on an artificial set. If zero sets are defined everything belongs to a phantom set. If sets exist at all there must be two or more and PowerShell will add a second to fix "wrong" code when there is only one.

Was the remark critical of what you are ?

It was critical of _my behavior_, based on your _conjecture_ about it - which has nothing to do with critiquing _arguments I made_.

It is unhelpful in general and needlessly makes things personal to speculate on what _motivation_ may or may not underlie others' comments. The only thing worth addressing is the content of the comments themselves.

there is no default set; Message belongs to both sets.

There _should be_, for the reasons stated. (Of course any untagged parameter belongs to _all_ sets, as always, but that is not the point).

I think we understand (as opposed to subscribe to) the respective positions, and at this point, to use the old saw, I suggest we agree to disagree.

Was the remark critical of what you are ?

It was critical of _my behavior_, based on your _conjecture_ about it - which has nothing to do with critiquing _arguments I made_.

The behaviour was the making of a case.
You have one very specific scenario where you want the language to be changed to figure out that that statements you _wrote_ did not specify your intent.
You wanted 3 valid combinations of parameters to be valid.
With A but Not B,
With B but Not A
How is PowerShell meant to know that it should create an ad hoc third set for neither ? Take this example, which I will say up front is NOT how it should be written.

function Set-user {
param (
    [Parameter(ParameterSet="ByID")]$ID,
    [Parameter(ParameterSet="ByName")]$Name,
    $newProperties
)

Here whatever is in the body of the function will NOT do its job properly without an ID or Name. Maybe it it contains something like this

If ($id) {$filter="ID = $id"}
else    {$filter = "Name Like '$Name%'"} 
Run-someSql -filter $filter -userProperties $newProperties. 

The author of the code emphatically did not intend this to run with neither parameter.
If that happens it will run apply the changes to any user with a name.
They should have said that the parameters are mandatory, AND if they specified a default set the command would prompt for a value instead of failing with an error. They didn't and this script has been used for years with no ill-effects. People's data getting deleted would be a strong argument for preserving the status quo.

All that you have said to this point says for n valid parameter combinations you only want to give names to n-1 of them, and that you find it to an imposition to give the third one a name (witness the infantile naming you used for that set).
When other cases were pointed out to you your response came across as "don't bother me with other cases, I only care about the code I've written for write-host.". ("Please focus on just this specific case in your reply."). It's quite reasonable to ask someone to stop displaying such an attitude and not to demand breaking change in order to save a few keystrokes an area where they demonstrate a lack understanding. Labelling both as "being obtuse" requires no special hypothesis about the person's state of mind, it's just a shorthand which, perhaps, doesn't work over cultural boundaries.

@jhoneill:

The behaviour was the making of a case.

Another user argues a point you don't agree with, so you tell them to stop making their point, by telling them to stop exhibiting a "a lack of intelligence or sensitivity", because to you that is the only conceivable reason for making that point?

I find that disconcerting; it is the opposite of a constructive debate conducted in good faith.


You have one very specific scenario where you want the language to be changed

My intent is the exact opposite: I want to _generalize useful behavior_ that is currently only available _inconsistently_ (irrespective of what may or may not have been the original design intent):

If an advanced function or script _doesn't explicitly declare a default parameter set_ via its [CmdletBinding()] attribute:

  • allow invocations without arguments as well as invocations comprising only parameters not explicitly assigned to any parameter set - _irrespective of the presence of parameters belonging to one or more explicitly named parameter sets_ and irrespective of whether these parameters are declared as _mandatory_ in their sets.

  • and make $PSCmdlet.ParameterSetName contain __AllParameterSets in that event.

I've also updated the initial post to make that clear.
@vexx32, I think that would also cover the case you mentioned above (although I haven't personally seen that symptom).

@jhoneill : All your Set-User example needs is to mark $ID, and $Name as mandatory, and make either ById or ByName the default parameter set.
Of course, as the previous example shows it _is_ perfectly legitimate and useful to write functions where _neither_ of these parameters are _required_.


@jhoneill:

The behaviour was the making of a case.

Another user argues a point you don't agree with, so you tell them to stop making their point, by telling them to stop exhibiting a ["a lack of intelligence or sensitivity"]

OK, you've resorted to latin (which doesn't impress me having studied it at school), and no reach for a dictionary (which owning several doesn't impress me either), for reasons I can only guess at - if it was to demonstrate linguistic expertise may be could use it for short reply to
_A response, when told at length and in detail why a demand is both dangerous to others and bad practice , which takes no account of the explanation and simply repeats the demand without adding supporting matter or addressing the reasons given for turning it down_

because I will gladly accept that it should replace "Please stop being obtuse".

You have one very specific scenario where you want the language to be changed

My intent is the exact opposite: I want to _generalize useful behavior_ that is currently only available _inconsistently_ (irrespective of what may or may not have been the original design intent):

The behaviour you want to genralize is only useful in specific case. In other cases it is potentially dangerous.

@jhoneill : All your Set-User example needs is to mark $ID, and $Name as mandatory, and make either ById or ByName the default parameter set.
Yes. I know that. What were the last words before the script "This is not how it should be written" But there are scripts out in the world which don't do that (I have fixed some of them) and reply on the present behaviour.

All your example needs to do is mark one of the two parameters as mandatory. It doesn't even need to set a default set. You demand that PowerShell should be changed so you don't need to put Mandatory against one of yours (Or strictly n-1) of yours. AND authors who rely on present behaviour go back and change scripts which would then become dangerous.

it _is_ perfectly legitimate and useful to write functions where _neither_ of these parameters are _required_.

It is . You just need to tell PowerShell and not have it make the assumption because the assumption is not safe.
One person can write the [bad] example I gave, another one can write the [bad] example you gave. PowerShell must make a default assumption, which will favour one of them. The assumption in the original design was to fail safely rather than work dangerously. Not running safe code because you weren't told that you should (which is what you are complaining about) is better as a principle than Running potentially unsafe code because you weren't told you should not.
Written with your intent the author gets a run time error and learns to add a "mandatory" or "defaultset" option. Written with the intent of my example the author deletes data.

However you gave this example.

function Write-Message {
  [CmdletBinding(DefaultParameterSetName = 'PleaseDontMakeMeDoThis')]
  param(
    [string] $Message = 'Completed.',

    [Parameter(ParameterSetName = 'err')] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn')] [switch] $AsWarning
  )

  $writeHostArgs = @{ Object = $Message }

  if ($AsError) { $writeHostArgs.ForegroundColor = 'Red' }
  elseif ($AsWarning) { $writeHostArgs.ForegroundColor = 'Yellow' }

  Write-Host @writeHostArgs

}

What this shows is a longer way do
Write-host -fore Red "xx" by instead typing
Write-message -error "xx"
In real-world use it would suggest an author who did not understand _why_ one should write to the error or warning streams and _not_ use write host so let's assume it was _purely_ for demo purposes...
You would not use

[Parameter(ParameterSetName = 'red')]  [switch]$red,
[Parameter(ParameterSetName = 'green')]  [switch]$green,
[Parameter(ParameterSetName = 'blue')]  [switch]$blue,
[Parameter(ParameterSetName = 'Yellow')]  [switch]$Yellow. 
&c  

It would be absurd because those are mutually exclusive values for the same thing.
What the example hasn't recognised (possibly because these cases often "normal" and "unusual" so use a switch) is that it is setting a message "level" which may be in one of three states "Error", "Warning" or "Normal" Instead of making those three values for a single parameter (with a Normal as default), two values are expressed as a parameters which must be in sets to prevent two being given, and there is a need to deduce a the third from the absence of the other two (and the user must deduce there is third state), ideally it should be

[validateSet("Normal","Error","Warning")]   
$Level = "Normal" 

This allows the user to supply the message and then tab complete values for the level, to see "normal" is an option and allow it to be selected explicitly or by default, and the code body is clear that there are three cases not just two.

switch ($level) {
"Normal" {Write-Information $msg}
"Error" {Write-Error $msg}
"Waring" {Write-Warning $msg}
}

_Good style_ is not to implement a choice between n possibilities for X, using n-1 switches and an unspecified default. Remove those cases and it comes down to cases like "Do you want your excel data formatted as a table, and if so, and what sort of table but if not would you like a named range instead". There are many ways to specify a table so none of the table parameters can be mandatory.
Should PowerShell make me say the single NamedRange parameter _is_ mandatory, or identify a default set? When the answer is "You have to write your code better" a change is only needed "better" is very onerous. How many times could you have tagged one of those parameters as mandatory which would fix your issue with the time that has gone into this discussion. ?

Can we please agree drop the personal aspect of this argument? It hasn't furthered this discussion in any way.

To your actual point @jhoneill I will simply say that the binder is woefully inadequate in any but very simple circumstances. As soon as you have a number of sets with at least one mandatory parameter each, it is completely impossible to tell the binder that you want to create a set with no mandatory parameters at all.

For an exercise in frustration, I invite you to attempt to add a parameter set to Where-Object that takes no mandatory parameters. The binder alternates between other defined sets, but will completely refuse to recognise a set that doesn't contain any mandatory parameters.

If you can figure it out, let me know; I've been trying for _weeks_ to get that going so I can resolve a different issue.

The binder needs improvement.

That said, I do see potential for confusion with users here if we take this suggestion as it is. From what I've seen, at least, it seems quite common for cmdlets and functions to leave optional parameters in "all sets" when the design is based around a specific number of sets. I can see how this suggestion would tend to break that.

I'm not sure what a better solution would be on the whole, at least not without massively complicating the binder and/or the methods in which parameter sets must be defined by the author of a command.

Given that the binder is a sensitive area, I would be inclined to think that any changes to it would require at minimum a fairly specific and extensive RFC, so I don't think a great deal is likely to come from this issue itself. There are a lot of edge cases I'd like to fix in the binder, in truth, but I think this suggestion is a bit too likely to break existing cmdlet and function designs.

I would, however, definitely appreciate some way to indicate to the binder that I _do_ want __AllParameterSets to be a valid choice when working through binding logic. There are definitely cases where it would be significantly simpler to design parameter sets if that was an available option. It would probably have to be an option in the [Cmdlet()] and/or [CmdletBinding()] attribute properties somewhere, I would think.

Fully agreed re the personal aspect, @vexx32 : I wish it had never entered the discussion, and my responses were geared toward eliminating it in the future.

In that spirit, allow me one last comment to address that aspect.

@jhoneill:

The sole reason I linked the quoted (English) phrase to the dictionary definition of the word _obtuse_ was to eliminate any uncertainty around what that word is commonly understood to mean.

@vexx32:

I've updated the initial post to clarify that the behavior should only apply if there's at least one untagged parameter present; in the absence of such, the no-argument invocation should default to the _one_ explicit parameter set, if there's only one; otherwise, explicitly naming a default should be _required_, as is already the case (except if there's only _two_ and only _one_ of them has a mandatory parameter).

It seems quite common for cmdlets and functions to leave optional parameters in "all sets" when the design is based around a specific number of sets.

To be clear: I'm not proposing that that be changed at all. Untagged parameters should continue to be implicitly part of all parameter sets.

What may be confusing is that _AllParameterSets does double duty:

  • as a "meta" parameter-set name simply signaling "I belong to all parameter sets, whichever ones may be defined".

  • as a concrete, implicitly defined parameter set that comprises the untagged parameters only, as well as the no-arguments case (if permitted).

With the above clarification re the logic only applying in the presence of at least one tagged parameter, I don't actually see any existing cases that would break - only _additional_ use cases would be enabled.

@mklement0 it is pretty common from what I've seen where the author doesn't _want_ __AllParameterSets to be its own set, when they have multiple defined sets.

As an example:
https://github.com/vexx32/PSWordCloud/blob/master/Module/src/NewWordCloudCommand.cs

I have several parameters that need to be in all the regular sets, but if you could call that with __AllParameterSets being the active set, that command would break (and currently works as intended).

@mklement0 Maybe it's a British usage: like the angle "acute" means "sharp" (and therefore quick-witted etc.) and "obtuse" means the opposite but it is used more in the sense "you're looking at this back-to-front", than "you are an idiot". (Possibly people muddle up 'approaching it obliquely' and "going off at tangent") But if you got the sense of "wantonly stupid" from it that was the never the intent. I think there are bits of this Joel doesn't get (see below) and I've assumed he has a brain the size of a planet :-) My biggest learning from being a teacher is that people learn things in different sequences and you can't conclude someone is ignorant because they don't know X, they may know Y and Z which are "advanced topics".

@vexx32 If someone complains about what I've said I tend to apologise or/and justify it, and that's not always the best personality trait.

to your actual point @jhoneill I will simply say that the binder is woefully inadequate in any but very simple circumstances. As soon as you have a number of sets with at least one mandatory parameter each, it is completely impossible to tell the binder that you want to create a set with no mandatory parameters at all.

No, I have that in multiple places just write
[cmdletbinding(defaultParameterSet="a")]
Where "a" is a set not referenced anywhere else.
see https://github.com/dfinke/ImportExcel/blob/master/Send-SQLDataToExcel.ps1
for an example with 4 sets each has at least one mandatory parameter and set is selected by the presence of one parameter and the absence of another. Running it with no parameters drops through with a warning message,

or see https://github.com/dfinke/ImportExcel/blob/master/PivotTable.ps1 where a mandatory parameter - the only one in that set - selects a set and no-parameters-in-either-set selects the other set

For an exercise in frustration, I invite you to attempt to add a parameter set to Where-Object that takes no mandatory parameters. The binder alternates between other defined sets, but will completely refuse to recognise a set that doesn't contain any mandatory parameters.

Like this

> New-ProxyCommand Where-Object | clip
#5 minutes later. 

>$x = ($Null ,1,2, $null , 3 )
>$x.Count
5

>$x = ($Null ,1,2, $null , 3 | where-object) 
>$x.count
3

https://gist.github.com/jhoneill/83e263836a12f1ccab1019f3ab0373dd

The binder needs improvement.
Agree.

That said, I do see potential for confusion with users here if we take this suggestion as it is.
Confusion as a minimum. Which isn't to say nothing should be done, but I think what is suggested here is the wrong thing for the wrong reason.

I would, however, definitely appreciate some way to indicate to the binder that I _do_ want __AllParameterSets to be a valid choice when working through binding logic.

See what I did above. Create one more set name which only appears as the default set.

GitHub
PowerShell module to import/export Excel spreadsheets, without Excel - dfinke/ImportExcel
GitHub
PowerShell module to import/export Excel spreadsheets, without Excel - dfinke/ImportExcel
Gist
Where-object with no parameter support. GitHub Gist: instantly share code, notes, and snippets.

@vexx32, I don't think my proposal would break your use case, because the intent is definitely to retain the existing belongs-to-ALL-parameter-sets-if-untagged logic.

__AllParameterSets becoming the effective set would apply _if and only if_:

  • _no_ default set is explicitly designated in [CmdletBinding()]
  • _and_ untagged parameters _only_ are bound during invocation

That is, in your example, passing just the untagged -ExcludeWord parameter (hypothetically) would still make $PSCmdlet.ParameterSetName reflect ColorBackground, the explicitly designated default set.

I really wish we could rename __AllParameterSets to something more sensible like Default _in the specific usage as a concrete, implicit default set_ - but that ship has obviously sailed.

@vexx32,

I've realized that my proposal does break certain edge cases, which are now summarized in the initial post.

I've also gone back to proposing that in the absence of an _explicit_ default parameter set parameter-less invocations should always be allowed (whether all-parameter-sets parameters are also declared or not), because the current behavior is questionable in that respect too - and is buggy: see #11201.

The proposal in the initial post now lays out simple rules that should be easy to document and remember.

I realize that nothing may come of this issue directly, but the initial post can now serve as the basis for an RFC and for assessing potential ramifications.

@vexx32:

After more discussion in #11201, I'm closing this in favor of a new proposal in https://github.com/PowerShell/PowerShell/issues/11201#issuecomment-559491980.

Like the proposal in this issue, it won't go anywhere without an RFC.
It would undoubtedly be an unacceptable breaking change, but hope it can get the discussion started to at least make it an optional feature.

The basic idea is to:

  • (a) _require_ designating a parameter set as the default in the presence of parameters with explicit parameter sets
  • (b) enforce that _at parse time_, with specific error messages that provide resolution guidance
  • (c) allow designating parameter set '' (empty string) as the default to explicitly allow the no-argument and all-parameter-set-parameters-only invocations and have $PSCmdlet.ParameterSetName then reflect '' for such invocations.
  • (d) prevent designating default parameter sets that aren't actually (implicitly) defined via [Parameter()] attributes.

As soon as you have a number of sets with at least one mandatory parameter each, it is completely impossible to tell the binder that you want to create a set with no mandatory parameters at all.

@vexx32 I'm trying to interpret this statement. I've been using __AllParameterSets to tell the binder that I want to create a set with no mandatory parameters at all:

function TwoSets{
    [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')]
    param ( $NoSet,
            [parameter(ParameterSetName = 'A', Mandatory = $true)]$A,
            [parameter(ParameterSetName = 'B', Mandatory = $true)]$B  )
    process { $PSCmdlet.ParameterSetName }
}

TwoSets        # __AllParameterSets
TwoSets -A 'a' # A
TwoSets -B 'b' # B

Now I'm wondering whether that only happens to work in that limited case. I _think_ the matter of "creating a set with no mandatory parameters at all" is the crux of #12619.

Edit: As @jhoneill suggested, it looks like DefaultParameterSetname = 'NotUsedForParameters' creates a set with no mandatory parameters. So that behavior doesn't seem to depend on __AllParameterSets, at least in this limited case.

Did you mean something different in the statement I quoted? Would the __AllParameterSets/NotUsedForParameters solution work for the cases you struggled with?

Nope. It works in some limited cases, but once you get more than a couple of parameter sets the binder fails to resolve the "nonexistent" / "_AllParameterSets" set at all, and starts demanding you supply one of the parameters for one of the mandatory sets, or outright fails.

@vexx32 I see now. I confirmed as much in this gist using Where-Object's parameters. Indeed merely naming a non-existent default parameter set _does not_ result in binding with no arguments.

The strange part is this: Merely removing the Not switch and parameter set _does_ result in binding with no arguments.

I can see now why this matter leads to confusion.

Yeah I'd love to refactor Where-Object to be able to handle things like 1..10 | Where-Object -ne 7 or just | Where-Object (to filter out nulls) but the parameter binder does _not_ like working like that unfortunately, the number of sets gets into the insane levels on that command & I can't see a way to make the binder work with that sensibly in its current state.

As an aside, @alx9r: Currently, PowerShell doesn't enforce that at least one parameter - whether explicitly or implicitly - be part of the designated default parameter set, so there is no strict need for the $NoSet parameter in your code; that said, my proposal here - unequivocally a breaking change - would change that.

Was this page helpful?
0 / 5 - 0 ratings