Counting objects fed to an AdvancedParameter in the pipeline

by cmcknz77 at 2013-04-02 16:33:08

This is either a bit of a ‘quirk’ or I’m totally not understanding the way the powershell pipeline works and missing something totally obvious.

I’ve got a test function that does ‘something’ and I want to perform a slightly different action inside the function dependent on the number of entries that have been passed to a specific parameter (only when there’s more than One).

function Test
{
[cmdletbinding()]
Param(
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
[String]$TestEntries
)
begin {
Write-Host "BeginTestEntryCount = $($TestEntries.count)"
}
process{
Write-Host "InProcessTestEntryCount = $($TestEntries.count)"
foreach($TestEntry in $TestEntries)
{
Write-Output $TestEntry
if($TestEntries.count -gt 1){Write-Host "Do something special with $TestEntry"}
}
}
}


If I name the parameter and pass in a number of entries (in this case 3) I get the expected result when I explicitly try to count the number of entries passed
to the ‘-Testentries’ parameter:-

[code2=powershell]PS C:> test -testentries 'one','two','three'
BeginTestEntryCount = 3
InProcessTestEntryCount = 3
one
Do something special with one
two
Do something special with two
three
Do something special with three[/code2]

However, when I try to pass a bunch of entries in through the pipeline all my ‘counts’ fail and the ‘special thing’ doesn’t happen.
[code2=powershell]PS C:> 'one','two','three'|Test
BeginTestEntryCount = 0
InProcessTestEntryCount = 1
one
InProcessTestEntryCount = 1
two
InProcessTestEntryCount = 1
three[/code2]

Any ideas why? Or how I can get the correct number of items that have been input to the parameter when I’m passing those items in through the pipeline?
by mjolinor at 2013-04-02 17:27:45
This is happening because the pipeline automatically "unrolls" arrays and collections and passes them to the pipeline one at a time ( but only one level deep). You can pass an array to the pipeline by making it a 2 D array so that it "unrolls" into a 1 D array. Another possibility is to accumulate all the objects from the pipeline into an array, and then work with that in the End block after the pipeline has finished reading everything, or use the automatic variable $input.
by MasterOfTheHat at 2013-04-03 06:48:00
Or just make it a 2-step process or do the first function inline in the other function, something like:
test -testentries (get-arrayofstuff)
by cmcknz77 at 2013-04-03 09:52:33
Are you saying that you cannot get that count if you’re using an Advanced Function with begin, process and end blocks and the pipeline?
Why does this work?

Function Sum1 {
$NoofElements=@($input).Count
$input.Reset()
Write-Host "BeginNoOfElements = $NoOfElements"
}


PS C:> 'one','two','three'| sum1
BeginNoOfElements = 3


And this Not work?
Function Sum {
begin{
$NoofElements=@($input).Count
$input.Reset()
Write-Host "BeginNoOfElements = $NoOfElements"
}
process{
$NoofElements=@($input).Count
$input.Reset()
Write-Host "ProcessNoOfElements = $NoOfElements"
}
end{
$NoofElements=@($input).Count
$input.Reset()
Write-Host "EndoOfElements = $NoOfElements"
}
}


PS C:> 'one','two','three'| sum
BeginNoOfElements = 0
ProcessNoOfElements = 1
ProcessNoOfElements = 1
ProcessNoOfElements = 1
EndoOfElements = 0
PS C]
Is the former (sum1) being processed before the ‘begin’ block?
by MasterOfTheHat at 2013-04-03 12:56:42
The pipeline is automagically exploding the array and feeding it to the "Sum" function one element at a time. What is going on looks basically like this:
$NoofElements=@($input).Count
Write-Host "BeginNoOfElements = $NoOfElements"

$arrNumbers = "one","two","three"
foreach($nbr in $arrNumbers)
{
$NoofElements=$nbr.Count
Write-Host "ProcessNoOfElements = $NoOfElements"
}

$NoofElements=@($input).Count
Write-Host "EndoOfElements = $NoOfElements"

Everything in the BEGIN block happens before the first element is passed to the function and everything in the END block happens after all elements have been passed to the function and execution is finished, but everything in the PROCESS block happens for each element passed to the array.

So to answer your question, no I don’t think you can get the count of all elements in an object coming down the pipeline. At least not any way that I’ve seen. The other option is to hold all of the elements in a collection in the PROCESS block and then check the count in your END block, but that doesn’t help you if you need to do different processing based on the number of elements.
by cmcknz77 at 2013-04-03 13:59:43
Ok - I kinda get it a bit more now for why the ‘Advanced’ function with the begin,process,end blocks works the way it does (thanks) but that still doesn’t explain why I get to be able to do what I need with a (for want of a better term) ‘dumb’ function (like ‘sum1’ in the example above) but cannot do it with an ‘Advanced’ one…

You’ve given me an idea though (one that fills me with dread but) putting my ‘Advanced’ function in a ‘dumb’ wrapper function lets me do what I need:
I just don’t understand why… And if anyone can point me towards an explanation I’d appreciate it… Because I feel that I’m going to be facing a ‘scoping nightmare of epic propertions’ if it’s really the only way to do it…

Function Sum1 {
$Sum1NoofElements=@($input).Count
$input.Reset()
Write-Host "Sum1NoOfElements = $Sum1NoOfElements"
Function Sum {
begin{
$NoofElements=@($input).Count
$input.Reset()
Write-Host "BeginNoOfElements = $NoOfElements"
if($Sum1NoOfElements -gt 1){Write-Host "Begin by doing Something Special With $input $input&quot;}<br> $input&#46;reset()<br> }<br> process{<br> $NoofElements=@($input)&#46;Count<br> $input&#46;Reset()<br> Write-Host &quot;ProcessNoOfElements = $NoOfElements&quot;<br> if($Sum1NoOfElements -gt 1){Write-Host &quot;Process Does Something Special With each $input&quot;}<br> $input&#46;reset()<br> }<br> end{<br> $NoofElements=@($input)&#46;Count<br> $input&#46;Reset()<br> Write-Host &quot;EndoOfElements = $NoOfElements&quot;<br> if($Sum1NoofElements -gt 1){Write-Host &quot;End by doing Something Special With $input $input"}
$input.reset()
}
}
$input | Sum
}


PS C:&gt; 1 | Sum1
Sum1NoOfElements = 1
BeginNoOfElements = 0
ProcessNoOfElements = 1
EndoOfElements = 0


PS C:&gt; 'two' | Sum1
Sum1NoOfElements = 1
BeginNoOfElements = 0
ProcessNoOfElements = 1
EndoOfElements = 0


PS C:&gt; 'one','2','three' | sum1
Sum1NoOfElements = 3
BeginNoOfElements = 0
Begin by doing Something Special With $input
ProcessNoOfElements = 1
Process Does Something Special With each one
ProcessNoOfElements = 1
Process Does Something Special With each 2
ProcessNoOfElements = 1
Process Does Something Special With each three
EndoOfElements = 0
End by doing Something Special With $input
by mjolinor at 2013-04-03 14:17:04
[quote]
You’ve given me an idea though (one that fills me with dread but) putting my ‘Advanced’ function in a ‘dumb’ wrapper function lets me do what I need:
I just don’t understand why… And if anyone can point me towards an explanation I’d appreciate it… Because I feel that I’m going to be facing a ‘scoping nightmare of epic propertions’ if it’s really the only way to do it…
[/quote]

You’ve got part of that answer already. I suggested early on, and others have followed, that you can move the processing into the End block. This runs after the pipeline has been emptied and there is no more input. At this point you have all the records (and therefore a count).

The reason it works in a "dumb" function is that without the Begin, Process, or End keywords, it will assume and implement End block behaviour. (As an aside, if it is a Filter rather than a Function the Process block is assumed), so you’d just be doing implicitly what’s been suggested explicitly.

I don’t see any reason this should be a ‘scoping nightmare’.