I've seen the following a lot in PowerShell, but what does it do exactly?
$_
This is the variable for the current value in the pipe line, which is called $PSItem in Powershell 3 and newer.
1,2,3 | %{ write-host $_ }
or
1,2,3 | %{ write-host $PSItem }
For example in the above code the %{} block is called for every value in the array. The $_ or $PSItem variable will contain the current value.
I think the easiest way to think about this variable like input parameter in lambda expression in C#. I.e. $_ is similar to x in x => Console.WriteLine(x) anonymous function in C#. Consider following examples:
PowerShell:
1,2,3 | ForEach-Object {Write-Host $_}
Prints:
1
2
3
or
1,2,3 | Where-Object {$_ -gt 1}
Prints:
2
3
And compare this with C# syntax using LINQ:
var list = new List<int> { 1, 2, 3 };
list.ForEach( _ => Console.WriteLine( _ ));
Prints:
1
2
3
or
list.Where( _ => _ > 1)
.ToList()
.ForEach(s => Console.WriteLine(s));
Prints:
2
3
According to this website, it's a reference to this, mostly in loops.
$_ (dollar underscore)
'THIS' token. Typically refers to the
item inside a foreach loop.
Task:
Print all items in a collection.
Solution. ... | foreach { Write-Host
$_ }
$_ is an alias for automatic variable $PSItem (introduced in PowerShell V3.0; Usage information found here) which represents the current item from the pipe.
PowerShell (v6.0) online documentation for automatic variables is here.
$_ is a variable created by the system usually inside block expressions that are referenced by cmdlets that are used with pipe such as Where-Object and ForEach-Object.
But it can be used also in other types of expressions, for example with Select-Object combined with expression properties. Get-ChildItem | Select-Object #{Name="Name";Expression={$_.Name}}. In this case the $_ represents the item being piped but multiple expressions can exist.
It can also be referenced by custom parameter validation, where a script block is used to validate a value. In this case the $_ represents the parameter value as received from the invocation.
The closest analogy to c# and java is the lamda expression. If you break down powershell to basics then everything is a script block including a script file a, functions and cmdlets. You can define your own parameters but in some occasions one is created by the system for you that represents the input item to process/evaluate. In those situations the automatic variable is $_.
$_ is an variable which iterates over each object/element passed from the previous | (pipe).
The $_ is a $PSItem, which is essentially an object piped from another command.
For example, running Get-Volume on my workstations returns Rows of PSItems, or objects
get-volume | select driveLetter,DriveType
driveLetter DriveType
----------- ---------
D Fixed
Fixed
C Fixed
A Removable
Driveletter and DriveType are properties
Now, you can use these item properties when piping the output with $_.(propertyName). (Also remember % is alias for Foreach-Object) For example
$vol = get-volume | select driveLetter,DriveType
$vol | Foreach-Object {
if($_.DriveType -eq "Fixed") {
"$($_.driveLetter) is $($_.driveType)"}
else{
"$($_.driveLetter) is $($_.driveType)"
}
}
Using Terinary in Powershell 7, I am able to shorten the logic while using properties from the Piped PSItem
Related
In the creation of a script I'm writing, I ran across the use of what appears to be a pipeline shortcut that I have never seen before.
For example:
$acl | select -expandproperty Access | ? {$_.IdentityReference -eq "AD\$Group"}
can be shortened to
$acl.Access.Where({$_.IdentityReference -eq "AD\$Group"})
What is this? $acl.Access makes sense to me as it's just a property reference but I do not see any such .Where() method being available on this object. As such it seems .Where({}) is some sort of pipeline shortcut for " | Where-Object {}". Where can I find documentation for such a thing? Do others like this exist?
Second question. I have a need to add a calculated property to my results, and within the property expression I have to perform piping as well, so where I would typically just reference $_ from the parent pipeline, this is lost within the expression statement.
I can get around this by using -PipelineVariable up within the pipeline, but it seems this only works when used with cmdlets and is not available when starting a pipeline with a variable (which I do often). Is there a way around this?
This works:
$acl = Get-Acl -Path "AD:\$dn"
$acl | select -expandproperty access -PipelineVariable AccessRight | ? {$_.IdentityReference -eq "AD\$Group"} | select *, #{N='ObjectTypeName';E={($ADGuidMap.GetEnumerator() | ? {$_.Value -eq $AccessRight.ObjectType}).Name}}
I'd really like to be able to do this with the shortcut as anywhere I can shorten my oneliner I would like to do. Unfortunately the following will not work as I cannot use pipelinevariable at this location.
$acl.Access.Where({$_.IdentityReference -eq "AD\$Group"}) -PipeLineVariable AccessRight | select *, #{N='ObjectTypeName';E={($ADGuidMap.GetEnumerator() | ? {$_.Value -eq $AccessRight.ObjectType}).Name}}
It's always bugged me about not being able to use Pipeline variables outside of cmdlets so I finally figured I'd ask if there was some sort of way around that.
As for the first question:
The .Where() and .ForEach() methods are intrinsic members, i.e. members that PowerShell implicitly makes available on objects of any type.
While they function similarly to their cmdlet counterparts, Where-Object and ForEach-Object, there are important differences.
See this answer for more information.
As an aside: You could have simplified your command even when using the Where-Object cmdlet, namely with simplified syntax:
$acl.Access | Where-Object IdentityReference -eq "AD\$Group"
As for the second question:
Because the .Where() method isn't a cmdlet, you cannot use the common -PipelineVariable parameter with it.
Given that .Access typically returns multiple objects, using the pipeline with -PipelineVariable enables an elegant solution.
If you do want to avoid the pipeline (operator), you can combine the .Where() and .ForEach() methods as follows, with the help of an intermediate (regular) variable:
$acl.Access.
Where({ $_.IdentityReference -eq "AD\$Group" }).
ForEach({
$aclEntry = $_
Select-Object -InputObject $aclEntry -Property *,
#{
N = 'ObjectTypeName'
E = { ($ADGuidMap.GetEnumerator().Where({ $_.Value -eq $aclEntry.ObjectType })).Name }
}
})
Update:
As you've discovered yourself, if you stick with a pipeline, you can combine -PipeLineVariable with Write-Output in order to capture each element of an array / collection in a pipeline variable as it flows through the pipeline, for use a later script block, as the following (contrived) example shows:
$array = 0..3
Write-Output $array -Pipeline arrayElement |
Where-Object { 0 -eq $_ % 2 } | # filter out odd numbers
ForEach-Object { $_ + 1 } | # add 1
ForEach-Object { "pipeline variable / `$_: $arrayElement / $_" }
Output, which shows that each element of input array $array was individually captured in pipeline variable $arrayElement:
pipeline variable / $_: 0 / 1
pipeline variable / $_: 2 / 3
Let's say we have an array of objects $objects. Let's say these objects have a "Name" property.
This is what I want to do
$results = #()
$objects | %{ $results += $_.Name }
This works, but can it be done in a better way?
If I do something like:
$results = objects | select Name
$results is an array of objects having a Name property. I want $results to contain an array of Names.
Is there a better way?
I think you might be able to use the ExpandProperty parameter of Select-Object.
For example, to get the list of the current directory and just have the Name property displayed, one would do the following:
ls | select -Property Name
This is still returning DirectoryInfo or FileInfo objects. You can always inspect the type coming through the pipeline by piping to Get-Member (alias gm).
ls | select -Property Name | gm
So, to expand the object to be that of the type of property you're looking at, you can do the following:
ls | select -ExpandProperty Name
In your case, you can just do the following to have a variable be an array of strings, where the strings are the Name property:
$objects = ls | select -ExpandProperty Name
As an even easier solution, you could just use:
$results = $objects.Name
Which should fill $results with an array of all the 'Name' property values of the elements in $objects.
To complement the preexisting, helpful answers with guidance of when to use which approach and a performance comparison.
Outside of a pipeline[1], use (requires PSv3+):
$objects.Name # returns .Name property values from all objects in $objects
as demonstrated in rageandqq's answer, which is both syntactically simpler and much faster.
Accessing a property at the collection level to get its elements' values as an array (if there are 2 or more elements) is called member-access enumeration and is a PSv3+ feature.
Alternatively, in PSv2, use the foreach statement, whose output you can also assign directly to a variable: $results = foreach ($obj in $objects) { $obj.Name }
If collecting all output from a (pipeline) command in memory first is feasible, you can also combine pipelines with member-access enumeration; e.g.:
(Get-ChildItem -File | Where-Object Length -lt 1gb).Name
Tradeoffs:
Both the input collection and output array must fit into memory as a whole.
If the input collection is itself the result of a command (pipeline) (e.g., (Get-ChildItem).Name), that command must first run to completion before the resulting array's elements can be accessed.
In a pipeline, in case you must pass the results to another command, notably if the original input doesn't fit into memory as a whole, use: $objects | Select-Object -ExpandProperty Name
The need for -ExpandProperty is explained in Scott Saad's answer (you need it to get only the property value).
You get the usual pipeline benefits of the pipeline's streaming behavior, i.e. one-by-one object processing, which typically produces output right away and keeps memory use constant (unless you ultimately collect the results in memory anyway).
Tradeoff:
Use of the pipeline is comparatively slow.
For small input collections (arrays), you probably won't notice the difference, and, especially on the command line, sometimes being able to type the command easily is more important.
Here is an easy-to-type alternative, which, however is the slowest approach; it uses ForEach-Object via its built-in alias, %, with simplified syntax (again, PSv3+):
; e.g., the following PSv3+ solution is easy to append to an existing command:
$objects | % Name # short for: $objects | ForEach-Object -Process { $_.Name }
Note: Use of the pipeline is not the primary reason this approach is slow, it is the inefficient implementation of the ForEach-Object (and Where-Object) cmdlets, up to at least PowerShell 7.2. This excellent blog post explains the problem; it led to feature request GitHub issue #10982; the following workaround greatly speeds up the operation (only somewhat slower than a foreach statement, and still faster than .ForEach()):
# Speed-optimized version of the above.
# (Use `&` instead of `.` to run in a child scope)
$objects | . { process { $_.Name } }
The PSv4+ .ForEach() array method, more comprehensively discussed in this article, is yet another, well-performing alternative, but note that it requires collecting all input in memory first, just like member-access enumeration:
# By property name (string):
$objects.ForEach('Name')
# By script block (more flexibility; like ForEach-Object)
$objects.ForEach({ $_.Name })
This approach is similar to member-access enumeration, with the same tradeoffs, except that pipeline logic is not applied; it is marginally slower than member-access enumeration, though still noticeably faster than the pipeline.
For extracting a single property value by name (string argument), this solution is on par with member-access enumeration (though the latter is syntactically simpler).
The script-block variant ({ ... }) allows arbitrary transformations; it is a faster - all-in-memory-at-once - alternative to the pipeline-based ForEach-Object cmdlet (%).
Note: The .ForEach() array method, like its .Where() sibling (the in-memory equivalent of Where-Object), always returns a collection (an instance of [System.Collections.ObjectModel.Collection[psobject]]), even if only one output object is produced.
By contrast, member-access enumeration, Select-Object, ForEach-Object and Where-Object return a single output object as-is, without wrapping it in a collection (array).
Comparing the performance of the various approaches
Here are sample timings for the various approaches, based on an input collection of 10,000 objects, averaged across 10 runs; the absolute numbers aren't important and vary based on many factors, but it should give you a sense of relative performance (the timings come from a single-core Windows 10 VM:
Important
The relative performance varies based on whether the input objects are instances of regular .NET Types (e.g., as output by Get-ChildItem) or [pscustomobject] instances (e.g., as output by Convert-FromCsv).
The reason is that [pscustomobject] properties are dynamically managed by PowerShell, and it can access them more quickly than the regular properties of a (statically defined) regular .NET type. Both scenarios are covered below.
The tests use already-in-memory-in-full collections as input, so as to focus on the pure property extraction performance. With a streaming cmdlet / function call as the input, performance differences will generally be much less pronounced, as the time spent inside that call may account for the majority of the time spent.
For brevity, alias % is used for the ForEach-Object cmdlet.
General conclusions, applicable to both regular .NET type and [pscustomobject] input:
The member-enumeration ($collection.Name) and foreach ($obj in $collection) solutions are by far the fastest, by a factor of 10 or more faster than the fastest pipeline-based solution.
Surprisingly, % Name performs much worse than % { $_.Name } - see this GitHub issue.
PowerShell Core consistently outperforms Windows Powershell here.
Timings with regular .NET types:
PowerShell Core v7.0.0-preview.3
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.005
1.06 foreach($o in $objects) { $o.Name } 0.005
6.25 $objects.ForEach('Name') 0.028
10.22 $objects.ForEach({ $_.Name }) 0.046
17.52 $objects | % { $_.Name } 0.079
30.97 $objects | Select-Object -ExpandProperty Name 0.140
32.76 $objects | % Name 0.148
Windows PowerShell v5.1.18362.145
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.012
1.32 foreach($o in $objects) { $o.Name } 0.015
9.07 $objects.ForEach({ $_.Name }) 0.105
10.30 $objects.ForEach('Name') 0.119
12.70 $objects | % { $_.Name } 0.147
27.04 $objects | % Name 0.312
29.70 $objects | Select-Object -ExpandProperty Name 0.343
Conclusions:
In PowerShell Core, .ForEach('Name') clearly outperforms .ForEach({ $_.Name }). In Windows PowerShell, curiously, the latter is faster, albeit only marginally so.
Timings with [pscustomobject] instances:
PowerShell Core v7.0.0-preview.3
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.006
1.11 foreach($o in $objects) { $o.Name } 0.007
1.52 $objects.ForEach('Name') 0.009
6.11 $objects.ForEach({ $_.Name }) 0.038
9.47 $objects | Select-Object -ExpandProperty Name 0.058
10.29 $objects | % { $_.Name } 0.063
29.77 $objects | % Name 0.184
Windows PowerShell v5.1.18362.145
Factor Command Secs (10-run avg.)
------ ------- ------------------
1.00 $objects.Name 0.008
1.14 foreach($o in $objects) { $o.Name } 0.009
1.76 $objects.ForEach('Name') 0.015
10.36 $objects | Select-Object -ExpandProperty Name 0.085
11.18 $objects.ForEach({ $_.Name }) 0.092
16.79 $objects | % { $_.Name } 0.138
61.14 $objects | % Name 0.503
Conclusions:
Note how with [pscustomobject] input .ForEach('Name') by far outperforms the script-block based variant, .ForEach({ $_.Name }).
Similarly, [pscustomobject] input makes the pipeline-based Select-Object -ExpandProperty Name faster, in Windows PowerShell virtually on par with .ForEach({ $_.Name }), but in PowerShell Core still about 50% slower.
In short: With the odd exception of % Name, with [pscustomobject] the string-based methods of referencing the properties outperform the scriptblock-based ones.
Source code for the tests:
Note:
Download function Time-Command from this Gist to run these tests.
Assuming you have looked at the linked code to ensure that it is safe (which I can personally assure you of, but you should always check), you can install it directly as follows:
irm https://gist.github.com/mklement0/9e1f13978620b09ab2d15da5535d1b27/raw/Time-Command.ps1 | iex
Set $useCustomObjectInput to $true to measure with [pscustomobject] instances instead.
$count = 1e4 # max. input object count == 10,000
$runs = 10 # number of runs to average
# Note: Using [pscustomobject] instances rather than instances of
# regular .NET types changes the performance characteristics.
# Set this to $true to test with [pscustomobject] instances below.
$useCustomObjectInput = $false
# Create sample input objects.
if ($useCustomObjectInput) {
# Use [pscustomobject] instances.
$objects = 1..$count | % { [pscustomobject] #{ Name = "$foobar_$_"; Other1 = 1; Other2 = 2; Other3 = 3; Other4 = 4 } }
} else {
# Use instances of a regular .NET type.
# Note: The actual count of files and folders in your file-system
# may be less than $count
$objects = Get-ChildItem / -Recurse -ErrorAction Ignore | Select-Object -First $count
}
Write-Host "Comparing property-value extraction methods with $($objects.Count) input objects, averaged over $runs runs..."
# An array of script blocks with the various approaches.
$approaches = { $objects | Select-Object -ExpandProperty Name },
{ $objects | % Name },
{ $objects | % { $_.Name } },
{ $objects.ForEach('Name') },
{ $objects.ForEach({ $_.Name }) },
{ $objects.Name },
{ foreach($o in $objects) { $o.Name } }
# Time the approaches and sort them by execution time (fastest first):
Time-Command $approaches -Count $runs | Select Factor, Command, Secs*
[1] Technically, even a command without |, the pipeline operator, uses a pipeline behind the scenes, but for the purpose of this discussion using the pipeline refers only to commands that use |, the pipeline operator, and therefore by definition involve multiple commands.
Caution, member enumeration only works if the collection itself has no member of the same name. So if you had an array of FileInfo objects, you couldn't get an array of file lengths by using
$files.length # evaluates to array length
And before you say "well obviously", consider this. If you had an array of objects with a capacity property then
$objarr.capacity
would work fine UNLESS $objarr were actually not an [Array] but, for example, an [ArrayList]. So before using member enumeration you might have to look inside the black box containing your collection.
(Note to moderators: this should be a comment on rageandqq's answer but I don't yet have enough reputation.)
I learn something new every day! Thank you for this. I was trying to achieve the same. I was directly doing this:
$ListOfGGUIDs = $objects.{Object GUID}
Which basically made my variable an object again! I later realized I needed to define it first as an empty array,
$ListOfGGUIDs = #()
I'm trying to do some reporting on Azure Policies. I'll eventually be filtering on dates, but having trouble filtering on anything, so present the following sample.
PS C:\>$defstrings = az policy definition list --management-group "mgsandbox" # returns an array of strings
PS C:\>$def = ConvertFrom-Json -InputObject ($defstrings -join "`n") -depth 99 # converts to an array of PSCustomObject
PS C:\>$def.count
2070
PS C:\>$sel = Where-Object -inputobject $def -FilterScript { $_.displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace" }
PS C:\>$sel.count
2070
PS C:\> $def[0].displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace"
False
While I might possibly find more than one hit on the displayName, there are clearly a non-zero set of displayNames that do not match the filter, yet the selection is getting all of them.
Any suggestions what's wrong with my syntax? It seems straightforward.
Do not use an -InputObject argument with Where-Object; instead, provide input via the pipeline:
# Use the pipeline to provide input, don't use -InputObject
$def | Where-Object -FilterScript { $_.displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace" }
In most cmdlets, the -InputObject parameter is a mere implementation detail whose purpose is to facilitate pipeline input and cannot be meaningfully used directly; see this answer for more information.
As for what you tried:
When you use -InputObject, an argument that is a collection (enumerable) is passed as a whole to the cmdlet, whereas using the same in the pipeline cause its enumeration, i.e. the collection's elements are passed, one by one.
A simplified example:
# Sample array.
$arr = 1, 2, 3
# WRONG: array is passed *as a whole*
# and in this case outputs *all* its elements.
# -> 1, 2, 3
Where-Object -InputObject $arr { $_ -eq 2 }
That is, the script block passed to Where-Object is executed once, with the automatic $_ variable bound to the array as a whole, so that the above is in effect equivalent to:
if ($arr -eq 2) { $arr }
Since $arr -eq 2 evaluates to $true in a Boolean context (the if conditional), $arr as a whole is output (although on output it is enumerated), giving the impression that no filtering took place.
The reason that $arr -eq 2 evaluates to $true is that the -eq operator, among others, supports arrays as its LHS, in which case the behavior changes to filtering, by returning the sub-array of matching elements instead of a Boolean, so that 1, 2, 3 -eq 2 yields #(2) (an array containing the one matching element, 2), and coercing #(2) to a Boolean yields $true ([bool] #(2)).[1]
Conversely, if the implied conditional yields $false (e.g., $_ -eq 5), no output is produced at all.
By contrast, if you use the pipeline, you'll get the desired behavior:
# Sample array.
$arr = 1, 2, 3
# OK: Array elements are enumerated, i.e.
# sent *one by one* through the pipeline.
# -> 2
$arr | Where-Object{ $_ -eq 2 }
Alternatively, you can bypass the pipeline by using the intrinsic .Where() method:
Note: This requires collecting all input in memory first; however, especially with data already in memory, this approach performs better than the pipeline approach:
# OK:
# -> 2 (wrapped in a collection)
#(1, 2, 3).Where({ $_ -eq 2 })
Note: .Where() always outputs an array-like collection, even when only a single object matches the filter. In practice, however, that usually doesn't matter.
[1] For a summary of PowerShell's to-Boolean coercion rules, see the bottom section of this answer.
I have a script that periodically generates a list of all files in a directory, and then writes a text file of the results to a different directory.
I'd like to change this so it checks the newest text file in the output directory, and only makes a new one if there's differences. It seemed simple enough.
Here's what I tried:
First I get the most recent file in the directory, grab the hash, and write my variable values to the console:
$lastFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$oldHash = Get-FileHash $lastFile | Select-Object Hash
Write-Host 'lastFile = '$lastFile
Write-Host 'oldHash = '$oldHash
Output:
lastFile = C:\ReportOutputDir\test1.txt
oldHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Then I do the exact same gci on the FileList dir, and create a new file (new_test.txt), then grab the hash of this file:
gci -Path C:\FileLists -File -Recurse -Name -Depth 2 | Sort-Object | out-file C:\ReportOutputDir\new_test.txt
$newFile = gci C:\ReportOutputDir | sort LastWriteTime | select -last 1 | Select-Object -ExpandProperty FullName
$newHash = Get-FileHash $newFile | Select-Object Hash
Write-Host 'newFile = '$newFile
Write-Host 'newHash = '$newHash
Output:
newFile = C:\ReportOutputDir\new_test.txt
newHash = #{Hash=E7787C54F5BAE236100A24A6F453A5FDF6E6C7333B60ED8624610EAFADF45521}
Finally, I attempt my -eq operator where I'd usually simply remove the newFile if it's equal. For now, I'm just doing a simple :
if ($newHash -eq $oldHash){
'files are equal'
}
else {'files are not equal'}
And somehow, I'm getting
files are not equal
What gives? Also, for the record I was originally trying to save the gci output to a variable and comparing the contents of the last file to the gci output, but was also having trouble with the -eq operator. Fairly new to powershell stuff so I'm sure I'm doing something wrong here.
Select-Object Hash creates an object with a .Hash property and it is that property that contains the hash string.
The object returned is of type [pscustomobject], and two instances of this type never compare as equal - even if all their property names and values are equal:
The reason is that reference equality is tested, because [pscustomobject] is a .NET reference type that doesn't define custom equality-testing logic.
Testing reference equality means that only two references to the very same instance compare as equal.
A quick example:
PS> [pscustomobject] #{ foo = 1 } -eq [pscustomobject] #{ foo = 1 }
False # !! Two distinct instances aren't equal, no matter what they contain.
You have two options:
Compare the .Hash property values, not the objects as a whole:
if ($newHash.Hash -eq $oldHash.Hash) { # ...
If you don't need a [pscustomobject] wrapper for the hash strings, use Select-Object's -ExpandProperty parameter instead of the (possibly positionally implied) -Property parameter:
Select-Object -ExpandProperty Hash
As for why the Write-Host output matched:
When you force objects to be converted to string representations - essentially, Write-Host calls .ToString() on its arguments - the string representations of distinct [pscustomobject] instances that have the same properties and values will be the same:
PS> "$([pscustomobject] #{ foo = 1 })" -eq "$([pscustomobject] #{ foo = 1 })"
True # Same as: '#{foo=1}' -eq '#{foo=1}'
However, you should not rely on these hashtable-like string representations to determine equality of [pscustomobject]s as a whole, because of the inherent limitations of these representations, which can easily yield false positives.
This answer shows how to compare [pscustomobject] instances as a whole, by comparing all of their property values, by passing all property names to Compare-Object -Property - but note that this assumes that all property values are either strings or instances of .NET value types or corresponding properties must again either reference the very same instance of a .NET reference type or be of a type that implements custom equality-comparison logic.
With PowerShell there's historically been no need for yield return; since that's essentially what the pipeline is.
However, with PS5's classes, methods cannot write to the pipeline. As such, are there any options to mimic yield return / pipeline behaviour from a Powershell class method?
Demo
Function
This code returns data to the pipeline; we can see that the variable $global:i is updated by the function, then the value's read by the next step in the pipeline before the next iteration of the function:
[int]$i = 0
function Get-PowerShellProcesses() {
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$global:i++; $_}
}
Get-PowerShellProcesses | %{"$i - $($_.ProcessName)}
Output:
1 - powershell
2 - powershell_ise
Class Method
If we do the same with a class's method everything's the same except that the full result set is gathered before being passed on to the pipeline.
[int]$i = 0
class Demo {
Demo(){}
[PSObject[]]GetPowershellProcesses() {
return Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{$Global:i++; $_}
}
}
$demo = New-Object Demo
$demo.GetPowerShellProcesses() | %{"$i - $($_.ProcessName)"}
Output:
2 - powershell
2 - powershell_ise
I'm guessing there's no solution; but hoping there is something.
Why does this matter?
In the above example obviously it doesn't. However, this does have an impact where we don't need the full result set; e.g. say we had a | Select-Object -First 10 after the function call, but had an expensive operation returning thousands of results, we'd see a significant performance hit.
What have you tried?
Inline Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
Error: Not all code path returns value within method.
Inline Return + Final Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
return
Error: Invalid return statement within non-void method
Inline Return + Final [void] / $null Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{return $_}
return [void] #or return $null
No error; but acts as if only the last return statement were called; so we get no data.
Yield Return:
Get-Process | ?{$_.ProcessName -like '*powershell*'} | %{yield return $_}
Error: The term 'yield' is not recognized ...
Workaround
The simple workaround is to use C# classes with yield return, or traditional PowerShell functions instead.
An answer to this question was shared by SeeminglyScience on GitHub; all credit to them.
You can use LINQ as a sort of work around. Here's how you could do your stack overflow example.
using namespace System.Collections.Generic
using namespace System.Diagnostics
[int]$i = 0
class Demo {
[IEnumerable[Process]] GetPowershellProcesses() {
return [Linq.Enumerable]::Select(
[Process[]](Get-Process *powershell*),
[Func[Process, Process]]{ param($p) $global:i++; $p })
}
}
$demo = New-Object Demo
$demo.GetPowerShellProcesses() | %{ "$i - $($_.ProcessName)" }
However, variables from that scope may or may not be available depending on if the SessionStateScope that the enumerable was created in is still active during enumeration. This would likely also be an issue in any implementation of yield in PowerShell with the way script blocks currently work.
I have no idea why one would like to involve a class here to begin with here but you can do this without involving the $global:i if that is your ultimate goal. ForEach-Object can take an init script block. Use that for setting up your loop local incrementer $i.
If one need to accumulate in a list I'd guess that $i could be a list as well.
function Get-PowerShellProcesses() {
Get-Process | Where-Object {$_.ProcessName -like '*powershell*'}
}
Get-PowerShellProcesses | ForEach-Object {$i = 1} {"$i - $($_.ProcessName)"; $i++}