UnInstall apps unattended

Hello everyone;

I have this script to uninstall apps unattended, this one gets the apps to remove, but has a error just the last sentence and I don’t understand why.

I just realized that the each where the argument is contains other values.

I leave the script to see what you see that I do not see.

From already thank you very much.

$AppstoRemove =  @(
'iManage Work FileSite (x86)'
'Workshare Professional'
'Drafting Assistant'
'Westlaw Solutions'
'Microsoft Office Professional Plus 2016')
 
foreach ($App in $AppstoRemove)  {
    $RegApp = Get-Itemproperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' |
        Select-Object DisplayName, DisplayVersion, UninstallString |
        Where-Object { $_.DisplayName -match "^*$app*"}
    if($RegApp) {
        if($RegApp.Uninstallstring -match '^msiexec')   {
             $UninstallString = "$($RegApp.UninstallString -replace '/I', '/X' ) /v /qn /norestart"
        } else {
             $UninstallString = $RegApp.UninstallString
        }
        $RegApp.UninstallString
        $pos = $UninstallString.ToLower().IndexOf('.exe')
        $exe =  $UninstallString.Substring(0,$pos + 4)
        $args = $UninstallString.Substring($pos + 4).trim()
        Write-Verbose "Starting '$exe' with arguments '$args'"
        Start-Process -FilePath $exe -ArgumentList $args -NoNewWindow -Wait  
    } else {
        "We did not find $App"
    }
}

This is the image:

Argument

I really appreciate you help.

Have a great day.

You have an invalid regex pattern for -match. Try

 -match “^$app”

Thank you for your reply.

If you make that change the script will not find any apps.

The search process is fine, you have no problem.

The problem is when the chain is assembled to execute the uninstallation, inside the chain includes the ID that is the part that goes between braces, and I don’t know how to remove it.

Look at the image of my first message.

You do realize you are ONLY getting 32bit applications? You should also include:

'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'

Then, get rid of duplicates and act on the remainder.

Hello Tonyd,

Yes, I do.

Thanks for your reply. :smiley:

Any chance your errors could be a result of attempting to uninstall the wrong version?

I spent more time then I care to think about trying to figure out the best way to get an “accurate” listing of software from both local and remote systems. In all that research, I did run across situations where the uninstall string for the “wrong” version would choke.

Just my $.02. Have a great rest of your day :slight_smile:

Are you saying that apps are not found?

First, you should use wildcard pattern here instead of regex, for example:

Where-Object { $_.DisplayName -like "*$app*"}

Secondly immediately after that instead of if($RegApp) you should verify the result is singular:

$Count = ($RegApp | Measure-Object).Count

if ($Count -gt 1)
{
       # This will happen when multiple registry entries are found,
       # in which case a user must be precise on what to remove
       Write-Error "Search pattern is too loose, please update search string for '$App'"
}
elseif ($Count -eq 1)
{
       # Uninstall code goes here
}
else
{
       # This error or case may also mean that search string is too strict!
       # not necessarily that app is not found
       Write-Error "Unable to find app '$App', search string might be too strict"
}

Error message is important because “not found” or “search invalid” are 2 very different things,
you should inform the user how to fix the problem.
In this case $RegApp from $AppstoRemove might be too strict, and this may be the reason why it is not found.

Having this construct in place, next step is to implement multiple registry locations to handle more cases:

Case, 64 bit system programs:

"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"

Case, 32 bit system programs:

"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"

Case, programs installed in user profile:

"$HCU\Software\Microsoft\Windows\CurrentVersion\Uninstall"

Where $HCU is registry root key of the user, aka. “Current user”.

To learn this key you must not loop trough registry HKCU hive, instead your function should implement -User parameter so that specific user key is then looked in HKCU hive.
This is important because multiple users may have same program installed each on it’s own.

for example your function or script starts with:

param(
    [Parameter()]
    $User
)

When you do this you should combine result into a variable, ex:

$Keys = @(
      "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
      "SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
      "$HCU\Software\Microsoft\Windows\CurrentVersion\Uninstall"
)

And then handle all keys:

foreach ($App in $AppstoRemove) {
    foreach ($Key in $Keys)
    {
           $RegApp = Get-Itemproperty $Key |
           Select-Object DisplayName, DisplayVersion, UninstallString |
           Where-Object { $_.DisplayName -like"*$app*"}

           # the rest of code...
    }
}

Keep in mind that in double foreach loop sample above you must not show errors until all keys have been processed to avoid spam or misinformation.

How do you learn registry key that is specifyed by -User parameter?

param(
    [Parameter()]
    $User
)

$NTAccount = New-Object -TypeName System.Security.Principal.NTAccount($User)
$HKU = $NTAccount.Translate([System.Security.Principal.SecurityIdentifier]).ToString()

You have the idea, now rewrite your function according to all this.

EDIT:
There is another issue with your code:

if($RegApp.Uninstallstring -match '^msiexec')   {
      $UninstallString = "$($RegApp.UninstallString -replace '/I', '/X' ) /v /qn /norestart"
} else {
      $UninstallString = $RegApp.UninstallString
}

This is wrong because if uninstall string does not start with “msiexec” you later populate $exe variable anyway, which cant bring any good beyond odd error messages.

Because uninstall string may be path only or something completely unusual, in which case you need to handle that case by parsing the string and implementing alternative methods to uninstall program.

Thank you for your reply.

Actually, I fix it…

This is the final result.

Write-Host "Removing Apps"
foreach ($App in $AppstoRemove) 
 {
    $RegApp = Get-Itemproperty 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' |
        Select-Object DisplayName, DisplayVersion, UninstallString, PSChildName |
        Where-Object { $_.DisplayName -match "^*$App*"}
    if($RegApp) {
        if($RegApp.Uninstallstring -match '^msiexec')
        {
             $UninstallString = "$($RegApp.UninstallString -replace '/I', '/X' ) /qn /passive /norestart"
        } else {
             $UninstallString = $RegApp.UninstallString
        }
        $RegApp.UninstallString
        $pos = $UninstallString.tolower().IndexOf('.exe')
        $exe =  $UninstallString.Substring(0,$pos + 4)
        $args = $UninstallString.Substring($pos + 4).trim()
        $exe,$args = ($RegApp.UninstallString).split(' ')
        $args -replace '/I','/X'
        Start-Process $Exe -ArgumentList "$args" -NoNewWindow -Wait
    } else {
        "We did not find $App"
    }
}

Feel free to improve it....

Happy holidays.

My eyes started to bleed before I spotted any functional difference in your fixed version of code compared to non working code from your original post.

But I’m glad to hear it’s fixed :slightly_smiling_face:

:rofl: :rofl: :rofl:

Thank you for your support.