"Parameter set cannot be resolved" when piping a parameter with specific combinations of other parameters

2.4k views Asked by At

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.

3

There are 3 answers

1
iRon On BEST ANSWER

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
1
Theo On

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'
5
Daniel On

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.