Search code examples
powershellvisual-studio-codepowershell-5.0

How do I reference the output of the previous "pipe" in a Powershell pipeline?


I wrote this code to get some (relative) file path:

function Get-ExecutingScriptDirectory() {
    return Split-Path $script:MyInvocation.MyCommand.Path  # returns this script's directory
}

$some_file_path = Get-ExecutingScriptDirectory | Join-Path -Path $_ -ChildPath "foo.json"

This threw the error:

Join-Path : Cannot bind argument to parameter 'Path' because it is null.
+ $some_file_path  = Get-ExecutingScriptDirectory | Join-Path -Path $_ -ChildPath "fo ...
+                                                     ~~
    + CategoryInfo          : InvalidData: (:) [Join-Path], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.JoinPathCommand

This indicates to me that the output of Get-ExecutingScriptDirectory is null - but it isn't - when I write the script out like this, it's fine:

$this_directory = Get-ExecutingScriptDirectory
$some_file_path = Join-Path -Path $this_directory -ChildPath "foo.json"

So the problem is that $_ is null. I would expect $_ to be reference to the previous pipe's standard output. The MSDN documentation also suggests this, but then it seems to immediately contradict itself:

$_ contains the current object in the pipeline object. You can use this variable in commands that perform an action on every object or on selected objects in a pipeline.

In the context of my code, $_ seems to qualify as the "current object in the pipeline object" - but I am not using this with a command that performs an action on every object or on selected objects in a pipeline.

$$ and $^ looked promising, but the MSDN documentation just says a few vague things here about lexical tokens. The documentation on $PSItem was equally terse.

What I would actually like to do is create a big pipeline:

$some_file_path = Get-ExecutingScriptDirectory | Join-Path -Path {{PREVIOUS STDOUT}} -ChildPath "foo.json" | Get-Content {{PREVIOUS STDOUT}} | Convert-FromJson {{PREVIOUS STDOUT}} | {{PREVIOUS STDOUT}}.data

I'd like to know where I'm going wrong both on a conceptual and a technical level.


Solution

  • You can do what you want by executing the command below:

    (Get-Content -Path (Get-ExecutingScriptDirectory | Join-Path -ChildPath "foo.json" | ConvertFrom-Json)).data
    

    Some commands support pipeline parameter binding. The options are pipeline by value or pipeline by property. A good reference for parameter binding is About Functions Advanced Parameters.

    Searching online for the command you are going to use will yield parameter binding information. For example Join-Path, has a parameters section. Each parameter will have a description including the field Accept pipeline input:. For a parameter to accept pipeline input, this must be True. Usually, it will say how the value can be passed into the pipeline (ByPropertyName or ByValue).

    ByPropertyName indicates that you must output an object that contains a property name that matches the parameter name. Then once the object is piped, the parameter will bind to the matching property name's value. See below for an example:

    $filePath = [pscustomobject]@{Path = 'c:\temp\test1\t.txt'}
    $filePath
    
    Path
    ----
    c:\temp\test1\t.txt
    
    $filePath.Path # This binds to -Path in Get-Content
    c:\temp\test1\t.txt
    $filepath | Get-Content 
    

    ByValue indicates that any value piped in will attempt to bind to that parameter. If there are type discrepancies at the binding time, an error will likely be thrown. See below for example:

    "c:\temp" | Join-Path -ChildPath "filepath" # c:\temp binds to `-Path`
    c:\temp\filepath
    

    Regarding $_, which is synonymous with $PSItem, is the current input object in a script block. You typically see this used with Foreach-Object and Where-Object. If you don't have a script block, you won't be able to use $_.

    You can technically pipe anything into Foreach-Object or Where-Object. Then the current pipeline object will be represented by $_. You don't need a collection as a single item can be piped in. See below:

    "c:\temp" | Foreach-Object { $_ }
    c:\temp
    
    $filePath | Foreach-Object { $_ }
    
    Path
    ----
    c:\temp\test1\t.txt
    
    $filePath | Foreach-Object { $_.Path }
    c:\temp\test1\t.txt