1

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.

2
  • 3
    Don't use += to add to an array (which you haven't even declared). Use List objects that have an .Add() method. Also applies to PowerShell 5.1 Commented Aug 13, 2022 at 21:13
  • 1
    Scriptblocks in a foreach-object -parallel are executed in isolated runspaces - see this question for how to modify variables in the main script’s scope… stackoverflow.com/questions/67570734/… Commented Aug 13, 2022 at 21:20

2 Answers 2

4

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.
Sign up to request clarification or add additional context in comments.

Comments

2

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.