Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a function that accepts multiple argument types from pipeline and command line?

I'm trying to write a function that takes multiple arguments, which can come either from the command line, or from the pipeline. The arguments can be strings or directory objects. The idea is that any of the following invocations should work:

Test-VEnv '.\MyPath', '.\AnotherPath'
Test-VEnv (dir)
'MyPath', 'AnotherPath' | Test-VEnv
dir | Test-VEnv

The following code almost works:

function Test-VEnv {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0,
            ValueFromPipeline=$True,
            ValueFromPipelineByPropertyName=$true)]
        [Alias('FullName')]
        [String[]]$Path
    )

    process {
        foreach ($P in $Path) {
            ...
        }
    }
}

It handles strings both from the pipeline and the command argument, and handles directory objects from the pipeline (via ValueFromPipelineByPropertyName and the FullName alias). But it doesn't handle directory objects on the command line, so

dir | Where-Object { Test-VEnv $_ }

fails, as it converts the directory objects to strings, which uses the Name property rather than FullName, and the subsequent code fails.

Can anyone tell me how to achieve what I want?

I am aware that even if I can get this to work, it may not be a particularly good design. But as far as I can tell, it's how the built in Test-Path works, so I want to try following standard behaviour before I invent my own...

like image 825
Paul Moore Avatar asked Mar 26 '12 19:03

Paul Moore


People also ask

How do you allow the pipeline input in a function?

Step 1: Allowing Pipeline Input To get this function to accept pipeline input, you must first define the appropriate parameter attribute. This attribute can either be ValueFromPipeline to accept pipeline input ByValue or ValueFromPipelineByPropertyName to accept pipeline input ByPropertyName.

How do I create a parameterized function in PowerShell?

PowerShell uses the parameter value order to associate each parameter value with a parameter in the function. When you use positional parameters, type one or more values after the function name. Positional parameter values are assigned to the $args array variable.

What attribute must be added to PowerShell functions to enable object pipelining?

This step requires adding a parameter attribute to the Thing parameter called ValueFromPipeline. By defining this parameter attribute, this parameter now knows how to bind the object coming in from the pipeline to the Thing parameter.


2 Answers

Since your parameter type is string it's coercing the file system info object into a string when you are not using the pipeline { Test-VEnv $_ }. If you call the ToString() method of either a System.IO.FileInfo or System.IO.DirectoryInfo object you'll see this. When you use the pipeline it binds the fullname alias giving you the full path.

You can see what PowerShell is doing to bind the input object using Trace-Command. Here is an example of how to use it:

trace-command -name parameterbinding -expression {(dir C:\)[0] | ? {Test-VEnv $_}} -pshost

Here is the important part of the output:

BIND arg [PerfLogs] to parameter [Path]
    Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
        result returned from DATA GENERATION: System.String[]
    COERCE arg to [System.String[]]
        Parameter and arg types the same, no coercion is needed.
    BIND arg [System.String[]] to param [Path] SUCCESSFUL

Test-Path does the same thing. Take a look at these three examples:

PS C:\Users\Andy> Test-Path (dir C:\)[0]
False
PS C:\Users\Andy> (dir C:\)[0] | Test-Path
True
PS C:\> Test-Path (dir C:\)[0]
True
  1. Since my PWD is not C:\ I get FALSE because the DirectoryInfo object is converted to string (ToString()) which only gives the folder name. This is because the pipeline wasn't used.

  2. Since the pipeline is used it works because it is binding to PsPath with this parameter:

    [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},
    
  3. Since the directory contains the folder the folder's name exists.

You might try the alias PsPath for your binding. This is what Test-Path uses:

param (
    [Parameter(Mandatory=$true, Position=0,
        ValueFromPipeline=$True,
        ValueFromPipelineByPropertyName=$true)]
    [Alias('PsPath')]
    [String[]] $Path
)

process {
    foreach ($P in $Path) {
        Get-Item $p
    }
}

Some tests:

Set-Location C:\
Write-Host 1
    Test-VEnv '.\Windows', '.\Program Files'
Write-Host 2
    Test-VEnv (dir)
Write-Host 3
    'Windows', 'Program Files' | Test-VEnv
Write-Host 4
    dir | Test-VEnv

Output:

1
    Directory: C:\
Mode                LastWriteTime     Length Name                                                       
----                -------------     ------ ----                                                       
d----         3/14/2012   3:41 AM            Windows                                                    
d-r--         3/24/2012   7:46 PM            Program Files                                              

2
d----         2/18/2012   4:32 AM            PerfLogs                                                   
d-r--         3/24/2012   7:46 PM            Program Files                                              
d-r--         3/25/2012   4:49 PM            Program Files (x86)                                        
d----          3/9/2012   9:57 PM            Python27                                                   
d-r--          3/4/2012   8:11 PM            Users                                                      
d----         3/14/2012   3:41 AM            Windows                                                    
-a---          3/4/2012   8:45 PM       1024 .rnd                                                       

3
d----         3/14/2012   3:41 AM            Windows                                                    
d-r--         3/24/2012   7:46 PM            Program Files                                              

4
d----         2/18/2012   4:32 AM            PerfLogs                                                   
d-r--         3/24/2012   7:46 PM            Program Files                                              
d-r--         3/25/2012   4:49 PM            Program Files (x86)                                        
d----          3/9/2012   9:57 PM            Python27                                                   
d-r--          3/4/2012   8:11 PM            Users                                                      
d----         3/14/2012   3:41 AM            Windows                                                    
-a---          3/4/2012   8:45 PM       1024 .rnd  
like image 110
Andy Arismendi Avatar answered Sep 20 '22 09:09

Andy Arismendi


@Andy gives some great information specifically addressing points in your question. My answer here is more of a supplement considering the broader implications. It probably only deserves to be a comment but the length and my included image prevent me from posting this as just a comment...

I recently examined the question of pipeline vs. direct input in Powershell with a specific goal towards making these input streams symmetric with respect to all classes of inputs and with respect to what defaults are applied. There are, by my reckoning, six equivalence classes of input to consider:

  • no input
  • null
  • empty
  • scalar
  • list of normal values
  • list of mixed values (i.e. some null or empty)

What one would typically expect when each of these inputs is sent to a function would be this corresponding list:

  • default value
  • null
  • empty
  • scalar
  • list of normal values
  • list of mixed values (i.e. some null or empty)

That is, with no input supplied the default value is used; otherwise the given value is used. This sounds almost trivial, practically a tautology, but there are some subtleties. Consider, for example, what does it mean to supply no input via the pipeline? Is it null or an empty collection? I contend the latter for, among other reasons, it allows the symmetry between streams I mentioned above. Furthermore, how you write both your function signature and your function body makes sometimes surprising impacts on some or all of these input classes with one or the other input stream. Thus, I further contend that there is a lot more to this "trivial" consideration than meets the eye at first glance. So much so that I wrote extensively about it in the article Down the Rabbit Hole- A Study in PowerShell Pipelines, Functions, and Parameters, published on Simple-Talk.com. Included with the article is a wallchart that shows a table of the six equivalence input classes and what you get for each with different function templates. Here is a thumbnail of the wallchart:

enter image description here

like image 43
Michael Sorens Avatar answered Sep 19 '22 09:09

Michael Sorens