Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I determine if a PowerShell function is running as part of a pipeline?

Can a PowerShell function determine if it is being run as part of a pipeline? I have a function which populates an array with instances of FileInfo which I would like to "yield" to the pipeline if the function is being run this way or produce some pretty output if the function is being invoked by itself from the command line.

function Do-Something {
    $file_infos = @()
    # Populate $file_infos with FileInfo instances...

    if (INVOKED_IN_PIPELINE) {
        return $file_infos
    }
    else {
        foreach ($file_info in $file_infos) {
            write-host -foregroundcolor yellow $file_info.fullname
        }
    }
}

Basically, I'm trying to figure out how to implement INVOKED_IN_PIPELINE. If it is run in a pipeline (e.g. Do-Something | format-table fullname), I would simply yield the array, but if run directly (e.g. Do-Something), it would pretty-print the contents of the array to the console.

Is there a way to do this? If there is a more "idiomatic" way to achieve this kind of thing, I would also be interested to know.

like image 761
Richard Cook Avatar asked Dec 28 '10 21:12

Richard Cook


2 Answers

This information is available as part of $PSCmdlet.MyInvocation. Here is a cmdlet you can use to experiment with this. What it does it to write out the contents of that property once for any command execution and then passes on the object (so you can manipulate the objects some more with bigger pipelines). What you'll see is that there is a property called PipelineLength which is equal to 1 when you run this command by itself and increases for each item in the pipeline. Also notice PipelinePosition. It tells you what position this command is in the pipeline.

NOTE: $PSCmdlet is only available when you write an advanced function (e.g. have the [CmdletBinding()] attribute.

function test-PSCmdlet
{
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true)]
$test
)
Begin{
    $once = $false
    }
process
    {
        if (!$once)
        {
            write-host ($PSCmdlet.MyInvocation |out-string)
            $once = $true
        }
        Write-Output $_
    }
}

Here are some examples:

PS C:\Users\jsnover.NTDEV> test-PSCmdlet

MyCommand        : test-PSCmdlet
BoundParameters  : {}
UnboundArguments : {}
ScriptLineNumber : 1
OffsetInLine     : 14
HistoryId        : 61
ScriptName       : 
Line             : test-PSCmdlet
PositionMessage  : 
                   At line:1 char:14
                   + test-PSCmdlet <<<< 
InvocationName   : test-PSCmdlet
PipelineLength   : 1
PipelinePosition : 1
ExpectingInput   : False
CommandOrigin    : Runspace


PS C:\Users\jsnover.NTDEV> gps lsass | test-PSCmdlet |Format-table Name,Id -auto

MyCommand        : test-PSCmdlet
BoundParameters  : {[test, System.Diagnostics.Process (lsass)]}
UnboundArguments : {}
ScriptLineNumber : 1
OffsetInLine     : 26
HistoryId        : 62
ScriptName       : 
Line             : gps lsass | test-PSCmdlet |Format-table Name,Id -auto
PositionMessage  : 
                   At line:1 char:26
                   + gps lsass | test-PSCmdlet <<<<  |Format-table Name,Id -aut
                   o
InvocationName   : test-PSCmdlet
PipelineLength   : 3
PipelinePosition : 2
ExpectingInput   : True
CommandOrigin    : Runspace


Name   Id
----   --
lsass 620
like image 161
Jeffrey Snover - MSFT Avatar answered Nov 04 '22 00:11

Jeffrey Snover - MSFT


The "idiomatic" way to do this is to output a specific object type and then define formatting data for that object. The object can be a custom (C#/VB-based object) or a named PSObject. The advantage of this approach is that you can just output these objects and if there is no further pipeline output formatting (ie use of a Format command) then you're defined default output formatting will get used. Otherwise, one of the Format commands can override that default formatting. Here's an example:

PS> $obj = new-object psobject -Property @{FName = 'John'; LName = 'Doe'; `
                                           BirthDate = [DateTime]"5/7/1965"}
PS> $obj.psobject.TypeNames.Insert(0, "MyNamespace.MyCustomTypeName")
PS> $obj

BirthDate                               FName                            LName
---------                               -----                            -----
5/7/1965 12:00:00 AM                    John                             Doe


PS> Update-FormatData .\MyCustomFormatData.ps1xml
PS> $obj

FName                     LName                     BirthDate
-----                     -----                     ---------
John                      Doe                       5/7/1965 12:00:00 AM

Notice how the default output is different the second time we sent $obj down the pipe. That's because it used the custom formatting instructions provided since there were no explicit formatting commands used.

BTW there is always a pipeline even for $obj because that is implicitly piped to Out-Default.

Here is the custom format definition that is defined in a .ps1xml file that I named MyCustomFormatData.xml:

<Configuration>
  <ViewDefinitions>
    <View>
      <Name>MyNamespace.MyCustomTypeName</Name>
      <ViewSelectedBy>
        <TypeName>MyNamespace.MyCustomTypeName</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>FName</Label>
            <Width>25</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>LName</Label>
            <Width>25</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>BirthDate</Label>
            <Width>25</Width>
            <Alignment>left</Alignment>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>FName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>LName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>BirthDate</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

For more examples of how to format custom objects look at the files output by this command:

Get-ChildItem $pshome\*.format.ps1xml
like image 37
Keith Hill Avatar answered Nov 04 '22 00:11

Keith Hill