Search code examples
powershellclosuresvariable-expansion

Powershell closure for .Where() in a string variable


I have a Generic List of hash tables derived from Uninstall keys in the registry, which I need to search. Each list member is an ordered dictionary containing these keys. The actual value of each key is defined separately since some are values direct from the registry and some are calculated.

[Ordered]@{
        displayName = 'DisplayName'
        displayVersion = 'DisplayVersion'
        installSource = 'InstallSource'
        publisher = 'Publisher'
        quietUninstallString = 'QuietUninstallString'
        uninstallParam = 'UninstallParam'
        uninstallPath = 'UninstallPath'
        uninstallString = 'UninstallString'
        installDate = 'InstallDate'
        keyID = '$keyID'
        keyName = '$keyName'
        keyGUID = '$keyGUID'
        keyPath = '$keyPath'
        architecture = '$architecture'
    } 

Once I have this collection in a variable $rawUninstallKeys I need to search it, and the search is convoluted because Autodesk tends to have multiple uninstall keys with the same or similar data, but only one will work. For example there will be a GUID key with a DisplayName of Autodesk Revit 2025 that provides a uninstall string that actually works and another GUID with a DisplayName of Revit 2025 that does nothing. So to extract the correct uninstallString I need to search for a key that matches the first pattern AND has a second key that matches the second pattern. And there are actually a ton of variations because Autodesk just sucks at any kind of consistency. So, I have this working now where I just do a foreach on the raw keys looking for the first pattern, and if I find it I derive the second pattern (in this case I replace 'Autodesk ' with $null) and then search with another foreach. And it does work, but dear god it's slow. So, I want to try using the .Where() method on the list to get the .NET optimization. But that means building a collection of closures to apply.

So, this will work

$rawUninstallKeys.Where({$_.publisher -like "Autodesk*"})

but I am struggling with how to get the closure into a variable that still allows $_ to expand appropriately. This is further complicated by the fact that my closure might simply be {$_.DisplayName} when I just need to see if that value exists at all, or {-not $_.DisplayName} when I only want a key that doesn't contain that property, or the first example where I need to compare the properties value to another value, which may be a wildcard. Effectively what I kind of need to do is expand the variable in the closure string within the .Where(), but this doesn't work

.Where({$($ExecutionContext.InvokeCommand.ExpandString($closure))})

Solution

  • Per @sirtao's comment, you don't need to stringify and then de-stringify your scriptblock - you can just pass it directly to .Where():

    Let's set up some test data:

    $rawUninstallKeys = [System.Collections.Generic.List[System.Collections.IDictionary]]::new()
    
    $rawUninstallKeys.Add(
        [ordered] @{
            publisher   = "Autodesk Revit 2025"
        }
    )
    
    $rawUninstallKeys.Add(
        [ordered] @{
            displayname = "displayname"
            publisher   = "Revit 2025"
        }
    )
    

    and then if you've got a series of filters you want to apply:

    $filters = @(
        { $_.Publisher -like 'Autodesk*' },
        { $_.DisplayName }
    )
    

    you can just do this:

    foreach( $filter in $filters )
    {
        write-host "testing for $filter"
        $rawUninstallKeys.Where($filter) | ft | out-string
    }
    

    which gives the output:

    testing for  $_.Publisher -like 'Autodesk*'
    
    Name                           Value
    ----                           -----
    publisher                      Autodesk Revit 2025
    
    testing for  $_.DisplayName
    
    Name                           Value
    ----                           -----
    displayname                    displayname
    publisher                      Revit 2025
    

    You can extend this to just run the filters in order until you find the first one that returns a result, if that's what you need...