Search code examples
powershellnestedhashtable

Powershell hashes: Is there an easy was of adding/updating values?


In Powershell you can use .Add to insert a new key/value pair into an existing hash. If the hash already contains the key, this leads to an error. The desired (by me :) behavior would be, for known keys, just udate the existing value to the provided one. I can do this with a lot of ink. Putting the .Add command in a try phrase and in the catch the changing of the value - which works fine, but the cost of ink!

Seriously, as I have this kind logic all over the place when parsing multiple configs (was this already set and needs updating or is it a new setting?), it makes for messy code:

# $msHashtable is potentially empty at this point or may not contain the key 
try {
    $myHashtable.Add($thisKey, $thisValue)
} 
catch {
    $myHashtable.$thisKey = $thisValue
}

Another issue with hashes that I have is this:

  • Assume you have a hashtabel $motherOfAll which will eventually contain other hashtables, which in turn will also contain hashtables.
  • Now you want to insert something into the bottommost layer of hashtables. You first need to check, that all the hashtables along the way exist and contain the proper keys.
  • If not, you have to insert a bunch of empty hashtables, which get filled with another empty one... not ad infinitum of course, but still ugly. More messy code. Is there a better way?

I can provide code if needed, but I hope the issues are clear enough. As there is so much other code than the relevant pieces in my real world example, I'll restrain from posting it now...

Best,

YeOldHinnerk


Solution

  • You already got a helpful answer for the first part of your question.

    This is my try at the second part - how to assign members of nested hash tables. There isn't an easy built-in syntax to set nested values while creating any not-yet-existing parent hash tables, so I've created a reusable function Set-TreeValue for that purpose.

    function Set-TreeValue( $HashTable, [String] $Path, $Value, [String] $PathSeparator = '\.' ) {
    
        # To detect errors like trying to set child of value-type leafs.
        Set-StrictMode -Version 3.0  
    
        do {
            # Split into root key and path remainder (", 2" -> split into max. 2 parts)
            $key, $Path = $Path -split $PathSeparator, 2
    
            if( $Path ) {
                # We have multiple path components, so we may have to create nested hash table.
                if( -not $HashTable.Contains( $key ) ) {
                    $HashTable[ $key ] = [ordered] @{}
                }
                # Enter sub tree. 
                $HashTable = $HashTable[ $key ]
            }
            else {
                # We have arrived at the leaf -> set its value
                $HashTable[ $key ] = $Value
            }
        }
        while( $Path )
    }
    

    Demo:

    $ht = [ordered] @{}
    
    Set-TreeValue $ht foo.bar.baz 42   # Create new value and any non-existing parents
    Set-TreeValue $ht foo.bar.baz 8    # Update existing value
    Set-TreeValue $ht foo.bar.bam 23   # Add another leaf
    Set-TreeValue $ht fop 4            # Set a leaf at root level
    #Set-TreeValue $ht fop.zop 16      # Outputs an error, because .fop is a leaf
    Set-TreeValue $ht 'foo bar' 15     # Use a path that contains spaces
    
    $ht | ConvertTo-Json -Depth 99     # Output the content of the hash table
    

    Output:

    {
      "foo": {
        "bar": {
          "baz": 8,
          "bam": 23
        }
      },
      "fop": 4,
      "foo bar": 15
    }
    

    NOTE: I've opted to create nested hash tables as OrderedDictionary as these are much more useful than regular ones (e. g. to ensure an order in a JSON output). Remove [ordered] if you want unordered hash tables (which propably have slight performance advantage).