Please see the following command I'm running to obtain the GUID of a particular application. It works as expected as you can see from the screenshot underneath.
Get-ChildItem -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall |
Get-ItemProperty |
Where-Object { $_.DisplayName -match "Dell SupportAssist" } |
Select-Object -Property DisplayName, UninstallString
What I'd like to do is call the uninstall command from the output GUID. So something like:
MsiExec.exe /X{OUTPUT}
or for this example
MsiExec.exe /X{95BD6E30-2B18-4FB0-B5AE-8250E5584831}
Can this be done?
I don't want to use a huge VBS script that I can't read that you found online, I'd like to understand it bit by bit ;)
I don't want to use Get-WmiObject Win32_Product - See https://gregramsey.net/2012/02/20/win32_product-is-evil/.
Your registry query returns a custom object with the uninstall command string as one of its properties. To invoke that command string you can create a scriptblock from that string and then invoke that scriptblock (e.g. via the call operator). Pipe the output of your PowerShell statements into a ForEach-Object loop like this:
... | ForEach-Object {
& ([Scriptblock]::Create($_.UninstallString))
}
You may try as #Ansgar mentioned something like:
Code edited with example for Dell Power*:
$DellSoftware = Get-ChildItem -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall |
Get-ItemProperty |
Where-Object { $_.DisplayName -like "dell power*" } |
Select-Object -Property DisplayName, UninstallString
$DellSoftware.UninstallString -match '(?<MSI>\w+.exe) (?<Args>\S+)'
Start-Process -FilePath $Matches.MSI -ArgumentList $Matches.Args
If you check the $Matches variable it should contain:
Name Value
---- -----
Args /X{18469ED8-8C36-4CF7-BD43-0FC9B1931AF8}
MSI MsiExec.exe
0 MsiExec.exe /X{18469ED8-8C36-4CF7-BD43-0FC9B1931AF8}
It returns a hash table with the grouped matches. This is why I am being able to separate the executable into the MSI key and the arguments into the args key. Then starting the process calling the executable as a FilePath param and providing the args for the ArgumentList param.
Then the result:
What is happeinig here is that the property UninstallString, returns, as you may guess - String. You need to tell to the PowerShell to treat this string as an expression.
To do so you need to use the & symbol in front, putting the statement, that you would like to execute in round brackets.
Same behaviour could be observed, when you try to launch a file from a location that contains white space. Because of it, powershell puts it in brackets, and to be able to execute it, puts the & in front - i.e. & 'C:\Program Files\'
Hope that clarifies the case :)
If it's an msi, it's very easy with the package commands:
get-package *dell* | Uninstall-Package -whatif
EDIT:
If you want the exact command... (-force in case it asks to install nuget)
Uninstall-Package 'Dell SupportAssist' -Force
Guys my colleague found this on another Stackoverflow post:
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "Dell SupportAssist" } | select UninstallString
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "Dell SupportAssist" } | select UninstallString
if ($uninstall64) {
$uninstall64 = $uninstall64.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall64 = $uninstall64.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall64 /quiet" -Wait
}
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall32 /quiet" -Wait
}
This works perfectly and includes switches for silent uninstall - It doesn't seem to use Win32_Product.
Any reason why this wouldn't be appropriate rolling out en masse compared to the suggestions above?
Related
I am creating a script where it searches through a bunch of nested folders to find the newest version of a software, which are in .msi form. My code currently can find the file and output it, but is not able to run the file.
I can use Select in the last line for the ForEach to output the correct file but when I change it to Start-Process, I get bombarded by errors.
$path="S:\\Releases\\Program"
$NoOfDirs=Get-ChildItem $path -Directory
ForEach($dir in $NoOfDirs){
Get-ChildItem "$path\$($dir.name)" -File -Recurse |
Where-Object {$_.LastWriteTime -gt ([DateTime]::Now.Adddays(-1))} |
Select-Object #{l='Folder';e={$dir.Name}},Name,LastWriteTime |
Sort-Object -pro LastWriteTime -Descending |
Start-Process -First 1
}
Is there a different command I should be using when running .msi files?
Since your code has to "search through a bunch of nested folders", I'd recommend using the -Recurse switch on Get-ChildItem.
Also use the -Filter parameter to have the search limited to .MSI files.
Something like this:
$path = "S:\Releases\Program"
$refDate = (Get-Date).Adddays(-1)
Get-ChildItem -Path $path -Filter '*.msi' -File -Recurse |
Where-Object {$_.LastWriteTime -gt $refDate} |
ForEach-Object {
# create the arguments for msiexec.exe.
# to find out more switches, open a commandbox and type msiexec /?
$msiArgs = '/i', ('"{0}"' -f $_.FullName), '/qn', '/norestart'
$exitCode = Start-Process 'msiexec.exe' -ArgumentList $msiArgs -NoNewWindow -Wait -PassThru
if ($exitCode -ne 0) {
# something went wrong. see http://www.msierrors.com/tag/msiexec-return-codes/
# to find out what the error was.
Write-Warning "MsiExec.exe returned error code $exitCode"
}
}
I wonder if you can help please? I have the need to write a Powershell script to execute an MSI script.
I also need to set a time out on the process (as the MSIs we're given sometimes hang).
I've seen that you can acheive this by using the Start-Job/ Wait-Job process
Obviously the code below in in a severe state of butchery currently
Thanks in advance
$timeoutSeconds = 20
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "My File" } | select UninstallString$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "Vix.Cbe.SalesTransaction" } | select UninstallString
Echo "uninstall32 :" $uninstall32
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
$32p = #("/X", "$uninstall32", "/b")
}
Echo "uninstall32 :" $uninstall32
Echo "u32 :" $32p
$32j = Start-Job msiexec -ArgumentList $32p
if (Wait-Job $32j -Timeout $timeoutSeconds) { Receive-Process $32j }
Remove-Process -force $32j
You don't have to mess with jobs to do that. Here is the easy way:
start Notepad -PassThru | select -ExpandProperty id | set id
sleep 60
kill $id -ea 0
This might not work if app spawns another app and exits as the Id will be wrong. In that case you will probably have to hunt it in the process list or via cmd line params.
Thanks to majkinetor I managed to alter the code sufficiently to acheive my goal.
The only thing is that obviously whether the process is still actively uninstalling or not, its killed after the TOSecs value.
This should be good enough for what I need.
So to explain for anyone else looking for a similar solution:
This process checks both 32 bit and 64 bit Registry entries for an MSI similar to the ServiceName (Urbancode Deploy parameter is the '${p:ServiceName}' passed to the script at runtime)
If it finds an entry it'll execute the uninstall code for the specific 32/64 MSI
/x = Uninstall
$uninstall64/32 = GUID of the Uninstall part to the MSI
/nq = quiet uninstall with no GUI (infact in isolation testing you get a Yes/No dialogue)
The uninstall will run for the amount of seconds that you set in $TOSecs
Hope this helps someone else
$TOSecs = 30
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "'${p:ServiceName}'" } | select UninstallString
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" | foreach { gp $_.PSPath } | ? { $_ -match "'${p:ServiceName}'" } | select UninstallString
if ($uninstall64) {
$uninstall64 = $uninstall64.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall64 = $uninstall64.Trim()
Write "Uninstalling 64bit " '${p:ServiceName}'
start-process "msiexec.exe" -arg "/X $uninstall64 /nq" -PassThru |
select -ExpandProperty id | set id
#Echo "proc id = "$id
sleep $TOSecs
kill $id -ea 0
}
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling 32bit " '${p:ServiceName}'
start-process "msiexec.exe" -arg "/X $uninstall32 /nq" -PassThru |
select -ExpandProperty id | set id
#Echo "proc id = "$id
sleep $TOSecs
kill $id -ea 0
}
I got this script from here and it work for uninstalling an application, but only when -match is used instead of -like in the first 2 lines, even when I use the entire app name.
The app's name includes the version, so I'd like to wildcard the name in the script to support "MyApp 2.4.1", etc. Thanks!
$uninstall32 = gci "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" |
foreach { gp $_.PSPath } | ? { $_ -like "MyApp*" } | select UninstallString
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" |
foreach { gp $_.PSPath } | ? { $_ -like "MyApp*" } | select UninstallString
if ($uninstall64) {
$uninstall64 = $uninstall64.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall64 = $uninstall64.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall64 /qb" -Wait
}
if ($uninstall32) {
$uninstall32 = $uninstall32.UninstallString -Replace "msiexec.exe","" -Replace "/I","" -Replace "/X",""
$uninstall32 = $uninstall32.Trim()
Write "Uninstalling..."
start-process "msiexec.exe" -arg "/X $uninstall32 /qb" -Wait
}
Get-ItemProperty produces an object, not a string. Check the DisplayName property instead of the object itself. You should also expand the uninstall string, so you don't need to use $uninstall64.UninstallString later.
$uninstall64 = gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" |
foreach { gp $_.PSPath } |
? { $_.DisplayName -like "MyApp*" } |
select -Expand UninstallString
Consider the following string " MyApp 2.3.4" while will look at the important difference between the two examples you refer to:
Like ? { $_ -like "MyApp*" }
Match ? { $_ -match "MyApp*" }
-Like is looking for a string starting with "MyApp" followed by anything. -Match is looking for the text "MyApp" followed by any character. The -Like would fail since there is a preceding space. -Match treats "MyApp*" as a regex string which looks for "MyApp" followed by any character. In this case it cares not about the space to it matches. I suspect the -match would fail if you changed it too ? { $_ -match "^MyApp*" } as the caret says start of string.
If you want -like to work in this case you should change it to ? { $_ -like "*MyApp*" }
Important
While I am correct about why your comparison was not working Ansgar answer addresses the reason this issue was happening to you in the first place.
With help of Powershell I need to find registry key, where Value Displayname like 'Cisco', and get from this key data from Value name 'Uninstallstring'.
I know this sounds somewhat strange, but this application has a different uninstall string on each computer.
So
ForEach-Object -InputObject (Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall') {
$single_item = $_ | Get-Item
$single_item_properties = $single_item | Get-ItemProperty | Select-Object -Property DisplayName,UninstallString | Where-Object {($_.DisplayName -like '*Cisco*TSP*')}
$uninstall_str = ($single_item_properties | Select UninstallString)
$str_to_execute=$uninstall_str.UninstallString -replace '" .*','"'
$str_to_execute
Start-Process -FilePath $str_to_execute -ArgumentList '-s','-f1"\\sandbox\Common.Installs\Utils\un.iss"' -Wait -PassThru
}
This script gives us the error
UninstallString
"C:\Program Files (x86)\InstallShield Installation Information{01A05F96-E34D-4308-965C-65DCA4AF114D}\setup.exe"
Start-Process : This command cannot be executed due to the error: The system cannot find the file specified.
The problem is that the result is in not String type.
And I can't convert it into String.
There are several issues here.
Although ForEach-Object does have the -InputObject parameter, it's primarily designed to take pipeline input. As the documentation says,
When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value—even if the value is a collection that is the result of a command, such as –InputObject (Get-Process)—is treated as a single object. Because InputObject cannot return individual properties from an array or collection of objects, it is recommended that if you use ForEach-Object to perform operations on a collection of objects for those objects that have specific values in defined properties, you use ForEach-Object in the pipeline
In your code, the scriptblock gets executed only once, and $single_item is actually an array containing the entire output of Get-ChildItem. In order to iterate over the results one element at a time, you need to either pipe the output to ForEach-Object...
Get-ChildItem 'HKLM:\software\microsoft\...' | ForEach-Object {
...or better yet, use the foreach control structure, which generally runs faster (though it uses more memory):
foreach ($single_item in (Get-ChildItem 'HKLM:\software\microsoft\...')) {
Piping each element to Get-Item is superfluous. You can pipe directly to Get-ItemProperty.
You don't need to select a property in order to filter on it with Where-Object. So, it's superfluous to use Select-Object -Property DisplayName,UninstallString, which creates a PSCustomObject with two properties, and then retrieve the UninstallString property of that PSCustomObject later in the code. Just filter with Where-Object first, then get the value of UninstallString with | Select -ExpandProperty UninstallString.
(See this answer for an explanation of the reason for using the -ExpandPropery switch.)
Using -ExpandProperty, you'll get errors if any keys are returned that do not have an UninstallString property, so you might want to add -ErrorAction SilentlyContinue to your Select-Object statement.
Putting it all together:
foreach ($single_item in (Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall')) {
$uninstall_str = $single_item `
| Get-ItemProperty `
| ?{$_.DisplayName -like '*Cisco*TSP*'} `
| Select-Object -ExpandProperty UninstallString -ErrorAction SilentlyContinue
$str_to_execute = $uninstall_str -replace '" .*','"'
Start-Process -FilePath $str_to_execute -ArgumentList '-s','-f1"\\sandbox\Common.Installs\Utils\un.iss"' -Wait -PassThru }
}
If there's guaranteed to be no more than one matching item, you can do it more compactly like this:
$str_to_execute = (
Get-ChildItem 'HKLM:\software\microsoft\windows\currentversion\uninstall' `
| Get-ItemProperty `
| ?{$_.DisplayName -like 'Microsoft*'}
).uninstallstring $uninstall_str -replace '" .*','"'
The more compact version will actually work even if there are multiple matching keys in PowerShell v3+, but in v2 it would return null if there's more than one.
BTW, ? is an abbreviation for Where-Object. Similarly, % is an abbreviation for ForEach-Object (only if you use it as a filter in a pipeline, but that's how you should be using it anyway.)
The output you show does not seem to match the code you provided. When I execute your code I get the uninstallstring based on my own search. When I first saw the output I thought "Oh... he forgot that he was passing an object with a property UninstallString to Start-Process". But when I looked at your code I could see that you accounted for that by doing $uninstall_str.UninstallString. So I am not sure what is exactly wrong but one improvement would be to use -ExpandProperty of Select-Object
$uninstall_str = $single_item_properties | Select -ExpandProperty UninstallString
$str_to_execute=$uninstall_str -replace '" .*','"'
But we can make a one liner if you prefer.
$str_to_execute = ($single_item_properties).UninstallString -replace '" .*','"'
I run the following code using PowerShell to get a list of add/remove programs from the registry:
Get-ChildItem -path hklm:\software\microsoft\windows\currentversion\uninstall `
| ForEach-Object -Process { Write-Output $_.GetValue("DisplayName") } `
| Out-File addrem.txt
I want the list to be separated by newlines per each program. I've tried:
Get-ChildItem -path hklm:\software\microsoft\windows\currentversion\uninstall `
| ForEach-Object -Process { Write-Output $_.GetValue("DisplayName") `n } `
| out-file test.txt
Get-ChildItem -path hklm:\software\microsoft\windows\currentversion\uninstall `
| ForEach-Object {$_.GetValue("DisplayName") } `
| Write-Host -Separator `n
Get-ChildItem -path hklm:\software\microsoft\windows\currentversion\uninstall `
| ForEach-Object -Process { $_.GetValue("DisplayName") } `
| foreach($_) { echo $_ `n }
But all result in weird formatting when output to the console, and with three square characters after each line when output to a file. I tried Format-List, Format-Table, and Format-Wide with no luck. Originally, I thought something like this would work:
Get-ChildItem -path hklm:\software\microsoft\windows\currentversion\uninstall `
| ForEach-Object -Process { "$_.GetValue("DisplayName") `n" }
But that just gave me an error.
Or, just set the output field separator (OFS) to double newlines, and then make sure you get a string when you send it to file:
$OFS = "`r`n`r`n"
"$( gci -path hklm:\software\microsoft\windows\currentversion\uninstall |
ForEach-Object -Process { write-output $_.GetValue('DisplayName') } )" |
out-file addrem.txt
Beware to use the ` and not the '. On my keyboard (US-English Qwerty layout) it's located left of the 1.
(Moved here from the comments - Thanks Koen Zomers)
Give this a try:
PS> $nl = [Environment]::NewLine
PS> gci hklm:\software\microsoft\windows\currentversion\uninstall |
ForEach { $_.GetValue("DisplayName") } | Where {$_} | Sort |
Foreach {"$_$nl"} | Out-File addrem.txt -Enc ascii
It yields the following text in my addrem.txt file:
Adobe AIR
Adobe Flash Player 10 ActiveX
...
Note: on my system, GetValue("DisplayName") returns null for some entries, so I filter those out. BTW, you were close with this:
ForEach-Object -Process { "$_.GetValue("DisplayName") `n" }
Except that within a string, if you need to access a property of a variable, that is, "evaluate an expression", then you need to use subexpression syntax like so:
Foreach-Object -Process { "$($_.GetValue('DisplayName'))`r`n" }
Essentially within a double quoted string PowerShell will expand variables like $_, but it won't evaluate expressions unless you put the expression within a subexpression using this syntax:
$(`<Multiple statements can go in here`>).
I think you had the correct idea with your last example. You only got an error because you were trying to put quotes inside an already quoted string. This will fix it:
gci -path hklm:\software\microsoft\windows\currentversion\uninstall | ForEach-Object -Process { write-output ($_.GetValue("DisplayName") + "`n") }
Edit: Keith's $() operator actually creates a better syntax (I always forget about this one). You can also escape quotes inside quotes as so:
gci -path hklm:\software\microsoft\windows\currentversion\uninstall | ForEach-Object -Process { write-output "$($_.GetValue(`"DisplayName`"))`n" }
Ultimately, what you're trying to do with the EXTRA blank lines between each one is a little confusing :)
I think what you really want to do is use Get-ItemProperty. You'll get errors when values are missing, but you can suppress them with -ErrorAction 0 or just leave them as reminders. Because the Registry provider returns extra properties, you'll want to stick in a Select-Object that uses the same properties as the Get-Properties.
Then if you want each property on a line with a blank line between, use Format-List (otherwise, use Format-Table to get one per line).
gci -path hklm:\software\microsoft\windows\currentversion\uninstall |
gp -Name DisplayName, InstallDate |
select DisplayName, InstallDate |
fl | out-file addrem.txt
The option that I tend to use, mostly because it's simple and I don't have to think, is using Write-Output as below. Write-Output will put an EOL marker in the string for you and you can simply output the finished string.
Write-Output $stringThatNeedsEOLMarker | Out-File -FilePath PathToFile -Append
Alternatively, you could also just build the entire string using Write-Output and then push the finished string into Out-File.