Windows keeps track of installed software, and to manually list, change, or uninstall, simply run this command:
appwiz.cpl
This opens the graphical wizard, and you can click the task.
But how would you automate this, and where do you find the listed information, i.e. to create software reports, or test whether a given software is installed or not?
Reading Installed Software From Registry
The information about installed software is stored in the Windows Registry in four different places, and it is almost trivial for PowerShell to read this:
# read all child keys (*) from all four locations and do not emit
# errors if one of these keys does not exist:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction Ignore |
# list only items with a displayname:
Where-Object DisplayName |
# show these registry values per item:
Select-Object -Property DisplayName, DisplayVersion, UninstallString, InstallDate |
# sort by displayname:
Sort-Object -Property DisplayName
The result looks similar to this:
DisplayName DisplayVersion UninstallString InstallDate
----------- -------------- --------------- -----------
64 Bit HP CIO Components Installer 22.2.1 MsiExec.exe /I{50229C72-539F-4E65-BEB5-F0491C5074B7} 20190907
7-Zip 19.00 (x64) 19.00 C:\Program Files\7-Zip\Uninstall.exe
Actipro WPF Controls 2017.2 17.2.0661 MsiExec.exe /I{DB4F94C0-D62F-41ED-81B9-078CDF246C5B} 20190909
Active Directory Authentication Library für SQL Server 15.0.1300.359 MsiExec.exe /I{088DDE47-955D-406C-848F-C1531DF2E049} 20190903
Adobe Acrobat Reader DC - Deutsch 20.006.20042 MsiExec.exe /I{AC76BA86-7AD7-1031-7B44-AC0F074E4100} 20200319
...
The bulk load is done by Get-ItemProperty
: this cmdlet can read from multiple registry keys, and when you specify a “*”, it automatically traverses all child keys.
Create Excel Report: Installed Software
If you’d rather like to produce an excel list, first make sure you have installed the awesome (and free) module ImportExcel:
Install-Module -Name ImportExcel -Scope CurrentUser
Next, pipe the results to Export-Excel
, and you are done:
# read all child keys (*) from all four locations and do not emit
# errors if one of these keys does not exist:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction Ignore |
# list only items with a displayname:
Where-Object DisplayName |
# show these registry values per item:
Select-Object -Property DisplayName, DisplayVersion, UninstallString, InstallDate |
# sort by displayname:
Sort-Object -Property DisplayName |
# export results to excel
Export-Excel
Implementation: Get-Software
Let’s bake this finding into a PowerShell function and add all the bells and whistles that make it a really useful and reusable command:
- Add a built-in filter so you can quickly search for a given software
- Add information about the architecture (64-Bit or 32-Bit), and the scope (installed for current user or all users)
- Add a property filter so PowerShell shows only a few most commonly used properties by default
function Get-Software
{
<#
.SYNOPSIS
Reads installed software from registry
.PARAMETER DisplayName
Name or part of name of the software you are looking for
.EXAMPLE
Get-Software -DisplayName *Office*
returns all software with "Office" anywhere in its name
#>
param
(
# emit only software that matches the value you submit:
[string]
$DisplayName = '*'
)
#region define friendly texts:
$Scopes = @{
HKLM = 'All Users'
HKCU = 'Current User'
}
$Architectures = @{
$true = '32-Bit'
$false = '64-Bit'
}
#endregion
#region define calculated custom properties:
# add the scope of the software based on whether the key is located
# in HKLM: or HKCU:
$Scope = @{
Name = 'Scope'
Expression = {
$Scopes[$_.PSDrive.Name]
}
}
# add architecture (32- or 64-bit) based on whether the registry key
# contains the parent key WOW6432Node:
$Architecture = @{
Name = 'Architecture'
Expression = {$Architectures[$_.PSParentPath -like '*\WOW6432Node\*']}
}
#endregion
#region define the properties (registry values) we are after
# define the registry values that you want to include into the result:
$Values = 'AuthorizedCDFPrefix',
'Comments',
'Contact',
'DisplayName',
'DisplayVersion',
'EstimatedSize',
'HelpLink',
'HelpTelephone',
'InstallDate',
'InstallLocation',
'InstallSource',
'Language',
'ModifyPath',
'NoModify',
'PSChildName',
'PSDrive',
'PSParentPath',
'PSPath',
'PSProvider',
'Publisher',
'Readme',
'Size',
'SystemComponent',
'UninstallString',
'URLInfoAbout',
'URLUpdateInfo',
'Version',
'VersionMajor',
'VersionMinor',
'WindowsInstaller',
'Scope',
'Architecture'
#endregion
#region Define the VISIBLE properties
# define the properties that should be visible by default
# keep this below 5 to produce table output:
[string[]]$visible = 'DisplayName','DisplayVersion','Scope','Architecture'
[Management.Automation.PSMemberInfo[]]$visibleProperties = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet',$visible)
#endregion
#region read software from all four keys in Windows Registry:
# read all four locations where software can be registered, and ignore non-existing keys:
Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction Ignore |
# exclude items with no DisplayName:
Where-Object DisplayName |
# include only items that match the user filter:
Where-Object { $_.DisplayName -like $DisplayName } |
# add the two calculated properties defined earlier:
Select-Object -Property *, $Scope, $Architecture |
# create final objects with all properties we want:
Select-Object -Property $values |
# sort by name, then scope, then architecture:
Sort-Object -Property DisplayName, Scope, Architecture |
# add the property PSStandardMembers so PowerShell knows which properties to
# display by default:
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $visibleProperties -PassThru
#endregion
}
There are plenty of learning points:
Showing Only Few Default Properties
When you run Get-Software
, only four properties are displayed by default, producing a nice and clean table output:
Get-Software
DisplayName DisplayVersion Scope Architecture
----------- -------------- ----- ------------
64 Bit HP CIO Components Installer 22.2.1 All Users 64-Bit
7-Zip 19.00 (x64) 19.00 All Users 64-Bit
Actipro WPF Controls 2017.2 17.2.0661 All Users 32-Bit
Active Directory Authentication Library für SQL Server 15.0.1300.359 All Users 64-Bit
Adobe Acrobat Reader DC - Deutsch 20.006.20042 All Users 32-Bit
Still, you can display any property you want when you add Select-Object
:
Get-Software | Select-Object -Property *
AuthorizedCDFPrefix :
Comments :
Contact :
DisplayName : 64 Bit HP CIO Components Installer
DisplayVersion : 22.2.1
EstimatedSize : 845
HelpLink :
HelpTelephone :
InstallDate : 20190907
InstallLocation :
InstallSource : C:\Windows\system32\spool\DRIVERS\x64\3\
Language : 1033
ModifyPath : MsiExec.exe /I{50229C72-539F-4E65-BEB5-F0491C5074B7}
NoModify :
PSChildName : {50229C72-539F-4E65-BEB5-F0491C5074B7}
PSDrive : HKLM
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{50229C72-539F-4E65-BEB5-F0491C5074B7}
PSProvider : Microsoft.PowerShell.Core\Registry
Publisher : HP Inc.
Readme :
Size :
SystemComponent : 1
UninstallString : MsiExec.exe /I{50229C72-539F-4E65-BEB5-F0491C5074B7}
URLInfoAbout :
URLUpdateInfo :
Version : 369229825
VersionMajor : 22
VersionMinor : 2
WindowsInstaller : 1
Scope : All Users
Architecture : 64-Bit
...
If you’d like to get more information on the techniques used to define the default properties, here are all the details.
Custom Filtering
Rather than leaving it to the user to add Where-Object
for custom filtering, the function sports the filter property -DisplayName that is named after the property it filters, so now it is simply to list exactly the software you are after:
Get-Software -DisplayName *Office*
DisplayName DisplayVersion Scope Architecture
----------- -------------- ----- ------------
Microsoft Office Professional Plus 2016 - de-de 16.0.12527.20278 All Users 64-Bit
Microsoft Office Professional Plus 2016 - en-us 16.0.12527.20278 All Users 64-Bit
Microsoft Visual Studio 2010 Tools for Office Runtime (x64) 10.0.60724 All Users 64-Bit
Microsoft Visual Studio 2010 Tools for Office Runtime (x64) 10.0.60729 All Users 64-Bit
Office 16 Click-to-Run Extensibility Component 16.0.12527.20278 All Users 32-Bit
Office 16 Click-to-Run Extensibility Component 64-bit Registration 16.0.12527.20278 All Users 64-Bit
Office 16 Click-to-Run Licensing Component 16.0.12527.20278 All Users 64-Bit
Office 16 Click-to-Run Localization Component 16.0.12527.20278 All Users 32-Bit
Office 16 Click-to-Run Localization Component 16.0.12527.20278 All Users 32-Bit
And since the output still contains all properties, you can create custom lists and include non-default properties when needed:
Get-Software -DisplayName *Office* | Select-Object -Property DisplayName, InstallSource
DisplayName InstallSource
----------- -------------
Microsoft Office Professional Plus 2016 - de-de
Microsoft Office Professional Plus 2016 - en-us
Microsoft Visual Studio 2010 Tools for Office Runtime (x64)
Microsoft Visual Studio 2010 Tools for Office Runtime (x64) c:\f3c1e4cb1e6fd583dbd7441e\
Office 16 Click-to-Run Extensibility Component c:\program files (x86)\microsoft office\root\integration\
Office 16 Click-to-Run Extensibility Component 64-bit Registration c:\program files (x86)\microsoft office\root\integration\
Office 16 Click-to-Run Licensing Component c:\program files (x86)\microsoft office\root\integration\
Office 16 Click-to-Run Localization Component c:\program files (x86)\microsoft office\root\integration\
Office 16 Click-to-Run Localization Component c:\program files (x86)\microsoft office\root\integration\
Calculated Properties
Thanks to calculated properties, the output includes important information about architecture and scope. These properties weren’t available in the registry and have been calculated based on the location of the registry keys.
Fine-Tuning Returned Properties
Get-Software
returns all registry values available. You can look up the details of the registry values here.
If you don’t need some of the information, i.e. AuthorizedCDFPrefix, or find that this information is always empty for you, simply remove the property from the list in $values
and tailor the output to your needs.
Maybe you are wondering why
Get-Software
usesSelect-Object
twice, and why I chose to hard-code the properties. Here is why:The first instance of
Select-Object
includes all properties, plus it adds the new calculated properties:# add the two calculated properties defined earlier: Select-Object -Property *, $Scope, $Architecture |
The second instance of
Select-Object
defines the final properties returned. This is crucial because not every registry key contains all of these values. PowerShell uses the properties of the first returned object to calculate tabular headers, so if by accident the first read software item had defined no or not all of the common registry values, PowerShell would not show the remaining values for any of the software items.By hard-coding the property names you are after, you guarantee that they be visible because
Select-Object
actually always clones objects and produces new objects with the properties you chose. This way, these new objects are guaranteed to have these properties, even if they are empty.