shorter/cleaner way of writing this

I'm wondering if there's a nicer way of looking for vlc's uninstaller executable in both 'program files' and 'program files (x86)'. At the moment I've defined the expected location of the executable but it seems a bit static.

$VLC_Uninstall = @( 
  'C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe',
  'C:\Program Files\VideoLAN\VLC\uninstall.exe'
)

Foreach ($uninstall in $VLC_Uninstall) {
    if (test-path $uninstall) {
    #do uninstall using $uninstall
    }
}

Posted Jun 20, 2017, 12:50 PM UTC · edited

1129

You've got this about as clean as you can get. One thing, you've got a comma after the 2nd entry in the $VLC_Uninstall array, which will throw an error when you try to make the declaration.

5
tapthrowOP•

ah yes initially I had a third entry in there which I removed and forgot about that extra comma.

1
KevMarBlogger•

You can do test-path and resolve-path with wildcards.

4
SaladProblems•

Does it install like a normal program? Usually you'd check the registry for the uninstall key and then execute the executable listed.

3
tapthrowOP•

ah yes! for some reason that slipped my mind even though the registry is used for detection purposes (sccm).

It does behave like a standard app. Keys are written to -HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\VLC media player or under the wow6432 node if using 64bit OS.

2
MPS-Tom•

Instead of hard coding the VLC Uninstall paths, you could grab them from the Reg. Sure, this complicates the process a bit, but it means you're only grabbing VLC uninstall paths stored in the reg, which should be valid. Plus, if VLC isn't installed in Program Files, it'll have the correct uninstall path. So no guessing where its installed!

something like:

# Get all Uninstall items from the Reg store as pipeline variable.
Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -PipelineVariable p | 
# Get each entries properties
Get-ItemProperty | 
# Select the properties
Select-SelectObject @{Name="Path";Expression={$p.name}},Displayname,DisplayVersion,Publisher,InstallDate,InstallLocation,Comments,UninstallString | 
#Filter the list based on items display name.
Where-Object {$_.DisplayName -like "*VLC*"}

You'll get an object back that you can then use in part of your script.

Path            : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\VLC media player
DisplayName     : VLC media player
DisplayVersion  : 2.2.6
Publisher       : VideoLAN
InstallDate     :
InstallLocation : C:\Program Files\VideoLAN\VLC
Comments        :
UninstallString : C:\Program Files\VideoLAN\VLC\uninstall.exe

So your resulting script will look like this:

# Get all Uninstall items from the Reg store as pipeline variable.
$VLC_Uninstall = Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall -PipelineVariable p | 
# Get each entries properties
Get-ItemProperty | 
# Select the properties
Select-object @{Name="Path";Expression={$p.name}},Displayname,DisplayVersion,Publisher,InstallDate,InstallLocation,Comments,UninstallString | 
#Filter the list based on items display name.
Where-Object {$_.DisplayName -like "*VLC*"}

# Loop through the results
foreach($uninstall in $VLC_Uninstall){
    # Test to ensure the path is valid
    if(test-path $uninstall.uninstallstring){
        # Start the uninstall process silently
        Start-Process -FilePath $uninstall.uninstallstring -ArgumentList "/S"
    }
}

This could be adapted to create a cmdlet that lets you uninstall a software based on its displayname (not just VLC).

This approach would cover ALL non-standard installation paths. So if your worried about people who might have installed VLC in to "C:\trying to hid this from the admin so he cant uninstall it on me\vlc" you'll get it ;)

3
tapthrowOP•

Thanks MPS-Tom - this pretty much covers the criteria I was after. I generally use the registry as a detection method for most applications as it covers most cases to indicate a product is installed but I didn't think about querying the same registry paths to get the uninstall strings.

1
MPS-Tom•

Glad to help!

1
tapthrowOP•

I ended up using your code as a base but this time for the detection of a particular product that has two seperate registry entries if you upgrade one version over the top (only one remains after a reboot).

This is used as a detection script within sccm. So far it's working as intended but I was wondering if it could be improved in any way in terms of layout or efficiency?

# Registry paths for uninstall data
$registry = @(
    'HKLM:\SOFTWARE\wow6432node\Microsoft\Windows\CurrentVersion\Uninstall', 
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
)

# Search for product
$Product = 'symantec endpoint protection'

# Version to test against 
$ProductVersion = '14.0.2349.0100'

# Test-Path registry paths, look for Product uninstall string 
$Uninstall =
$registry | ForEach-Object {
    if (Test-Path $_ ) {   
        Get-ChildItem $_ |
        Get-ItemProperty | 
        Select-object Displayname,DisplayVersion,Publisher,InstallDate,InstallLocation,Comments,UninstallString | 
        Where-Object {$_.DisplayName -like "*$Product*"}        
    }
}

# If upgrading from a previous version you'll have multiples entries for Symantec until windows restarts
$uninstall | ForEach-Object {
    if ([version]$_.displayversion -ge [version]$ProductVersion) {
    write-host "installed"
    } else {}
}
1
MPS-Tom•

This looks pretty clean to me. Maybe others will have input though. One thing I've learned about recently that I've been working in to my scripts are using PSD1 files as a config file for my scripts.

You can use Import-LocalizedData to read that PSD1 file and store the contents as a variable.

So in your example, you could take everything above # Test-Path registry paths, look for Product uninstall string and store it as a hashtable in a config.psd1 (could be named anything) file.

That file would look like this:

@{
    registry = @(
        'HKLM:\SOFTWARE\wow6432node\Microsoft\Windows\CurrentVersion\Uninstall', 
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
    ),
    Product = 'symantec endpoint protection',
    ProductVersion = '14.0.2349.0100'
}

Then in your script file you would do

$config = Import-LocalizedData -baseDirectory $PSScriptRoot -Filename config.psd1

and then where you refrenced your variables, you would need to extend them as properties off of $config like:

[version]$config.ProductVersion and {$_.DisplayName -like "*$($config.Product)*"}

This gives you the ability to update the product you're trying to grab, with out actually having to edit the script its self. You could have multiple configs for multiple types of software with out having to duplicate the core code of the script.

You could go a step further and add a param to your script that takes the path of a config file, so you can call your script with the config file path as an argument in SCCM.

1
w1gray•

Could always put them in a CSV and import it using Import-Csv

2
tapthrowOP•

I think this would make it even more clunky having to work with an additional external file.

2
Soxcks13•

I don't know how scalable you want this to be... honestly, if code works for the task at hand, leave it alone. If you're 110% sure you're going to use it again for a similar function, then re-factor it as a function.

If you want to re-use this for other programs, most programs install under the two directories you mentioned. So you could take the common "C:\Program Files" and "C:\Program Files (x86)" out of the array and define them as a static "installation path" array (which you can add to in future). Then instead of using an array for the programs being uninstalled, you could use a hashtable where the key is the folder it would be in and the value is the product-specific uninstall file.

... But you probably now realize that what I said before holds true :-) Your code works! In the Perl language, we say TMTOWTDI (There's More Than One Way To Do It), which is a principle that means 1+ 3 == 2 + 2, and neither statement is "better".

2
tapthrowOP•

I was thinking of the outliers where the user has chosen to install VLC other than Program Files\VideoLAN\VLC but in a domain environment that should be very few if any. This is not really specific to VLC but just thinking about ways to make my code more scalable and robust.

1
kittehprimo•

Alternatively you could use the Get-WMIObject cmdlet to get the win32_product object class, filter that for what you need then call $app.uninstall() and not have to worry about locating where the uninstall.exe is, but either way those will do the same thing.

2
Lee_Dailey•

howdy kittehprimo,

my understanding is that the WMI method is strongly disrecommended. it forces a reconfig of every MSI on the system every time it is accessed. at least, i think that is what folks have found. [blush] i can't recall it exactly - nor can i find it with my search-fu.

take care,
lee

1
kittehprimo•

Ive been using it like that for months and haven't had a problem, all the get-wmi object method entails is

$app = Get-WmiObject -Class win32_product  | Where-Object -Property name -Like -Value NAME

$null = $app.uninstall()        

There are of course better ways to do it like using IDs, but thats the quick and dirty way and it works great for me, and I also havent seen anything on the interwebs where that would force a "reconfig", its not doing anything to any other object other than searching through properties then calling a method on what you want.

Could be wrong, but i doubt it does what you suggested.

2
tapthrowOP•
2
Lee_Dailey•

howdy tapthrow,

thank you for the link! [grin] i couldn't find anything at all ... [blush]

take care,
lee

1
Lee_Dailey•

howdy kittehprimo,

i didn't mean that it failed to work. [grin]

please look at the post by tapthrow for some data to back up my admittedly vague statement. it really is a bad idea if it can be avoided.

here's a direct link to what tapthrows link refers to ...
Event log message indicates that the Windows Installer reconfigured all installed applications

take care,
lee

1
kittehprimo•

Those articles mostly reference using it in group objects, my point was more about an interactive/script session. We're a small shop so we dont have AD set up for personal machines, and i havent noticed any of the problems described in either article.

YMMV apparently.

2
Aperture_Kubi•

Only shorter way I can think of is to use foreach-item.

$VLC_Uninstall | foreach-item{ 
    if (test-path $_) 
    then{#stuff}
}

But that's just trimming characters.

2
NathanielArnoldR2•
$args = '/silent'

'C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe',
'C:\Program Files\VideoLAN\VLC\uninstall.exe' |
  Where-Object {Test-Path $_} |
  ForEach-Object {& $_ $args}
1
tapthrowOP•

This is a little shorter but I find it a little harder to look at personally as I'd still class myself as a rookie in regards to powershell.

1
jheinikel•

Here is a way to use a switch statement. Not sure if its shorter/cleaner, but another way to look at it.

$Paths = "C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe","C:\Program Files\VideoLAN\VLC\uninstall.exe" | where{(Test-Path $_) -eq $True}
Switch ($Paths){
    "C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe" {Start-Process "C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe" -Wait}
    "C:\Program Files\VideoLAN\VLC\uninstall.exe" {Start-Process "C:\Program Files\VideoLAN\VLC\uninstall.exe" -Wait}
}

A few more lines, but cleaner:

$Path1 = "C:\Program Files (x86)\VideoLAN\VLC\uninstall.exe"
$Path2 = "C:\Program Files\VideoLAN\VLC\uninstall.exe"
$Paths = $Path1,$Path2 | where{(Test-Path $_) -eq $True}
Switch ($Paths){
    $Path1 {Start-Process $Path1 -Wait}
    $Path2 {Start-Process $Path2 -Wait}
}
1