Note:
- This answer addresses the question as asked.
- js2010's helpful answer shows a much more convenient alternative that avoids the original problem, via the
PackageManagement
module's Get-Package
and Uninstall-Package
cmdlets. Uninstall-Package
supports uninstalling MSI-installed software (-ProviderName msi
), but seemingly not "programs" (-ProviderName Programs
); I'm unclear on whether uninstallation of Windows Update packages (-ProviderName msu
) is supported.
Note, however, that these providers are only (directly) available in Windows PowerShell[1] - by contrast, PowerShell (Core) as of v7.3.3 lacks these package providers altogether, and it's unclear (to me) whether they will ever be added.
Problem:
The uninstallation command lines stored in the UninstallString
/ QuietUninstallString
registry values[2] are designed for no-shell / from-cmd.exe
invocations.
They therefore can fail from PowerShell if you pass them to Invoke-Expression
, namely if they contain unquoted characters that have no special meaning outside shells / to cmd.exe
, but are metacharacters in PowerShell, which applies to {
and }
in your case.
Solutions:
You have two options:
(a) Simply pass the uninstallation string as-is to cmd /c
- Note that - unlike when you call
msiexec.exe
directly from PowerShell or directly from cmd.exe
- calling via cmd /c
results in synchronous execution of msiexec
, which is desirable.
(b) Split the uninstallation string into executable and argument list, which allows you to call the command via Start-Process
, which can give you more control over the invocation.
- Be sure to use the
-Wait
switch to ensure that the installation completes before your script continues.
Note: The following commands assume that the uninstall string is contained in variable $UninstallString
(the equivalent of $UninstallPath.UninstallString
in your code):
Implementation of (a):
# Simply pass the uninstallation string (command line) to cmd.exe
# via `cmd /c`.
# Execution is synchronous (blocks until the command finishes).
cmd /c $UninstallString
$exitCode = $LASTEXITCODE
The automatic $LASTEXITCODE
variable can then be queried for the command line's exit code.
Implementation of (b):
# Split the command line into executable and argument list.
# Account for the fact that the executable name may be double-quoted.
if ($UninstallString[0] -eq '"') {
$unused, $exe, $argList = $UninstallString -split '"', 3
}
else {
$exe, $argList = $UninstallString -split ' ', 2
}
# Use Start-Process with -Wait to wait for the command to finish.
# -PassThru returns an object representing the process launched,
# whose .ExitCode property can then be queried.
$ps = if ($argList) {
Start-Process -Wait -PassThru $exe $argList
} else {
Start-Process -Wait -PassThru $exe
}
$exitCode = $ps.ExitCode
You could also add -NoNewWindow
to prevent console program-based uninstallation command lines from running in a new console window, but note that the only way to capture their stdout / stderr output via Start-Process
is to redirect them to files, using the -RedirectStandardOutput
/ -RedirectStandardError
parameters.
Edition-specific / future improvements:
The Start-Process
-based method is cumbersome for two reasons:
You cannot pass whole command lines and must instead specify the executable and arguments separately.
In Windows PowerShell (whose latest and final version is 5.1) you cannot pass an empty string or array to the (positionally implied) -ArgumentList
parameter (hence the need for two separate calls above).
- This problem has been fixed in the cross-platform, install-on-demand PowerShell (Core) edition (versions 6 and above).
[1] If you don't mind the extra overhead, you can (temporarily) import the Windows PowerShell PackageManagement
module even from PowerShell (Core), using the Windows PowerShell compatibility feature:
Import-Module -UseWindowsPowerShell PackageManagement
.
[2] As shown in your question, they are stored in the HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall
(64-bit applications) and HKEY_LOCAL_MACHINE\Wow6432Node \Software\Microsoft\Windows\CurrentVersion\Uninstall
(32-bit applications) registry keys.