2

I have written a function which uses four parameters, and four parameter sets. The first parameter, $Path, is unassigned to a set, and therefore belongs to all sets. It is also mandatory, and the only parameter that can be passed from the pipeline. However, when I do this using certain combinations of the other three parameters (all of which belong to some combination of the fours sets) when invoking the function at the end of the pipeline, I get an error indicating the set is ambiguous.

Here is my function:

function Foo-Bar {
    [CmdletBinding(DefaultParameterSetName = 'A')]

    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]] $Path,

        [Parameter(ParameterSetName = 'A')]
        [Parameter(ParameterSetName = 'A-Secure')]
        [Switch] $OutputToConsole,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'B')]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'B-Secure')]
        [int] $OutputMode,

        [Parameter(Mandatory = $true,
            ParameterSetName = 'A-Secure')]
        [Parameter(Mandatory = $true,
            ParameterSetName = 'B-Secure')]
        [Switch] $Login
    )

    $PSCmdlet.ParameterSetName
}

All possible combinations of parameters are as follows:

PS C:\> Foo-Bar -Path "C:\Test.jpg"
A
PS C:\> Foo-Bar -Path "C:\Test.jpg" -OutputToConsole
A
PS C:\> Foo-Bar -Path "C:\Test.jpg" -OutputToConsole -Login
A-Secure
PS C:\> Foo-Bar -Path "C:\Test.jpg" -Login
A-Secure
PS C:\> Foo-Bar -Path "C:\Test.jpg" -OutputMode 1
B
PS C:\> Foo-Bar -Path "C:\Test.jpg" -OutputMode 1 -Login
B-Secure

Passing $Path through the pipeline alone, or with these other combinations of parameters works fine:

PS C:\> "C:\Test.jpg" | Foo-Bar
A
PS C:\> "C:\Test.jpg" | Foo-Bar -OutputToConsole
A
PS C:\> "C:\Test.jpg" | Foo-Bar -OutputToConsole -Login
A-Secure
PS C:\> "C:\Test.jpg" | Foo-Bar -OutputMode 1 -Login
B-Secure

But these two combinations result in an error:

PS C:\> "C:\Test.jpg" | Foo-Bar -Login
Foo-Bar: 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.
PS C:\> "C:\Test.jpg" | Foo-Bar -OutputMode 1
Foo-Bar: 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.

It seems like the biggest difference between these outcomes is $OutputToConsole, the only parameter which is optional in both of its sets. It seems as though piping a mandatory parameter causes it to become mandatory itself. On the other hand, the most confusing result involves $OutputMode, as both of its sets use distinct combinations of exclusively mandatory parameters. Set B occurs when using both $Path and $OutputMode, and that's it. So how is it that "C:\Test.jpg" | Foo-Bar -OutputMode 1 is being considered ambiguous?

I would be very grateful to anyone who can shed some light on this for me.

Alex
  • 23
  • 6

3 Answers3

5

Don't ask me why.
(The workaround below returns the same syntax for: Foo-Bar -?)
Personally, I find parameter sets quiet confusing and verbose (therefore I did this purpose for Hierarchical Parameter Scripting #13746)

Anyways, as a possible workaround; put the Path parameter in all the parametersets:

function Foo-Bar {
    [CmdletBinding(DefaultParameterSetName = 'A')]

    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'A')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'B')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'A-Secure')]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'B-Secure')]
        [ValidateNotNullOrEmpty()]
        [string[]] $Path,

        [Parameter(ParameterSetName = 'A')]
        [Parameter(ParameterSetName = 'A-Secure')]
        [Switch] $OutputToConsole,

        [Parameter(Mandatory = $true, ParameterSetName = 'B')]
        [Parameter(Mandatory = $true, ParameterSetName = 'B-Secure')]
        [int] $OutputMode,

        [Parameter(Mandatory = $true, ParameterSetName = 'A-Secure')]
        [Parameter(Mandatory = $true, ParameterSetName = 'B-Secure')]
        [Switch] $Login
    )

    $PSCmdlet.ParameterSetName
}

"C:\Test.jpg" | Foo-Bar -Login
A-Secure
iRon
  • 20,463
  • 10
  • 53
  • 79
  • 1
    Thank you very much, this is exactly the workaround I was hoping for. Although, given the fact that the documentation explicitly indicates "parameters that don't have an assigned parameter set name belong to all parameter sets", I have to wonder if this behaviour is the result of a bug? By the way, I hope they implement your hierarchical parameter syntax at some point, it seems much more convenient than parameter sets. – Alex May 29 '21 at 20:48
1

Parameter sets must contain at least one unique parameter

Declaring parameter sets To create a parameter set, you must specify the ParameterSetName keyword of the Parameter attribute for every parameter in the parameter set. For parameters that belong to multiple parameter sets, add a Parameter attribute for each parameter set.

The Parameter attribute enables you to define the parameter differently for each parameter set. For example, you can define a parameter as mandatory in one set and optional in another. However, each parameter set must contain at least one unique parameter.

Parameters that don't have an assigned parameter set name belong to all parameter sets.

Source: Declaring Parameters Sets


Update - Closer Look

Foo-Bar

ParameterSetName Parameters
---------------- ----------
A                -Path <string[]> [-OutputToConsole] [<CommonParameters>]
A-Secure         -Path <string[]> -Login [-OutputToConsole] [<CommonParameters>]
B-Secure         -Path <string[]> -OutputMode <int> -Login [<CommonParameters>]
B                -Path <string[]> -OutputMode <int> [<CommonParameters>]

When you enter

C:\Test.jpg" | Foo-Bar -Login

PoSH cannot determine whether it should go with A-Secure or B-Secure. It does not matter that in B-Secure you have an additional mandatory parameter (-OutputMode). From what it has received it cannot tell which to go with because there are multiple matching choices. The combination is not unique.

"C:\Test.jpg" | Foo-Bar -OutputMode 1

Same here with only OutputMode. It has received a Path and a OutputMode argument but this matches both B-Secure and B. Not unique.

Microsoft Measure-Lines Example

ParameterSetName Parameters
---------------- ----------
Path             [-Path] <string[]> [-Lines] [-Words] [-Characters] [-Recurse] [<CommonParameters>]
PathAll          [-Path] <string[]> -All [-Recurse] [<CommonParameters>]
LiteralPath      -LiteralPath <string> [-Lines] [-Words] [-Characters] [<CommonParameters>]
LiteralPathAll   -LiteralPath <string> -All [<CommonParameters>]

At first glance the Measure-Lines example seems similar, but the difference lies in the fact that the first 2 parameter sets take a Path argument while the last 2 sets take a LiteralPath argument. This makes them unique enough for Powershell to know what parameter set to use when, for example, the -All switch is used. When used with -Path parameter it goes with the PathAll set and LiteralPathAll is used when -LiteralPath is provided.

Daniel
  • 4,792
  • 2
  • 7
  • 20
  • I don't think I understand. I'm looking at the example they give on that page, and based on my understanding of how they define a "unique" parameter (one that doesn't belong to one or more sets, differentiating them from the sets that it does belong to), it seems like all of my sets do contain a unique parameter. Could you break down for me which set(s) are missing a unique parameter? I also notice that they phrase this differently further down the page: "Each parameter set must have a unique parameter or a unique combination of parameters". My sets definitely meet the second criteria. – Alex May 27 '21 at 05:34
  • @AsanaWiosna, maybe my updated answer helps – Daniel May 27 '21 at 06:53
  • Also I just realized that the text about parameter sets requiring a unique parameters is out of context and not the reason for trouble you are having. You do have uniquely defined parameter sets, it's just that in the way they are defined there is possibility for this ambiguity I describe in the answer. – Daniel May 27 '21 at 07:07
  • Thank you very much for the additional clarification. Your point that "it does not matter that in B-Secure you have an additional mandatory parameter" was particularly surprising to me, however I can understand why that would be the case. My only remaining question then, is why does it only become a problem when piping parameters? e.g. `Foo-Bar -Path "C:\Test.jpg" -Login` works just fine and correctly identifies the parameter set as "A-Secure", but `"C:\Test.jpg" | Foo-Bar -Login` fails altogether. – Alex May 29 '21 at 20:52
  • You're welcome. It was surprising to me too. I actually came to this conclusion through some testing with Trace-Command. I personally would just apply iRon's fix and forget about it :) – Daniel May 29 '21 at 20:56
0

Following iRons answer, you can also drasticaly reduce the number of parametersets here I think:

function Foo-Bar {
    [CmdletBinding(DefaultParameterSetName = 'None')]

    param (
        [ValidateNotNullOrEmpty()]
        [string[]] $Path,

        [Parameter(ParameterSetName = 'A-Secure')]
        [Switch] $OutputToConsole,

        [Parameter(Mandatory = $true, ParameterSetName = 'B-Secure')]
        [int] $OutputMode,

        [Parameter(Mandatory = $true, ParameterSetName = 'A-Secure')]
        [Parameter(Mandatory = $true, ParameterSetName = 'B-Secure')]
        [Switch] $Login
    )

    $PSCmdlet.ParameterSetName
}

Foo-Bar -Login                                            # --> chosen param set: 'A-Secure' because that the first one mentioned on the Login parameter
Foo-Bar -Path 'D:\Test\blah.txt'                          # --> chosen param set: 'None' because that is the DefaultParameterSetName
Foo-Bar -Path 'D:\Test\blah.txt' -OutputMode 5            # --> prompts you to also supply parameter 'Login'
Foo-Bar -Path 'D:\Test\blah.txt' -OutputToConsole -Login  # --> parameter -OutputMode now not available; chosen param set: 'B-Secure'
Foo-Bar -Path 'D:\Test\blah.txt' -OutputMode 5 -Login     # --> parameter -OutputToConsole now not available; chosen param set: 'A-Secure'

# parameters '-Path' and '-Login' are always available for both 'A-Secure' and 'B-Secure'
Theo
  • 57,719
  • 8
  • 24
  • 41
  • I appreciate the suggestion, but I chose the parameter sets I did very deliberately, as I wanted the behaviour to emulate that of a much larger function that I've been working on (which is what prompted me to ask this question). In particular, `Path` should be a mandatory parameter, and `Login` should be an optional addition to `OutputMode`. But, if `Login` is used, it should preclude the use of certain other parameters, which is why it has its own two parameter sets, `A-Secure` and `B-Secure`. – Alex May 29 '21 at 21:00