Home > Mobile >  How to return a list from ForEach-Object in powershell 7.2.1
How to return a list from ForEach-Object in powershell 7.2.1

Time:08-14

I have a library of Powershell 5.1 scripts and I want to rewrite some of them in Powershell 7.2.1. Mainly because of the new parallel execution of ForEach-Object.

Here is simplified example of script written in Powershell 5.1 that Test-Connection ForEach-Object in $computers list and add pc either to $OnlinePc list or $OfflinePc list.

$color = "Gray"
$Major = ($PSVersionTable.PSVersion).Major
$Minor = ($PSVersionTable.PSVersion).Minor
Write-Host "My Powershell version: " -NoNewline -ForegroundColor $color
Write-Host "$Major.$Minor"
Write-Host

$computers = @(
    '172.30.79.31',
    '172.30.79.32',
    '172.30.79.33',
    '172.30.79.34',
    '172.30.79.35',
    '172.30.79.36',
    '172.30.79.37'
)

Write-Host "List of all computers:" -ForegroundColor $color
$computers

foreach ($pc in $computers) {
    if (Test-Connection -Count 1 $pc -ErrorAction SilentlyContinue) {
        $OnlinePc =$pc
    }
    else {
        $OfflinePc =$pc
    }
}

Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$OnlinePc

Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$OfflinePc

Write-Host
pause

And here is same script rewtitten in Powershell 7.2.1

$color = "Gray"
$Major = ($PSVersionTable.PSVersion).Major
$Minor = ($PSVersionTable.PSVersion).Minor
$Patch = ($PSVersionTable.PSVersion).Patch
Write-Host "My Powershell version: " -NoNewline -ForegroundColor $color
Write-Host "$Major.$Minor.$Patch"
Write-Host

$computers = @(
    '172.30.79.31',
    '172.30.79.32',
    '172.30.79.33',
    '172.30.79.34',
    '172.30.79.35',
    '172.30.79.36',
    '172.30.79.37'
)

Write-Host "List of all computers:" -ForegroundColor $color
$computers

$computers | ForEach-Object -Parallel {
    if (Test-Connection -Count 1 $_ -ErrorAction SilentlyContinue) {
        $OnlinePc =$_
    }
    else {
        $OfflinePc =$_
    }
}

Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$OnlinePc

Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$OfflinePc

Write-Host
pause

Here is picture of output from both scripts.

Outputs

I tried to edit the ForEach-Object syntax in many ways, but I can't get it to work the same way it worked in 5.1, any tips would be apperacited.

PS: Computers 172.30.79.32 and 172.30.79.33 are offline, the others are online.

CodePudding user response:

As commenters noted, script blocks in a ForEach-Object -Parallel don't have direct access to surrounding variables as they run in isolated runspaces.

While you could use the $using keyword to work around this situation (as show in this QA), a more idiomatic approach is to capture the output of ForEach-Object in a variable. This automatically produces an array if more than one objects are output. By storing the online state in a property, we can later split that array to get two separate lists for online and offline PCs.

$computerState = $computers | ForEach-Object -Parallel {
    [pscustomobject]@{
        Host = $_
        Online = Test-Connection -Count 1 $_ -ErrorAction SilentlyContinue -Quiet
    }
}

Write-Host
Write-Host "List of online computers:" -ForegroundColor $color
$computerState.Where{ $_.Online -eq $true }.Host

Write-Host
Write-Host "List of offline computers:" -ForegroundColor $color
$computerState.Where{ $_.Online -eq $false }.Host

Notes:

  • [pscustomobject]@{…} dynamically creates an object and implicitly outputs it, which gets captured in $computerState, automatically creating an array if necessary. This is more efficient than using the array = operator, which has to reallocate and copy the array elements for each new element to be added, because arrays are actually of fixed size (more info).
  • Parameter -Quiet is used so that Test-Connection outputs a [bool] value to be stored in the Online property.
  • .Where{…} is an intrinsic method that PowerShell provides for all objects. Similarly to Where-Object it acts as a filter, but is faster and the syntax is more succinct.
  • Finally by writing .Host we make use of PowerShell's convenient member access enumeration feature, which creates an array from the Host property of the filtered $computerState items.

CodePudding user response:

When using ForEach-Object -Parallel each object is processed in a different runspace and thread (as mentioned by mclayton in a comment). Variables are not accessible across runspaces in most cases. Below are 2 possible alternative ways to use -Parallel

The following code does not assign or use any variables inside the Foreach-Object -Parallel block. Instead online computers are assigned to a variable outside.

$computers = @(
    'localhost',
    'www.google.com',
    'notarealcomputer',
    'www.bing.com',
    'www.notarealwebsitereally.com'
)

# Assign the output of foreach-object command directly into a variable rather than assigning variables inside
$onlinePc = $computers | ForEach-Object -Parallel {
    $_ | Where-Object { Test-Connection -Count 1 -Quiet $_ -ErrorAction SilentlyContinue }
}

# determine offline computers by checking which are not in $onlinePc
$offlinePc = $computers | Where-Object { $_ -notin $onlinePc }

Write-Host '--- Online ---' -ForegroundColor Green
$onlinePc
Write-Host
Write-Host '--- Offline ---' -ForegroundColor Red
$offlinePc

This next method uses a synchronized hashtable and the $using statement to collect the data inside the -Parallel block

$computers = @(
    'localhost',
    'www.google.com',
    'notarealcomputer',
    'www.bing.com',
    'www.notarealwebsitereally.com'
)

# Create a hashtable containing empty online and offline lists
$results = @{
    'online'  = [System.Collections.Generic.List[string]]::new()
    'offline' = [System.Collections.Generic.List[string]]::new()
}

# thread-safe wrapped hashtable
$syncResults = [hashtable]::Synchronized($results)

$computers | ForEach-Object -Parallel {
    if ($_ | Test-Connection -Quiet -Count 2 -ErrorAction SilentlyContinue) {
        ($using:syncResults).online.add($_)
    }
    else {
        ($using:syncResults).offline.add($_)
    }
}

$results

Output

Name                           Value
----                           -----
online                         {localhost, www.bing.com, www.google.com}
offline                        {www.notarealwebsitereally.com, notarealcomputer}

I do not claim to be an expert. There are likely better ways to do this. Just thought I'd share a couple that I know.

  • Related