<# .SYNOPSIS Silently uninstalls an application by its display name, using whatever uninstall entry the app registered. .DESCRIPTION Looks through the 64-bit and 32-bit Uninstall keys in HKLM for entries whose DisplayName matches. MSI-based entries are removed with msiexec /x and the product code; everything else uses the QuietUninstallString, or the UninstallString plus arguments you supply. Each removal is logged, and the script returns one result per entry plus an exit code ConfigMgr understands (0, 3010, or the failure). If the name matches more than one entry, it stops unless you add -AllowMultiple. .PARAMETER DisplayName Name as shown in Programs and Features. Wildcards are allowed, e.g. 'Cherwell*'. .PARAMETER Publisher Optional publisher filter, also wildcard-friendly. Handy when the app name is generic. .PARAMETER SilentArgs Arguments to add to a non-MSI UninstallString when the app doesn't register a QuietUninstallString. .PARAMETER AllowMultiple Remove every match instead of stopping when more than one entry matches. .PARAMETER LogPath Folder for msiexec logs. Defaults to the Windows temp folder. .EXAMPLE .\Uninstall-ApplicationByName.ps1 -DisplayName 'Cherwell*' -WhatIf .EXAMPLE .\Uninstall-ApplicationByName.ps1 -DisplayName 'Some Agent' -SilentArgs '/S' #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$DisplayName, [string]$Publisher = '*', [string]$SilentArgs, [switch]$AllowMultiple, [string]$LogPath = (Join-Path $env:SystemRoot 'Temp') ) function Get-UninstallEntry { # Read both registry views directly, so a 32-bit PowerShell host still sees 64-bit apps. $views = if ([Environment]::Is64BitOperatingSystem) { 'Registry64', 'Registry32' } else { 'Default' } foreach ($view in $views) { $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey('LocalMachine', $view) $root = $base.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall') if (-not $root) { continue } foreach ($keyName in $root.GetSubKeyNames()) { $key = $root.OpenSubKey($keyName) [pscustomobject]@{ KeyName = $keyName View = $view DisplayName = $key.GetValue('DisplayName') DisplayVersion = $key.GetValue('DisplayVersion') Publisher = $key.GetValue('Publisher') WindowsInstaller = $key.GetValue('WindowsInstaller') SystemComponent = $key.GetValue('SystemComponent') UninstallString = $key.GetValue('UninstallString') QuietUninstall = $key.GetValue('QuietUninstallString') } $key.Close() } $root.Close(); $base.Close() } } function Split-CommandLine { param([string]$CommandLine) if ($CommandLine -match '^\s*"([^"]+)"\s*(.*)$') { return $Matches[1], $Matches[2] } if ($CommandLine -match '^\s*(.+?\.exe)\s*(.*)$') { return $Matches[1], $Matches[2] } return $CommandLine, '' } $okCodes = 0, 1605 # 1605 = already gone $rebootCodes = 1641, 3010 $matched = @(Get-UninstallEntry | Where-Object { $_.DisplayName -like $DisplayName -and "$($_.Publisher)" -like $Publisher -and $_.SystemComponent -ne 1 -and ($_.UninstallString -or $_.QuietUninstall) }) if ($matched.Count -eq 0) { Write-Verbose "Nothing matching '$DisplayName' is installed." exit 0 } if ($matched.Count -gt 1 -and -not $AllowMultiple) { $list = ($matched | ForEach-Object { "$($_.DisplayName) $($_.DisplayVersion)" }) -join '; ' Write-Error "'$DisplayName' matches $($matched.Count) entries ($list). Tighten the name or add -AllowMultiple." exit 1 } $results = foreach ($entry in $matched) { $productCode = if ($entry.KeyName -match '^\{[0-9A-Fa-f\-]{36}\}$') { $entry.KeyName } elseif ("$($entry.UninstallString)" -match '\{[0-9A-Fa-f\-]{36}\}') { $Matches[0] } if ($productCode -and ($entry.WindowsInstaller -eq 1 -or "$($entry.UninstallString)" -match 'msiexec')) { $log = Join-Path $LogPath ("Uninstall_{0}.log" -f ($productCode -replace '[{}]')) $file = Join-Path $env:SystemRoot 'System32\msiexec.exe' $arguments = "/x $productCode /qn /norestart REBOOT=ReallySuppress /L*v `"$log`"" } elseif ($entry.QuietUninstall) { $file, $arguments = Split-CommandLine $entry.QuietUninstall } elseif ($SilentArgs) { $file, $arguments = Split-CommandLine $entry.UninstallString $arguments = "$arguments $SilentArgs".Trim() } else { Write-Warning "$($entry.DisplayName) has no quiet uninstall. Rerun with -SilentArgs for its installer type." [pscustomobject]@{ DisplayName = $entry.DisplayName; Version = $entry.DisplayVersion; ExitCode = 1; Status = 'NoSilentOption' } continue } if (-not $PSCmdlet.ShouldProcess("$($entry.DisplayName) $($entry.DisplayVersion)", "Run: $file $arguments")) { continue } try { $proc = Start-Process -FilePath $file -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden -ErrorAction Stop $code = $proc.ExitCode } catch { Write-Warning "Couldn't start the uninstaller for $($entry.DisplayName): $($_.Exception.Message)" $code = 1 } $status = if ($okCodes -contains $code) { 'Removed' } elseif ($rebootCodes -contains $code) { 'RebootRequired' } else { 'Failed' } [pscustomobject]@{ DisplayName = $entry.DisplayName; Version = $entry.DisplayVersion; ExitCode = $code; Status = $status } } $results $failures = @($results | Where-Object Status -in 'Failed', 'NoSilentOption') if ($failures) { exit $failures[0].ExitCode } if ($results | Where-Object Status -eq 'RebootRequired') { exit 3010 } exit 0