Skip to content

Parameters Should Support Mutual Exclusion Natively #5175

Closed

Description

Problem

Parameter sets are great for grouping parameters into sets that work together. This can, to some degree, also be used to make parameters exclusive. The problem arises when you have mutually exclusive parameters that need to exist in all parameter sets. The current implementation would require creating additional parameter sets to accommodate the actual parameter sets and the mutually exclusive parameters.

For very simplistic demonstration, lets say you have a New-Widget:

function New-Widget {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName="Size")]
        [int]$Height,
        [Parameter(ParameterSetName="Size")]
        [int]$Width,
        [Parameter(ParameterSetName="Size")]
        [int]$Length,

        [Parameter(ParameterSetName="Color")]
        [int]$Red,
        [Parameter(ParameterSetName="Color")]
        [int]$Green,
        [Parameter(ParameterSetName="Color")]
        [int]$Blue
    )
}

Widgets can be defined either by color or size. Now, let say that all widgets, regardless of begin defined by size or color, can either have a flavor or a sound but not both. Currently, to accommodate this you would need to do the following:

function New-Widget {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName="SizeFlavor")]
        [Parameter(ParameterSetName="SizeSound")]
        [int]$Height,
        [Parameter(ParameterSetName="SizeFlavor")]
        [Parameter(ParameterSetName="SizeSound")]
        [int]$Width,
        [Parameter(ParameterSetName="SizeSound")]
        [Parameter(ParameterSetName="SizeFlavor")]
        [int]$Length,

        [Parameter(ParameterSetName="ColorFlavor")]
        [Parameter(ParameterSetName="ColorSound")]
        [int]$Red,
        [Parameter(ParameterSetName="ColorSound")]
        [Parameter(ParameterSetName="ColorFlavor")]
        [int]$Green,
        [Parameter(ParameterSetName="ColorSound")]
        [Parameter(ParameterSetName="ColorFlavor")]
        [int]$Blue,

        [Parameter(ParameterSetName="SizeFlavor")]
        [Parameter(ParameterSetName="ColorFlavor")]
        [string]$Flavor,

        [Parameter(ParameterSetName="SizeSound")]
        [Parameter(ParameterSetName="ColorSound")]
        [string]$Sound
    )
}

This can get wildly out of hand if you have a third or fourth set of mutually exclusive parameters. This becomes difficult to maintain and to add additional parameters. Another option is to have logic in the body of the function to check for mutually exclusive Parameters at the cost of the mutual exclusion not being as discoverable.

In some instances you may be able to refactor into additional cmdlets or make use of custom types. But that doesn't always work for command line utilities meant to ease user burden at the cost of code complexity.

Proposal

I propose that parameters should support mutual exclusion natively. I don't have a solid implementation plan, but one idea would be to add a Parameter Attribute property like ParameterMutexName. Using the example above it would work like this:

function New-Widget {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName="Size")]
        [int]$Height,
        [Parameter(ParameterSetName="Size")]
        [int]$Width,
        [Parameter(ParameterSetName="Size")]
        [int]$Length,

        [Parameter(ParameterSetName="Color")]
        [int]$Red,
        [Parameter(ParameterSetName="Color")]
        [int]$Green,
        [Parameter(ParameterSetName="Color")]
        [int]$Blue,

        [Parameter(ParameterSetName="Size", ParameterMutexName="FlavorOrSound")]
        [Parameter(ParameterSetName="Color", ParameterMutexName="FlavorOrSound")]
        [string]$Flavor,
        [Parameter(ParameterSetName="Size", ParameterMutexName="FlavorOrSound")]
        [Parameter(ParameterSetName="Color", ParameterMutexName="FlavorOrSound")]
        [string]$Sound
    )
}

Parameter binding logic would then check if more than one parameter is supplied in a ParameterMutexName and throw. This is flexible enough to accommodate parameters being exclusive in one set while not exclusive in other sets. This could also accommodate multiple Mutex sets within a parameter set.

Activity

  1. added
    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassif
    Issue-Enhancementthe issue is more of a feature request than a bug
    on Oct 19, 2017
  2. SteveL-MSFT commented on Oct 19, 2017

    @SteveL-MSFT
    Member
  3. markekraus commented on Oct 20, 2017

    @markekraus
    ContributorAuthor

    Steve Lee (@SteveL-MSFT) Interesting. When I was thinking about this my first thought was to add an attribute like what is in that RFC but I immediately realized that it makes things more painful. That's when I thought to add the parameter attribute property.

    Would a solution to this need to work for both classes and parameters? I'm not sure doing something like that is really the correct approach for class properties. It just feels wrong. Was something like what I suggest here ever discussed? It doesn't look like it from the RFC's history, but maybe an offline discussion took place.

  4. SteveL-MSFT commented on Oct 20, 2017

    @SteveL-MSFT
    Member

    I don't think your specific proposal came up when we original thought about this. I think the big challenge was having something that was easy to author and also easy to read (once authored) particularly when cmdlets get extended and more parameters get added (like what you experienced with the web cmdlets).

  5. iSazonov commented on Oct 20, 2017

    @iSazonov
    Collaborator

    I thought about this too. It is not difficult to implement mutual exclusion parameters on top level but not for subparameters - definitions with attributers becomes a mystery.
    In fact, we must define a graph. We can do it in a variety of ways, (even with Json and Yaml).

  6. markekraus commented on Oct 20, 2017

    @markekraus
    ContributorAuthor

    Steve Lee (@SteveL-MSFT) Any thoughts on the readability and ease of coding for what I proposed?

    definitions with attributers becomes a mystery

    Ilya (@iSazonov) Can you expand on that? I'm not sure I understand.

    In fact, we must define a graph.

    Could you psuedo code an example of what that would look like?

  7. iSazonov commented on Oct 20, 2017

    @iSazonov
    Collaborator
    ParameterSet1
        ||
        \/
    MandatoryParam1 -> Param11 -> Param111
                    -> Param12
    
    MandatoryParam2 -> Param21 -> Param211
                    -> Param22 -> Param221
                    -> Param23 -> Param221
    
    

    Note the Param221.

  8. SteveL-MSFT commented on Oct 20, 2017

    @SteveL-MSFT
    Member

    Mark Kraus (@markekraus) it seems it would be just as complicated with existing parameter sets once you get to several [ParameterMutexName] attributes.

    Personally, when you start having a large number of attributes that scroll off the screen, it makes it hard to mentally map all the sets and exclusions. So I think I prefer something at the [CmdletBinding] level to declare the exclusions and maybe that new thing is mutually exclusive to parametersets.

    If we use Invoke-RestMethod as the canonical example, today if you get the help syntax:

    Invoke-RestMethod [-Uri] <uri> [-Method <WebRequestMethod>] [-FollowRelLink] [-MaximumFollowRelLink <int>] [-ResponseHeadersVariable <string>] [-UseBasicParsing] [-WebSession <WebRequestSession>] [-SessionVariable <string>] [-Credential <pscredential>] [-UseDefaultCredentials] [-CertificateThumbprint <string>] [-Certificate <X509Certificate>] [-SkipCertificateCheck] [-UserAgent <string>] [-DisableKeepAlive] [-TimeoutSec <int>] [-Headers <IDictionary>] [-MaximumRedirection <int>] [-Proxy <uri>] [-ProxyCredential <pscredential>] [-ProxyUseDefaultCredentials] [-Body <Object>] [-ContentType <string>] [-TransferEncoding <string>] [-InFile <string>] [-OutFile <string>] [-PassThru] [-PreserveAuthorizationOnRedirect] [-SkipHeaderValidation] [<CommonParameters>]
    
    Invoke-RestMethod [-Uri] <uri> -NoProxy [-Method <WebRequestMethod>] [-FollowRelLink] [-MaximumFollowRelLink <int>] [-ResponseHeadersVariable <string>] [-UseBasicParsing] [-WebSession <WebRequestSession>] [-SessionVariable <string>] [-Credential <pscredential>] [-UseDefaultCredentials] [-CertificateThumbprint <string>] [-Certificate <X509Certificate>] [-SkipCertificateCheck] [-UserAgent <string>] [-DisableKeepAlive] [-TimeoutSec <int>] [-Headers <IDictionary>] [-MaximumRedirection <int>] [-Body <Object>] [-ContentType <string>] [-TransferEncoding <string>] [-InFile <string>] [-OutFile <string>] [-PassThru] [-PreserveAuthorizationOnRedirect] [-SkipHeaderValidation] [<CommonParameters>]
    
    Invoke-RestMethod [-Uri] <uri> -CustomMethod <string> -NoProxy [-FollowRelLink] [-MaximumFollowRelLink <int>] [-ResponseHeadersVariable <string>] [-UseBasicParsing] [-WebSession <WebRequestSession>] [-SessionVariable <string>] [-Credential <pscredential>] [-UseDefaultCredentials] [-CertificateThumbprint <string>] [-Certificate <X509Certificate>] [-SkipCertificateCheck] [-UserAgent <string>] [-DisableKeepAlive] [-TimeoutSec <int>] [-Headers <IDictionary>] [-MaximumRedirection <int>] [-Body <Object>] [-ContentType <string>] [-TransferEncoding <string>] [-InFile <string>] [-OutFile <string>] [-PassThru] [-PreserveAuthorizationOnRedirect] [-SkipHeaderValidation] [<CommonParameters>]
    
    Invoke-RestMethod [-Uri] <uri> -CustomMethod <string> [-FollowRelLink] [-MaximumFollowRelLink <int>] [-ResponseHeadersVariable <string>] [-UseBasicParsing] [-WebSession <WebRequestSession>] [-SessionVariable <string>] [-Credential <pscredential>] [-UseDefaultCredentials] [-CertificateThumbprint <string>] [-Certificate <X509Certificate>] [-SkipCertificateCheck] [-UserAgent <string>] [-DisableKeepAlive] [-TimeoutSec <int>] [-Headers <IDictionary>] [-MaximumRedirection <int>] [-Proxy <uri>] [-ProxyCredential <pscredential>] [-ProxyUseDefaultCredentials] [-Body <Object>] [-ContentType <string>] [-TransferEncoding <string>] [-InFile <string>] [-OutFile <string>] [-PassThru] [-PreserveAuthorizationOnRedirect] [-SkipHeaderValidation] [<CommonParameters>]
    

    It's already hard to figure out what to do (and this doesn't include the new capabilities Mark Kraus (@markekraus) is adding). Thinking out loud here. The current syntax should look more like:

    Invoke-RestMethod [-Uri] <uri> [-Method <method> |  -CustomMethod <string>] [-FollowLink [-MaximumFollowLink <int>]] [-ResponseHeadersVariable <string> [-UseBasicParsing] [-SessionVariable] [-WebSession <session> | -Credential <cred> | -UseDefaultCredentials | -CertificateThumbprint <string> | -Certificate <cert>] [-SkipCertificateCheck] [-UserAgent <string>] [-DisableKeepAlive]...
    

    And if we simplify it a bit and not including some of the params to keep it smaller:

    Invoke-RestMethod [-Uri] <uri> [{Method}] [-FollowLink [-MaxFollowLink]] [{Auth}] ...
    {Method} = -Method | -CustomMethod
    {Auth} = -WebSession | -Credential | -UseDefaultCredentials | -CertThumb | -Cert
    

    and we turn this into some sort of declarative syntax:

    Invoke-RestMethod [Uri] [Method] [Auth] [FollowLink] ...
    Uri
        [-Uri] <uri>
    Auth
        [-WebSession <session>]
        [-Credential <cred>]
        [-UseDefaultCredentials]
        [-CertThumb <string>]
        [-Cert <string]
    

    So how to put this into code? (has to work for both C# and script)

  9. markekraus commented on Oct 20, 2017

    @markekraus
    ContributorAuthor

    It's almost like the entire parameter system would need to be redone or at least a new one added in parallel with the existing one. Part of the problem is that the existing system is too cumbersome. but completely breaking it seems almost a like non-starter. hmmm

  10. iSazonov commented on Oct 21, 2017

    @iSazonov
    Collaborator

    We could borrow method overloads idea from C#. In this case, we only need to find the appropriate entry point into the cmdlet from several.

  11. markekraus commented on Oct 21, 2017

    @markekraus
    ContributorAuthor

    Ilya (@iSazonov) I'm not sure how you apply the same logic to exclusive parameters of the same type with different names. I have hard time visualizing this as being less work for the user. You'd need to basically define each unique binding signature. If you have 3 parameter sets and 3 exclusive sets, that a lot unique signatures to define. But maybe I don't have a good idea of what you are proposing. What is some mock code how how that would look from a user perspective?

  12. lzybkr commented on Oct 21, 2017

    @lzybkr
    Contributor

    Parameter sets already are the equivalent of C# overloads.

    Some random thoughts:

    • A fully general solution might not be necessary or easy to express
    • A reasonable goal is to express the most common cases

    A not fully formed idea - instead of specifying mutually exclusive parameter, how about expressing compatible parameter sets?

    [CmdletBinding()]
    [CompatibleParameterSets("A", @("C", "D"))]
    [CompatibleParameterSets("B", @("C", "D"))]
    param(
        [Parameter(ParameterSetName="A"))]$A,
        [Parameter(ParameterSetName="B"))]$B,
        [Parameter(ParameterSetName="C"))]$C,
        [Parameter(ParameterSetName="D"))]$D
    )

    So this says:

    • A is compatible with C and D
    • B is also compatible with C and D
      If we assume this is transitive, then C is compatible with both A and B, and the same for D.

    This gives us a syntax like: cmd [-A | -B] [-C | -D]

    In terms of the implementation - after binding is finished, it's possible we won't have a single parameter set, but that's OK because we now know which parameter sets are compatible.

    I wonder if this idea helps reduce the need for a large number of [Parameter] attributes.

  13. markekraus commented on Oct 21, 2017

    @markekraus
    ContributorAuthor

    Jason Shirk (@lzybkr) I'm trying to wrap my brain around how to use that in the instance where you have, say, 2 governing parameter sets, but then you have 2 sets of mutex parameters

    There is set A and set B. Set A includes normal parameters A1 and A2. Set B includes normal parameters B1 and B2. There is a C and D which are mutex but belong to both A and B sets. There is also E and F which are mutex and can belobn to both A and B sets.

    the syntax is something like

    -A [-A1] [-A2] [-C | -D] [-E | -F]
    -B [-B1] [-B2] [-C | -D] [-E | -F]
    

    With my equally not-fully-formed proposal, it would look like this:

    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName="A")]$A,
        [Parameter(ParameterSetName="A")]$A1,
        [Parameter(ParameterSetName="A")]$A2,
    
        [Parameter(ParameterSetName="B")]$B,
        [Parameter(ParameterSetName="B")]$B1,
        [Parameter(ParameterSetName="B")]$B2,
    
        [Parameter(ParameterSetName="A", ParameterMutexName='CD')]
        [Parameter(ParameterSetName="B", ParameterMutexName='CD')]
        $C,
    
        [Parameter(ParameterSetName="A", ParameterMutexName='CD')]
        [Parameter(ParameterSetName="B", ParameterMutexName='CD')]
        $D,
    
        [Parameter(ParameterSetName="A", ParameterMutexName='EF')]
        [Parameter(ParameterSetName="B", ParameterMutexName='EF')]
        $E,
    
        [Parameter(ParameterSetName="A" ParameterMutexName='EF')]
        [Parameter(ParameterSetName="B" ParameterMutexName='EF')]
        $F
    )
  14. markekraus commented on Oct 21, 2017

    @markekraus
    ContributorAuthor

    If only this were possible:

    [CmdletSyntax("
    -A [-A1] [-A2] [-C | -D] [-E | -F]
    -B [-B1] [-B2] [-C | -D] [-E | -F]
    ")]
    [CmdletBinding()]
    Param(
        $A, $B, $C, $D, $E, $F
    )
  15. 7 remaining items

  16. markekraus commented on Oct 22, 2017

    @markekraus
    ContributorAuthor

    It is full C# pattern.

    Forgive me... I have no clue what that is (reminder that I'm now only 8 months into C# 😉 ).

    The cmdlet author cannot miss because he is making the code.

    Sure they can. Just try using your syntax for the web cmdlets.. You will likely have well over 100 definitions. You are definitely going to miss one. I have a pretty deep understanding of these cmdlets and I'm not confident I wouldn't miss one.

  17. iSazonov commented on Oct 22, 2017

    @iSazonov
    Collaborator

    You will likely have well over 100 definitions.

    This problem exists for any new syntax. Any optimization found for one will apply to the other. :-)

    Thanks for Yaml sample.

  18. markekraus commented on Oct 22, 2017

    @markekraus
    ContributorAuthor

    This problem exists for any new syntax

    Ilya (@iSazonov) It doesn't have to. The JSON and YAML options pull of that level of complexity with way less code.

  19. lzybkr commented on Oct 23, 2017

    @lzybkr
    Contributor

    I'm really liking the "PowerShell figures it out from your syntax" approach - and my primary reasoning - it should be an inverse function - meaning we might have a verbose way to specify the same with attributes, but nobody would use that going forward.

  20. iSazonov commented on Oct 24, 2017

    @iSazonov
    Collaborator

    Original C# overloads look cumbersome.

    [String]::Format
    
    OverloadDefinitions
    -------------------
    static string Format(string format, System.Object arg0)
    static string Format(string format, System.Object arg0, System.Object arg1)
    static string Format(string format, System.Object arg0, System.Object arg1, System.Object arg2)
    static string Format(string format, Params System.Object[] args)
    static string Format(System.IFormatProvider provider, string format, System.Object arg0)
    static string Format(System.IFormatProvider provider, string format, System.Object arg0, System.Object arg1)
    static string Format(System.IFormatProvider provider, string format, System.Object arg0, System.Object arg1, System.Object arg2)
    static string Format(System.IFormatProvider provider, string format, Params System.Object[] args)

    After we remove types it looks simpler.:

    OverloadDefinitions
    -------------------
    Format(format, arg0)
    Format(format, arg0, arg1)
    Format(format, arg0, arg1, arg2)
    Format(format, args)
    Format(provider, format, arg0)
    Format(provider, format, arg0, arg1)
    Format(provider, format, arg0, arg1, arg2)
    Format(provider, format, args)

    For example above signatures will be ('+' - optional parameter):

    $A, +$A1, +$A2, +$C, +$E
    $A, +$A1, +$A2, +$C, +$F
    $A, +$A1, +$A2, +$D, +$E
    $A, +$A1, +$A2, +$D, +$F
    
    $B, +$B1, +$B2, +$C, +$E
    $B, +$B1, +$B2, +$C, +$F
    $B, +$B1, +$B2, +$D, +$E
    $B, +$B1, +$B2, +$D, +$F

    For Add-Member cmdlet ($PassThru we can exclude at all):

    $InputObject, $TypeName, +$PassThru
    $InputObject, $MemberType, $Name, +$Force, +$PassThru, +$SecondValue, +TypeName, +$Value
    $InputObject, $NotePropertyMembers, +$Force, +$PassThru, +$TypeName
    $InputObject, $NotePropertyName, $NotePropertyValue, +$Force, +$PassThru, +$TypeName
  21. markekraus commented on Oct 24, 2017

    @markekraus
    ContributorAuthor

    To add to the problems... currently ValueFromPipeline, ValueFromPipelineByPropertyName, DontShow, etc can be configured per parameter set how do we jive that with any of the new syntax proposals? more sigils? do we not?

  22. iSazonov commented on Oct 24, 2017

    @iSazonov
    Collaborator

    For general solution we should allow set attributes in parametersets. This allows us to use the exact type to InputObject.

  23. SteveL-MSFT commented on Nov 19, 2017

    @SteveL-MSFT
    Member

    Related to #5435, we should provide the user with more specific error message about why they couldn't use two or more parameters together because they are mutually exclusive (and additionally a detailed error if insufficient parameters are provided to resolve a parameter set)

  24. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  25. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  26. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  27. microsoft-github-policy-service commented on Nov 23, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifIssue-Enhancementthe issue is more of a feature request than a bugResolution-No ActivityIssue has had no activity for 6 months or more

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions