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...
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.
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.
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.
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
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.
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},
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
@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:
What one would typically expect when each of these inputs is sent to a function would be this corresponding list:
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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With