Powershell Array parameter from pipline - powershell

I'm trying to duplicate a typical powershell -Computername parameter that's available from the pipeline and as a normal parameter using CmdletBinding and ValueFromPipeline. My challenge is that I'm getting different results from specifying the parameter versus piping in values.
My code looks like this:
[CmdletBinding()]
param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)] [string[]]$ComputerName
)
BEGIN { "Begin script`n-----" }
PROCESS {
" `$ComputerName '$ComputerName'"
" `$_ '$_'"
" +++ Count: " + $ComputerName.Count
foreach($computer in $ComputerName) {
" `$computer '$computer'"
}
" -----"
}
END { "Complete" }
When I run this using a pipeline, I get this:
PS> (1, 2, 3) | .\BPEParamTest.ps1
Begin script
-----
$ComputerName '1'
$_ '1'
+++ Count: 1
$computer '1'
-----
$ComputerName '2'
$_ '2'
+++ Count: 1
$computer '2'
-----
$ComputerName '3'
$_ '3'
+++ Count: 1
$computer '3'
-----
Complete
However, when run with a parameter, I get different results:
PS> .\BPEParamTest.ps1 -ComputerName (1, 2, 3)
Begin script
-----
$ComputerName '1 2 3'
$_ ''
+++ Count: 3
$computer '1'
$computer '2'
$computer '3'
-----
Complete

I always use the following construction. This works for arrays in a parameter as well as from the pipeline:
[CmdletBinding()]
param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)] [string[]]$ComputerName
)
process {
foreach($computer in $computername){
#do the stuff
}
Full explanation: The process block is run once for each item in the pipeline, so that's how it handles lists on the pipeline (i.e. $computername is set to each item in turn). If you pass the values as a parameter, the $computername is set to the list which is why there's a loop.

.\BPEParamTest.ps1 -ComputerName (1, 2, 3) does not use the pipeline but rather a single input (a 3-element array). In contrast, (1, 2, 3) | .\BPEParamTest.ps1 uses the pipeline (3 separate inputs).
To work around this, you can check bound parameters and determine if there is pipeline input. Here is a short example:
function out-item {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$TRUE)]
$item
)
begin {
$PipelineInput = -not $PSBoundParameters.ContainsKey("item")
write-host "Pipeline input? $PipelineInput"
$n = 0
}
process {
if ($PipelineInput) {
$_
$n++
}
else {
$item | foreach-object {
$_
$n++
}
}
}
end {
write-host "Output $n item(s)"
}
}
With this function, both 1,2,3 | out-item and out-item 1,2,3 produce the same output (except for the "Pipeline input?" part).

Related

What is the best way to structure an advanced multi-threaded Powershell function to work both on the pipeline as well as non-pipeline calls?

I have a function that flattens directories in parallel for multiple folders. It works great when I call it in a non-pipeline fashion:
$Files = Get-Content $FileList
Merge-FlattenDirectory -InputPath $Files
But now I want to update my function to work both on the pipeline as well as when called off the pipeline. Someone on discord recommended the best way to do this is to defer all processing to the end block, and use the begin and process blocks to add pipeline input to a list. Basically this:
function Merge-FlattenDirectory {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position = 0,ValueFromPipeline)]
[string[]]
$InputPath
)
begin {
$List = [System.Collections.Generic.List[PSObject]]#()
}
process {
if(($InputPath.GetType().BaseType.Name) -eq "Array"){
Write-Host "Array detected"
$List = $InputPath
} else {
$List.Add($InputPath)
}
}
end {
$List | ForEach-Object -Parallel {
# Code here...
} -ThrottleLimit 16
}
}
However, this is still not working on the pipeline for me. When I do this:
$Files | Merge-FlattenDirectory
It actually passes individual arrays of length 1 to the function. So testing for ($InputPath.GetType().BaseType.Name) -eq "Array" isn't really the way forward, as only the first pipeline value gets used.
My million dollar question is the following:
What is the most robust way in the process block to differentiate between pipeline input and non-pipeline input? The function should add all pipeline input to a generic list, and in the case of non-pipeline input, should skip this step and process the collection as-is moving directly to the end block.
The only thing I could think of is the following:
if((($InputPath.GetType().BaseType.Name) -eq "Array") -and ($InputPath.Length -gt 1)){
$List = $InputPath
} else {
$List.Add($InputPath)
}
But this just doesn't feel right. Any help would be extremely appreciated.
You might just do
function Merge-FlattenDirectory {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position = 0,ValueFromPipeline)]
[string[]]
$InputPath
)
begin {
$List = [System.Collections.Generic.List[String]]::new()
}
process {
$InputPath.ForEach{ $List.Add($_) }
}
end {
$List |ForEach-Object -Parallel {
# Code here...
} -ThrottleLimit 16
}
}
Which will process the input values either from the pipeline or the input parameter.
But that doesn't comply with the Strongly Encouraged Development Guidelines to Support Well Defined Pipeline Input (SC02) especially for Implement for the Middle of a Pipeline
This means if you correctly want to implement the PowerShell Pipeline, you should directly (parallel) process your items in the Process block and immediately output any results from there:
function Merge-FlattenDirectory {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position = 0,ValueFromPipeline)]
[string[]]
$InputPath
)
begin {
$SharedPool = New-ThreadPool -Limit 16
}
process {
$InputPath |ForEach-Object -Parallel -threadPool $Using:SharedPool {
# Process your current item ($_) here ...
}
}
}
In general, script authors are advised to use idiomatic PowerShell which often comes down to lesser object manipulations and usually results in a correct PowerShell pipeline implementation with less memory usage.
Please let me know if you intent to collect (and e.g. order) the output based on this suggestion.
Caveat
The full invocation of the ForEach-Object -Parallel cmdlet itself is somewhat inefficient as you open and close a new pipeline each iteration. To resolve this, my whole general statement about idiomatic PowerShell falls a bit apart, but should be resolvable by using a steppable pipeline.
To implement this, you might use the ForEach-Object cmdlet as a template:
[System.Management.Automation.ProxyCommand]::Create((Get-Command ForEach-Object))
And set the ThrottleLimit of the ThreadPool in the Begin Block
function Merge-FlattenDirectory {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position = 0,ValueFromPipeline)]
[string[]]
$InputPath
)
begin {
$PSBoundParameters += #{
ThrottleLimit = 4
Parallel = {
Write-Host (Get-Date).ToString('HH:mm:ss.s') 'Started' $_
Start-Sleep -Seconds 3
Write-Host (Get-Date).ToString('HH:mm:ss.s') 'finished' $_
}
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('ForEach-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$InputPath.ForEach{ $steppablePipeline.Process($_) }
}
end {
$steppablePipeline.End()
}
}
1..5 |Merge-FlattenDirectory
17:57:40.40 Started 3
17:57:40.40 Started 2
17:57:40.40 Started 1
17:57:40.40 Started 4
17:57:43.43 finished 3
17:57:43.43 finished 1
17:57:43.43 finished 4
17:57:43.43 finished 2
17:57:43.43 Started 5
17:57:46.46 finished 5
Here's how I would write it with comments where I have changed it.
function Merge-FlattenDirectory {
[CmdletBinding()]
param (
[Parameter(Mandatory,Position = 0,ValueFromPipeline)]
$InputPath # <this may be a string, a path object, a file object,
# or an array
)
begin {
$List = #() # Use an array for less than 100K objects.
}
process {
#even if InputPath is a string for each will iterate once and set $p
#if it is an array of strings add each. If it is one or more objects,
#try to find the right property for the path.
foreach ($p in $inputPath) {
if ($p -is [String]) {$list += $p }
elseif ($p.Path) {$list += $p.Path}
elseif ($p.FullName) {$list += $p.FullName}
elseif ($p.PSPath) {$list += $p.PSPath}
else {Write-warning "$P makes no sense"}
}
}
end {
$List | ForEach-Object -Parallel {
# Code here...
} -ThrottleLimit 16
}
}
#iRon That "write for the middle of the pipeline" in the docs does not mean write everything in the process block .
function one { #(1,2,3,4,5) }
function two {
param ([parameter(ValueFromPipeline=$true)] $p )
begin {Write-host "Two begins" ; $a = #() }
process {Write-host "Two received $P" ; $a += $p }
end {Write-host "Two ending" ; $a; Write-host "Two ended"}
}
function three {
param ([parameter(ValueFromPipeline=$true)] $p )
begin {Write-host "three Starts" }
process {Write-host "Three received $P" }
end {Write-host "Three ended" }
}
one | two | three
One is treated as an end block.
One, two and three all run their begins (one's is empty).
One's output goes to the process block in two, which just collects the data. Two's end block starts after one's end-block ends, and sends output
At this point three's process block gets input. After two's end block ends, three's endblock runs.
Two is "in the middle" it has a process block to deal with multiple piped items (if it were all one 'end' block it would only process the last one).

PowerShell: How to only return an [PSCustomObject] not an array with other things in it? [duplicate]

This question already has an answer here:
Powershell Join-Path showing 2 dirs in result instead of 1 - accidental script/function output
(1 answer)
Closed 1 year ago.
I have a cmdlet that returns an [PSCustomObject] . This is done 2 cmdlets deep. At every return it adds another things and then instead of returning the [PSCustomObject] it returns an array of 3 items.
This object has string properties and at every level it adds another leading space to them.
I just want to return the [PSCustomObject] no array. How to do this ?
The object return from the last cmdlet
$ident
LocalTime UtcTime Data
--------- ------- ----
5/6/2021 3:43:35 PM 5/6/2021 10:43:35 PM New-NodeIdentifier "4"
5/6/2021 3:43:35 PM 5/6/2021 10:43:35 PM New-NodeGenerationIdentifier "4"
$ident.Count
3
2 method deap , 2 leading spaces
"$($ident.String)"
" 4"
"$($ident[2].String)"
"4"
The functions
Function New-NodeIdentifier{
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[String]$Identifier,
[Switch]$NoCmdletEntryOutput
)
Begin{}
Process{
Start-CmdletEntry "New-NodeIdentifier `"$($Identifier)`"" -NoCmdletEntryOutput:$NoCmdletEntryOutput
#determin identifyer type
$l = $Identifier.split(".")
[double]$OutNumber = $null
$IsGen = $true
foreach( $c in $l ) {
Write-Host $c
if(-not [double]::TryParse($c,[ref]$OutNumber)) {
$IsGen = $false
}
}
# create the indentifier
if($IsGen) {
$ni = New-NodeGenerationIdentifier -Generation $Identifier
write-host "nni `"$($ni.String)`" type $($ni.GetType())"
return $ni
} else {
$ni = New-NodeHwSkuIdentifier -HwSku $Identifier
write-host "nni `"$($ni.String)`" type $($ni.GetType())"
return $ni
}
}
End{}
}
Function New-NodeGenerationIdentifier{
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[String]$Generation,
[Switch]$NoCmdletEntryOutput
)
Begin{}
Process{
Start-CmdletEntry "New-NodeGenerationIdentifier `"$($Generation)`"" -NoCmdletEntryOutput:$NoCmdletEntryOutput
$vmi = [PSCustomObject]#{
Generation ="$($Generation)"
Type ='Generation'
String ="$($Generation)"
}
write-host "nngi `"$($vmi.String)`" type $($vmi.GetType())"
return $vmi
}
End{}
}
With the link from mklement0 , I figured this out. So yes this was a victim of PowerShell's implicit output behavior. My Start-CmdletEntry returned something which then become part of the return payload.

Getting quick Server Ping Results in Powershell

I have written below PowerShell script to get the ping results of server and it also generates the log for the same. My script works fine, also generates log but it take so much time as there are many servers. Is there any way I can quickly get the results. I have tried start-job but that generates error.
$logpath = $logpath +"\" +$Logname
Function Log($txt)
{
if (! (Test-Path $logpath)) {
New-Item $logpath |Out-Null
}
$date = Get-Date -Format "dd_MMM_yyyy_hh:mm:ss - "
$txt1 = ($date + $txt)
Add-Content "$logpath" "$txt1"
Add-Content "$logpath" " "
}
$ServerDetails=import-csv $Servercsv
foreach($servertest in $ServerDetails)
{
if((Test-Connection -ComputerName $servertest.servers -Count 2))
{
Log("'" + $servertest.servers + "' Successfully started operation")
Write-Host "Started Operation Successfully"
}
if(!(Test-Connection -ComputerName $servertest.servers -Count 2))
{
Log("'" + $servertest.servers + "'Servers are not pinging")
Write-Host "Servers are not pinging"
}
}
Assumed your "log" function works as expected you could make the rest of your code a little more efficient:
$ServerList = import-csv $Servercsv
foreach ($ComputerName in $ServerList) {
$Connection = Test-Connection -ComputerName $ComputerName.servers -Count 1 -Quiet
if ($Connection) {
Log("'" + $ComputerName.servers + "' Successfully started operation")
Write-Host "Started Operation Successfully"
}
Else {
Log("'" + $ComputerName.servers + "'Servers are not pinging")
Write-Host "Servers are not pinging"
}
}
For the speed effort for pinging, try this one out and compare speeds relative to how you are doing/getting from yours.
Final Super-Fast Ping Command
function Test-OnlineFast
{
param
(
# make parameter pipeline-aware
[Parameter(Mandatory,ValueFromPipeline)]
[string[]]
$ComputerName,
$TimeoutMillisec = 1000
)
begin
{
# use this to collect computer names that were sent via pipeline
[Collections.ArrayList]$bucket = #()
# hash table with error code to text translation
$StatusCode_ReturnValue =
#{
0='Success'
11001='Buffer Too Small'
11002='Destination Net Unreachable'
11003='Destination Host Unreachable'
11004='Destination Protocol Unreachable'
11005='Destination Port Unreachable'
11006='No Resources'
11007='Bad Option'
11008='Hardware Error'
11009='Packet Too Big'
11010='Request Timed Out'
11011='Bad Request'
11012='Bad Route'
11013='TimeToLive Expired Transit'
11014='TimeToLive Expired Reassembly'
11015='Parameter Problem'
11016='Source Quench'
11017='Option Too Big'
11018='Bad Destination'
11032='Negotiating IPSEC'
11050='General Failure'
}
# hash table with calculated property that translates
# numeric return value into friendly text
$statusFriendlyText = #{
# name of column
Name = 'Status'
# code to calculate content of column
Expression = {
# take status code and use it as index into
# the hash table with friendly names
# make sure the key is of same data type (int)
$StatusCode_ReturnValue[([int]$_.StatusCode)]
}
}
# calculated property that returns $true when status -eq 0
$IsOnline = #{
Name = 'Online'
Expression = { $_.StatusCode -eq 0 }
}
# do DNS resolution when system responds to ping
$DNSName = #{
Name = 'DNSName'
Expression = { if ($_.StatusCode -eq 0) {
if ($_.Address -like '*.*.*.*')
{ [Net.DNS]::GetHostByAddress($_.Address).HostName }
else
{ [Net.DNS]::GetHostByName($_.Address).HostName }
}
}
}
}
process
{
# add each computer name to the bucket
# we either receive a string array via parameter, or
# the process block runs multiple times when computer
# names are piped
$ComputerName | ForEach-Object {
$null = $bucket.Add($_)
}
}
end
{
# convert list of computers into a WMI query string
$query = $bucket -join "' or Address='"
Get-WmiObject -Class Win32_PingStatus -Filter "(Address='$query') and timeout=$TimeoutMillisec" |
Select-Object -Property Address, $IsOnline, $DNSName, $statusFriendlyText
}
}
Test-OnlineFast -ComputerName google.de, powershellmagazine.com, 10.10.10.200, 127.0.0.1
<#
# Results
Address Online DNSName Status
------- ------ ------- ------
127.0.0.1 True DESKTOP-7AAMJLF Success
google.de True google.de Success
powershellmagazine.com True powershellmagazine.com Success
10.10.10.200 False Request Timed Out
#>
Test-Connection with an array of targetnames and the asjob parameter is fast (powershell 5.1). Unresponsive addresses have a null responsetime. Annoyingly, the property names don't match the column headings.
$list = 'microsoft.com','yahoo.com'
$results = test-connection $list -count 1 -asjob | receive-job -wait
$results | select address,responsetime
address responsetime
------- ------------
yahoo.com 39
microsoft.com

Accept lists of computers in many forms in a Powershell pipeline

I am trying to create a script that accepts lists of computers in various forms from many different 3rd party sources that I don't control. These various sources return computers sometimes as a simple array of strings, sometimes as a Powershell Object, sometimes as a hash. I want my script to take any of these types of lists and get the computer name(s), put it in an array. Then perform the actual processing.
Here are some examples of data that might be given to my script that I am trying to create.
#('system-01','system-02') | \\path\share\mycommand.ps1
#(#{"ComputerName" = "system-01";OtherKey = 'foo'},
#{"ComputerName" = "system-02";OtherKey = 'foo'}) | \\path\share\mycommand.ps1
#([PSCustomObject]#{
ComputerName = 'system-01'
OtherProperties = 'foo'
},
[PSCustomObject]#{
ComputerName = 'system-02'
OtherProperties = 'foo'
}) | \\path\share\mycommand.ps1
My script currently looks like.
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ParameterSetName="ComputerHash")]
[hashtable]$ComputerHash,
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ParameterSetName="ComputerArray")]
[String[]]$ComputerName,
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ParameterSetName="ComputerObject")]
[Object[]]$ComputerObjects
)
BEGIN
{
# create an empty array to build up a list of the computers
$computers=#()
}
PROCESS
{
# get all the computers from the pipeline
switch ($PsCmdlet.ParameterSetName)
{
"ComputerArray" { $computers+=$ComputerName; break}
"ComputerHash" { $computers+=$ComputerHash.ComputerName; break}
"ComputerObject"{ $computers+=$ComputerObjects.ComputerName; break}
}
}
END
{
$computers | % {
# do the stuff
"Do something on $_"
}
}
Unfortunately, I am currently getting the Parameter set cannot be resolved ... error. How do I make my script so that it will basically accept any kind of pipeline input, and then do the right thing? Is there some simpler method I should be using?
What you could do, instead of making different parameter sets, is to just deal with it after you get the info. So something like:
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True)]
$ComputerList
)
BEGIN
{
# create an empty array to build up a list of the computers
$computers=Switch($ComputerList.GetType()){
{$_.Name -eq 'Hashtable'}{$ComputerList.ComputerName;Continue}
{$ComputerList -is [Array] -and $ComputerList[0] -is [String]}{$ComputerList;Continue}
{$ComputerList -is [Array] -and $ComputerList[0] -is [Object]}{$ComputerList.ComputerName}
}
}
PROCESS
{
}
END
{
$computers | % {
# do the stuff
"Do something on $_"
}
}
Or maybe even easier:
BEGIN{$Computers=If($ComputerList[0] -is [String]){$ComputerList}Else{$ComputerList.ComputerName}}
Edit: As pointed out, we don't get the piped data in the BEGIN or END blocks, so my idea works, but my script doesn't. Instead we have to do things in the PROCESS block as Zoredache has stated. He already posted his code, and I'm sure it works wonderfully, but I figured I'd post a modified version of mine so my answer wouldn't continue to be wrong, because, well, I don't like having wrong answers out there.
Function Test-Function1{
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True)]
$ComputerList
)
BEGIN
{
}
PROCESS
{
[string[]]$computers+=Switch($ComputerList){
{$_ -is [Hashtable]}{$_.ComputerName;Continue}
{$_ -is [String]}{$_;Continue}
{$_ -is [Object]}{$_|Select -Expand Computer*}
}
}
END
{
$computers | % {
# do the stuff
"Do something on $_"
}
}
}
Then when I piped data to it as such:
#{'ComputerName'='Server1','Server2'}|Test-Function1
[pscustomobject]#{'Computer'='Server1'},[pscustomobject]#{'ComputerName'='Server2'}|Test-Function1
'Server1','Server2'|Test-Function1
They all responded with the expected output of:
Do something on Server1
Do something on Server2
#TheMadTechnician pointed me in the right direction, but his code seemed to have some errors, and didn't quiet work the way I wanted. Here is the code that seems to do everything I want.
[CmdletBinding()]
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True)]
$PipelineItem
)
BEGIN
{
$ComputerList=#()
}
PROCESS
{
$ComputerList+=
Switch($PipelineItem.GetType()) {
{$_.Name -eq 'String'}
{$PipelineItem}
{$_.Name -eq 'Hashtable'}
{$PipelineItem.ComputerName}
{$_.Name -eq 'PSCustomObject' -and (Get-Member -MemberType Properties -Name "Computer" -InputObject $PipelineItem)}
{$PipelineItem.Computer}
{$_.Name -eq 'PSCustomObject' -and (Get-Member -MemberType Properties -Name "ComputerName" -InputObject $PipelineItem)}
{$PipelineItem.ComputerName}
}
}
END
{
$ComputerList | % {
# do the stuff
"Do something on $_"
}
}

How does Select-Object stop the pipeline in PowerShell v3?

In PowerShell v2, the following line:
1..3| foreach { Write-Host "Value : $_"; $_ }| select -First 1
Would display:
Value : 1
1
Value : 2
Value : 3
Since all elements were pushed down the pipeline. However, in v3 the above line displays only:
Value : 1
1
The pipeline is stopped before 2 and 3 are sent to Foreach-Object (Note: the -Wait switch for Select-Object allows all elements to reach the foreach block).
How does Select-Object stop the pipeline, and can I now stop the pipeline from a foreach or from my own function?
Edit: I know I can wrap a pipeline in a do...while loop and continue out of the pipeline. I have also found that in v3 I can do something like this (it doesn't work in v2):
function Start-Enumerate ($array) {
do{ $array } while($false)
}
Start-Enumerate (1..3)| foreach {if($_ -ge 2){break};$_}; 'V2 Will Not Get Here'
But Select-Object doesn't require either of these techniques so I was hoping that there was a way to stop the pipeline from a single point in the pipeline.
Check this post on how you can cancel a pipeline:
http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx
In PowerShell 3.0 it's an engine improvement. From the CTP1 samples folder ('\Engines Demos\Misc\ConnectBugFixes.ps1'):
# Connect Bug 332685
# Select-Object optimization
# Submitted by Shay Levi
# Connect Suggestion 286219
# PSV2: Lazy pipeline - ability for cmdlets to say "NO MORE"
# Submitted by Karl Prosser
# Stop the pipeline once the objects have been selected
# Useful for commands that return a lot of objects, like dealing with the event log
# In PS 2.0, this took a long time even though we only wanted the first 10 events
Start-Process powershell.exe -Args '-Version 2 -NoExit -Command Get-WinEvent | Select-Object -First 10'
# In PS 3.0, the pipeline stops after retrieving the first 10 objects
Get-WinEvent | Select-Object -First 10
After trying several methods, including throwing StopUpstreamCommandsException, ActionPreferenceStopException, and PipelineClosedException, calling $PSCmdlet.ThrowTerminatingError and $ExecutionContext.Host.Runspace.GetCurrentlyRunningPipeline().stopper.set_IsStopping($true) I finally found that just utilizing select-object was the only thing that didn't abort the whole script (versus just the pipeline). [Note that some of the items mentioned above require access to private members, which I accessed via reflection.]
# This looks like it should put a zero in the pipeline but on PS 3.0 it doesn't
function stop-pipeline {
$sp = {select-object -f 1}.GetSteppablePipeline($MyInvocation.CommandOrigin)
$sp.Begin($true)
$x = $sp.Process(0) # this call doesn't return
$sp.End()
}
New method follows based on comment from OP. Unfortunately this method is a lot more complicated and uses private members. Also I don't know how robust this - I just got the OP's example to work and stopped there. So FWIW:
# wh is alias for write-host
# sel is alias for select-object
# The following two use reflection to access private members:
# invoke-method invokes private methods
# select-properties is similar to select-object, but it gets private properties
# Get the system.management.automation assembly
$smaa=[appdomain]::currentdomain.getassemblies()|
? location -like "*system.management.automation*"
# Get the StopUpstreamCommandsException class
$upcet=$smaa.gettypes()| ? name -like "*upstream*"
filter x {
[CmdletBinding()]
param(
[parameter(ValueFromPipeline=$true)]
[object] $inputObject
)
process {
if ($inputObject -ge 5) {
# Create a StopUpstreamCommandsException
$upce = [activator]::CreateInstance($upcet,#($pscmdlet))
$PipelineProcessor=$pscmdlet.CommandRuntime|select-properties PipelineProcessor
$commands = $PipelineProcessor|select-properties commands
$commandProcessor= $commands[0]
$null = $upce.RequestingCommandProcessor|select-properties *
$upce.RequestingCommandProcessor.commandinfo =
$commandProcessor|select-properties commandinfo
$upce.RequestingCommandProcessor.Commandruntime =
$commandProcessor|select-properties commandruntime
$null = $PipelineProcessor|
invoke-method recordfailure #($upce, $commandProcessor.command)
1..($commands.count-1) | % {
$commands[$_] | invoke-method DoComplete
}
wh throwing
throw $upce
}
wh "< $inputObject >"
$inputObject
} # end process
end {
wh in x end
}
} # end filter x
filter y {
[CmdletBinding()]
param(
[parameter(ValueFromPipeline=$true)]
[object] $inputObject
)
process {
$inputObject
}
end {
wh in y end
}
}
1..5| x | y | measure -Sum
PowerShell code to retrieve PipelineProcessor value through reflection:
$t_cmdRun = $pscmdlet.CommandRuntime.gettype()
# Get pipelineprocessor value ($pipor)
$bindFlags = [Reflection.BindingFlags]"NonPublic,Instance"
$piporProp = $t_cmdRun.getproperty("PipelineProcessor", $bindFlags )
$pipor=$piporProp.GetValue($PSCmdlet.CommandRuntime,$null)
Powershell code to invoke method through reflection:
$proc = (gps)[12] # semi-random process
$methinfo = $proc.gettype().getmethod("GetComIUnknown", $bindFlags)
# Return ComIUnknown as an IntPtr
$comIUnknown = $methinfo.Invoke($proc, #($true))
I know that throwing a PipelineStoppedException stops the pipeline. The following example will simulate what you see with Select -first 1 in v3.0, in v2.0:
filter Select-Improved($first) {
begin{
$count = 0
}
process{
$_
$count++
if($count -ge $first){throw (new-object System.Management.Automation.PipelineStoppedException)}
}
}
trap{continue}
1..3| foreach { Write-Host "Value : $_"; $_ }| Select-Improved -first 1
write-host "after"