12

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...

Paul Moore
  • 6,569
  • 6
  • 40
  • 47

4 Answers4

9

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  
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • OK, so what you are saying is that my function does behave like test-path. My apologies, I appear to have got my testing a bit muddled - thanks for clarifying for me. So does that mean it's not possible to achieve the result I wanted? (At least not without messy explicit type checks in the code) – Paul Moore Mar 27 '12 at 08:45
  • @PaulMoore Are you expecting the full path bound as a string to `$Path` regardless if the pipeline is used or not? – Andy Arismendi Mar 27 '12 at 09:11
  • @PaulMoore Try using `PsPath` this is what `Test-Path` uses. I updated my answer with some examples. – Andy Arismendi Mar 27 '12 at 19:17
  • Yes, I'm expecting a complete pathname. PSPath works about the same as FullName (works fine for all cases except "Test-VEnv (dir \)"). In practice, PSPath isn't as good for me, as it includes the provider, which I don't need, but that's a minor point. The key thing is, as you pointed out above, that argument input gets coerced to string and there's no "argument by property name" option. The easiest way is probably a property check or coercion within the code. Or don't worry too much, it's a fairly obscure case anyway :-) – Paul Moore Mar 29 '12 at 16:23
7

@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

Michael Sorens
  • 35,361
  • 26
  • 116
  • 172
0
function Install-PathTransformation 
{
    [CmdletBinding()]
    param()

    if (-not $script:my_pathtransformation_types) {
      $script:my_pathtransformation_types = Add-Type -TypeDefinition @"
        using System;
        using System.IO;
        using System.Management.Automation;

        public class ValidPathTransformationAttribute : ArgumentTransformationAttribute {
            public bool Resolve {
                get;
                set;
            }

            public override Object Transform(EngineIntrinsics engineIntrinsics, Object inputObject) {
                PSObject psobj = inputObject as PSObject;
                if (psobj != null)
                    inputObject = psobj.BaseObject;
                if (inputObject == null)
                    return inputObject;

                FileSystemInfo test1 = inputObject as FileSystemInfo;
                if (test1 != null)
                    return test1.FullName; // no need for further checks, path shoul de qualified!

                PathInfo test2 = inputObject as PathInfo;
                if (test2 != null)
                    return test2.Path;     // no need for further checks, path shoul de qualified!

                string test3 = inputObject as string;
                if (test3 == null)
                    test3 = (string)LanguagePrimitives.ConvertTo(inputObject, typeof(string));
                if (Resolve)
                    test3 = engineIntrinsics.SessionState.Path.GetUnresolvedProviderPathFromPSPath(test3);
                else if (!engineIntrinsics.SessionState.Path.IsValid(test3))
                    throw new ArgumentTransformationMetadataException("Invalid path value: " + test3);
                return test3;
            }
        }
"@
    }
    return $script:my_pathtransformation_types
}


Install-PathTransformation

function A(
    [parameter(Mandatory=$false, ValueFromPipeline=$true)]
    [ValidPathTransformation(Resolve=$true)]
    [string] # optional, transformation returns always string
    $z) { 
  Process {
    Write-Host $("{0}: {1}" -f $z.GetType().FullName, $z)
  }
}

& {
    'mumu', 10, 10.5, ""
    dir $env:Temp | select -First 5
} | A

How it works:
1) Create a Transformation Attribute to process the Parameter value.
2) During transformation, if Value is FileSystemInfo or PathInfo we take the value within, if not we convert value to string and make sure that "path" is valid (and resolve path if needed).
3) When applied, the result of Transformation is always string.

Marian
  • 1
0

Does it work if you change the type of $path from String[] to [System.IO.DirectoryInfo[]]?

Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • It's not clear if the OP wants to process both FileInfos and DirectoryInfos or not. Since the example is using the path my guess is both are being used. – Andy Arismendi Mar 27 '12 at 09:34
  • You're probably right. I added this based on: 'The arguments can be strings or directory objects'. – Shay Levy Mar 27 '12 at 09:55
  • If `Test-VEnv` is only supposed to operate on directories this is what I'd do to. – Andy Arismendi Mar 27 '12 at 10:01
  • Test-VEnv could be passed FileInfo objects but would always fail on them anyway (a venv is a type of directory with a particular structure). – Paul Moore Mar 27 '12 at 15:32
  • If I specify a DirectoryInfo parameter type, Test-VEnv fails with Cannot process argument transformation on parameter 'Path'. Cannot convert the xxx value of type "System.IO.FileInfo" to type "System.IO.DirectoryInfo". – Paul Moore Mar 27 '12 at 15:39
  • I think the crux of the matter is that while there is a parameter attribute for ValueFromPipelineByPropertyName, there is no corresponding attribute for ValueFromArgumentsByPropertyName, so you can make it work with either one for pipeline input using the parameter attributes, but not for argument list input. For that, you'll have to "roll your own". – mjolinor Mar 27 '12 at 16:17
  • Yes, that's basically the issue I think. – Paul Moore Mar 29 '12 at 16:24