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
CodePudding user response:
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( $ht, [String] $path, $value ) {
# 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 '\.', 2
if( $path ) {
# We have multiple path components, so we may have to create nested hash table.
if( -not $ht.Contains( $key ) ) {
$ht[ $key ] = [ordered] @{}
}
# Enter sub tree.
$ht = $ht[ $key ]
}
else {
# We have arrived at the leaf -> set its value
$ht[ $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 # 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).
CodePudding user response:
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} = "[email protected]"
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