I am attempting to put the output of compare-object. I am new to Powershell and unfortunately don't know the ins and outs yet.
My command is as follows:
Compare-Object -referenceObject $(Get-Content "c:\temp\mek\123-first.txt") -differenceObject $(Get-Content "c:\temp\mek\123-second.txt") | %{$_.Inputobject} | sort-object | out-file "c:\temp\mek\results.txt"
The contents of my files are as follows (simply comparing Windows services):
systemname name state startmode
---------- ---- ----- ---------
D7MCYP AdobeARMservice Stopped Auto
D7MCYP AdobeFlashPlayerUpdateSvc Stopped Manual
D7MCYP AeLookupSvc Stopped Manual
My results of the compare-object are as follows:
BL3C4V wudfsvc Stopped Auto
BL3C4V wudfsvc Stopped Manual
D7MCYP AdobeARMservice Running Auto
D7MCYP AdobeARMservice Stopped Auto
Now if anyone could help output to keep the first 2 columns per server and the different values of columns 3,4 to new columns (5,6). It would also be nice if I get titles too. For example:
Server Service Before State Before Mode After State After Mode
BL3C4V wudfsvc Stopped Auto Stopped Manual
D7MCYP AdobeARMservice Running Auto Stopped Auto
Note: The code below is an exercise in parsing plain-text data into objects for more robust, flexible handling.
Ideally, however, processing should start out with objects rather than plain text,
which is why starting with PowerShell cmdlets such as Get-Service rather than the text output from external utilities is preferable.
Assuming that all entries in each input file have a matching server + service-name entry in the respective other file:
$f1, $f2 = "c:\temp\mek\123-first.txt", "c:\temp\mek\123-second.txt"
Compare-Object (Get-Content $f1) (Get-Content $f2) | ForEach-Object {
$i = 0; $ht = #{}; $vals = -split $_.InputObject
foreach($col in 'Server', 'Service', 'State', 'Mode') {
$ht.$col = $vals[$i++]
}
$ht.Before = $_.SideIndicator -eq '<='
[pscustomobject] $ht
} | Group-Object Server, Service | ForEach-Object {
$ndxBefore, $ndxAfter = if ($_.Before) { 0, 1 } else { 1, 0 }
[pscustomobject] #{
Server = $_.Group[0].Server
Service = $_.Group[0].Service
'State Before' = $_.Group[$ndxBefore].State
'Mode Before' = $_.Group[$ndxBefore].Mode
'State After' = $_.Group[$ndxAfter].State
'Mode After' = $_.Group[$ndxAfter].Mode
}
} | Sort-Object Server, Service |
Format-Table
Note:
The above formats the output for display (using Format-Table), without sending it to a file.
You can append | Out-File "c:\temp\mek\results.txt" to save the same representation to a file.
However, note that the command - before Format-Table is applied - returns objects with individual properties, so you can output to a file in a variety of formats, such as by using Export-Csv, for instance.
Example output:
Server Service State Before Mode Before State After Mode After
------ ------- ------------ ----------- ----------- ----------
D7MCYP AdobeFlashPlayerUpdateSvc Stopped Manual Stopped Auto
D7MCYP AeLookupSvc Stopped Manual Started Manual
Explanation:
A single, long pipeline is used, which makes the code concise and memory-efficient.
The pipeline breaks down as follows:
Comparison:
Compare-Object compares the array of lines from the two input files returned by the Get-Content calls, and outputs [pscustomobject] instances representing the differences found, with string property .SideIndicator indicating whether the line at hand (accessible via .InputObject) is unique to the LHS (the 1st input file) - <= - or the RHS (2nd input file) - >=
Transformation to custom objects:
The script block ({ ... }) passed to ForEach-Object is executed for each input object (represented as $_).
-split $_.InputObject splits the "difference line" at hand into fields by runs of whitespace and stores the resulting fields as an array in $vals.
$ht is an auxiliary hashtable that is used to map the field values to field names.
$ht.Before adds a Boolean entry to indicate whether the difference line at hand is from the "before file" (the 1st input file) or not.
[pscustomobject] $ht converts the aux. hashtable into a custom object and outputs it (sends it through the pipeline).
Grouping:
Group-Object is used to group the resulting objects can by shared Server and Service property values, resulting in a [Microsoft.PowerShell.Commands.GroupInfo] instance representing each grouping.
Transformation to combined custom objects:
Again, ForEach-Object is used to perform per-input object processing.
[pscustomobject] #{ ... } is used to construct each combined output object, again using an auxiliary hashtable.
$_.Group contains the input objects that form each group - in our case, $_.Group[0] and $_.Group[1] are the converted-to-objects input lines representing a given server-service combination.
By definition, both input objects have the same .Server and .Service values, so blindly using $_.Group[0]'s values for the combined output object will do.
By contrast, the * Before and * After properties the appropriate input object (whether from the 1st or 2nd file), which is why the array indices $ndxBefore and $ndxAfter are chosen accordingly, via the previously added .Before property
Sorting:
Sort-Object sorts the resulting objects by the specified properties.
Output formatting:
Output-formatting cmdlet Format-Table ensures that the sorted objects are presented as a table.
Related
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.
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 am using hashtables in powershell for multiple entries my hastable is working fine, but for the single entry its throwing the error by not picking the value it meant to be. Suppose I am having a Line
Aman;Indore;789858585;23
Raman;Delhi;785458545;35
for this two entry, my hashtable is working fine when i am giving command for example userinput.name[0] so it will pick Aman as the value and user.input.name[1] then it picks Raman as the value.
But the same code with single entry of
Aman;Indore;789858585;23
when I am giving userinput.name[0] so it is picking up A. The first letter of Aman it's picking instead of the complete name.
You might not even realise it, but you're using a PowerShell feature called Member Enumeration which was introduced in PowerShell 3.0.
Basically, if you attempt to access a named property on an array, if the property doesn't exist on the array object itself then PowerShell will look for that property on all the items in the array and return those values in a new array instead.
For example:
PS> $csv = #"
Aman;Indore;789858585;23
Raman;Delhi;785458545;35
"#
PS> $data = $csv | ConvertFrom-Csv -Delimiter ";" -Header #("Name", "Location", "Mobile", "Age");
PS> $data
Name Location Mobile Age
---- -------- ------ ---
Aman Indore 789858585 23
Raman Delhi 785458545 35
PS> $data.Name
Aman
Raman
# note this returns an array of objects thanks to member enumeration
PS> $data.Name.GetType().FullName
System.Object[]
PS> $data.Name[0]
Aman
In your case, $data.Name returns a new array containing the value of the Name property on all the items in $data - effectively #("Aman", "Raman"). So, when you use $data.Name[0], you're retrieving the first item in the array created by member enumeration - i.e. Aman - and all's well in the world...
Now, the catch is that if there's only one item in the array created by member enumeration it gets "unrolled" to be the value of the first item:
PS> $csv = #"
Aman;Indore;789858585;23
"#
PS> $data = $csv | ConvertFrom-Csv -Delimiter ";" -Header #("Name", "Location", "Mobile", "Age");
PS> $data
# Name Location Mobile Age
# ---- -------- ------ ---
# Aman Indore 789858585 23
# note this returns a single string because the "member enumeration" array
# gets unrolled if there's only one item
PS> $data.Name.GetType().FullName
System.String
PS> $data.Name
# Aman
PS> $data.Name[0]
# A
And in your case the $data.Name[0] is equivalent to "Aman"[0] which returns A.
To fix this, rather than inadvertently use member enumeration by doing $data.Name[0], where the result can vary based on the number of items in the array, you can use one of the following:
PS> $data[0].Name
Aman
PS> #($data.Name)[0]
Aman
The first option is probably more performant in the general case, but the second is a useful workaround in some cases where the first won't work (e.g. when dealing with return values from functions / where-object, etc).
Given the following input XML sample -- assume multiple LogMessage entries.
<LogMessages>
<LogMessage time="2017-12-08 11:44:05.202" messageID="A10">
<![CDATA[Long non-xml string here containing <TS "2017120811431218"> somewhere in the body"]>
</LogMessage>
</LogMessages>
I am using the following code to capture the values of attributes time, messageID, and capture a group in the CDATA.
[xml]$xml = Get-Content input.xml
$xml.LogMessages.LogMessage | Where-Object {$_.messageID -eq "A10"} | Select-Object -Property time,messageID,#{Name="A10Timestamp"; Expression=$_."#cdata-section" -match '<TS "(?<group>[0-9]{16})">' | Select-Object $Matches['group'] }} `
| Export-Csv output.csv -NoTypeInformation
Output looks like:
time messageID Group
---- --------- ---------------
2017-12-08 11:43:12.183 S6F1 #{2017120811431218=}
The #{ and } wrapping the captured group value is undesired. I am concerned about this particular use of the $Matches variable...I think what gets printed is a Match object and not the group string that it matched on... or something like this.
What is going on and how do I get the entries in the Group column appear as 2017120811431218 and not #{2017120811431218=}?
The Match operator returns a boolean and populates the $Matches variable with the matching results.
In other words, you should void what is returned by the -Match operator (and not pipe it) and than simply return the $Matches['group'] to the Expression:
Expression={$Void = $_."#cdata-section" -match '<TS "(?<group>[0-9]{16})">'; $Matches['group']}
I am working on simple reporting one-liner and I can't get it to work.
I am retrieving the ESXi hosts by cmdlet Get-VMHost. Get-VMHost is piped into Get-VMHostSysLogServer.
As the output I get the Host and Port properties. I would like to display the following properties:
Name (from Get-VMHost)
Host and Port (both from Get-VMHostSysLogServer).
How can I achieve this?
Here's one solution, that builds a custom object with the properties that you want:
Get-VMHost | ForEach-Object {
$SysLog = $_ | Get-VMHostSysLogServer
ForEach ($SysLogServer in $SysLog) {
$Result = #{
VMHost = $_.name
SysLogHost = $SysLogServer.Host
Port = $SysLogServer.Port
}
New-Object -TypeName PSObject -Property $Result
}
}
Explanation:
Uses a ForEach-Object loop to iterate through each VM Host returned by Get-VMHost (represented as the current pipeline item via $_).
Gets the SysLogServers of each Host in to $SysLog
Loops through all SysLogServers, building a hashtable #{ } of the desired properties.
Uses New-Object to output an object using the properties in the Hashtable which is returned to the Pipeline.
If you'd like to capture the results to a variable, just add $YouVar = before Get-VMHost.
If you want to send on the results to another cmdlet that accepts pipeline input (such as Export-CSV) you can do that directly as the end, just append | Export-CSV Your.csv after the last closing }. This is the benefit of using ForEach-Object as the outer loop, it supports the pipeline.