PowerShell for Developers - Functions
Pipeline
We’ve been using it already quite a bit in the past chapters, but lets take a moment and introduce, properly, the pipeline. Pipeline’ing is powered in PowerShell using the pipe operator |
. It passes data from one command, to another command. That other command had better be able to use that data. How? Well there is not magic here, there is conventions instead.
Let’s take a look at the help for our friend Get-Item
, we do that as by typing help Get-Item
or in our case help Get-Item -Parameter Path
which is asking for the help for the Path parameter specifically:
> help get-item -Parameter Path
-Path <String[]>
Specifies the path to an item. Get-Item gets the item at the specified location. Wildcards are permitted. This
parameter is required, but the parameter name ("Path") is optional.
Use a dot (.) to specify the current location. Use the wildcard character (*) to specify all the items in the
current location.
Required? true
Position? 1
Default value
Accept pipeline input? true (ByValue, ByPropertyName)
Accept wildcard characters? true
Did you not get this? You likely need to install the help, run Update-Help
and it will do so. If you did get this, you’ll see the line that talks about Accept Pipeline Input?
and that it states true
but more importantly that we can pass either ByValue or ByPropertyName. Let us explore both of those for a moment.
By Value Pipeline’ing
ByValue pipelines are the easiest to understand, in this case we can see from the help above we, the value for Path is expected to a String[]
(a string array).
> dir | %{ $_.FullName }
C:\source\Highway\MVC\build
C:\source\Highway\MVC\src
C:\source\Highway\MVC\.gitignore
C:\source\Highway\MVC\license.txt
C:\source\Highway\MVC\make.ps1
C:\source\Highway\MVC\NDesk.Options.dll
C:\source\Highway\MVC\OnRamper.exe
C:\source\Highway\MVC\push.ps1
C:\source\Highway\MVC\README.markdown
C:\source\Highway\MVC\setv.ps1
So here we have taken a directory listing, which is objects as we have learned previously, and then done a ForEach-Object
on that to select just the FullName property. FullName is a string, and so we are sending an array of strings out to the console currently. How, lets send that same data to Get-Item:
> dir | %{ $_.FullName } | Get-Item
Directory: C:\source\Highway\MVC
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 5/4/2013 10:44 PM build
d---- 5/2/2013 8:37 PM src
-a--- 5/2/2013 2:19 PM 259 .gitignore
-a--- 5/2/2013 2:19 PM 16896 license.txt
-a--- 5/4/2013 11:11 AM 211 make.ps1
-a--- 5/2/2013 11:46 PM 22016 NDesk.Options.dll
-a--- 5/4/2013 6:36 PM 15872 OnRamper.exe
-a--- 5/4/2013 12:16 PM 62 push.ps1
-a--- 5/2/2013 2:19 PM 17183 README.markdown
-a--- 5/4/2013 11:26 AM 332 setv.ps1
Wait … uhm … what? Sure, we just took a bunch of FileSystemInfo objects and dumped them to the console, you know how that formats them? As a directory listing of course. But that means we’ve been successful in binding that data to Get-Item. Prove it? Ok…
> dir | %{ $_.FullName } | Get-Item | %{$_.GetType()}
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True DirectoryInfo System.IO.FileSystemInfo
True True DirectoryInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
True True FileInfo System.IO.FileSystemInfo
So we have just bound ByValue, we’ve passed an array and it went to Path because of the value it was.
By Property Name Pipeline’ing
So how do we pass ByPropertyName? Let us continue the above example:
> dir | %{ @{ Path=$_.FullName} }
Name Value
---- -----
Path C:\source\Highway\MVC\build
Path C:\source\Highway\MVC\src
Path C:\source\Highway\MVC\.gitignore
Path C:\source\Highway\MVC\license.txt
Path C:\source\Highway\MVC\make.ps1
Path C:\source\Highway\MVC\NDesk.Options.dll
Path C:\source\Highway\MVC\OnRamper.exe
Path C:\source\Highway\MVC\push.ps1
Path C:\source\Highway\MVC\README.markdown
Path C:\source\Highway\MVC\setv.ps1
So here we have created a bunch of Hashtables that contain a property named Path. Now this is to simple, it doesn’t make that point that we could have other data included in these hashtables. So I’m going to add some of that, but limit the number of files:
> dir *.ps1 | %{ @{ Path=$_.FullName; Size=$_.Length; Updated=$_.LastWriteTime} }
Name Value
---- -----
Path C:\source\Highway\MVC\make.ps1
Size 211
Updated 5/4/2013 11:11:03 AM
Path C:\source\Highway\MVC\push.ps1
Size 62
Updated 5/4/2013 12:16:29 PM
Path C:\source\Highway\MVC\setv.ps1
Size 332
Updated 5/4/2013 11:26:16 AM
Ok, three entries, each with three properties, and we’re good … Right? sigh No. So you’ll see from the output, these are not properties. They are entries in a Hashtable, and are outputted vertically under Name and Value because of this. We can easily turn this into a real object with properties though, using a cast to PSCustomObject
which is the PowerShell dynamic
object.
> dir *.ps1 | %{ [PSCustomObject]@{ Path=$_.FullName; Size=$_.Length; Updated=$_.LastWriteTime} }
Path Size Updated
---- ---- -------
C:\source\Highway\MVC\make.ps1 211 5/4/2013 11:11:03 AM
C:\source\Highway\MVC\push.ps1 62 5/4/2013 12:16:29 PM
C:\source\Highway\MVC\setv.ps1 332 5/4/2013 11:26:16 AM
Alright, now we have the horizontal labels for our properties, and values below that. Awesome. Now lets pipe that to Get-Item:
> dir *.ps1 | %{ [PSCustomObject]@{ Path=$_.FullName; Size=$_.Length; Updated=$_.LastWriteTime} } | Get-Item
Directory: C:\source\Highway\MVC
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 5/4/2013 11:11 AM 211 make.ps1
-a--- 5/4/2013 12:16 PM 62 push.ps1
-a--- 5/4/2013 11:26 AM 332 setv.ps1
Bingo, we bound Path to Get-Item. That gives you an example now of both types of Pipeline’ing.
Functions
Now that we understand pipelines, how do we start to create reusable functionality? Well, to do that we need to write functions. And so, lets look at this in practice with everyone’s favorite demo … Hello World!
Basic Script Blocks
We can create a script block simply by using a set of curly braces { }
. Like so:
> { "Hello World!" }
"Hello World!"
That output is kind of odd, right? It didn’t output the string, because that would not have the quotes. What type of object did that return?
> { "Hello World!" }.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True ScriptBlock System.Object
Oh, so it’s a script block! Ok, is that the string representation of the block then?
> { "Hello World!" }.ToString()
"Hello World!"
Ah! Yep, that’s what happened. So how do I run a script block? Just stick a .
or &
in front of it.
> .{ "Hello World!" }
Hello World!
> &{ "Hello World!" }
Hello World!
Yep, both of those do indeed execute, we lose the quotes, and all is well. So we now have a code block.
Named Functions
But what if I want to name that script block? Easy, we define a function:
> function HW { "Hello World!" }
>
Done, we’ve defined that block now as HW. How do I run it? I type HW
of course!
> HW
Hello World!
Now I can assign a script block simply to a variable if I want, but if I do so, then I still need to use &
or .
to execute it, where-as functions are called by name. See:
> $hw = { "Hello World!" }
> $hw
"Hello World!"
> &$hw
Hello World!
> .$hw
Hello World!
But functions also have an important other aspect, which is that they can have parameters. So let’s create a function which takes a parameter, but lets say we want to pass it a location:
> function HW {
>> param($location)
>> "Hello $location!"
>> }
>>
> HW Dallas
Hello Dallas!
Now, we can specify types for parameters, so that we can’t pass bad data:
> function HW {
>> param([int]$location)
>> "Hello $location!"
>> }
>>
> HW Dallas
HW : Cannot process argument transformation on parameter 'location'. Cannot convert value "Dallas" to type
"System.Int32". Error: "Input string was not in a correct format."
At line:1 char:4
+ HW Dallas
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [HW], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,HW
> hw 123
Hello 123!
See that we got an error now when we passed the Dallas string, but when we passed 123, we succeeded. Now we can change this pipe in an array, passing ByValue:
> 1..5 | HW
Hello 0!
Huh… that didn’t do what we expected. I guess we’ll have to give a hint that we want that Parameter to be pipelined.
> function HW { param( [Parameter(ValueFromPipeline=$true)][int]$location )
>> "Hello $location" }
>>
> 1..5 | HW
Hello 5
Ok, but still not “correct”. Why? Because as it happens, we’re using the simple form of a script blocks. A script block is actually defined by three sections: Begin, Process, and End. By default, if we don’t specify a section, we get End. What are the differences? Begin runs once, before pipleline values are bound. Process is run once for each member of the pipeline. End runs after all members have been process. How do we know that we get End by default? Look at the value we got, it was the last value of the pipeline.
> function HW { param( [Parameter(ValueFromPipeline=$true)][int]$location )
>> BEGIN { "Beginning : $location" }
>> PROCESS {"Processing : $location"}
>> END {"Ending: $location"}}
>>
> 1..5 | HW
Beginning : 0
Processing : 1
Processing : 2
Processing : 3
Processing : 4
Processing : 5
Ending: 5
So here we have redefined our function, and given it a Begin, Process and End block. And we can see that $location, because it is marked from pipeline, is not set until we are in Process, and then we run process 5 times, and finally we run ending once.
Branching
So… it is not programming without if blocks, right? Well we’ve got those:
> function HW { param( [Parameter(ValueFromPipeline=$true)][int]$location )
>> BEGIN { "Beginning : $location" }
>> PROCESS { if(($location % 2) -eq 0) { "Processing : $location" } else { "Else" } }
>> END {"Ending: $location"}}
>>
> 1..5 | HW
Beginning : 0
Else
Processing : 2
Else
Processing : 4
Else
Ending: 5
Looping
First … don’t loop, pipeline. But when you must loop, do so these ways:
> function DoWhile { $i = 1; do { Write-Host $i; $i++ } while ($i -le 5) }
> DoWhile
1
2
3
4
5
> function WhileLoop { $i = 1; while ($i -le 5) { Write-Host $i;$i++} }
> WhileLoop
1
2
3
4
5
> function ForLoop { for ($i=1;$i -le 5;$i++) {Write-Host $i} }
> ForLoop
1
2
3
4
5
> function ForEachLoop { $ints=@(1..5); foreach ($i in $ints) {Write-Host $i} }
> ForEachLoop
1
2
3
4
5
Those cover all of the major types of looping, and do so in a clean way, very similar to the C# syntax in all cases.