Evaluating a condition with AND and OR statements combined - powershell

I'm parsing files which all have following structure:
A=B #A==test
This means that variable A will be set to B, only if A equals 'test'
Conditions can get more complex as well
C=D #C==test1,D==test2
This means that C will be set to D if C equals 'test1' AND D equals 'test2'
So far so good, I parse the individual conditions one by one, and stop as soon as one evaluates to false.
Conditions can also have OR statements:
E=F #E==test3,(F==test4|F==test5),username!=abc
This means that E will be set to F if E equals 'test3' AND (F equals 'test4' OR F equals 'test5') AND username does not equal 'abc'
I'm stuck on evaluating this last condition. What is a good approach to evaluate such an expression?
Currently I'm parsing each condition into 3 arrays, being 'lefthands', 'operators, 'righthands'.
The condition #C==test1,D==test2 gets parsed into:
$lefthands=(C, D)
$righthands=(test1,test2)
$operands=(==, ==)
Afterwards I loop over the arrays and do the evaluation based on the operand that is currently being used.
if $operands[$i] -eq "==" ( return ($lefthands[$i] -eq $righthands[$i] }
As soon as one of the conditions returns false, I stop evaluating the complete condition.
This works fine if there are only AND statements, but with an OR statement present this does no longer work.

Looks like you've got a fairly straightforward infix expression grammar that goes something like this:
variable = [ a-z | A-Z ]
string = [ a-z | A-Z | 0-9 ]
atom = variable [ "==" | "!=" ] string
expr = [ atom | "(" expr ")" | expr [ "," | "|" ] expr ]
Assuming you can nest expressions to an arbitrary depth and you want to handle operator precedence properly (e.g. does w,x|y,z mean ((w,x)|y),z) or (w,x)|(y,z) or even w,(x|y),z, all of which give different results) you're going to struggle to evaluate the expression with basic string manipulation and you might need use a more complicated approach:
Lex the string into tokens
Change the order of the tokens (infix -> postfix) to make it easier to calculate results
Evaluate the expression given some values for variables
Lexing
This basically breaks the expression string down into a list of logical chunks - for example A==test might become identifier "A", operator "==", identifier "test".
It's called "infix" notation because the operator sits in-between the operands - e.g. A==test.
function ConvertTo-InfixTokens
{
param( [string] $Source )
$chars = $Source.ToCharArray();
$index = 0;
while( $index -lt $chars.Length )
{
$token = [pscustomobject] [ordered] #{
"Type" = $null
"Value" = [string]::Empty
};
$char = $chars[$index];
switch -regex ( $char )
{
# var or str
"[a-zA-z0-9]" {
$token.Type = "identifier";
$token.Value += $char;
$index += 1;
while( ($index -lt $chars.Length) -and ($chars[$index] -match "[a-zA-z0-9]") )
{
$token.Value += $chars[$index];
$index += 1;
}
}
# eq
"=" {
if( $chars[$index+1] -eq "=" )
{
$token.Type = "eq";
$token.Value = $chars[$index] + $chars[$index+1];
$index += 2;
}
else
{
throw "LEX01 - unhandled char '$($chars[$index+1])'";
}
}
# ne
"!" {
if( $chars[$index+1] -eq "=" )
{
$token.Type = "ne";
$token.Value = $chars[$index] + $chars[$index+1];
$index += 2;
}
else
{
throw "LEX02 - unhandled char '$($chars[$index+1])'";
}
}
"," {
$token.Type = "and";
$token.Value = $char;
$index += 1;
}
"\|" {
$token.Type = "or";
$token.Value = $char;
$index += 1;
}
"\(" {
$token.Type = "lb";
$token.Value = $char;
$index += 1;
}
"\)" {
$token.Type = "rb";
$token.Value = $char;
$index += 1;
}
default {
throw "LEX03 - unhandled char '$char'";
}
}
write-output $token;
}
}
Examples:
PS> ConvertTo-InfixTokens -Source "A==test"
Type Value
---- -----
identifier A
eq ==
identifier test
PS> ConvertTo-InfixTokens -Source "E==test3,(F==test4|F==test5),username!=abc"
Type Value
---- -----
identifier E
eq ==
identifier test3
and ,
lb (
identifier F
eq ==
identifier test4
or |
identifier F
eq ==
identifier test5
rb )
and ,
identifier username
ne !=
identifier abc
Convert to Postfix
Infix notation looks more natural because it's similar to normal maths, but it relies on you handling the precedence rules for operations when you evaluate expressions - e.g. is 1 + 1 * 2 + 2 equal to (((1 + 1) * 2) + 2) => 6 or 1 + (1 * 2) + 2 => 5 or (1 + 1) * (2 + 2) => 8? - which means you might need to look ahead to see what other operators are coming in case they need to be processed first.
It's easier to evaluate the expression if we convert it to postfix notation (or Reverse Polish Notation) - see this answer for some additional advantages. This basically puts the operands first and then follows with the operator - e.g. 1 + 1 * 2 + 2 becomes 1 1 2 * + 2 +, which will be logically processed as: ((1 (1 2 *) +) 2 +) => ((1 2 +) 2 +) => (3 2 +) => 5
The following code will apply this conversion:
# based on https://www.tutorialspoint.com/Convert-Infix-to-Postfix-Expression#:~:text=To%20convert%20infix%20expression%20to,maintaining%20the%20precedence%20of%20them.
function Convert-InfixToPostfix
{
param( [pscustomobject[]] $Tokens )
$precedence = #{
"or" = 1
"and" = 2
"eq" = 9
"ne" = 9
};
$stack = new-object System.Collections.Generic.Stack[pscustomobject];
for( $i = 0; $i -lt $Tokens.Length; $i++ )
{
$token = $Tokens[$i];
switch( $token.Type )
{
"identifier" {
write-output $token;
}
"lb" {
$stack.Push($token);
}
"rb" {
while( $stack.Peek().Type -ne "lb" )
{
write-output $stack.Pop();
}
$null = $stack.Pop();
}
default {
# must be a known operator
if( -not $precedence.ContainsKey($token.Type) )
{
throw "PF01 - unknown operator '$($token | ConvertTo-Json -Depth 99 -Compress)' at index $i";
}
while( ($stack.Count -gt 0) -and ($precedence[$token.Type] -le $precedence[$stack.Peek().Type]) )
{
write-output $stack.Pop();
}
$stack.Push($token);
}
}
}
while( $stack.Count -gt 0 )
{
write-output $stack.Pop();
}
}
Examples:
PS> $infix = ConvertTo-InfixTokens -Source "A==test"
PS> $postfix = Convert-InfixToPostfix -Tokens $infix
PS> $postfix
Type Value
---- -----
identifier A
identifier test
eq ==
PS> $infix = ConvertTo-InfixTokens -Source "E==test3,(F==test4|F==test5),username!=abc"
PS> $postfix = Convert-InfixToPostfix -Tokens $infix
PS> $postfix
Type Value
---- -----
identifier E
identifier test3
eq ==
identifier F
identifier test4
eq ==
identifier F
identifier test5
eq ==
or |
and ,
identifier username
identifier abc
ne !=
and ,
Evaluation
The last step is to take the postfix tokens and evaluate them. "All" we need to do is read the tokens from left to right - when we get an operand we put it on a stack, and when we get an operator we read some operands off the stack, apply the operator and push the result back onto the stack...
function Invoke-PostfixEval
{
param( [pscustomobject[]] $Tokens, [hashtable] $Variables )
$stack = new-object System.Collections.Generic.Stack[pscustomobject];
for( $i = 0; $i -lt $Tokens.Length; $i++ )
{
$token = $Tokens[$i];
if( $token.Type -eq "identifier" )
{
$stack.Push($token);
}
elseif( $token.Type -in #( "eq", "ne" ) )
{
$str = $stack.Pop().Value;
$var = $stack.Pop();
if( -not $Variables.ContainsKey($var.Value) )
{
throw "undefined variable '$($var.Value)' in token '$($token | ConvertTo-Json -Depth 99 -Compress)' at index $i";
}
$val = $Variables[$var.Value];
$result = switch( $token.Type ) {
"eq" { $val -eq $str }
"ne" { $val -ne $str }
default { throw "unhandled operator '$($token.Type)' in token '$($token | ConvertTo-Json -Depth 99 -Compress)' at index $i"; }
}
#write-host "'$val' $($token.Type) '$str' => $result";
$stack.Push([pscustomobject] [ordered] #{ "Type" = "bool"; Value = $result });
}
elseif( $token.Type -in #( "and", "or" ) )
{
$left = $stack.Pop().Value;
$right = $stack.Pop().Value;
$result = switch( $token.Type ) {
"and" { $left -and $right }
"or" { $left -or $right }
default { throw "unhandled operator '$($token.Type)' in token '$($token | ConvertTo-Json -Depth 99 -Compress)' at index $i"; }
}
#write-host "'$left' $($token.Type) '$right' => $result";
$stack.Push([pscustomobject] [ordered] #{ "Type" = "bool"; Value = $result });
}
else
{
throw "EVAL01 - unhandled token '$($token | ConvertTo-Json -Depth 99 -Compress)' at index $i";
}
}
return $stack.Peek().Value;
}
Examples:
PS> $variables = [ordered] #{
"A" = "test";
"C" = "test1";
"D" = "test2";
"E" = "test3";
"F" = "test4";
"username" = "abc"
}
PS> $infix = ConvertTo-InfixTokens -Source "A==test"
PS> $postfix = Convert-InfixToPostfix -Tokens $infix
PS> $value = Invoke-PostfixEval -Tokens $postfix -Variables $variables
PS> $value
True
PS> $infix = ConvertTo-InfixTokens -Source "E==test3,(F==test4|F==test5),username!=abc"
PS> $postfix = Convert-InfixToPostfix -Tokens $infix
PS> $value = Invoke-PostfixEval -Tokens $postfix -Variables $variables
PS> $value
False # fails because "username!=abc" is false
PS> $infix = ConvertTo-InfixTokens -Source "E==test3,F==test4|F==test5,username!=abc"
PS> $postfix = Convert-InfixToPostfix -Tokens $infix
PS> $value = Invoke-PostfixEval -Tokens $postfix -Variables $variables
PS> $value
True # operator precedence means it's evaluated as "(E==test3,F==test4)|(F==test5,username!=abc)" and "(E==test3,F==test4)" is true
You'll probably want to add more error handling for production code, and write some Pester tests to make sure it works for a larger variety of inputs, but it worked for the limited tests I did here. If you find any examples that don't work feel free to post them in comments...

Related

How to use powershell to reorder a string to obfuscate a hidden message?

Just for fun a friend and I are trying to find a creative way to send coded messages to eachother using steganography.I stumbled upon doing something like whats shown below and I have been struggling trying to write a function to automate the process.
this is a secret message
can be turned into:
("{2}{1}{0}{3}"-f'ecret m','is a s','this ','essage')
splitting the string and using reordering seems to be the way to go.
So the string needs to be split in random splits between 5-10 characters
.
The index of the original positions need to be saved
the splits need to be swapped around
and the new indexes sorted as to reorder the message properly
i've just really been struggling
help is appreciated
Just for fun .... 😉🤡
$InputMessage = 'this is a secret message'
$SplittedString = $InputMessage -split '' | Select-Object -Skip 1 | Select-Object -SkipLast 1
[array]::Reverse($SplittedString)
foreach ($Character in $SplittedString) {
if ($Character -notin $CharacterList) {
[array]$CharacterList += $Character
}
}
foreach ($Character in ($InputMessage -split '' | Select-Object -Skip 1 | Select-Object -SkipLast 1)) {
$Index = [array]::indexof($CharacterList, $Character)
$Output += "{$Index}"
}
$Result = "'$Output' -f $(($CharacterList | ForEach-Object {"'$_'"}) -join ',')"
$Result
And the output of this would be:
'{6}{10}{9}{3}{5}{9}{3}{5}{2}{5}{3}{0}{8}{7}{0}{6}{5}{4}{0}{3}{3}{2}{1}{0}' -f 'e','g','a','s','m',' ','t','r','c','i','h'
And the output of this would be:
this is a secret message
And now if you want to go fancy with it you remove the curly braces and the quotes and the commas and the -f and add only the numbers and characters to the data. ;-)
Not exactly what you're looking for but this might give you something to start with:
class Encode {
[string] $EncodedMessage
[int[]] $Map
[int] $EncodingComplexity = 3
Encode ([string] $Value) {
$this.Shuffle($Value)
}
Encode ([string] $Value, [int] $Complexity) {
$this.EncodingComplexity = $Complexity
$this.Shuffle($Value)
}
[void] Shuffle([string] $Value) {
$set = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!##$%^&*()_-+=[{]};:<>|./?'
$ref = [Collections.Generic.HashSet[int]]::new()
$ran = [random]::new()
$enc = [char[]]::new($Value.Length * $this.EncodingComplexity)
for($i = 0; $i -lt $enc.Length; $i++) {
$enc[$i] = $set[$ran.Next($set.Length)]
}
for($i = 0; $i -lt $Value.Length; $i++) {
do {
$x = $ran.Next($enc.Length)
} until($ref.Add($x))
$enc[$x] = $Value[$i]
}
$this.EncodedMessage = [string]::new($enc)
$this.Map = $ref
}
}
class Decode {
static [string] DecodeMessage ([Encode] $Object) {
return [Decode]::DecodeMessage($Object.EncodedMessage, $Object.Map, $Object.EncodingComplexity)
}
static [string] DecodeMessage ([string] $EncodedMessage, [int[]] $Map) {
return [Decode]::DecodeMessage($EncodedMessage, $Map, 3)
}
static [string] DecodeMessage ([string] $EncodedMessage, [int[]] $Map, [int] $Complexity) {
$decoded = [char[]]::new($EncodedMessage.Length / $Complexity)
for($i = 0; $i -lt $decoded.Length; $i++) {
$decoded[$i] = $EncodedMessage[$Map[$i]]
}
return [string]::new($decoded)
}
}
Encoding a message:
PS /> $message = 'this is a secret message'
PS /> $encoded = [Encode] $message
PS /> $encoded
EncodingComplexity EncodedMessage Map
------------------ -------------- ---
3 B$h^elu2w#CeeHH^qa siQJ)t}es:.a3 ema=eN(GiIcsO;tst1 .fsg}eSUk7ms4 N>rfe# {49, 2, 41, 27…}
For decoding the message you can either use the object of the type Encode or you can give your friend the Encoded Message and the Map to decode it ;)
PS /> [Decode]::DecodeMessage($encoded)
this is a secret message
PS /> [Decode]::DecodeMessage('B$h^elu2w#CeeHH^qa siQJ)t}es:.a3 ema=eN(GiIcsO;tst1 .fsg}eSUk7ms4 N>rfe#', $encoded.Map)
this is a secret message

Creating Arraylist of Arraylist by slicing existing arraylist

I have the following variable defined
$A = New-Object -TypeName "System.Collections.ArrayList"
Now I add n elements to it :
$A.Add(1..n)
Now I want to divide $A into p parts of k elements each(The last one might have lesser elements if p*k>$A.count).
How do I do that?
You can use a function to split an array into several smaller arrays.
Below a slighty adapted version of that function found here:
function Split-Array {
[CmdletBinding(DefaultParametersetName = 'ByChunkSize')]
Param(
[Parameter(Mandatory = $true, Position = 0)]
$Array,
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'ByChunkSize')]
[ValidateRange(1,[int]::MaxValue)]
[int]$ChunkSize,
[Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'ByParts')]
[ValidateRange(1,[int]::MaxValue)]
[int]$Parts
)
$items = $Array.Count
switch ($PsCmdlet.ParameterSetName) {
'ByChunkSize' { $Parts = [Math]::Ceiling($items / $ChunkSize) }
'ByParts' { $ChunkSize = [Math]::Ceiling($items / $Parts) }
default { throw "Split-Array: You must use either the Parts or the ChunkSize parameter" }
}
# when the given ChunkSize is larger or equal to the number of items in the array
# use TWO unary commas to return the array as single sub array of the result.
if ($ChunkSize -ge $items) { return ,,$Array }
$result = for ($i = 1; $i -le $Parts; $i++) {
$first = (($i - 1) * $ChunkSize)
$last = [Math]::Min(($i * $ChunkSize) - 1, $items - 1)
,$Array[$first..$last]
}
return ,$result
}
In your case you could use it like:
$p = 4 # the number of parts you want
$subArrays = Split-Array $A.ToArray() -Parts $p
or
$k = 4 # the max number items in each part
$subArrays = Split-Array $A.ToArray() -ChunkSize $k
Here is a function I came up with to chunk your System.Collections.ArrayList into a nested array list of p parts. It uses a System.Collections.Specialized.OrderedDictionary to group the size k chunks by index / chunksize, which is then rounded down to the nearest integer using System.Math.Floor. It then only fetches the groups with keys from 0 to $Parts.
function Split-ArrayList {
[CmdletBinding()]
param (
# Arraylist to slice
[Parameter(Mandatory=$true)]
[System.Collections.ArrayList]
$ArrayList,
# Chunk size per part
[Parameter(Mandatory=$true)]
[ValidateRange(1, [int]::MaxValue)]
[int]
$ChunkSize,
# Number of parts
[Parameter(Mandatory=$true)]
[ValidateRange(1, [int]::MaxValue)]
[int]
$Parts
)
# Group chunks into hashtable
$chunkGroups = [ordered]#{}
for ($i = 0; $i -lt $ArrayList.Count; $i++) {
# Get the hashtable key by dividing the index by the chunk size
# Round down to nearest integer using Math.Floor
[int]$key = [Math]::Floor($i / $ChunkSize)
# Add new arraylist for key if it doesn't exist
# ContainsKey is not supported for ordered dictionary
if ($chunkGroups.Keys -notcontains $key) {
$chunkGroups.Add($key, [System.Collections.ArrayList]::new())
}
# Add number to hashtable
[void]$chunkGroups[$key].Add($ArrayList[$i])
}
# Create nested ArrayList of parts
$result = [System.Collections.ArrayList]::new()
for ($key = 0; $key -lt $Parts; $key++) {
[void]$result.Add($chunkGroups[$key])
}
$result
}
Usage:
$A = [System.Collections.ArrayList]::new(1..10)
Split-ArrayList -ArrayList $A -ChunkSize 4 -Parts 1 |
ForEach-Object { "{ " + ($_ -join ", ") + " }" }
# { 1, 2, 3, 4 }
Split-ArrayList -ArrayList $A -ChunkSize 4 -Parts 2 |
ForEach-Object { "{ " + ($_ -join ", ") + " }" }
# { 1, 2, 3, 4 }
# { 5, 6, 7, 8 }
Split-ArrayList -ArrayList $A -ChunkSize 4 -Parts 3 |
ForEach-Object { "{ " + ($_ -join ", ") + " }" }
# { 1, 2, 3, 4 }
# { 5, 6, 7, 8 }
# { 9, 10 }
Note: I didn't really account for the cases where you might want to exclude Parts, so I made every parameter mandatory. You can amend the function to be more flexible with different inputs.

Powershell 'x or y' assignment

There are several languages that provide either a defaulting or logical or mechanism for assignment:
a = b || c;
a = b or c
a="${b:-$c}"
a = b ? b : c;
So far the only equivalent I've found in Powershell Core is the exceedingly verbose:
$a = if ($b) { $b } else { $c }
which in some cases has to become
$a = if ($b -ne $null) { $b } else { $c }
Is there a better alternative [edit:] which doesn't sacrifice readability?
There's no || short-circuit operator in PowerShell assignments, and nothing equivalent to Perl's // "defined-or" operator - but you can construct a simple null coalesce imitation like so:
function ?? {
param(
[Parameter(Mandatory=$true,ValueFromRemainingArguments=$true,Position=0)]
[psobject[]]$InputObject,
[switch]$Truthy
)
foreach($object in $InputObject){
if($Truthy -and $object){
return $object
}
elseif($object -ne $null){
return $object
}
}
}
Then use like:
$a = ?? $b $c
or, if you want to just have return anything that would have evaluated to $true like in your first example:
$a = ?? $b $c -Truthy
So a simple method for reducing this:
$a = if ($b -ne $null) { $b } else { $c }
Would be to use the fact that true and false are just one or zero and could be used as an array index like so:
$a = #($c, $b)[$b -ne $null]

Append number to end of string in Powershell for loop

Trying to append the index value of a for loop to the end of a string, but I'm having some trouble getting the desired result.
Here's a short code block of what I'm working with:
EDIT:
$apiKey = "ZZZ-ZZZZZ"
$octoHeader = #{ "X-Octopus-ApiKey" = $apiKey }
# GET Source deployment process from Deployment Scaffold project
$json = irm "http://octopusserver/api/deploymentprocesses/deploymentprocess-projects-123" -Headers $octoHeader
$DeploymentSteps = $json.Steps
# Hash table to hold steps
$steps = #{}
function Get-StepType
(
$StepType
)
{
foreach ( $Step in $DeploymentSteps | where { $_.Name -eq $StepType } )
{
return $Step
}
}
function Copy-Steps
(
$StepType,
$StepCount
)
{
$Step = Get-StepType -StepType $StepType
1..$StepCount | % {
$Step.Id = ''
$Step.Name += $_
# Add step to hash
$steps.Add("step$($steps.Count + 1)", $step)
}
}
Copy-Steps -StepType 'Service' -StepCount 2
$steps
And this is the result:
Name Value
---- -----
step1 #{Id=; Name=Service12; Actions=S...
step2 #{Id=; Name=Service12; Actions=S...
The result I'm after is: Name=Service - 1, Name=Service - 2, etc. I see what's happening, but I'm not sure the proper way to get what I'm after.
Any help is greatly appreciated.
In your function Copy-Steps you need to actually copy steps:
function Copy-Steps
(
$StepType,
$StepCount
)
{
$OriginalStep = Get-StepType -StepType $StepType
1..$StepCount | % {
$Step=$OriginalStep.PSObject.Copy()
$Step.Id = ''
$Step.Name += $_
# Add step to hash
$steps.Add("step$($steps.Count + 1)", $step)
}
}
1..5 | % { [String]::Format("Step {0}", $_) }
or thereabouts

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
}