Huddled Masses
You can do more than breathe for free...
Browse: Home / Using Script Functions in the PowerShell Pipeline ( Take Two )

Using Script Functions in the PowerShell Pipeline ( Take Two )

By Joel 'Jaykul' Bennett on 13-Apr-2009

One of the consistent questions about PowerShell is: what’s the best way to write a script or a function to process pipeline objects and be able to take it’s parameters as a normal function?

Of scripts and functions

The first thing to know is that in PowerShell, there’s really no difference between a script (just a file with a .ps1 ending) and a function as you’ll see written below. If you take a function Get-Square, and remove the first and last lines ( Function Get-Square { … } ) you could put them in a file called “Get-Square.ps1” in your PATH, you use them exactly the way you would the function that was pre-loaded into memory by dot-sourcing or pasting it on the command line. Of course, doing that easily requires writing the function parameters on their own line using PARAM(...) syntax, which is why I recommend doing that.

Of functions and the pipeline

When your script or function is used on the pipeline its begin block is called once when the pipeline starts up, and then the process block is called repeatedly: once to process each pipeline object, and finally, the end block is called after all the objects have been processed through the whole pipeline. If you don’t understand that, you should play with this function, try calling it by passing a series of numbers through multiple instances of it:


function Test-SquarePipe {
   PARAM( $label, $color="White" )
   BEGIN   { Write-Host "Begin $Label" -Foreground $color }
   PROCESS {
      Write-Host "$Label `t $_" -Foreground $color
      $_ * $_
   }
   END     { Write-Host "End $Label"   -Foreground $color }
}

# for example ...
1..5 | Test-SquarePipe one cyan | Test-SquarePipe two yellow | Test-SquarePipe three green
 

Incidentally, if you don’t specify the begin, process, or end blocks, the body of your function is treated as the end block. This is so that you can use the special $Input variable, which collects all the things passed in on the pipeline, and thus only works in the end block. That would allow the following function to behave the same way regardless of whether it was invoked on the pipeline or by passing an $InputObject. Notice, however, the difference between this, and the function above.


Function Test-SquareEnd {
PARAM( $label, $color="White", $InputObject )
   BEGIN   { Write-Host "Begin $Label" -Foreground $color }
   PROCESS { Write-Host "$Label `t $_" -Foreground $color }
   END     {
      ## because one of $Input or $InputObject must be null:
      Foreach($item in $Input + $InputObject) {
         Write-Host "$Label `t $item" -Foreground $color
         $item * $item
      }
   }
}

# and test it like this
1..5 | Test-SquareEnd one cyan | Test-SquareEnd two yellow | Test-SquareEnd three green

# Or like this
1..5 | Test-SquarePipe one cyan | Test-SquareEnd two yellow | Test-SquareEnd three green
 

Our challenge

The basic idea here is to rewrite that function such that it can be used to process a set of numbers from either the pipeline or an argument, without interfering with the processing of other parameters. We require that the function process items as they come in, rather than waiting until it’s received all input before processing them the way Test-SquareEnd does.

In PowerShell 2.0 this would be easy

In the current CTP 3 of PowerShell 2, you just specify ValueFromPipeline=$true for the parameter you want to set from the pipeline, and the function will work the same way whether you pass the numbers as a parameter or along the pipeline — you can even control which attribute of the objects on the pipeline will be used, but that’s a whole other article.


Function Get-Square {
PARAM(
   $label
,  [ConsoleColor]$color = "White"
,  [Parameter(ValueFromPipeline=$true)]
   [Int[]]$InputObject
)
   BEGIN   { Write-Host "Begin $Label" -Foreground $color }
   END     { Write-Host "End $Label"   -Foreground $color }
   PROCESS {
      ForEach($i in $InputObject) {
         Write-Host "$Label $i" -Foreground $color
         $i * $i
      }
   }
}

## Either way we call these, they have the same output
## Unlike what Get-SquarePipe or Get-SquareEnd
1,2,3,4 | Get-Square one green | Get-Square two cyan
Get-Square one green 1,2,3,4   | Get-Square two cyan
 

Of course, PowerShell 2.0 is still in beta status, and even after it’s released you may need to write scripts that are backwards compatible to PowerShell 1.0, and the excercise of doing so may help you to understand more about how PowerShell functions work, and particularly how they behave in the pipeline.

Our solution

Since in a PowerShell 1.0 function it’s not really supported directly, we need to do some extra work:


function Get-Square {
   PARAM(
      $label = ""
   ,  [ConsoleColor]$color = "White"
   ,  [Int[]]$InputObject = $null
   )
   BEGIN {
      if ($InputObject) {
         ## If you accepted additional params, you'd need to pass those in
         Write-Output $InputObject | &($MyInvocation.InvocationName) -Label $label -Color $color
         ## break
      } else {
         Write-Host "Begin $Label" -Foreground $color
      }
   }
   PROCESS {
      ## If you specify a type for $InputObject, test for that here
      if($_ -is [Int]) {
         Write-Host "$Label $_" -Foreground $color
         $_ * $_
      } elseif($_) {
         throw "$_ is not a System.Int32"
      }
   }
   END {
      if(!$InputObject) {
         Write-Host "End $Label"   -Foreground $color
      }
   }
}

 

An explanation

Of course, this is just an example method, squaring things isn’t that exciting — but what’s special about it is that the output is almost exactly the same whether you call it with parameters Get-Square 1,2,3,4 or on the pipeline: 1,2,3,4 | Get-Square.

The trick is that it actually executes the same way in either case:

  1. If you call it by passing the int (or array of ints) as an argument ($InputObject), it calls itself and passes those values on the pipeline.
  2. When the integers are passed on the pipeline, the special pipeline iterator variable $_ is set, and the process block is executed.
  1. When it has to (re)invoke itself, it passes any other parameters as parameters, which means you need to have default values for them.

There is one tiny difference in the processing, which in real-world use is practically never noticeable (you can see it in our example if you call it like this):


Get-Square one green 1,2,3 | Get-Square two cyan
1,2,3 | Get-Square one green | Get-Square two cyan
 

You’ll see that unlike the PowerShell 2.0 pipeline function, when you pass the numbers as a parameter, the first one of them actually gets passed through the process block before the begin block of the second function on the pipeline is called. This usually doesn’t have any effect, but it’s something to keep in the back of your head.

A few precautions:

  • You have to default values for parameters, because you’ll be passing them all as named parameters, in the re-invoke step, and $null can cause problems.
  • If you need to do additional processing in the begin block, you should only do so in an ELSE case: when $InputObject is null. That way, the code will only execute once each time you call the function.
  • The same goes for the end block: you have to keep your code in an If(!$InputObject) block to avoid executing it twice (when you pass the values as an argument, and it re-invokes itself).
  • The test cases in the process block must wrap all of your process block code, so that you don’t process the arguments twice, and you shouldn’t refer to $InputObject, but instead should use the automatic $_ variable which is the value passed when the function is (re)invoked via the pipeline.

Here’s some sample output, in case you’re wondering:


PS> Get-Square test cyan 2,3,4
Begin test
test 2
4
test 3
9
test 4
16
End test

PS> 2,3,4|Get-Square test cyan
Begin test
test 2
4
test 3
9
test 4
16
End test
 

Similar Posts:

  • Parenthesis in PowerShell
  • ShowUI: Handling Events and Producing Output
  • More Custom Attributes for PowerShell (Parameter Transformation)
  • ShowUI: the tutorial walkthrough
  • What Scope Am I In?

Posted in Huddled | Tagged Advanced Functions, Pipeline, PowerShell, PowerShell Functions

« Previous Next »

Lijit Search

Tags

.Net .Net 2008 Scripting Games Automation Bugs Design Development Funny Gadgets GeoShell GUI Huddled Masses Internet licensing Microsoft Modules My Software News Personal PInvoke Pipeline Politics PoshCode PoshConsole PowerBoots PowerShell PowerShell Functions PowerTips Rants Recommender Repository Scripting ShowUI Software Solutions Textile Tips User Group UserInterface WalkThrough WebHosting Windows 7 WordPress WPF Xml

About Huddled Masses

This is web site is dedicated to the musings of Joel Bennett (aka Jaykul) about technology, software, software development, the web, and the world.

Any resemblance of the views expressed and the views of my employer, my terminal, or the view out my window are purely coincidental. The resemblance between them and my own views is non-deterministic. The question of the existence of views in the absence of anyone to hold them is left as an exercise for the reader.

P.S.: I occasionally link to things I think are great. When I do, I occasionally find a "referral code" so I can make a little cash. I promise that I don't link to anything just because of that cash (I wouldn't cross the street for the amount of cash those links bring in, never mind write a whole blog post) ... but I do not promise that things I link to will stay great as time passes, nor that you will agree with me about their greatness!

Archives

  • April 2012
  • February 2012
  • January 2012
  • October 2011
  • August 2011
  • July 2011
  • June 2011
  • March 2011
  • February 2011
  • January 2011

Copyright © 2012 Joel Bennett.

Powered by WordPress and Hybrid.