How does $_ work when piping in PowerShell? - powershell

I'm confused how the $_ variable works in certain contexts of piping. In this example for backing up a Bitlocker key:
Get-BitlockerVolume | % {$_.KeyProtector | ? RecoveryPassword | Backup-BitlockerKeyProtector -MountPoint $_.MountPoint}
This is how I read it in English:
Get all BitLockerVolume objects
For each BitLockerVolume object, pipe the KeyProtector fields forwards
Pipe KeyProtector objects forwards further for those with a RecoverPassword
Run the Backup-BitlockerKeyProtector, and supply the MountPoint
However, MountPoint is a field of the BitLockerVolume object, as shown here:
PS C:\Windows\system32> Get-BitLockerVolume | Get-Member | Where-Object {$_.Name -eq "MountPoint"}
TypeName: Microsoft.BitLocker.Structures.BitLockerVolume
Name MemberType Definition
---- ---------- ----------
MountPoint Property string MountPoint {get;}
So, for the entire block wrapped in brakcets { }, will the $_ variable ALWAYS be the same through any amount of piping? For example, the object we are piping forwards is changing. It's no longer a BitLockerVolume Object, but instead a KeyProtector object. So will the $_ always refer to the BitLockerVolume object in this case, or will it change further down the pipeline depending on different types of objects piped further through the chain?

So $_ is the info from the current pipe.
1,2 | %{
$_
}
response
1
2
while
1,2 | %{
"a","b" | %{
$_
}
}
response
a
b
a
b
We can see in the first that the output from %_ is from the last info given which is 1,2. While the next example still loops 1,2 but the output is from the pipe inside a,b.
There are ways around this by storing the first pipe information into a variable in the second pipe
1,2 | %{
$Num = $_
"a","b" | %{
$Num
}
}
which case the output is
1
1
2
2
In the example you gave lets look at it formated
Get-BitlockerVolume | % {
$_.KeyProtector | ? RecoveryPassword | Backup-BitlockerKeyProtector -MountPoint $_.MountPoint
}
You have 2 different pipes. The First is getting 'BitlockerVolumevolume'.
The second starts with you sending the BitlockerVolume's KeyProtector.
Its like saying
For each Bitlocker volume, Get KeyProtector.
For each KeyProtector, Get me ones that have the member RecoveryPassword
Foreach KeyProtector with member RecoveryPassword, Backup Bitlocker Key Protector Using KeyProtector's Mountpoints
So on one final note I would also assume the example you gave wouldnt work.
What you might be looking for is this...
Get-BitlockerVolume | % {
$MountPoint = $_.MountPoint
$_.KeyProtector | ? RecoveryPassword | Backup-BitlockerKeyProtector -MountPoint $MountPoint -KeyProtectorId $_.KeyProtectorId
}

Let's expand the aliases and fill in the implied parameters. $_ can only be used inside script blocks '{ }' that are options to cmdlets. Just because you're in a pipe, doesn't mean you can use $_ . The $_ here belongs to Foreach-Object. Where-Object is using a comparison statement.
Get-BitlockerVolume | Foreach-Object -Process {
$_.KeyProtector | Where-Object -Property RecoveryPassword |
Backup-BitlockerKeyProtector -MountPoint $_.MountPoint
}

I know there are already good answers here, but I feel one important question was not addressed. The question of what happens to $_ throughout the Foreach-Object {} block when there is nesting. I am going use ArcSet's example since that was the answer selected.
1,2 | % {
"$_ before second foreach"
'a','b' | % {
"$_ inside second foreach"
}
"$_ after second foreach"
}
1 before second foreach
a inside second foreach
b inside second foreach
1 after second foreach
2 before second foreach
a inside second foreach
b inside second foreach
2 after second foreach
Notice that $_ becomes the current object being processed by the code within the Foreach-Object {} blocks. When entering the second Foreach-Object block, $_ changes. When exiting the second Foreach-Object block, $_ changes back to the object that will be continued to be processed by the remainder of the first block. So $_ neither remains the same nor is lost during the block processing. You will need to either assign $_ as another variable or in applicable situations use the -PipelineVariable switch to access those objects within different blocks.

Id' like to build on ArcSet's answer just a little bit. Since I finally understood the value of the $PSItem is changing when the type changes down the pipeline, I ran this code to do a little check.
Get-BitLockerVolume | % {$_.GetType()}
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False BitLockerVolume System.Object
True False BitLockerVolume System.Object
True False BitLockerVolume System.Object
Here we can see some objects returned by the pipeline are of the BitLockerVolume type.
Now, based on my original question/example, if we pipe it further based on KeyProtector the object type will change for the $PSItem variable.
Get-BitLockerVolume | % { $_.KeyProtector | ? RecoveryPassword | % {$_.GetType()}}
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False BitLockerVolumeKeyProtector System.Object
True False BitLockerVolumeKeyProtector System.Object
So at this point, at the end of the pipeline, we execute some other cmdlet like Backup-BitlockerKeyProtector and reference the $PSItem variable, aka $_, then it will refer to the object types last passed through the pipeline, in this case, the objects would be of the BitLockerVolumeKeyProtector type.

Related

PowerShell: Grep through object string representation [duplicate]

Let's say you have a giant object - one which may or may not have nested arrays / objects,
# Assuming 'user1' exists in the current domain
$obj = Get-ADUser 'user1' -Properties *
and I want to search that object for the string SMTP case-insensitively...
What I tried
$obj | Select-String "SMTP"
But it does not work because the match is inside a nested Collection... to be concise, it sits inside the property $obj.proxyAddresses.
If I run $obj.proxyAddress.GetType() it returns:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ADPropertyValueCollection System.Collections.CollectionBase
What's the best way to go about this? I know you could loop through the properties and look for it manually using wildcard matching or .Contains(), but I'd prefer a built in solution.
Thus, it would be a grep for objects and not only strings.
Here's one solution. It can be very slow depending on what depth you search to; but a depth of 1 or 2 works well for your scenario:
function Find-ValueMatchingCondition {
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[PSObject]$InputObject
,
[Parameter(Mandatory = $true)]
[ScriptBlock]$Condition
,
[Parameter()]
[Int]$Depth = 10
,
[Parameter()]
[string]$Name = 'InputObject'
,
[Parameter()]
[System.Management.Automation.PSMemberTypes]$PropertyTypesToSearch = ([System.Management.Automation.PSMemberTypes]::Properties)
)
Process {
if ($InputObject -ne $null) {
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property #{Name=$Name;Value=$InputObject}
}
#also test children (regardless of whether we've found a match
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
}
}
}
Get-AdUser $env:username -Properties * `
| Find-ValueMatchingCondition -Condition {$_ -like '*SMTP*'} -Depth 2
Example Results:
Value Name
----- ----
smtp:SomeOne#myCompany.com InputObject.msExchShadowProxyAddresses
SMTP:some.one#myCompany.co.uk InputObject.msExchShadowProxyAddresses
smtp:username#myCompany.com InputObject.msExchShadowProxyAddresses
smtp:some.one#myCompany.mail.onmicrosoft.com InputObject.msExchShadowProxyAddresses
smtp:SomeOne#myCompany.com InputObject.proxyAddresses
SMTP:some.one#myCompany.co.uk InputObject.proxyAddresses
smtp:username#myCompany.com InputObject.proxyAddresses
smtp:some.one#myCompany.mail.onmicrosoft.com InputObject.proxyAddresses
SMTP:some.one#myCompany.mail.onmicrosoft.com InputObject.targetAddress
Explanation
Find-ValueMatchingCondition is a function which takes a given object (InputObject) and tests each of its properties against a given condition, recursively.
The function is divided into two parts. The first part is the testing of the input object itself against the condition:
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property #{Name=$Name;Value=$InputObject}
}
This says, where the value of $InputObject matches the given $Condition then return a new custom object with two properties; Name and Value. Name is the name of the input object (passed via the function's Name parameter), and Value is, as you'd expect, the object's value. If $InputObject is an array, each of the values in the array is assessed individually. The name of the root object passed in is defaulted as "InputObject"; but you can override this value to whatever you like when calling the function.
The second part of the function is where we handle recursion:
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
The If statement checks how deep we've gone into the original object (i.e. since each of an objects properties may have properties of their own, to a potentially infinite level (since properties may point back to the parent), it's best to limit how deep we can go. This is essentially the same purpose as the ConvertTo-Json's Depth parameter.
The If statement also checks the object's type. i.e. for most primitive types, that type holds the value, and we're not interested in their properties/methods (primitive types don't have any properties, but do have various methods, which may be scanned depending on $PropertyTypeToSearch). Likewise if we're looking for -Condition {$_ -eq 6} we wouldn't want all strings of length 6; so we don't want to drill down into the string's properties. This filter could likely be improved further to help ignore other types / we could alter the function to provide another optional script block parameter (e.g. $TypeCondition) to allow the caller to refine this to their needs at runtime.
After we've tested whether we want to drill down into this type's members, we then fetch a list of members. Here we can use the $PropertyTypesToSearch parameter to change what we search on. By default we're interested in members of type Property; but we may want to only scan those of type NoteProperty; especially if dealing with custom objects. See https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.psmembertypes?view=powershellsdk-1.1.0 for more info on the various options this provides.
Once we've selected what members/properties of the input object we wish to inspect, we fetch each in turn, ensure they're not null, then recurse (i.e. call Find-ValueMatchingCondition). In this recursion, we decrement $Depth by one (i.e. since we've already gone down 1 level & we stop at level 0), and pass the name of this member to the function's Name parameter.
Finally, for any returned values (i.e. the custom objects created by part 1 of the function, as outlined above), we prepend the $Name of our current InputObject to the name of the returned value, then return this amended object. This ensures that each object returned has a Name representing the full path from the root InputObject down to the member matching the condition, and gives the value which matched.
Note: This answer contains background information and offers a quick-and-dirty approach that requires no custom functionality.
For a more more thorough, systematic approach based on reflection via a custom function, see JohnLBevan's helpful answer.
Select-String operates on strings, and when it coerces an input object of a different type to a string, it essentially calls .ToString() on it, which often yields generic representations such as the mere type name and typically not an enumeration of the properties.
Note that an object's .ToString() representation is not the same as PowerShell's default output to the console, which is much richer.
If all you're looking for is to find a substring in the for-display string representation of an object, you can pipe to Out-String -Stream before piping to Select-String:
$obj | Out-String -Stream | Select-String "SMTP"
Out-String creates a string representation that is the same as what renders to the console by default (it uses PowerShell's output-formatting system); adding -Stream emits that representation line by line, whereas by default a single, multi-line string is emitted.
Note: Recent versions of PowerShell come with convenience function oss, which wraps Out-String -Stream:
$obj | oss | Select-String "SMTP"
Of course, this method will only work if the for-display representation actually shows the data of interest - see caveats below.
That said, searching in the for-display representations is arguably what Select-String should do by default - see GitHub issue #10726
Caveats:
If the formatted representation happens to be tabular and your search string is a property name, the value of interest may be on the next line.
You can address this by forcing a list-style display - where each property occupies a line of its own (both name and value) - as follows:
$obj | Format-List | Out-String -Stream | Select-String "SMTP"
If you anticipate multi-line property values, you can use Select-String's -Context parameter to include lines surrounding a match, such as -Context 0,1 to also output the line after a match.
If you know that the values of interest are in a collection-valued property, you can use $FormatEnumerationLimit = -1 to force listing of all elements (by default, only the first 4 elements are displayed).
Caveat: As of PowerShell Core 6.1.0, $FormatEnumerationLimit is only effective if set in the global scope - see this GitHub issue.
However, once you hit the need to set preference variable $FormatEnumerationLimit, it's time to consider the more thorough solution based on a custom function in John's answer.
Values may get truncated in the representation, because Out-String assumes a fixed line width; you can use -Width to change that, but be careful with large numbers, because tabular representations then use the full width for every output line.

Powershell eq operator saying hashes are different, while Write-Host is showing the opposite

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.

How to search an object for a value?

Let's say you have a giant object - one which may or may not have nested arrays / objects,
# Assuming 'user1' exists in the current domain
$obj = Get-ADUser 'user1' -Properties *
and I want to search that object for the string SMTP case-insensitively...
What I tried
$obj | Select-String "SMTP"
But it does not work because the match is inside a nested Collection... to be concise, it sits inside the property $obj.proxyAddresses.
If I run $obj.proxyAddress.GetType() it returns:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ADPropertyValueCollection System.Collections.CollectionBase
What's the best way to go about this? I know you could loop through the properties and look for it manually using wildcard matching or .Contains(), but I'd prefer a built in solution.
Thus, it would be a grep for objects and not only strings.
Here's one solution. It can be very slow depending on what depth you search to; but a depth of 1 or 2 works well for your scenario:
function Find-ValueMatchingCondition {
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[PSObject]$InputObject
,
[Parameter(Mandatory = $true)]
[ScriptBlock]$Condition
,
[Parameter()]
[Int]$Depth = 10
,
[Parameter()]
[string]$Name = 'InputObject'
,
[Parameter()]
[System.Management.Automation.PSMemberTypes]$PropertyTypesToSearch = ([System.Management.Automation.PSMemberTypes]::Properties)
)
Process {
if ($InputObject -ne $null) {
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property #{Name=$Name;Value=$InputObject}
}
#also test children (regardless of whether we've found a match
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
}
}
}
Get-AdUser $env:username -Properties * `
| Find-ValueMatchingCondition -Condition {$_ -like '*SMTP*'} -Depth 2
Example Results:
Value Name
----- ----
smtp:SomeOne#myCompany.com InputObject.msExchShadowProxyAddresses
SMTP:some.one#myCompany.co.uk InputObject.msExchShadowProxyAddresses
smtp:username#myCompany.com InputObject.msExchShadowProxyAddresses
smtp:some.one#myCompany.mail.onmicrosoft.com InputObject.msExchShadowProxyAddresses
smtp:SomeOne#myCompany.com InputObject.proxyAddresses
SMTP:some.one#myCompany.co.uk InputObject.proxyAddresses
smtp:username#myCompany.com InputObject.proxyAddresses
smtp:some.one#myCompany.mail.onmicrosoft.com InputObject.proxyAddresses
SMTP:some.one#myCompany.mail.onmicrosoft.com InputObject.targetAddress
Explanation
Find-ValueMatchingCondition is a function which takes a given object (InputObject) and tests each of its properties against a given condition, recursively.
The function is divided into two parts. The first part is the testing of the input object itself against the condition:
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property #{Name=$Name;Value=$InputObject}
}
This says, where the value of $InputObject matches the given $Condition then return a new custom object with two properties; Name and Value. Name is the name of the input object (passed via the function's Name parameter), and Value is, as you'd expect, the object's value. If $InputObject is an array, each of the values in the array is assessed individually. The name of the root object passed in is defaulted as "InputObject"; but you can override this value to whatever you like when calling the function.
The second part of the function is where we handle recursion:
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
The If statement checks how deep we've gone into the original object (i.e. since each of an objects properties may have properties of their own, to a potentially infinite level (since properties may point back to the parent), it's best to limit how deep we can go. This is essentially the same purpose as the ConvertTo-Json's Depth parameter.
The If statement also checks the object's type. i.e. for most primitive types, that type holds the value, and we're not interested in their properties/methods (primitive types don't have any properties, but do have various methods, which may be scanned depending on $PropertyTypeToSearch). Likewise if we're looking for -Condition {$_ -eq 6} we wouldn't want all strings of length 6; so we don't want to drill down into the string's properties. This filter could likely be improved further to help ignore other types / we could alter the function to provide another optional script block parameter (e.g. $TypeCondition) to allow the caller to refine this to their needs at runtime.
After we've tested whether we want to drill down into this type's members, we then fetch a list of members. Here we can use the $PropertyTypesToSearch parameter to change what we search on. By default we're interested in members of type Property; but we may want to only scan those of type NoteProperty; especially if dealing with custom objects. See https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.psmembertypes?view=powershellsdk-1.1.0 for more info on the various options this provides.
Once we've selected what members/properties of the input object we wish to inspect, we fetch each in turn, ensure they're not null, then recurse (i.e. call Find-ValueMatchingCondition). In this recursion, we decrement $Depth by one (i.e. since we've already gone down 1 level & we stop at level 0), and pass the name of this member to the function's Name parameter.
Finally, for any returned values (i.e. the custom objects created by part 1 of the function, as outlined above), we prepend the $Name of our current InputObject to the name of the returned value, then return this amended object. This ensures that each object returned has a Name representing the full path from the root InputObject down to the member matching the condition, and gives the value which matched.
Note: This answer contains background information and offers a quick-and-dirty approach that requires no custom functionality.
For a more more thorough, systematic approach based on reflection via a custom function, see JohnLBevan's helpful answer.
Select-String operates on strings, and when it coerces an input object of a different type to a string, it essentially calls .ToString() on it, which often yields generic representations such as the mere type name and typically not an enumeration of the properties.
Note that an object's .ToString() representation is not the same as PowerShell's default output to the console, which is much richer.
If all you're looking for is to find a substring in the for-display string representation of an object, you can pipe to Out-String -Stream before piping to Select-String:
$obj | Out-String -Stream | Select-String "SMTP"
Out-String creates a string representation that is the same as what renders to the console by default (it uses PowerShell's output-formatting system); adding -Stream emits that representation line by line, whereas by default a single, multi-line string is emitted.
Note: Recent versions of PowerShell come with convenience function oss, which wraps Out-String -Stream:
$obj | oss | Select-String "SMTP"
Of course, this method will only work if the for-display representation actually shows the data of interest - see caveats below.
That said, searching in the for-display representations is arguably what Select-String should do by default - see GitHub issue #10726
Caveats:
If the formatted representation happens to be tabular and your search string is a property name, the value of interest may be on the next line.
You can address this by forcing a list-style display - where each property occupies a line of its own (both name and value) - as follows:
$obj | Format-List | Out-String -Stream | Select-String "SMTP"
If you anticipate multi-line property values, you can use Select-String's -Context parameter to include lines surrounding a match, such as -Context 0,1 to also output the line after a match.
If you know that the values of interest are in a collection-valued property, you can use $FormatEnumerationLimit = -1 to force listing of all elements (by default, only the first 4 elements are displayed).
Caveat: As of PowerShell Core 6.1.0, $FormatEnumerationLimit is only effective if set in the global scope - see this GitHub issue.
However, once you hit the need to set preference variable $FormatEnumerationLimit, it's time to consider the more thorough solution based on a custom function in John's answer.
Values may get truncated in the representation, because Out-String assumes a fixed line width; you can use -Width to change that, but be careful with large numbers, because tabular representations then use the full width for every output line.

How to refer to entire pipeline object without a script block

With the advent of PowerShell V3 instead of having to write:
Get-Process | Where { $_.ProcessName -match "win" }
...one could now write the more terse:
Get-Process | Where ProcessName -match "win"
... a clear win (ahem) for shell use.
Now let's say I had a simple array of strings, call it $stuff. Is it possible to reduce this:
$stuff | Where { $_ -match "win" }
...in an analogous way to the first example, i.e. removing the script block and referring to the entire object, in this case?
To reduce
$stuff | Where { $_ -match "win" }
you can always do it like this (works for all powershell version):
$stuff = "a","b","win", "winona", "d", "windows"
$stuff -match "win"
win
winona
windows
The form Get-Process | Where ProcessName -match "win" is called comparison statement. It's poorly a documented feature, as Where-Object's documentation doesn't really explain what those are about.
The reason comparison statement works for Get-Process but not for $stuff is that the former returns an array of System.Diagnostics.Process objects whlist the later is String. The comparison statements expects property name for filtering.
Let's look at what's available in each array member. First off processes like so,
$proc = get-process
gm -InputObject $proc[0] -MemberType property
TypeName: System.Diagnostics.Process
Name MemberType Definition
...
ProcessName Property string ProcessName {get;}
...
So, there is a ProcessName property, thus Where-Object can filter with it as seen.
The string array:
$stuff = #("foo", "bar", "zoffo", "qazzer")
gm -InputObject $stuff[0] -MemberType property
TypeName: System.String
Name MemberType Definition
Length Property int Length {get;}
There is only one property in a String which is it's length. Sure enough, it can be used for filtering like so,
$stuff | where length -ne 3
zoffo
qazzer
$stuff | where length -eq 3
foo
bar
As there are no other property typed members, the only way to filter the string array is by the classic script block mode.

Powershell - When an object is placed on the pipeline, can it be used again later?

I have a collection of objects which I am trying to return multiple sets of data from. Each time I want to select one of the properties of the object and sort it uniquely.
The problem I'm getting is that I only get results the first time I use the collection. This leads me to wonder whether the objects are being "consumed" when I put them on the pipeline.
To illustrate with an example:
$results = getMyCollectionOfObjects
$results | select-object property1 | sort-object -unique property1
$results | select-object property2 | sort-object -unique property2
$results | select-object property3 | sort-object -unique property3
As far as I can tell this should result in 3 distinct lists of all the possible values for each of the three properties.
However for me it's like the $results value is being "used up" the first time it is selected from.
Is this the case and what should I do instead?
Hope this is clear.
That's how it rolls... you have to explicitly pipe them to Out-Default to get rid of that odd behavior. Otherwise it will try to display property1 for 2nd and 3rd set too. You removed it from $results, so it comes back blank.
HTH
Bartek
So this had me tripped up and scratching my head or a minute as well. The answer it turns out is really quite simple: the Select-Object cmdlet returns an object of type Selected.System.Management.Automation.PSCustomObject. That is then passed down the pipeline to following two selects, but since there are no longer any matching properties (they were discarded from the first select) - nothing is output. Consider the following example:
# declare an array and populate it
$results = #()
$results = $results + (New-Object PSobject -Property #{
P1 = "One"
P2 = "Two"
P3 = "Three"
})
$results = $results + (New-Object PSobject -Property #{
P1 = "Uno"
P2 = "Dos"
P3 = "Tres"
})
$results | select P1
$results | select P2
$results | select P3
As you described, I was only getting output from the first select. I them took the suggestion from BartekB to put | Out-Default at the end of each line and it started working. I investigated further by replacing that with | Get-Member to view the object the was being placed on the pipeline:
$results | select -Property P1 | get-member
TypeName: Selected.System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
P1 NoteProperty System.String P1=One
In a nutshell, the Out-Default is required to actually force it to display on the console instead of passing it along to the next statement. It would seem this behavior is implied upon completing a statement in the interactive shell, but behaves a bit differently when fully scripted.