Search code examples
powershelluninstallationuninstallstring

How to uninstall MSIs using the Uninstall Path


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.


Solution

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