Search code examples
powershellfunctionhashtablepowershell-7.3parameter-splatting

PowerShell: How can I pass a nested hash table to my function that accepts 3 parameters?


I created this function but I don't know how to make it work. here is the code:

function ProgramRegistry {
    param (
        
    [Parameter(Mandatory=$false)][HashTable]$HashTable,
    [Parameter(Mandatory=$false)][String]$AlertPath,
    [Parameter(Mandatory=$false)][String]$AlertName,
    [Parameter(Mandatory=$false)][String]$AlertValue
     )

    

     foreach ($AlertPath in $HashTable.Values){
        foreach($AlertName in $HashTable.Values){
            foreach($AlertValue in $HashTable.Values){
                          
  
    New-Item -Path $AlertPath -Force | Out-Null
    New-ItemProperty -Path $AlertPath -Name $AlertName -Value $AlertValue -PropertyType DWORD -Force
    }


                  
            }
        }
     }
 


$keys = [ordered]@{


    key1 = @{
        AlertPath = 'Path'
        AlertName = 'Name'
        AlertValue = 'Value'

    }

    key2 = @{

        AlertPath = 'Path'
        AlertName = 'Name'
        AlertValue = 'Value'

    }

    # and so on...

}

ModifyRegistry @keys

ModifyRegistry -AlertPath "path" -AlertName "name" -AlertValue "value"

I want to be able to call the function in 2 different ways (as shown in the script)

  1. either by defining its 3 parameters explicitly in one line.
  2. or by passing a nested hash table consisting of multiple objects each having the function's 3 parameters.

how can I achieve that?

I want to only modify the function and not the way I call it. I need to call it a bunch of times and want to keep the code for doing it as minimal as possible, like this ModifyRegistry @keys . it's okay if the function itself is complicated and long but I want calls to function to take very little code like that. instead of nested hash table, I could just call the function repeatedly but it'd be too much repeated code and that's what I want to avoid.


Solution

  • You may modify your function to accept a hashtable of hashtables. You just need to provide some logic to check if the hashtable received is a hashtable containing other hashtables that have the values you need or if it is instead a single hashtable containing the values you need. Also needed is to handle the other parameters still when not providing a hashtable. The example below shows how I would do this in an advanced function utilizing a begin, process, and end block. In the begin block we only need to create the collection object that we will use to sift out the inputs. The process block is repeated for each input object received when using the pipeline. If supplying the arguments to the function directly this process block will only run once. We will use this process block to determine and add our input objects to the $inputs arraylist we created. In the end block we will perform the actual processing on each of the objects that we've collected.

    function ProgramRegistry {
        [cmdletbinding()]
        param (
            # ValueFromPipeline attribute will allow piping hashtables to the function
            [Parameter(Mandatory = $false, ValueFromPipeline)][HashTable]$HashTable,
            [Parameter(Mandatory = $false)][String]$AlertPath,
            [Parameter(Mandatory = $false)][String]$AlertName,
            [Parameter(Mandatory = $false)][String]$AlertValue
        )
    
        begin {
            # Create an arraylist to collect hashtables for later processing in end block
            $inputs = [System.Collections.ArrayList]::new()
        }
    
        process {
            if ($HashTable) {
                if ($HashTable.ContainsKey('AlertPath')) {
                    # if single hashtable is received with 'AlertPath' key add to inputs for processing in end block
                    $inputs.Add($HashTable) | Out-Null
                }
                else {
                    foreach ($value in $HashTable.Values) {
                        # check if value of key is a hashtable
                        if ($value -is [hashtable]) {
                            # check if hashtable contains key "AlertPath" and if so add to $inputs for processing in end block
                            if ($value.ContainsKey('AlertPath')) {
                                $inputs.Add($value) | Out-Null
                            }
                            else {
                                Write-Warning "Invalid hashtable format - missing 'AlertPath' key"
                            }
                        } else {
                            Write-Warning "Object is not a hashtable"
                        }
                    }
                }
            }
            else {
                # process when not a hashtable by creating a hashtable and adding to $inputs
                $inputs.Add(@{
                        AlertPath  = $AlertPath
                        AlertName  = $AlertName
                        AlertValue = $AlertValue
                    }) | Out-Null
            }
        }
    
        end {
            # Process hashtables collected in $inputs 
            foreach ($hash in $inputs) {
                # your code here
                [pscustomobject]@{
                    Path  = $hash.AlertPath
                    Name  = $hash.AlertName
                    Value = $hash.AlertValue 
                }
            }
        }
    }
    
    
    
    $keys = [ordered]@{
        key1 = @{
            AlertPath  = 'Path1'
            AlertName  = 'Name1'
            AlertValue = 'Value1'
        }
        key2 = @{
            AlertPath  = 'Path2'
            AlertName  = 'Name2'
            AlertValue = 'Value2'
        }
        # and so on...
    }
    
    
    ProgramRegistry -HashTable $keys
    # or 
    $keys | ProgramRegistry
    # or even
    ProgramRegistry -HashTable $keys.key1 #or $keys.key1 | ProgramRegistry
    

    If pipeline and advanced function is not wanted you can still do something similar without begin, process, and end blocks. I use nested function 'processit' so that I don't have to repeat the processing logic multiple times

    function ProgramRegistry {
    
        param (
            [Parameter(Mandatory = $false)][HashTable]$HashTable,
            [Parameter(Mandatory = $false)][String]$AlertPath,
            [Parameter(Mandatory = $false)][String]$AlertName,
            [Parameter(Mandatory = $false)][String]$AlertValue
        )
        
    
        # create nested function to process each hash table.  
        function processit {
            param([hashtable]$hash)
    
            # add processing logic here
            [pscustomobject]@{
                Path  = $hash.AlertPath
                Name  = $hash.AlertName
                Value = $hash.AlertValue
            }
        }
    
        if ($HashTable) {
            if ($HashTable.ContainsKey('AlertPath')) {
                # if single hashtable is received with 'AlertPath' key process it
                processit -hash $HashTable
            }
            else {
                foreach ($value in $HashTable.Values) {
                    # check if value of key is a hashtable
                    if ($value -is [hashtable]) {
                        # check if hashtable contains key "AlertPath" and if so process it
                        if ($value.ContainsKey('AlertPath')) {
                            processit -hash $value
                        }
                        else {
                            Write-Warning "Invalid hashtable format - missing 'AlertPath' key"
                        }
                    }
                    else {
                        Write-Warning 'Object is not a hashtable'
                    }
                }
            }
        }
        else {
            processit @{AlertPath = $AlertPath; AlertName = $AlertName; AlertValue = $AlertValue }
        }
    }
        
    

    Update in response to your question regarding using the key name as AlertName

    function ProgramRegistry {
        param (
            [Parameter(Mandatory = $false)][HashTable]$HashTable,
            [Parameter(Mandatory = $false)][String]$AlertPath,
            [Parameter(Mandatory = $false)][String]$AlertName,
            [Parameter(Mandatory = $false)][String]$AlertValue
        )
        # create nested function to process each hash table.  
        function processit {
            param([hashtable]$hash)
            # add processing logic here
            [pscustomobject]@{
                Path  = $hash.AlertPath
                Name  = $hash.AlertName
                Value = $hash.AlertValue
            }
        }
        if ($HashTable) {
            if ($HashTable.ContainsKey('AlertPath')) {
                # if single hashtable is received with 'AlertPath' key process it
                processit -hash $HashTable
            }
            else {
                foreach ($item in $HashTable.GetEnumerator()) {
                    if ($item.Value -is [hashtable]) {
                        # check if the hashtable has AlertPath and AlertValue keys
                        if ($item.Value.ContainsKey('AlertPath') -and $item.Value.ContainsKey('AlertValue')) {
                            $hash = $item.Value
                            # check if hashtable contains an AlertName key.  
                            if (-not $hash.ContainsKey('AlertName')){
                                # If not use parent key name
                                $hash.AlertName = $item.Key
                            }
                            processit -hash $hash
                        }
                        else {
                            Write-Warning "Invalid hashtable format - missing AlertPath and/or AlertValue key"
                        }
                    }
                    else {
                        Write-Warning "Item does not contain a hashtable"
                    }
                }
            }
        }
        else {
            processit @{AlertPath = $AlertPath; AlertName = $AlertName; AlertValue = $AlertValue }
        }
    }
    
    

    Calling the function

    $items = @{
        Alert1 = @{
            AlertPath  = 'Path1'
            AlertValue = 'Value1'
        }
        Alert2 = @{
            AlertPath  = 'Path2'
            AlertValue = 'Value2'
        }
        Alert3 = @{
            AlertName = 'Overridden AlertName'
            AlertPath  = 'Path3'
            AlertValue = 'Value3'
        }
        Alert4 = @{
            AlertValue = 'Value2'
        }
        Alert5 = "just a string"
    }
    
    ProgramRegistry -HashTable $items
    

    Output

    WARNING: Item does not contain a hashtable
    
    WARNING: Invalid hashtable format - missing AlertPath and/or AlertValue key
    Path  Name                 Value
    ----  ----                 -----
    Path3 Overridden AlertName Value3
    Path2 Alert2               Value2
    Path1 Alert1               Value1