Windows PowerShell 5.1, the version that’s built into Windows, shipped back in 2016 with the release of Windows 10 1607 and Windows Server 2016, with support for older OSes back to Windows 7 and Windows Server 2008. That was almost eight years ago. What has changed with PowerShell 5.1 since then? While there are more PowerShell modules and scripts available, the PowerShell 5.1 engine itself is pretty much stuck in time, with no significant changes in those eight years. That doesn’t mean PowerShell itself hasn’t continued to advance, with a number of releases of the new .NET Core-based, cross-platform PowerShell adding lots of improvements, all the way up to today’s PowerShell 7.4 release.
But even with those improvements, there’s still a lot of places where using anything beyond PowerShell 5.1 is harder than it should be. Examples:
- Intune platform scripts, which only support PowerShell 5.1.
- Intune remediation scripts, which also only support PowerShell 5.1.
- Intune app requirement scripts, which only support PowerShell 5.1.
- ConfigMgr scripts (in the software library, which can be deployed to computers or collections), which only support PowerShell 5.1.
- ConfigMgr app detection scripts, which only support PowerShell 5.1.
- Group policy scripts for logon, logoff, startup, and shutdown, which only support PowerShell 5.1.
- ConfigMgr OS deployment “Run PowerShell script” steps, which only support PowerShell 5.1.
- MDT “Run PowerShell script” steps, which only support PowerShell 5.1.
See the theme there? So why do these only support PowerShell 5.1 and not the newer releases? There are likely a few reasons:
- IT people aren’t asking for support for later versions of PowerShell, because they aren’t familiar with the later versions, because the products don’t support later versions.
- Later versions of PowerShell are not (and likely will never be, due to support lifecycle misalignments) preinstalled in Windows, which means there’s a bootstrapping problem: you need to get the later version installed before you can use it.
So how do we solve those issues? A good first step would be to work around the product limitations so that you can use PowerShell 7 directly, and maybe at some point in the future Microsoft will add support for this other Microsoft technology (radical though, I know).
We’re somewhat used to these workarounds, as we’ve been forced to do things like this over the years, e.g. to run 64-bit PowerShell scripts from 32-bit agents. That required small modifications to PowerShell scripts to re-launch themselves in a proper 64-bit process, e.g.:
if ($ENV:PROCESSOR_ARCHITEW6432 -eq "AMD64") {
&"$ENV:WINDIR\SysNative\WindowsPowershell\v1.0\PowerShell.exe" -File $PSCOMMANDPATH
exit $LASTEXITCODE
}
Write-Host "Hello world"
How would we do the same thing for PowerShell 7? Well, we can certainly re-launch a running script using “pwsh.exe”, but that only solves half of the problem if PowerShell 7 isn’t already installed. Sure, there are some scenarios where you could just deploy the PowerShell 7 installation MSI in advance of trying to use it, but that doesn’t always work — you might not be able to get PowerShell 7 installed in time for the script to use it. The easiest solution for that is to just have the script itself install PowerShell 7, then it can re-launch itself using the newly-installed PowerShell 7 version.
I tried a variety of mechanisms for installing PowerShell 7 and ran into a variety of issues (especially around WinGet, OOBE, and Autopilot), so I ended up abandoning that approach and switching to a super-simple way provided by the PowerShell team:
Invoke-Expression "& { $(Invoke-RestMethod https://aka.ms/install-powershell.ps1) } -UseMSI"
What does that do? It downloads and runs a script (install-powershell.ps1) that itself downloads and runs the latest PowerShell 7 MSI. Perfect, one line to get PowerShell 7 installed. (Notice that there is no separate install of .NET 8, which PowerShell 7 uses. That’s because PowerShell 7 installs with its own copy of .NET 8, contained in the same install folder as the PowerShell 7 files.) But we don’t need to do that if a sufficient PowerShell 7 version is already installed, so it’s useful to add some logic to check on that:
# Check the current installed PowerShell version
if (Test-Path "HKLM:\Software\Microsoft\PowerShellCore\InstalledVersions") {
$version = Get-ChildItem "HKLM:\Software\Microsoft\PowerShellCore\InstalledVersions" | Get-ItemPropertyValue -Name SemanticVersion | Measure-Object -Maximum
$currentVersion = $version.Maximum
Write-Host "Current PowerShell version = $currentVersion"
} else {
Write-Host "No PowerShell LTS version found."
$currentVersion = "0.0.0"
}
Then there’s another potential challenge: The newly installed version of PowerShell 7 may not be in the path (the system path would be modified, but there’s no guarantee that new processes can see that change until after a reboot). We could fix that by using an explicit path (C:\Program Files\PowerShell\7\pwsh.exe), or we could just manually pull the latest path value — either way works.
And finally, we can re-execute the script using the same sort of logic that was used for 64-bit from 32-bit:
Try {
& pwsh.exe -File $Script
}
Catch {
Throw "Failed to start $PSCOMMANDPATH"
}
For simplicity, I wrapped all of that logic into a script called PS7Bootstrap.ps1 that is published to the PowerShell Gallery and to GitHub:
To wrap this all together, I modified a previous script that I had published for renaming a device as part of an Autopilot v2 device provisioning process to include this chunk of logic:
# Relaunch as PowerShell 7 if necessary
if ($PSVersionTable.PSVersion.Major -ne 7) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted
Install-Script PS7Bootstrap -Force -ErrorAction Ignore
PS7Bootstrap.ps1 $PSCommandPath
Exit $LASTEXITCODE
}
That logic checks to see if the script is already running in PowerShell 7, and if not, it installs the PS7Bootstrap.ps1 script from the PowerShell gallery and then runs it, passing along the full path name of the current script so that it can re-execute the script using PowerShell 7. (If you need to handle parameter passing, you could leave off the $PSCommandPath variable and then run “pwsh.exe -ExecutionPolicy bypass -File $PSCommandPath” yourself, adding additional parameters to the command line.)
The complete SimpleRename7.ps1 script is below:
# Bail out if we aren't in OOBE
$TypeDef = @"
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Api
{
public class Kernel32
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int OOBEComplete(ref int bIsOOBEComplete);
}
}
"@
Add-Type -TypeDefinition $TypeDef -Language CSharp
$IsOOBEComplete = $false
$hr = [Api.Kernel32]::OOBEComplete([ref] $IsOOBEComplete)
if ($IsOOBEComplete) {
Write-Host "Not in OOBE, nothing to do."
exit 0
}
# Relaunch as PowerShell 7 if necessary
if ($PSVersionTable.PSVersion.Major -ne 7) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted
Install-Script PS7Bootstrap -Force -ErrorAction Ignore
PS7Bootstrap.ps1 $PSCommandPath
Exit $LASTEXITCODE
}
# Get device information
$systemEnclosure = Get-CimInstance -ClassName Win32_SystemEnclosure
$details = Get-ComputerInfo
# Get the new computer name: use the asset tag (maximum of 13 characters), or the
# serial number if no asset tag is available (replace this logic if you want)
if (($null -eq $systemEnclosure.SMBIOSAssetTag) -or ($systemEnclosure.SMBIOSAssetTag -eq "")) {
$assetTag = $details.BiosSerialNumber
} else {
$assetTag = $systemEnclosure.SMBIOSAssetTag
}
if ($assetTag.Length -gt 13) {
$assetTag = $assetTag.Substring(0, 13)
}
if ($details.CsPCSystemTypeEx -eq 1) {
$newName = "D-$assetTag"
} else {
$newName = "L-$assetTag"
}
# Is the computer name already set? If so, bail out
if ($newName -ieq $details.CsName) {
Write-Host "No need to rename computer, name is already set to $newName"
Exit 0
}
# Set the computer name
Write-Host "Renaming computer to $($newName)"
Rename-Computer -NewName $newName -Force
We can specify that in Intune as a platform script:

When it runs, it will initially be running in PowerShell 5.1. Since this is designed to run on a brand-new OS install, during OOBE, it will install PowerShell 7 on the device and then re-execute itself in PowerShell 7 to rename the computer.
The same basic approach works for most of the other cases mentioned above (Intune, SCCM, Group Policy, MDT): let your script start in PowerShell 5.1, then relaunch in PowerShell 7. The OS deployment / task sequence steps that would run in Windows PE would be more challenging — that’s a topic for a future date. But steps that run in the new/full OS could either leverage this approach, or just have an initial step that installs the PowerShell 7 MSI so that later “Run command line” steps can use it directly.
PowerShell 7 really is better than PowerShell 5.1, with many new features added (as well as annoyances removed) over the eight years since PowerShell 5.1 was released. For other types of activities (e.g. administration of cloud services), it’s easy to switch, but for the device administration/provisioning/deployment scenarios above it does require a little extra (but worthwhile) work.






9 responses to “Using PowerShell 7 as a replacement for Windows PowerShell 5.1”
Any good recommendations for posts highlighting what PS 7 does better? i.e. How can I justify making these changes to our environment?
LikeLike
It’s an accumulation of eight years of changes. You can look through the release notes for all the 6.x and 7.x versions at https://learn.microsoft.com/en-us/previous-versions/powershell/scripting/overview and see the extent of the changes — what’s valuable to you is likely different than what’s valuable to me. I switch just to get rid of silly limitations in 5.1 that have already been addressed in 7 — too many times I’ve had to research 5.1 issues only to find out that 7 already addressed the issue. So I gain from “less lost time.”
LikeLike
Yes, for Windows 10/11, you only need to solve the running problem of PowerShell 5.1 to pass the level.
Unless the next generation operating system, or Microsoft replaces the initial PowerShell version of the system with 7.* or above, the packager can solve the new problem.
LikeLike
Michael,
Can you speak to if this affects scripts within a tanium provisioning process?
LikeLike
For Tanium sensors, you wouldn’t want to have them dynamically install PowerShell 7 — would take too long. But you could push out a package that installs PowerShell 7 so that sensors could use it. For Tanium Provision, this mechanism would work fine in the new OS, but would have the same challenges in Windows PE.
LikeLike
Does PowerShell 7 have the module support that’s in PowerShell 5? Most of my work uses these modules.
Calling a assembly from a dll over an over is something the majority of system configurators and maintaners are just not going to do.
LikeLike
PowerShell 7 either provides the equivalent models, or falls back to using the PowerShell 5.1 modules (sometimes both).
LikeLike
Kind of comparing apples to oranges here. Powershell 5 is Windows powershell. Versions after that are Powershell Core, which is cross platform powershell. It has some stuff that 5 doesn’t but is also missing some stuff that 5 has since it is no longer meant just for Windows. Windows comes with the Windows version which doesn’t seem weird to me. Might be nice if Microsoft was still actively developing the Windows version but Microsoft be Microsoft 🙂
LikeLike
That is the core (no pun intended) problem: PowerShell 5.1’s problems are usually already addressed in PowerShell 7. But since PowerShell 5.1 is frozen in time, you’re stuck with those problems until you change. And with the current 7.4 release, there’s really very little missing.
LikeLike