Installed Software

Retrieve information about installed software and find out versions, uninstall strings, and more.

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-Softwarereturns 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 uses Select-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.