In the past, I've shared a variety of PowerShell approaches that you can use to inventory what versions of PowerShell are installed. But I think I now have the best approach short of searching the hard drive for powershell.exe and pwsh.exe, which I suppose is still a possibility and something I should write.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Instead, I'm relying on the fact that if PowerShell or some version of PowerShell 7 was installed, the executable should be found with Get-Command. By the way, the code I came up with is designed to be run on a Windows platform. On non-Windows systems, PowerShell 7 or preview are the only version you can install and those operating systems already have proper tooling for checking installed packages.
Using Get-Command
In Windows, I can start with Get-Command.
$cmd = Get-Command -name powershell.exe -commandtype Application
If the file is found, I can run it and extract version information from the host. This will be in addition to version information available from Get-Command.
&$cmd.path -nologo -noprofile -command { Get-Host}
With is information I can create a custom object.
[pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = $cmd.Name
FileVersion = $cmd.Version
PSVersion = $psh.Version
Comments = $null
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
I'm including the computer name which will be helpful later when I'm querying a bunch of remote machines. I could have used $env:Computername since I expect this will only be used on Windows platforms, but it might not. In which case, I need to use the .NET Framework since $env:Computername isn't available on non-Windows platforms. I'm also grabbing the operating system using Get-CimInstance.
$os = (Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption).caption
And you'll also see that I am creating a custom object with a defined typename of 'PSInstalledInfo'.
The Inventory Script
Of course, you want to see the code.
#requires -version 5.1
#Use Get-Command to test for installed versions of PowerShell
[cmdletbinding()]
Param()
Write-Verbose "Searching for PowerShell installations on $([System.Environment]::MachineName)"
#build a list to hold the results
$list = [System.Collections.Generic.list[object]]::New()
#get the operating system
$os = (Get-CimInstance -ClassName Win32_OperatingSystem -Property Caption).caption
#Windows Powershell
Try {
Write-Verbose "Testing for Windows PowerShell"
$cmd = Get-Command -Name powershell.exe -ErrorAction stop
if ($cmd) {
Write-Verbose "Using $($cmd.path)"
$psh = &$cmd.path -nologo -noprofile -command { Get-Host }
$result = [pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = $cmd.Name
FileVersion = $cmd.Version
PSVersion = $psh.Version
Comments = $null
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
Remove-Variable cmd
$list.Add($result)
}
}
Catch {
Write-Verbose "Windows PowerShell not installed on $([Environment]::MachineName)."
}
#test for PowerShell 2.0 engine or feature
Write-Verbose "Testing for Windows PowerShell 2.0 engine or feature"
Try {
#get computersystem roles to determine if running on a server or client
$cs = Get-CimInstance -ClassName win32_Computersystem -Property Roles -ErrorAction Stop
$rolestring = $cs.roles -join ","
Write-Verbose "Detected roles $rolestring"
if ($rolestring -match 'Server_NT|Domain_Controller') {
Write-Verbose "Running Get-WindowsFeature"
$f = Get-WindowsFeature PowerShell-V2
}
else {
Write-Verbose "Running Get-WindowsOptionalFeature"
$f = Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root
}
if ($f.installed -OR $f.State -eq 'Enabled') {
$result = [pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = "powershell.exe"
FileVersion = $null
PSVersion = "2.0"
Comments = "Windows PowerShell 2.0 feature enabled or installed"
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
$list.Add($result)
}
}
Catch {
Write-Verbose "Windows PowerShell 2.0 not installed on $([Environment]::MachineName)."
}
#PowerShell 7
Write-Verbose "Testing for PowerShell 7"
Try {
$cmd = Get-Command -Name "pwsh.exe" -CommandType Application -ErrorAction Stop
if ($cmd) {
foreach ($item in $cmd) {
Write-Verbose "Using $($item.path)"
$psh = &$item.path -nologo -noprofile -command { Get-Host }
$PSVer = $psh.version
#test fpr SSH
Write-Verbose "Testing for SSH on $([Environment]::MachineName)"
$ProgressPreference = "SilentlyContinue"
$ssh = Test-NetConnection -ComputerName ([Environment]::MachineName) -Port 22 -WarningAction SilentlyContinue -InformationLevel Quiet
If ($ssh) {
$note = "SSH detected"
}
else {
$note = ""
}
$result = [pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = $item.Name
FileVersion = $item.Version
PSVersion = $PSVer
Comments = $note.Trim()
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
$list.Add($result)
} #foreach item
Remove-Variable cmd
}
}
Catch {
Write-Verbose "PowerShell 7 not installed on $([Environment]::MachineName)."
}
Write-Verbose "Testing for PowerShell 7 preview"
#filter out preview if running this command IN a preview
if ($host.version.PSSemVerPreReleaseLabel) {
Write-Verbose "PowerShell preview detected"
$n = "pwsh.exe"
}
else {
$n = "pwsh-preview.cmd"
}
Try {
$cmd = Get-Command -Name $n -CommandType Application -ErrorAction Stop
if ($cmd) {
foreach ($item in $cmd) {
Write-Verbose "Using $($item.path)"
$psh = &$item.path -nologo -noprofile -command { Get-Host }
$PSVer = $psh.version
if ($psver.PSSemVerPreReleaseLabel) {
[string]$note = $psver.PSSemVerPreReleaseLabel
}
else {
[string]$note = ""
}
#test fpr SSH
Write-Verbose "Testing for SSH on $([Environment]::MachineName)."
$ProgressPreference = "SilentlyContinue"
$ssh = Test-NetConnection -ComputerName ([Environment]::MachineName) -Port 22 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($ssh) {
$note += " SSH detected"
}
$result = [pscustomobject]@{
PSTypeName = "PSInstallInfo"
Name = $item.Name
FileVersion = $item.Version
PSVersion = $PSVer
Comments = $note.Trim()
Computername = [System.Environment]::MachineName
OperatingSystem = $os
}
if ($list.Psversion -notcontains $result.PSVersion) {
$list.Add($result)
}
else {
Write-Verbose "Skipping duplicate version $($result.version)"
}
} #foreach item
}
}
Catch {
Write-Verbose "PowerShell 7 preview not installed on $([Environment]::MachineName)."
}
#write the results to the pipeline
$list | Sort-Object -Property PSVersion
As you look through the code I'm searching for PowerShell and pwsh versions. I'm also testing if ssh is enabled on systems running PowerShell 7. I didn't want to rely on checking the sshd service because I didn't want to make an assumption. Although I am assuming port 22. You'll also see that I am specifying a computername for Test-NetConnection. In my testing, if I don't specify a computername, PowerShell tries to connect to a public IP address.
Here's how it looks on my local machine.
Because this is a script, I can use it with Invoke-Command to query remote Windows computers, assuming of course that PowerShell remoting is enabled.
Doing More with Type
Of course, since I have a custom object, I can do more. Such as define a setup of default properties.
Update-TypeData -TypeName PSInstallInfo -DefaultDisplayPropertySet "Name","FileVersion","PSVersion","Comments","ComputerName" -force
This gets rid of the pesky RunspaceID property you get when using Invoke-Command.
Or, I can really get going and define a custom format file which I did using my trusty New-PSFormatXML command.
<!--
Format type data generated 07/13/2021 12:14:45 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 07/13/2021 12:14:45 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>PSInstallInfo</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>"{0} [{1}]" -f $_.Computername,$_.OperatingSystem</ScriptBlock>
<Label>Computername</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths
<AutoSize />.-->
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
<Width>19</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>FileVersion</Label>
<Width>15</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PSVersion</Label>
<Width>15</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Comments</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>FileVersion</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code|serverremotehost") -AND ($_.name -match "preview")) {
"$([char]27)[38;5;219m$($_.PSVersion)$([char]27)[0m"
}
else {
$_.PSVersion
}
</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
if (($host.name -match "console|code|serverremotehost") -AND ($_.comments-match "SSH detected")) {
<!-- replace SSH Detected with an ANSI sequence-->
$_.comments -replace "SSH detected","$([char]27)[1;38;5;155mSSH detected$([char]27)[0m"
}
else {
$_.comments
}
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
I'm using ANSI sequences to highlight PowerShell previews and SSH. All I need to do is import it into my session.
Update-Formatdata c:\scripts\psinstalledinfo.format.ps1xml
Now, I get very meaningful output.
Your Turn
I hope you'll give this a try. There's certainly room for you to add your own touches. I left the Comments property mostly unused for future use. Or you may want to grab additional information like file timestamps. Have fun.
The inventory script works well for versions of PowerShell installed by Windows Installer. However, it does not find the side by side installations of PowerShell 7. Personally. I have three versions of pwsh – 7.1.x (installed via MSI) as well as the latest 7.2 preview and the build of the day. These last two are NOT found by this script sadly. Get-Command does not find these versions – you would need to use something like Get-ChildItem -Path c:\pwsh.exe -Recurse -EA 0
You are correct. No, it won’t. But I’m working on the assumption that someone like you is an outlier. I would envision this script as being useful to inventory servers.
See if this is any better for you: https://jdhitsolutions.com/blog/powershell/8492/searching-for-powershell-with-cim/