2

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Dave
  • 29
  • 1
  • 7
  • 1
    `& cmd /c $UninstallPath.UninstallString /norestart`. Programs sometimes allow you to uninstall silently, all you need to do is ensure the key has `.QuietUninstallString` property. You can also google some of the switches for the specific product but, the invocation operator (`&`) should do the trick for you in regards to your question. – Abraham Zinala Jul 14 '21 at 17:33
  • Thank you, for this case a silent uninstall is not needed so I'm not worried about it being silent and the product is not regularly available so googling for switches was out of the question. – Dave Jul 15 '21 at 12:25

2 Answers2

4

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you, this explains why i was getting error constantly. after a small break i managed to it working working using the following. `$unin = $UninstallPath.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X","" $unin = $unin.Trim() start-process "msiexec.exe" -arg "/X $unin /qb" -Wait` – Dave Jul 15 '21 at 12:40
  • Glad to hear it, @Dave. Note that the implementation of (b) above should work generally with _all_ uninstallation strings (unless you need to modify the arguments). – mklement0 Jul 15 '21 at 12:47
3

Or for example:

get-package *chrome* | uninstall-package
js2010
  • 23,033
  • 6
  • 64
  • 66