Star InactiveStar InactiveStar InactiveStar InactiveStar Inactive
 

One of the important best-practice rules in scripting (and programming) is to avoid using constant values for things that can vary. Using constants or "hard-coding" variable values is prone to error and requires continual maintenance. So, wherever you can, use variables and wildcards for variable values and use constants for the few things in life that don't change, e.g. New-Variable pi -Value [Math]::Pi -Option Constant

This issue arose in a SAPIEN forum post (thanks for posting!) in which someone hard-coded the path to program that a PowerShell Studio installer file (.msi) installs. That doesn't seem like a particularly risky thing, but paths change and, in this case, we changed a path to conform to an updated Windows standard. So, instead of hard-coding a file system path, the best way to find an installation directory is to use the same technique that Windows uses. It doesn't change often and it is very unlikely to change without a version change that you can check in your script.

If you need to use a file path or URL in a script, here are a few guidelines:

  • Get the path at run time from a reliable location
  • Place the path variable at the top of a script (in script scope), so it's very obvious to anyone who is maintaining the script.

 

Reliable techniques for finding the installation directory

How do you find the path to a Windows program dynamically at run time? There are a few different techniques, but they really perform differently. I used the new performance monitoring report graph in PowerShell Studio and PrimalScript to test them.

Use CIM / WMI

The Win32_Product class returns an object that contains the path to the installation directory for a program. You need to include the program name, but you can take it from a parameter and use wildcards to hedge your bet.

NOTE: Darren Mar-Elia (@grouppolicyguy) warns not to use Win32_Product because accessing it updates
the installers, potentially changing installer settings: Why-win32_product-is-bad-news. Thanks to Liviu
on Facebook for the link.

For example, this command uses the Win32_Product class to get the installation directory of my current copy of PowerShell Studio 2016.

PS C:\> (Get-CimInstance -ClassName Win32_Product | Where-Object Name -Like "*PowerShell*Studio*").InstallLocation
C:\Program Files\SAPIEN Technologies, Inc\PowerShell Studio 2016\

Unfortunately, CIM is notoriously slow. Getting the class without any filtering takes an average 30 seconds.

Get-CimInstance -ClassName Win32_Product

Here's the performance data from the Output tab:

>> Execution time: 00:00:29
>> Script Ended
>> Max. CPU: 7 %  Max. Memory: 18.28 MB

 

Here's the performance graph from the Performance tab. CIM must be caching and dumping in the middle of the operation.

 CIM

 

In a 30-second operation, the filtering time is negligible and well within the margin of error. Here are variations of the command with filtering to get the InstallationLocation property of the CIM object. In each case, I copied the performance data from the Output tab and placed it in comments before the command.

CIMInstalledLocation

 

Here's the comparative graph of the three commands in sequence. There's really not much you can do to speed this up when the base operation is so slow.

InstalledLocationGraph

 

Let's try a different strategy.

Search the registry: InstallProperties

You can also search the registry for the installation directory. The InstallProperties registry key for the program has an InstallLocation value that stores the installation path. Here's the InstallProperties key for my current version of PowerShell Studio in Regedit.

InstallLocationRegedit

 

Let's use PowerShell get the installation directory in that InstallLocation registry value.

Use the Get-ChildItem cmdlet to get all of the InstallProperties registry keys. Then, look for the InstallProperties key whose DisplayName property matches the program name (or a name pattern with wildcards). Because DisplayName is a registry value, not a key, use the Get-ItemProperty cmdlet to get it. When you find a match for the program name, use Get-ItemProperty on the same registry key to get the data in the InstallLocation registry value.

The Get-ItemProperty commands look repetitive (e.g. (Get-ItemProperty -Path $entryName -Name DisplayName).DisplayName), but the Windows PowerShell registry provider returns a custom object for the DisplayName registry value, so you need to get the DisplayName property of the DisplayName custom object. Same with all registry values, including InstallLocation.

$ProgramName = "*PowerShell*Studio*"
$inst = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties"
foreach ($entry in $inst)
{
    $entryName = $entry.Name -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'
    if ((Get-ItemProperty -Path $entryName -Name DisplayName).DisplayName -like $ProgramName)
    {
        (Get-ItemProperty -Path $entryName -Name InstallLocation).InstallLocation
    }
}

Searching the registry isn't fast, but at ~5 seconds, it's much faster than the CIM/WMI query.

#>> Execution time: 00:00:05
#>> Script Ended
#>> Max. CPU: 25 %  Max. Memory: 13.27 MB

 Screenshot 2016 12 12 14.47.47

 

Let's make this faster! After reading Aaron Jensen's (@pshdo) registry code in the awesome Carbon module, I replaced the original Get-ItemProperty calls with calls to the getValue() method of registry keys (Microsoft.Win32.RegistryKey), like the ones that Get-ChildItem returns in the registry. The getValue() method takes the name of a registry value in the registry key, like DisplayName and InstallLocation and returns its data.

The code is much simpler...

$ProgramName = "*PowerShell*Studio*"
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties" |
Where-Object { $_.getValue('DisplayName') -like $ProgramName } | ForEach-Object { $_.getValue('InstallLocation')}

And faster, using about the same amount of resource.

>> Execution time: 00:00:03
>> Script Ended
>> Max. CPU: 25 %  Max. Memory: 13.90 MB

Screenshot 2016 12 14 09.54.42

 

Search the registry: Uninstall

In the Carbon module (I recommend it!), Aaron Jensen has a Get-ProgramInstallInfo function that returns a wealth of data about program installation, including the path to the installation directory. It gets installation information for all users, not just tthe current user. Be sure to run elevated ('Run as adminstrator') or you get a bunch of Access Denied errors with your output.

PS C:\ps-test> (Get-ProgramInstallInfo -Name "*PowerShell*Studio*").InstallLocation
C:\Program Files\SAPIEN Technologies, Inc\PowerShell Studio 2016\

To get this data, Carbon searches the Uninstall keys in the registry:

  • HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall
  • HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
  • HKU:\*\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'

To do the HKEY_USERS search, he creates a PSDrive for the hive:

New-PSDrive -Name 'HKU' -PSProvider Registry -Root 'HKEY_USERS'

And, it's super-fast: 525 milliseconds. The PowerShell Studio graph and output show it as 4 seconds, but that includes the 4 seconds that it takes to import the module.

 

Finding an installation directory in a script

When I use this registry searching code in a script, I enclose the code in a function with a parameter that accepts wildcard characters. I also add some error handling with error text that is targeted to the audience. In this case, the audience is an operations person, so the error message is more technical than it would be for a naive end user. I also return a custom object with details about the program so the end-user can verify the program before using the installer path.

Get this function in a GitHub gistGet-InstallationPath.ps1

 

function Get-InstallPath
{
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    Param
    (
        [Parameter(Mandatory = $true)]
        [SupportsWildcards()]
        [string]
        $ProgramName
    )
    
    $result = @()
    if ($inst = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\*\Products\*\InstallProperties" -ErrorAction SilentlyContinue)
    {
        $inst | Where-Object {
            ($DisplayName = $_.getValue('DisplayName')) -like $ProgramName
        } |
        ForEach-Object     {
            $result += [PSCustomObject]@{
                'DisplayName' = $displayName
                'Publisher' = $_.getValue('Publisher')
                'InstallPath' = $_.getValue('InstallLocation')
            }
        }
    }
    else
    {
        Write-Error "Cannot get the InstallProperties registry keys."
    }
    
    if ($result)
    {
        return $result
    }
    else
    {
        Write-Error "Cannot get the InstallProperties registry key for $ProgramName"
    }
}

 

To make the variable that stores the installation directory easy to find, put it in script scope. In this sample script, we call our Get-InstallPath function and save the results in the $InstallationDirectory parameter in script scope ($script:InstallationDirectory).

# In a script ...
Param
(
    [Parameter(Mandatory = $true)]
    [SupportsWildcards()]
    [string]
    $Program
) #----------------------- # Variables #----------------------- $script:InstallationDirectory = '' ... #----------------------- # Functions #----------------------- function Get-InstallPath {...} ... #----------------------- # Main #----------------------- if ($script:InstallationDirectory = (Get-InstallPath -ProgramName $Program).InstallPath) { ... }

 

When we write scripts, we might think that a path won't change, but especially for installation directories, that's not a safe assumption. Be sure to use a method that gets the actual path at run time. And, use the PowerShell Studio performance graph and output to optimize your code.   

June Blender is a technology evangelist at SAPIEN Technologies, Inc. and a Microsoft Cloud and Datacenter MVP. You can reach her at This email address is being protected from spambots. You need JavaScript enabled to view it. or follow her on Twitter at @juneb_get_help.

 

If you have questions about our products, please post in our support forum.
For licensed customers, use the forum associated with your product in our Product Support Forums for Registered Customers.
For users of trial versions, please post in our Former and Future Customers - Questions forum.
Copyright © 2024 SAPIEN Technologies, Inc.