1

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

2 Answers2

3

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

zett42
  • 25,437
  • 3
  • 35
  • 72
1

Use the index operator to reference a specific entry by key and then assign a new value to that entry:

$hashtable = @{}

# this will add a new key/value entry
$hashtable['abc'] = 1

# this will overwrite the existing value associated with the key `abc`
$hashtable['abc'] = 2

If you have a large code base with many existing calls to .Add($key, $value) and would like to avoid refactoring every call, you can modify the behavior of the hashtable itself so that Add acts like the indexer:

function New-NonStrictHashTable {
  return @{} |Add-Member -MemberType ScriptMethod -Name Add -Value {
    param($key,$value)

    $this[$key] = $value
  } -Force -PassThru
}

Now you can do:

# Create hashtable with Add() overriden
$hashtable = New-NonStrictHashTable

$key,$value = 'key','value'

# This works like before
$hashtable.Add($key, $value)

# This works too now, it simply updates the existing entry
$hashtable.Add($key, 'some other value')

This will work for any PowerShell script statement that calls $hashtable.Add() because resolution of ETS methods (like the one we attached to the hashtable with Add-Member) takes precedence over the underlying .NET method.


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?

The desired behavior you describe here is found in Perl and is known as autovivification:

my %users;

# the nested hashes $users{YeOldHinnerk} and $users{YeOldHinnerk}{contact_details} 
# will automatically come into existence when this assignment is evaluated
$users{YeOldHinnerk}{contact_details}{email_address} = "yoh@domain.tld"

The Wikipedia article linked above gives an example of how to implement similar behavior in C#, which can be adapted for PowerShell as follows:

Add-Type -TypeDefinition @'
using System.Collections.Generic;

public class AVD
{
    private Dictionary<string, object> _data = new Dictionary<string, object>();

    public object this[string key]
    {
        get {
            if(!_data.ContainsKey(key)){
                _data[key] = new AVD();
            }
            return _data[key];
        }
        set {
            _data[key] = value;
        }
    }
}
'@

Now we can take advantage of PowerShell's native index access syntax:

PS ~> $autovivifyingHashtable = [AVD]::new()
PS ~> $autovivifyingHashtable['a']['b']['c'] = 123
PS ~> $autovivifyingHashtable['a'] -is [AVD]
True
PS ~> $autovivifyingHashtable['a']['b'] -is [AVD]
True
PS ~> $autovivifyingHashtable['a']['b']['c']
123
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206