Updating hash table values in a 'foreach' loop in PowerShell - powershell

I'm trying to loop through a hash table and set the value of each key to 5 and PowerShell gives an error:
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
foreach($key in $myHash.keys){
$myHash[$key] = 5
}
An error occurred while enumerating through a collection:
Collection was modified; enumeration operation may not execute..
At line:1 char:8
+ foreach <<<< ($key in $myHash.keys){
+ CategoryInfo : InvalidOperation: (System.Collecti...tableEnumer
ator:HashtableEnumerator) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
What gives and how do I resolve this problem?

You can't modify Hashtable while enumerating it. This is what you can do:
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
$myHash = $myHash.keys | foreach{$r=#{}}{$r[$_] = 5}{$r}
Edit 1
Is this any simpler for you:
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
foreach($key in $($myHash.keys)){
$myHash[$key] = 5
}

There is a much simpler way of achieving this. You cannot change the value of a hashtable while enumerating it because of the fact that it's a reference type variable. It's exactly the same story in .NET.
Use the following syntax to get around it. We are converting the keys collection into a basic array using the #() notation. We make a copy of the keys collection, and reference that array instead which means we can now edit the hashtable.
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
foreach($key in #($myHash.keys)){
$myHash[$key] = 5
}

You do not need to clone the whole hashtable for this example. Just enumerating the key collection by forcing it to an array #(...) is enough:
foreach($key in #($myHash.keys)) {...

Use clone:
foreach($key in ($myHash.clone()).keys){
$myHash[$key] = 5
}
Or in the one-liner:
$myHash = ($myHash.clone()).keys | % {} {$myHash[$_] = 5} {$myHash}

I'm new to PowerShell, but I'm quite a fan of using in-built functions, because I find it more readable. This is how I would tackle the problem, using GetEnumerator and Clone. This approach also allows one to reference to the existing hash values ($_.value) for modifying purposes.
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
$myHash.Clone().GetEnumerator() | foreach-object {$myHash.Set_Item($_.key, 5)}

You have to get creative!
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
$keys = #()
[array] $keys = $myHash.keys
foreach($key in $keys)
{
$myHash.Set_Item($key, 5)
}
$myHash
Name Value
---- -----
c 5
a 5
b 5

As mentioned in a previous answer, clone is the way to go. I had a need to replace any null values in a hash with "Unknown" nd this one-liner does the job.
($record.Clone()).keys | %{if ($record.$_ -eq $null) {$record.$_ = "Unknown"}}

$myHash = #{
Americas = 0;
Asia = 0;
Europe = 0;
}
$countries = #("Americas", "Asia", "Europe", "Americas", "Asia")
foreach($key in $($myHash.Keys))
{
foreach($Country in $countries)
{
if($key -eq $Country)
{
$myHash[$key] += 1
}
}
}
$myHash

$myHash = #{
Americas = 0;
Asia = 0;
Europe = 0;
}
$countries = #("Americas", "Asia", "Europe", "Americas", "Asia")
foreach($key in $($myHash.Keys))
{
foreach($Country in $countries)
{
if($key -eq $Country)
{
$myHash[$key] += 1
}
}
}
Updating a hash value if array elements matched with a hash key.

It seems when you update the hash table inside the foreach loop, the enumerator invalidates itself. I got around this by populating a new hash table:
$myHash = #{}
$myHash["a"] = 1
$myHash["b"] = 2
$myHash["c"] = 3
$newHash = #{}
foreach($key in $myHash.keys){
$newHash[$key] = 5
}
$myHash = $newHash

Related

Adding a column to a datatable in powershell

I am trying to add a column to data I have imported (and will export) as a CSV.
I am importing a CSV:
What I want to do add another column, perhaps "10/15/22" when the process runs, and then update the values under that date.
In effect, the document will grow to the right, adding a column each time it is run.
I have an object called $test. It will have values like:
$test.users = "7"
$test.SomeBSValue = "22"
$test.Computers = "52"
When all is said and done, my output should look like:
Adding to the list any values I have that are not part of the list already, but recording the values I have under the heading for the date.
So, if the script is run and collects 100 data points, those data point would all be in the CSV under the date.
I would have thought this would be easy, but now I am drawing a complete blank.
I've considered (but have not coded) even trying to put into a GUI grid view and then reading the data back and writing the CSV (but there should be an easier way, right?)
Since you don't actually use it as a CSV we can treat it like regular content.
Say we have a file in C:\test called test.csv that looks as follows:
"Settings","08/15/22","09/15/22"
"Users",0,0
"Computers",0,1
"SomeValue1",0,2
"SomeValue2",0,2
"SomeValue3",0,2
"Stat1",0,10
"Stat2",7,0
"Stat3",0,0
"SomeBSValue",1,2
We can import it, add the row from the object to each corresponding row and right the file to test2.csv.
$test = #{
Settings = "10/15/22"
users = "7"
Computers = "52"
SomeValue1 = "22"
SomeValue2 = "24"
SomeValue3 = "25"
Stat1 = "4"
Stat2 = "3"
Stat3 = "2"
SomeBSValue = "1"
}
$content = Get-Content "C:\test\test.csv"
$newContent = #()
foreach($row in $content){
foreach($key in $test.Keys){
if($row -like "*$key*"){
$row = $row + "," + $test."$key"
$newContent += $row
}
}
}
$newContent | Out-File "C:\test\test2.csv"
After running the script it will have added the values from the object:
"Settings","08/15/22","09/15/22",10/15/22
"Users",0,0,7
"Computers",0,1,52
"SomeValue1",0,2,22
"SomeValue2",0,2,22
"SomeValue3",0,2,22
"Stat1",0,10,4
"Stat2",7,0,4
"Stat3",0,0,4
Edit:
If you want the date between quotes, replace $row = $row + "," + $test."$key" with this:
if($key -eq "Settings"){
$row = $row + "," + '"' + $test."$key" + '"'
}else{
$row = $row + "," + $test."$key"
}
This idea is pretty terrible idea, as you stated, "grow to the right" is definitely not a good approach and you should consider a better way of doing it, data should always expand vertically.
As for the solution, you can create new columns easily with Select-Object and dynamically generated calculated properties.
Note, this should definitely not be considered an efficient approach. This will be slow because Select-Object is slow.
function Add-Column {
param(
[Parameter(ValueFromPipeline, DontShow, Mandatory)]
[object] $InputObject,
[Parameter(Mandatory)]
[string] $ColumnName,
[Parameter(Mandatory)]
[string] $ReferenceProperty,
[Parameter(Mandatory)]
[hashtable] $Values
)
begin {
$calculatedProp = #{ N = $ColumnName }
}
process {
$calculatedProp['E'] = { 0 }
if($value = $InputObject.$ReferenceProperty) {
if($Values.ContainsKey($value)) {
$calculatedProp['E'] = { $Values[$value] }
}
}
$InputObject | Select-Object *, $calculatedProp
}
}
Usage
Import-Csv path\to\csv | Add-Column -ColumnName '09/15/22' -ReferenceProperty Settings -Values #{
users = "7"
SomeBSValue = "22"
Computers = "52"
}
Result
Settings 08/15/22 09/15/22
-------- -------- --------
Users 0 7
Computers 0 52
SomeValue1 0 0
SomeValue2 0 0
SomeValue3 0 0
Stat1 0 0
Stat2 7 0
Stat3 0 0
SomeBSValue 1 22
This function allows then pipe into Export-Csv at ease:
Import-Csv path\to\csv | Add-Column ... | Export-Csv path\to\newCsv

Hash Table, Multiples values in Key, Foreach Loop Powershell

I have filled the keys with the necessary values,
Every key will have multiples values
$vms = #{}
$vms.template += $templateName
$vms.name += $vmName
$vms.EsxiHostName += $esxiHostName
$vms.datastore += $datastoreName
$vms.network += $networkName
$vms.FolderLocation += $folderName
$vms.vCPU += $vCPU
$vms.CoresPerCPU += $vmCores
$vms.Memory += $vmRam
$vms.IP += $vmIP
$vms.SubnetMask += $vmMask
$vms.gateway += $vmGateway
$vms.DNS1 += $vmDns1
$vms.DNS2 += $vmDns2
$vms.Description += $vmDescription
$vms.TrendMicroScanDay += $tmscanday
$vms.inventory_billing_owner += $inventoryBillingOwner
And now what I want to do is something like this because I want to use these variables in another commands.
foreach ($vm in $vms) {
#Assign Variables
$VCTemplate = $vm.template
$VMName = $vm.Name
$VMHost = $vm.EsxiHostName
$Datastore = $vm.datastore
$NetworkName = $vm.network
$FolderLocation = $vm.FolderLocation
$vCPU = $vm.vCPU
$CoresPerCPU = $vm.CoresPerCPU
$Memory = $vm.Memory
$VMIP = $vm.IP
$SubnetMask = $vm.SubnetMask
$GW = $vm.Gateway
$DNS1 = $vm.DNS1
$DNS2 = $vm.DNS2
$Description = $VM.Description
$TrendMicroScanDay = $VM.TrendMicroScanDay
$inventory_billing_owner = $VM.inventory_billing_owner
}
It seems foreach loop doesn't work this way and I try to find information about it but was not possible
Someone know how can I work with a Foreach Loop and a Hash Table with multiples values per key?
Thanks
EDIT:
Thanks Mclayton for answer, I tried your solutions
First I want to send you what is inside of $vms
PS C:\Users\me\Desktop> $vms
Name Value
---- ----- SubnetMask {255.255.255.0, 255.255.255.255} description {TEST, Test 2}
Memory {4, 8}
name {Name1, Test 2}
vCPU {4, 8}
ip {10.10.10.1, 20.20.20.1} datastore {vsanDatastore, vsanDatastore} dns2 {10.10.10.5, 20.20.20.5}
gateway {10.10.10.3, 20.20.20.3}
template {ESSQLTEMPLATE01, WIN 10 Template}
FolderLocation {Office Domain, SysAdmin LAB}
TrendMicroScanDay {Day5, Day5}
CoresPerCPU {4, 8}
dns1 {10.10.10.4, 20.20.20.4}
EsxiHostName {es1esxi01p, es1esxi02p}
network {servers, data2}
Then with the first option running this for test
for($i = 0; $i -lt $vms.template.Length; $i++ )
{
$VCTemplate = $vms.template[$i];
$VMName2 = $vms.Name[$i];
}
PS C:\Users\me\Desktop> $VCTemplate
WIN 10 Template
I'm getting the second value, maybe I didn’t understand what you were saying
And with the second option, I was thinking what to use in the foreach ($something in $something_else)
but I ran this:
$vm3 = #()
$vm3 += new-object PSCustomObject -Property ([ordered] #{
Template = $vms.template
Name = $vms.name
EsxiHostName = $vms.EsxiHostName
datastore = $vms.datastore
network = $vms.network
FolderLocation = $vms.FolderLocation
vCPU = $vms.vCPU
CoresPerCPU = $vms.CoresPerCPU
Memory = $vms.Memory
IP = $vms.IP
SubnetMask = $vms.SubnetMask
gateway = $vms.gateway
DNS1 = $vms.DNS1
DNS2 = $vms.DNS2
Description = $vms.Description
TrendMicroScanDay = $vms.TrendMicroScanDay
})
foreach ($vm in $vm3)
{
write-host 'This is '$vm.template
}
And this was the result
PS C:\Users\me\Desktop> foreach ($vm in $vm3)
{
write-host 'This is '$vm.template
}
This is ESSQLTEMPLATE01 WIN 10 Template
In your code, $vms is a single hashtable object, and if you foreach() over a single object the loop will only run once. The fact that all of $vms's properties (e.g. $vms.template) are arrays doesn't make any difference to this.
If you really need to use a single hastable with properties that are parallel arrays, what you'll need to do is something like:
for($i = 0; $i -lt $vms.template.Length; $i++ )
{
$VCTemplate = $vms.template[$i];
$VMName = $vms.Name[$i];
... etc ...
... now do stuff with the $i'th vm ...
write-host $vmName;
}
but a better alternative would be to create $vms as an array of objects with #() (note round brackets not squiggly ones) - e.g.
$vms = #()
foreach( $something in $something_else )
{
$vms += new-object PSCustomObject -Property ([ordered] #{
Template = $something.template
Name = $something.name
... etc ...
})
}
and then you can iterate over $vms:
foreach ($vm in $vms)
{
write-host $vm.Name
}

Generate 2 different list in one foreach loop with powershell

I stucked in foreach part.I couldn't find any solution for generating 2 different lists in one foreach loop.I used 2 foreach but it didn't help.Below side I shared my desire output.
My code:
$InStuff = #'
a
b
c
'#.Split("`n").Trim()
$InStuff2 = #'
1
2
3
'#.Split("`n").Trim()
$SPart_1 = 'application="'
$SPart_2 = ' path='
$SPart_3 = ' name='
$SPart_4 = ' application'
foreach ($IS_Item in $InStuff) {
foreach ($IS2_Item in $InStuff2) {
$UName = $IS_Item
$UName2 = $IS2_Item
$Sentence = -join (
$SPart_1, $UName,
$SPart_2, $UName2,
$SPart_3, $UName2,
$SPart_4
)
''
$Sentence
}
}
Fail output :
application="a path=1 name=1 application
application="a path=2 name=2 application
application="a path=3 name=3 application
application="b path=1 name=1 application
application="b path=2 name=2 application
application="b path=3 name=3 application
application="c path=1 name=1 application
application="c path=2 name=2 application
application="c path=3 name=3 application
My desire output :
application="a path=1 name=1 application
application="b path=2 name=2 application
application="c path=3 name=3 application
Thank you
use a for loop:
$InStuff = #'
a
b
c
'#.Split("`n").Trim()
$InStuff2 = #'
1
2
3
'#.Split("`n").Trim()
$SPart_1 = 'application="'
$SPart_2 = ' path='
$SPart_3 = ' name='
$SPart_4 = ' application'
for ($i = 0; $i -lt $InStuff.count; $i++) {
$Sentence = -join (
$SPart_1, $InStuff[$i],
$SPart_2, $InStuff2[$i],
$SPart_3, $InStuff2[$i],
$SPart_4
), ''
$Sentence
}
This will likely go wrong if your input arrays are not the same length, so it is not that safe. Perhaps using a hash or custom object would be a better idea:
$arr = #()
$arr += new-object PSCustomObject -property #{application='a';path=1;name=1}
$arr += new-object PSCustomObject -property #{application='b';path=2;name=2}
$arr += new-object PSCustomObject -property #{application='c';path=3;name=3}
$arr | % { 'application="{0} path={1} name={2}' -f $_.application, $_.path, $_.name }
#arco444 is right, no matter what you will have problems if your lists are different lengths. You should reconsider how you are collecting and formatting the data. Here is an alternative method:
$InStuff = "a","b","c"
$InStuff2 = 1,2,3
$listCount = $InStuff.Count
$x = 0
do {
$strOut = "application= `"path = {0} name = {1} application`"" -f $InStuff[$x], $InStuff2[$x]
$strOut
$x++
}
while ( $x -lt $listCount )
Not sure what you want with a stray " in there, I've added one to enclose the output:
application= "path = a name = 1 application"
application= "path = b name = 2 application"
application= "path = c name = 3 application"
If you plan to use this output for further processing by PowerShell, like putting it in a csv with Export-Csv then you should forgo the application text and create an object instead:
$InStuff = "a","b","c"
$InStuff2 = 1,2,3
$listCount = $InStuff.Count
$x = 0
do {
[pscustomobject]#{
path = $InStuff[$x]
name = $InStuff2[$x]
}
$x++
}
while ( $x -lt $listCount )
While that's not exactly what you are asking for, it's been my experience that data in this format is far more useful:
path name
---- ----
a 1
b 2
c 3
you can add lines to
[pscustomobject]#{
path = $InStuff[$x]
name = $InStuff2[$x]
}
for the additional text (if it's a must) and do something like this:
[pscustomobject]#{
type = "application"
path = $InStuff[$x]
name = $InStuff2[$x]
}
and that will add a column for the word application

Lookup table with compound key

I am looking for efficient lookup tables in PowerShell
Naive approach is not efficient for large data
$data = `
#(
#{
A = 1;
B = 2;
C = 3;
},
#{
A = 4;
B = 5;
C = 6;
},
#{
A = 7;
B = 8;
C = 9;
}
)
# looking for value C base on pair (A, B)
function FindC
{
param
(
[int] $A,
[int] $B
)
$data | `
Where-Object -FilterScript { ($_.A -eq $A) -and ($_.B -eq $B) } | `
Select-Object -ExpandProperty C
}
I think, efficient way would be to use hashtables but it is unclear what to do with compound keys. We cannot use hashtables, PSObject or PSCustomObject because they don't have equality implemented
> #{ A = 1; B = 2; } -eq #{ A = 1; B = 2; }
False
> [PSObject] #{ A = 1; B = 2; } -eq [PSObject] #{ A = 1; B = 2; }
False
> [PSCustomObject] #{ A = 1; B = 2; } -eq [PSCustomObject] #{ A = 1; B = 2; }
False
I don't want to implement my own data classes for such simple cases. So the only thing I found so far is tuple
> [Tuple]::Create(1, 2) -eq [Tuple]::Create(1, 2)
True
So we can use tuples
$hashtable = #{}
foreach ($entry in $data)
{
$hashtable[[Tuple]::Create($entry.A, $entry.B)] = $entry.C
}
function FindC
{
param
(
[int] $A,
[int] $B
)
$hashtable[[Tuple]::Create($A, $B)]
}
Anyone knows more elegant way to achieve that?
If you want to use Hashtables then you can override the Equals method and provide your own definition for equality. Check this question on how to do this: How to overload the PowerShell inbuilt class's methods

Merging hashtables in PowerShell: how?

I am trying to merge two hashtables, overwriting key-value pairs in the first if the same key exists in the second.
To do this I wrote this function which first removes all key-value pairs in the first hastable if the same key exists in the second hashtable.
When I type this into PowerShell line by line it works. But when I run the entire function, PowerShell asks me to provide (what it considers) missing parameters to foreach-object.
function mergehashtables($htold, $htnew)
{
$htold.getenumerator() | foreach-object
{
$key = $_.key
if ($htnew.containskey($key))
{
$htold.remove($key)
}
}
$htnew = $htold + $htnew
return $htnew
}
Output:
PS C:\> mergehashtables $ht $ht2
cmdlet ForEach-Object at command pipeline position 1
Supply values for the following parameters:
Process[0]:
$ht and $ht2 are hashtables containing two key-value pairs each, one of them with the key "name" in both hashtables.
What am I doing wrong?
Merge-Hashtables
Instead of removing keys you might consider to simply overwrite them:
$h1 = #{a = 9; b = 8; c = 7}
$h2 = #{b = 6; c = 5; d = 4}
$h3 = #{c = 3; d = 2; e = 1}
Function Merge-Hashtables {
$Output = #{}
ForEach ($Hashtable in ($Input + $Args)) {
If ($Hashtable -is [Hashtable]) {
ForEach ($Key in $Hashtable.Keys) {$Output.$Key = $Hashtable.$Key}
}
}
$Output
}
For this cmdlet you can use several syntaxes and you are not limited to two input tables:
Using the pipeline: $h1, $h2, $h3 | Merge-Hashtables
Using arguments: Merge-Hashtables $h1 $h2 $h3
Or a combination: $h1 | Merge-Hashtables $h2 $h3
All above examples return the same hash table:
Name Value
---- -----
e 1
d 2
b 6
c 3
a 9
If there are any duplicate keys in the supplied hash tables, the value of the last hash table is taken.
(Added 2017-07-09)
Merge-Hashtables version 2
In general, I prefer more global functions which can be customized with parameters to specific needs as in the original question: "overwriting key-value pairs in the first if the same key exists in the second". Why letting the last one overrule and not the first? Why removing anything at all? Maybe someone else want to merge or join the values or get the largest value or just the average...
The version below does no longer support supplying hash tables as arguments (you can only pipe hash tables to the function) but has a parameter that lets you decide how to treat the value array in duplicate entries by operating the value array assigned to the hash key presented in the current object ($_).
Function
Function Merge-Hashtables([ScriptBlock]$Operator) {
$Output = #{}
ForEach ($Hashtable in $Input) {
If ($Hashtable -is [Hashtable]) {
ForEach ($Key in $Hashtable.Keys) {$Output.$Key = If ($Output.ContainsKey($Key)) {#($Output.$Key) + $Hashtable.$Key} Else {$Hashtable.$Key}}
}
}
If ($Operator) {ForEach ($Key in #($Output.Keys)) {$_ = #($Output.$Key); $Output.$Key = Invoke-Command $Operator}}
$Output
}
Syntax
HashTable[] <Hashtables> | Merge-Hashtables [-Operator <ScriptBlock>]
Default
By default, all values from duplicated hash table entries will added to an array:
PS C:\> $h1, $h2, $h3 | Merge-Hashtables
Name Value
---- -----
e 1
d {4, 2}
b {8, 6}
c {7, 5, 3}
a 9
Examples
To get the same result as version 1 (using the last values) use the command: $h1, $h2, $h3 | Merge-Hashtables {$_[-1]}. If you would like to use the first values instead, the command is: $h1, $h2, $h3 | Merge-Hashtables {$_[0]} or the largest values: $h1, $h2, $h3 | Merge-Hashtables {($_ | Measure-Object -Maximum).Maximum}.
More examples:
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {($_ | Measure-Object -Average).Average} # Take the average values"
Name Value
---- -----
e 1
d 3
b 7
c 5
a 9
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {$_ -Join ""} # Join the values together
Name Value
---- -----
e 1
d 42
b 86
c 753
a 9
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {$_ | Sort-Object} # Sort the values list
Name Value
---- -----
e 1
d {2, 4}
b {6, 8}
c {3, 5, 7}
a 9
I see two problems:
The open brace should be on the same line as Foreach-object
You shouldn't modify a collection while enumerating through a collection
The example below illustrates how to fix both issues:
function mergehashtables($htold, $htnew)
{
$keys = $htold.getenumerator() | foreach-object {$_.key}
$keys | foreach-object {
$key = $_
if ($htnew.containskey($key))
{
$htold.remove($key)
}
}
$htnew = $htold + $htnew
return $htnew
}
Not a new answer, this is functionally the same as #Josh-Petitt with improvements.
In this answer:
Merge-HashTable uses the correct PowerShell syntax if you want to drop this into a module
Wasn't idempotent. I added cloning of the HashTable input, otherwise your input was clobbered, not an intention
added a proper example of usage
function Merge-HashTable {
param(
[hashtable] $default, # Your original set
[hashtable] $uppend # The set you want to update/append to the original set
)
# Clone for idempotence
$default1 = $default.Clone();
# We need to remove any key-value pairs in $default1 that we will
# be replacing with key-value pairs from $uppend
foreach ($key in $uppend.Keys) {
if ($default1.ContainsKey($key)) {
$default1.Remove($key);
}
}
# Union both sets
return $default1 + $uppend;
}
# Real-life example of dealing with IIS AppPool parameters
$defaults = #{
enable32BitAppOnWin64 = $false;
runtime = "v4.0";
pipeline = 1;
idleTimeout = "1.00:00:00";
} ;
$options1 = #{ pipeline = 0; };
$options2 = #{ enable32BitAppOnWin64 = $true; pipeline = 0; };
$results1 = Merge-HashTable -default $defaults -uppend $options1;
# Name Value
# ---- -----
# enable32BitAppOnWin64 False
# runtime v4.0
# idleTimeout 1.00:00:00
# pipeline 0
$results2 = Merge-HashTable -default $defaults -uppend $options2;
# Name Value
# ---- -----
# idleTimeout 1.00:00:00
# runtime v4.0
# enable32BitAppOnWin64 True
# pipeline 0
In case you want to merge the whole hashtable tree
function Join-HashTableTree {
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$SourceHashtable,
[Parameter(Mandatory = $true, Position = 0)]
[hashtable]
$JoinedHashtable
)
$output = $SourceHashtable.Clone()
foreach ($key in $JoinedHashtable.Keys) {
$oldValue = $output[$key]
$newValue = $JoinedHashtable[$key]
$output[$key] =
if ($oldValue -is [hashtable] -and $newValue -is [hashtable]) { $oldValue | ~+ $newValue }
elseif ($oldValue -is [array] -and $newValue -is [array]) { $oldValue + $newValue }
else { $newValue }
}
$output;
}
Then, it can be used like this:
Set-Alias -Name '~+' -Value Join-HashTableTree -Option AllScope
#{
a = 1;
b = #{
ba = 2;
bb = 3
};
c = #{
val = 'value1';
arr = #(
'Foo'
)
}
} |
~+ #{
b = #{
bb = 33;
bc = 'hello'
};
c = #{
arr = #(
'Bar'
)
};
d = #(
42
)
} |
ConvertTo-Json
It will produce the following output:
{
"a": 1,
"d": 42,
"c": {
"val": "value1",
"arr": [
"Foo",
"Bar"
]
},
"b": {
"bb": 33,
"ba": 2,
"bc": "hello"
}
}
I just needed to do this and found this works:
$HT += $HT2
The contents of $HT2 get added to the contents of $HT.
The open brace has to be on the same line as ForEach-Object or you have to use the line continuation character (backtick).
This is the case because the code within { ... } is really the value for the -Process parameter of ForEach-Object cmdlet.
-Process <ScriptBlock[]>
Specifies the script block that is applied to each incoming object.
This will get you past the current issue at hand.
I think the most compact code to merge (without overwriting existing keys) would be this:
function Merge-Hashtables($htold, $htnew)
{
$htnew.keys | where {$_ -notin $htold.keys} | foreach {$htold[$_] = $htnew[$_]}
}
I borrowed it from Union and Intersection of Hashtables in PowerShell
I wanted to point out that one should not reference base properties of the hashtable indiscriminately in generic functions, as they may have been overridden (or overloaded) by items of the hashtable.
For instance, the hashtable $hash=#{'keys'='lots of them'} will have the base hashtable property, Keys overridden by the item keys, and thus doing a foreach ($key in $hash.Keys) will instead enumerate the hashed item keys's value, instead of the base property Keys.
Instead the method GetEnumerator or the keys property of the PSBase property, which cannot be overridden, should be used in functions that may have no idea if the base properties have been overridden.
Thus, Jon Z's answer is the best.
To 'inherit' key-values from parent hashtable ($htOld) to child hashtables($htNew), without modifying values of already existing keys in the child hashtables,
function MergeHashtable($htOld, $htNew)
{
$htOld.Keys | %{
if (!$htNew.ContainsKey($_)) {
$htNew[$_] = $htOld[$_];
}
}
return $htNew;
}
Please note that this will modify the $htNew object.
Here is a function version that doesn't use the pipeline (not that the pipeline is bad, just another way to do it). It also returns a merged hashtable and leaves the original unchanged.
function MergeHashtable($a, $b)
{
foreach ($k in $b.keys)
{
if ($a.containskey($k))
{
$a.remove($k)
}
}
return $a + $b
}
I just wanted to expand or simplify on jon Z's answer. There just seems to be too many lines and missed opportunities to use Where-Object. Here is my simplified version:
Function merge_hashtables($htold, $htnew) {
$htold.Keys | ? { $htnew.ContainsKey($_) } | % {
$htold.Remove($_)
}
$htold += $htnew
return $htold
}