'PowerShell : Changing the value of an object in an array
I often consult this site but today is a big day as I am asking my first question!
I regularly code in PowerShell but I'm stuck on one point. To explain it to you I will take an example.
Let's imagine that I create a table in which I enter the result of 2 Get-ADComputer:
$tab = @()
$tab += Get-ADComputer PORT-CAEN-15 | Select-Object Name, Test
$tab += Get-ADComputer PORT-CAEN-24 | Select-Object Name, Test
The result of the table is the following :
Name Test
---- ----
PORT-CAEN-15 {}
PORT-CAEN-24 {}
How can I modify the "Test" property for each computer ?
Thank you for your answer and don't hesitate to tell me if I'm not using the right terms.
Solution 1:[1]
Use a loop. The easiest will be one of the foreach variants:
Warning: While you can change properties and run methods on items in a
foreachorForEach-Objectloop, avoid reassigning the item to a new value. In other .NET languages like C#, assigning a new value like$_ = 'new string'will throw an exception. With PowerShell, no exception occurs but the new value will not persist outside of that loop iteration.This warning still applies to concurrent (thread-safe) collections. These are special collection types in the
System.Collections.Concurrentnamespace which normally are not subject to this limitation, but the same behavior occurs in PowerShell and new values are not retained when the next iteration begins. I recommend aforloop in such a scenario where you would modify the collection itself.
# We can pipe the computer names to Get-ADComputer instead of concatenating arrays
$computers =
'PORT-CAEN-15',
'PORT-CAEN-24' | Get-ADComputer
# foreach is concise and fast, but can't send results down the pipeline by default
foreach( $computer in $computers ) {
$computer.Test = 'Whatever should go here, can be an object, string, etc.'
}
# More flexible than the foreach keyword, but this is the slowest variant. You can
# send the output down the pipeline though, which can be a huge boon
$computers | Foreach-Object {
$_.Test = 'Same concept, different loop. $_ and $PSItem reference the current element'
}
# Faster than ForEach-Object and allows you do pass data down the
# pipeline without using special tricks
$computers.ForEach({
$_.Test = '$_ and $PSItem have the same meaning here as well.'
})
foreachis a keyword and lets you loop over each element in the array, giving each iteration the variable name you choose.You cannot pipe data into a
foreachloop, but due to its nature you really wouldn't need to.A downside of this is that you also cannot send results further down the pipeline, but you can wrap the loop with the sub-expression operator
$()to make it work anyway (this trick does not work if you use the group-expression operator()):$(foreach( $var in 1..10 ) { "Value is $_" }) | Write-Warning # Simple pipeline demonstration showing it worksYou can also wrap
foreachin aScriptBlockif you want to stream the data and not wait for the loop to finish for the output to return, and execute with the call operator& {LOOP HERE}. However, you might run into some issues with referencing your defined variables (ScriptBlockshave their own scope), so make sure you understand howScriptBlockswork before using this trick.
ForEach-Objectis a cmdlet and also lets you loop over every item in an array.This is the slowest variant, but this is also the only one that (in PowerShell 7+) grants a
-Parallelflag in order to iterate over your loop in-parallel, which can increase processing performance. Unfortunately, there is not an equivalent parameter for earlier versions.When
foreachis piped to (e.g.1, 2, 3 | foreach { }), it is processed as an alias toForEach-Object. This is a weird design decision and can be confusing so watch out for it.Do not provide an array directly as an argument to
-InputObject. Pipe the collection toForEach-Objectinstead. This is becauseForEach-Objectdoesn't actually loop over the collection itself, the pipeline will unroll the collection and runForEach-Objectonce for every item in the collection. If you take the pipeline out of the mix,ForEach-Objectwill "iterate" once for the array object you passed in, and will not execute for each element inside of the array.You can see this in action like so:
# Define array $array = 1, 2, 3, "four", 5 # Correct. Loops once for each element in the array $array | ForEach-Object { "Value is $_" } # Incorrect. The array is not iterated over, # only the array object itself is processed ForEach-Object -InputObject $array { "Value is $_" }
Array.ForEachis an intrinsic method that is added to all PowerShell objects. It is both fast to execute and allows you to pass the returned data down the pipeline.- There is no equivalent of
-Parallelfor this so parallel execution would need to be implemented by you. IMO this is far from a dealbreaker. - This looks very similar to the
Array.ForEach<T>(T[], Action)definition, except there are additional overloads and none of are generic. See this page for more information about theArray.ForEachandArray.Whereintrinsic methods.- Interestingly, unlike other intrinsic members neither
.Arrayor.ForEachshow up as members when an object is piped toGet-Member -Force, which is supposed to show intrinsic members and compiler-generated members.
- Interestingly, unlike other intrinsic members neither
- There is no equivalent of
Solution 2:[2]
There are lots of ways to do that. A straightforward approach would be to simply loop the results afterward:
$tab = @()
$tab += Get-ADComputer PORT-CAEN-15 | Select-Object Name, Test
$tab += Get-ADComputer PORT-CAEN-24 | Select-Object Name, Test
ForEach($Computer in $tab)
{
$Computer.Test = "New Value..."
}
$tab
Results should be:
Name Test
---- ----
PYEXADM1 New Value...
PXEXADM1 New Value...
However, there really isn't an immediate need to use a post-loop. Consider that Test is not normal property returned by Get-ADComputer, what you're really doing is returning [PSCustomObject]'s inheriting the Name property from the AD computer object, but adding the custom property "Test". So instead of looping after the fact, we can simply expand that into a calculated property.
'PORT-CAEN-15', 'PORT-CAEN-24' |
Get-ADComputer |
Select-Object Name, @{Name = 'Test'; Expression = { $_.DNSHostName }}
This example does away with the use of the += operator. That's generally a best practice because it's a lot slower than letting PowerShell accumulate the results for you. After that a hash table is provided that defines the name of the new property and an expression whose return value will be the value of the new property.
Note: The expression can be anything, it doesn't even have to relate back to the object that was passed into Select-Object, I used $_.DNSHostName just to demonstrate how to reference back to the piped object.
Moreover, and to cite the piping concerns from Bender the Greatest's nice answer, you can continue piping after the Select-Object see below scaffolding:
'PORT-CAEN-15', 'PORT-CAEN-24' |
Get-ADComputer |
Select-Object Name, @{Name = 'Test'; Expression = { $_.DNSHostName }} |
ForEach-Object {
# Some other commands...
}
Note: ForEach-Object is just an example above. anything that will take those objects as input can be put there...
Solution 3:[3]
Using Select-Object you can add so-called calculated properties to the object:
Select-Object Name,
@{Name = 'Test'; Expression = {'Whatever you want here'}}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | Theo |
