Last week I was helping someone out in the PowerShell forum at ScriptingAnswers.com. The specific problem is irrelevant;l however I learned something in the process that will affect how I write my own PowerShell functions from now on. Not being a developer I never picked up on this subtle (at least to me) distinction. Here’s the deal.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
I suggested a solution to the original problem that would require two functions. My initial thought was to write a PowerShell expression that would take the results of the first function and pipe them to the second function. Since I wanted the second function to accept pipelined input, I wrote it with a Begin, Process and End script blocks.
Function Sample {
BEGIN {
#code runs once before any pipelined objects are processed
}
PROCESS {
#code runs for each pipelined object, $_
}
END {
#code runs once after every object has been processed
}
} #end function
The BEGIN and END script blocks are optional, but I actually wanted to use the BEGIN script block to test a condition. If the condition failed then I wanted the function to abort. Here’s what I thought I could do:
PS C:\> functionA | functionB –myParam foo
In my mind, functionA would run and then the results would be passed to functionB. If the parameter test failed, according the code in my begin script block, then the second function would display an error and stop. But that is not what happened. I put in some debug messages and it looked like the second function was starting before the first! Here are some sample functions that demonstrate this behavior.
function Do-A {
write-host "Starting A" -fore green
for ($i=1;$i-le 5;$i++) {
$i
}
write-host "Ending A"-fore Green
} #end A
Function Do-B {
Begin {
write-host "Starting B" -fore Magenta
}
Process {
$_*2
sleep -mill 100
}
End {
write-host "Ending B" -fore Magenta
}
} #end B
I expect the numbers 1-5 to be piped to the second function and multiplied by 2.
PS C:\> Do-A | Do-B
Starting B
Starting A
2
4
6
8
10
Ending A
Ending B
I get the results ok, but notice that I get the message from the Begin block in the second function first. This means that code like this will also not work as expected.
Function Do-A {
write-host "Starting A" -fore green
for ($i=1;$i-le 5;$i++) {
$i
}
write-host "Ending A"-fore Green
} #end A
Function Do-B {
Param([switch]$bail)
Begin {
write-host "Starting B" -fore Magenta
if ($bail) {
write-warning "Bailing"
return
}
}
Process {
$_*2
sleep -mill 100
}
End {
write-host "Ending B" -fore Magenta
}
} #end B
PS C:\> Do-A | Do-B
Starting B
WARNING: Bailing
Starting A
2
4
6
8
10
Ending A
Ending B
The reason for this behavior I learned is that in a pipelined expression, the Begin scriptblocks are all processed first. In my situation, only the second function had a Begin script block so it was executed first. This makes sense and now that I understand that behavior I can code accordingly.
function Do-A {
write-host "Starting A" -fore green
for ($i=1;$i-le 5;$i++) {
$i
}
write-host "Ending A"-fore Green
} #end A
Function Do-B {
Param([switch]$bail)
Begin {
write-host "Starting B" -fore Magen
}
Process {
if ($bail) {
write-warning "Bailing"
return
}
$_*2
sleep -mill 100
}
End {
write-host "Ending B" -fore Magenta
}
} #end B
Now if I pipe the functions, I get my desired effect.
PS C:\> Do-A | Do-B -bail
Starting B
Starting A
WARNING: Bailing
WARNING: Bailing
WARNING: Bailing
WARNING: Bailing
WARNING: Bailing
Ending A
Ending B
My examples are overly broad and the point I want to emphasize is that in a pipelined expression all the Begin script blocks are executed first. This is not necessarily a bad thing, merely behavior you have to understand. As an admin I had a different expectation as to PowerShell’s behavior.
Here’s one more example with yet another function thrown in.
Function Foo-A {
Write-Host "Starting Foo-A" -ForegroundColor green
dir $env:temp | where {$_.lastwritetime -gt (Get-Date).AddDays(-1)}
Write-Host "Ending Foo-A" -ForegroundColor green
} #end A
Function Foo-B {
begin {
Write-Host "Starting Foo-B" -ForegroundColor magenta
}
process {
# write ("{0} {1} {2}" -f $_.fullname,$_.LastWriteTime,$_.length)
Remove-Item $_.fullname -recurse -whatif
}
end {
Write-Host "ending Foo-B" -ForegroundColor magenta
}
} #end B
Function Foo-C {
Begin {
Write-Host "Starting Foo-C" -ForegroundColor Cyan
}
Process {
#not really doing anything
sleep -milliseconds 100
}
end {
Write-Host "ending Foo-C" -ForegroundColor Cyan
}
} #end C
Foo-A | Foo-B | Foo-C
If you run:
PS C:\> Do-A | Do-B | Do-C
You will see that the begin script blocks for B and C run first. End script blocks, by the way seem to run in order. You will see this behavior on PowerShell v1 and v2.
If you run into any problems developing your own PowerShell solutions, please drop by the forums at ScriptingAnswers.com.
I believe that BOTH the begin blocks and END blocks are run in pipeline order. It doesn’t looks like that in your output only because you’re writing the “begin” message from A from the END block 😉
I thought I had made that clear. If I have these functions:
Function Foo-A {
Write-Host "Starting Foo-A" -ForegroundColor green
dir $env:temp | where {$_.lastwritetime -gt (Get-Date).AddDays(-1)}
Write-Host "Ending Foo-A" -ForegroundColor green
} #End A
Function Foo-B {
begin {
Write-Host "Starting Foo-B" -ForegroundColor magenta
}
process {
#Write ("{0} {1} {2}" -f $_.fullname,$_.LastWriteTime,$_.length)
# Remove-Item $_.fullname -recurse -whatif
write $_.fullname
}
end {
Write-Host "ending Foo-B" -ForegroundColor magenta
}
} #End B
Function Foo-C {
Begin {
Write-Host "Starting Foo-C" -ForegroundColor Cyan
}
Process {
#not really doing anything
write-host "sleeping on $_" -fore Cyan
sleep -milliseconds 100
}
end {
Write-Host "ending Foo-C" -ForegroundColor Cyan
}
} #End C
And then run Foo-A | Foo-B | Foo-C I will get output like this:
Starting Foo-B
Starting Foo-C
Starting Foo-A
sleeping on C:\Users\Jeff\AppData\Local\Temp\msohtmlclip1
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRA4BF.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRB851.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRC69E.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRC6B5.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRC7CF.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRD5D3.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRD8E2.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\CVRE109.tmp.cvr
sleeping on C:\Users\Jeff\AppData\Local\Temp\jusched.log
Ending Foo-A
ending Foo-B
ending Foo-C
which is what I expect. The begin script blocks run in order, the process script blocks run in order and then the end script blocks run in order. Foo-A is a “standard” function without any defined script blocks. I think we’re saying the same thing.
I am reading PowerShell in Action again and this morning I accidentaly found this:
“In a pipeline, the begin clause is run for all cmdlets in in the pipeline. Then the process clause is run for the first cmdlet. … Finally the end clauses are all run.” Sequencing is cover again in chapter 7 but I haven’t it now on my PC so can’t check. But I am sure you have it.
David
I suppose I should read Bruce’s book again.