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.
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 thatTest-Connection
outputs a[bool]
value to be stored in theOnline
property. .Where{…}
is an intrinsic method that PowerShell provides for all objects. Similarly toWhere-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 theHost
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.