I am trying to get the uninstall paths of a set of applications and uninstall them. So far i an get the list of uninstall paths. but i am struggling to actually uninstall the programs.
My code so far is.
$app = @("msi1", "msi2", "msi3", "msi4")
$Regpath = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($apps in $app){
$UninstallPath = Get-ItemProperty $Regpath | where {$_.displayname -like "*$apps*"} | Select-Object -Property UninstallString
$UninstallPath.UninstallString
#Invoke-Expression UninstallPath.UninstallString
#start-process "msiexec.exe" -arg "X $UnistallPath /qb" - wait
}
this will return the following results:
MsiExec.exe /X{F17025FB-0401-47C9-9E34-267FBC619AAE}
MsiExec.exe /X{20DC0ED0-EA01-44AB-A922-BD9932AC5F2C}
MsiExec.exe /X{29376A2B-2D9A-43DB-A28D-EF5C02722AD9}
MsiExec.exe /X{18C9B6D0-DCDC-44D8-9294-0ED24B080F0C}
Im struggling to find away to execute these uninstall paths and actually uninstall the MSIs.
I have tried to use Invoke-Expression $UninstallPath.UninstallString
but it just displays the windows installer and gives me the option for msiexec.
I have also tried to use start-process "msiexec.exe" -arg "X $UnistallPath /qb" - wait
however this gives the same issue.
Note:
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.Problem:
The uninstallation command lines stored in the UninstallString
/ QuietUninstallString
registry values[2] are designed for no-shell / from-cmd.exe
invocations.[3]
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
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.
-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):
Situationally appending options to the command line isn't as straightforward as just appending, say, ' /quiet /norestart'
, because that won't work if the command line is merely an unquoted executable path without spaces, e.g., C:\Program Files\WinRAR\uninstall.exe
- see this answer for a solution.
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).
[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, and possibly also - for user-specific installations - in their HKEY_CURRENT_USER
counterparts; given that HKEY_CURRENT_USER
only refers to the current user, looking for other users' user-specific installations would require more work.
[3] Hypothetically, it is possible to author valid no-shell command lines that break when called from cmd.exe
(e.g. foo.exe a&b
or foo.exe "A \"B & C\ D"
), but that rarely, if ever, happens in practice.