'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 foreach or ForEach-Object loop, 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.Concurrent namespace 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 a for loop 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.'
})
  • foreach is 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 foreach loop, 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 works
      
    • You can also wrap foreach in a ScriptBlock if 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 (ScriptBlocks have their own scope), so make sure you understand how ScriptBlocks work before using this trick.

  • ForEach-Object is 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
      -Parallel flag 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 foreach is piped to (e.g. 1, 2, 3 | foreach { }), it is processed as an alias to ForEach-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 to ForEach-Object instead. This is because ForEach-Object doesn't actually loop over the collection itself, the pipeline will unroll the collection and run ForEach-Object once for every item in the collection. If you take the pipeline out of the mix, ForEach-Object will "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.ForEach is 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 -Parallel for 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 the Array.ForEach and Array.Where intrinsic methods.
      • Interestingly, unlike other intrinsic members neither .Array or .ForEach show up as members when an object is piped to Get-Member -Force, which is supposed to show intrinsic members and compiler-generated members.

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