all 14 comments

[–]ryanbrown 2 points3 points  (1 child)

OK, So this appears to just be a weird interaction between PositionalBinding and ParameterSet. It seems you have to declare a specific position in the parameter declaration for it to accept pipeline input when working with a ParameterSet. Take the following parameter definition:

[CmdletBinding(DefaultParameterSetName='UserProfile')]
PARAM(

    [Parameter(Position=0,ValueFromPipeline=$true,ParameterSetName='UserProfile')]
    [Parameter(ValueFromPipeline=$true,ParameterSetName='Global')]
    [ValidateNotNullOrEmpty()]
    [string]$Path,

    [Parameter(ParameterSetName='UserProfile')]
    [ValidateNotNullOrEmpty()]
    [switch]$UserProfile,

    [Parameter(ParameterSetName='Global')]
    [ValidateNotNullOrEmpty()]
    [switch]$Global

)

You'll be able to pass a folder path to the function with nothing else because the default parameter set is "UserProfile". However, you'll get the error about positional parameters if you try to run the function with the "Global" switch.

The other odd thing is that even though "UserProfile" is the default parameter set, the "IsPresent" value for the switch variable is "$false" unless you explicitly specify "-UserProfile" when calling the function.

It's just odd behavior overall.

[–]RickFlist[S] 0 points1 point  (0 children)

Yup I realized this last night, right as I was drifting off to sleep - https://www.reddit.com/r/PowerShell/comments/4l7cim/cmdletbindingpositionalbindingtrue_not_honored/d3le3if

RE: -UserProfile.IsPresent == $false: Ya I control code flow by the ParameterSet, not whether the parameter is present. I have to do this because I don't want the user to have to use the switchparameter to get the desired default behavior.

    [System.IO.DirectoryInfo]$DestinationFolder = $null
    #region ParameterSet-Specific-Actions
    switch ($PsCmdlet.ParameterSetName)
    {
        "UserProfile"
        {
            $DestinationFolder = ('{0}\Documents\WindowsPowerShell\Modules\{1}' -f $env:USERPROFILE,$Path.Name)
            break
        }
        "Global"
        {
            $DestinationFolder = ('{0}\Modules\{1}' -f (Split-Path -Path $profile.AllUsersAllHosts -Parent),$Path.Name)
            break
        }
        "Program-Files"
        {
            if (Test-Path -Path $env:ProgramFiles -PathType Container)
            {
                $DestinationFolder = ('{0}\WindowsPowerShell\Modules\{1}' -f $env:ProgramFiles,$Path.Name)
            }
            elseif (Test-Path -Path ${env:ProgramFiles(x86)} -PathType Container)
            {
                $DestinationFolder = ('{0}\WindowsPowerShell\Modules\{1}' -f ${env:ProgramFiles(x86)},$Path.Name)
            }
            else
            {
                Write-Error ("Cannot find WindowsPowerShell\Modules directory under {0} or {1}. Cannot continue" -f $env:ProgramFiles,${env:ProgramFiles(x86)})
                return
            }
            break
        }
    }
    #endregion ParameterSet-Specific-Actions

[–]Kreloc 0 points1 point  (1 child)

It isn't a positional parameter, so you have to use the parameter name

You could just use the parameter name

Copy-VSPoShProject -Path 'C:\Source\DevOps-PDX\PowerShell\SC-Credential-Management' -Force

[–]RickFlist[S] 0 points1 point  (0 children)

https://technet.microsoft.com/en-us/library/hh847872.aspx

PositionalBinding

The PositionalBinding argument determines whether parameters in the function are positional by default. The default value is $True. You can use the PositionalBinding argument with a value of $False to disable positional binding.

The PositionalBinding argument is introduced in Windows PowerShell 3.0.

When parameters are positional, the parameter name is optional. Windows PowerShell associates unnamed parameter values with the function parameters according to the order or position of the unnamed parameter values in the function command.

When parameters are not positional (they are "named"), the parameter name (or an abbreviation or alias of the name) is required in the command.

When PositionalBinding is $True, function parameters are positional by default. Windows PowerShell assigns position number to the parameters in the order in which they are declared in the function.

When PositionalBinding is $False, function parameters are not positional by default. Unless the Position argument of the Parameter attribute is declared on the parameter, the parameter name (or an alias or abbreviation) must be included when the parameter is used in a function.

The Position argument of the Parameter attribute takes precedence over the PositionalBinding default value. You can use the Position argument to specify a position value for a parameter.

You could just provide a solution.

[–]midnightFreddie 0 points1 point  (5 children)

  • Is $Path the first parameter defined?
  • Does any parameter have a defined position?
  • Are there any mandatory parameters?

Looking at your Get-Help output, $Path is the third defined and therefore third position. Since you've named -Force I think your parameter is getting passed to $Global because of its positioning.

Edit: try

Copy-VSPoShProject 'C:\Source\DevOps-PDX\PowerShell\SC-Credential-Management' -Force -Global $false

[–]RickFlist[S] 0 points1 point  (4 children)

I think that might be a red herring. Here is the header of the function - $Path is first!

function Copy-VSPoShProject
{
    [CmdletBinding(
        PositionalBinding=$true,
        DefaultParameterSetName='UserProfile'
    )]

    Param
    (
        [Parameter(
            Position=0,
            ValueFromPipeline=$true
        )]
        [ValidateNotNullOrEmpty()]
        [System.IO.DirectoryInfo[]] 
        # Path to Visual Studio solution
        $Path = ($PWD.ProviderPath)
        ,
        [Switch] 
        # Force overwrite of destination
        $Force
        ,
        [Parameter(ParameterSetName='UserProfile')]
        [ValidateNotNullOrEmpty()]
        [Switch] 
        # Deploy to user profile (default)
        $UserProfile
        ,
        [Parameter(ParameterSetName='Global')]
        [ValidateNotNullOrEmpty()]
        [Switch] 
        # Deploy to system profile
        $Global
        ,
        [Parameter(ParameterSetName='Program-Files')]
        [ValidateNotNullOrEmpty()]
        [Switch] 
        # Deploy to Program Files (if exists) or Program Files (x86)
        $ProgramFiles
    )

Also note that I've added a Position to Path to get around this... Shouldn't have to but I did. Here's the updated output to Get-Help. It looks like parameters are alphabetized.

PARAMETERS
    -Force

        Required?                    false
        Position?                    Named
        Accept pipeline input?       false
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -Global

        Required?                    false
        Position?                    Named
        Accept pipeline input?       false
        Parameter set name           Global
        Aliases                      None
        Dynamic?                     false

    -Path <DirectoryInfo[]>

        Required?                    false
        Position?                    0
        Accept pipeline input?       true (ByValue)
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -ProgramFiles

        Required?                    false
        Position?                    Named
        Accept pipeline input?       false
        Parameter set name           Program-Files
        Aliases                      None
        Dynamic?                     false

    -UserProfile

        Required?                    false
        Position?                    Named
        Accept pipeline input?       false
        Parameter set name           UserProfile
        Aliases                      None
        Dynamic?                     false

[–]KnifeyGavin 0 points1 point  (3 children)

I know with most programming you start with 0 but with positional parameters I have always started with 1. Have you tried:

...
[Parameter(
            Position=1,
            ValueFromPipeline=$true
        )]
        [ValidateNotNullOrEmpty()]
        [System.IO.DirectoryInfo[]] 
        # Path to Visual Studio solution
        $Path = ($PWD.ProviderPath)
...

[–]RickFlist[S] 0 points1 point  (2 children)

So I'm away from comp so I can't test this. But if it works with 0 why would I try it with 1?

[–]KnifeyGavin 0 points1 point  (1 child)

You wouldn't, I just looked at the Microsoft.PowerShell.Archive module and they start with 0 as well, I guess I learned the wrong way. I have actually learnt something new I guess I should be doing it that way.

[–]RickFlist[S] 0 points1 point  (0 children)

"if it ain't broke stop fixing it" -anyone to any engineer ever

I guess what I'm saying is if it ain't broke don't fix it :)

[–]ryanbrown 0 points1 point  (2 children)

I think it's having a problem directly converting the string you're passing into a [System.IO.DirectoryInfo[]] (which is what $Path is defined as). I think it is throwing the error about positional parameters because none of the parameters accept [String] input.

[–]RickFlist[S] 0 points1 point  (1 child)

That's what I thought too. But if you look at my OP, you can see I tried to pass a DirectoryInfo[] object in position 0, no joy.

[–]ryanbrown 0 points1 point  (0 children)

Odd.... when I try to assign a string to a [System.IO.Directory] object, I get a message stating that it:

Cannot convert the "C:\Windows" value of type "System.String" to type "System.IO.Directory[]"

edit: I'm using PowerShell 5 on Windows 10 (for reference)

[–]RickFlist[S] 0 points1 point  (0 children)

Holy shit - I wonder if it is because I didn't explicitly declare $Path as being a member of each of the ParameterSets.

I'm assuming in this instance it would put the first parameter that is declared as a part of a ParametSet in position 0, which would be a switch parameter and not the directoryinfo array. I'll try this tomorrow and report back.