1
votes

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.

2
BTW, in editing your question I noticed that it's not clear if you really need a loop at all. Is it possible that multiple keys might match *Cisco*TSP*, and you want to execute the uninstall commands for all of them, or is there going to be only one on each computer? In the latter case, I don't see the need to use either ForEach-Object or foreach, since you're only retrieving the data of a single string value from the registry. - Adi Inbar
On each computer where i want to run this script installed only one Cisco product, thus search query should work - mnovak
On second thought, you still need the iteration, since you don't know the name of the key and need to find it with a where filter. However, see my updated answer. - Adi Inbar

2 Answers

0
votes

There are several issues here.

  1. 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\...')) {
    
  2. Piping each element to Get-Item is superfluous. You can pipe directly to Get-ItemProperty.

  3. 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.)

  4. 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.)

0
votes

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 '" .*','"'