How to uninstall MSIs using the Uninstall Path

3k views Asked by At

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.

2

There are 2 answers

2
mklement0 On BEST ANSWER

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.[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

    • 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):

  • 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).

    • 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, 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.

6
js2010 On

Or for example:

get-package *chrome* | uninstall-package