Powershell fails to use keyword 'param' in the second calling function - powershell

function param-simple {
Param([string]$user_, [string]$host_, [Parameter(position=0,ValueFromRemainingArguments)]$params_)
write-host "[*]user: $user_"
write-host "[*]host: $host_"
for($i=0; $i -lt $params_.count; $i++){ write-host " [$i] $($params_[$i])" }
}
function param-twice {
Param([string]$first_, [string]$second_, [string[]][Parameter(position=0,ValueFromRemainingArguments)]$params1_)
write-host "[-] first: $first_"
write-host "[-] second: $second_"
$params2_ = #()
$params1_ | foreach-object { $params2_ += $_ }
param-simple #params2_ # also not work for #params1_, but work for #args
}
function param-simple works properly if it's called directly, but it will not if it's called through the function param-twice
D:\MyTour\PowerShell01>param-simple -first f -second s -user u -host h 1 2
[*]user: u
[*]host: h
[0] -first
[1] f
[2] -second
[3] s
[4] 1
[5] 2
D:\MyTour\PowerShell01>param-twice -first f -second s -user u -host h 1 2
[-] first: f
[-] second: s
[*]user:
[*]host:
[0] -user
[1] u
[2] -host
[3] h
[4] 1
[5] 2
Would you kindly look into it?

Related

Powershell pull last 5 minutes of data from CSV

I have a log file that generates new data every five minutes, I am attempting to pull the last five minutes of the log file and pull specific data from the last five minutes. Currently I have code to convert it from .log to .csv with headers of "Date, Time, Error1, Error2, Error3". However, every attempt I have tried thus far is not pulling the data correctly.
The Date and Time of the CSV are formatted as: "dd/MM/yyyy","hh:mm:ss.ms"
Powershell does not give any visible errors, but the errorCOLLECTION.csv does not generate
The Current code I have:
Copy-Item -Path "C:\ProgramData\Blah\Blah\Blah Blah\error.log" -Destination "C:\Windows\Blah\Blah\Logs\Temp\Blah Blah\" -PassThru
Import-Csv "C:\Windows\Blah\Blah\Logs\Temp\Blah Blah\error.log" -delimiter "," -Header Date, Time, Error1, Error2, Error3 |
Export-Csv -NoTypeInformation "C:\Windows\Blah\Blah\Logs\Temp\Blah Blah\error.csv"
$referenceTime = '{0:dd/MM/yyyy,HH:mm:ss.ms}' -f (Get-Date '2019/02/25,19:09:00.590').AddMinutes(-5)
$regexSearch = '\bSdata:\s*\[(\d{2})]'
switch -Regex -File "C:\Windows\Blah\Blah\Logs\Temp\Blah Blah\error.csv" {
$regexSearch {
if (($_ -split ',')[0] -gt $referenceTime) {
set-content "C:\Windows\Blah\Blah\Logs\Temp\Blah Blah\errorCOLLECTION.csv"
}
}
}
In response to Theo an example of the Log file:
29/11/2022,10:48:48.693,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>lpszKeyName [CommsKey]</X><SF>key_lib.cpp</SF><SL>1177</SL>
29/11/2022,10:48:48.693,MDMSP,DC,<PID>6200</PID><TID>5772</TID><G><X>W</X><HS>65535</HS><RI>0</RI></G><F>SXIO::ReceiveIOMessage</F><AR><AN>RetrieveMessage</AN><RV><I4>0</I4></RV><P><N>szMessage</N><S>messageCategory: 0x3 messageType: 0x554cc006 messageID: 0xd6d7928</S></P><P><N>response</N><S>hservice: 43 ucClass: 3 usTLen: 4 TData: [33 01 00 12] ucSLen: 1 SData: [00] ucMLen: 0 ucRSlen: 0 ucRClen: 0</S></P></AR><SF>SXIO.cpp</SF><SL>833</SL>
29/11/2022,10:48:48.693,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>Return Value [0]</X><X>lptKeyDetail [caKeyName [CommsKey] usKeyId [2] usKeyspaceId [4] wTypeOfAccess [0x2] bIsIV [0] bMasterKey [0] bLoadedFlag [1] bIsDouble [0]]</X><SF>key_lib.cpp</SF><SL>1214</SL>
29/11/2022,10:48:48.693,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>lpszKeyName [MACKey]</X><SF>key_lib.cpp</SF><SL>1177</SL>
29/11/2022,10:48:48.693,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>Return Value [0]</X><X>lptKeyDetail [caKeyName [MACKey] usKeyId [3] usKeyspaceId [3] wTypeOfAccess [0x4] bIsIV [0] bMasterKey [0] bLoadedFlag [0] bIsDouble [1]]</X><SF>key_lib.cpp</SF><SL>1214</SL>
29/11/2022,10:48:48.694,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>lpszKeyName [PEKey]</X><SF>key_lib.cpp</SF><SL>1177</SL>
29/11/2022,10:48:48.694,PINSP,DC,<PID>6324</PID><TID>2996</TID><F>INFO_GET_KEY_DETAIL</F><X>Return Value [0]</X><X>lptKeyDetail [caKeyName [PEKey] usKeyId [4] usKeyspaceId [4] wTypeOfAccess [0x2] bIsIV [0] bMasterKey [0] bLoadedFlag [0] bIsDouble [1]]</X><SF>key_lib.cpp</SF><SL>1214</SL>
29/11/2022,10:48:48.694,PINSP,FW,<PID>6324</PID><TID>2996</TID><G><X>W</X><HS>44</HS><RI>2267</RI></G><F>PostWFSResult</F><F>WFSResultData</F><P><N>hWnd</N><PT>263508</PT></P><P><N>lpWFSResult->RequestID</N><U4>2267</U4></P><P><N>lpWFSResult->hService</N><U4>44</U4></P><P><N>lpWFSResult->hResult</N><H>0</H></P><P><N>lpWFSResult->u.dwCommandCode</N><U4>401</U4></P><P><N>lpStatus</N><OB><M><N>fwDevice</N><U2>0</U2></M><M><N>fwEncStat</N><U2>0</U2></M><M><N>lpszExtra</N><PT>00000000</PT></M><M><N>guidlight</N><S>0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0</S></M><M><N>fwAutoBeepMode</N><U2>2</U2></M><M><N>dwCertificateState</N><U4>4</U4></M><M><N>wDevicePosition</N><U2>3</U2></M><M><N>usPowerSaveRecoveryTime</N><U2>0</U2></M><M><N>wAntiFraudModule</N><U2>0</U2></M></OB></P><SF>FWResultImpl.cpp</SF><SL>4638</SL><E>WFS_GETINFO_COMPLETE</E>
29/11/2022,10:48:48.697,PINSP,FW,<PID>6324</PID><TID>2996</TID><G><X>W</X><HS>44</HS><RI>2268</RI></G><F>PostWFSResult</F><F>WFSResultData</F><P><N>hWnd</N><PT>7014346</PT></P><P><N>lpWFSResult->RequestID</N><U4>2268</U4></P><P><N>lpWFSResult->hService</N><U4>44</U4></P><P><N>lpWFSResult->hResult</N><H>0</H></P><P><N>lpWFSResult->u.dwCommandCode</N><U4>408</U4></P><SF>FWResultImpl.cpp</SF><SL>4638</SL><E>WFS_GETINFO_COMPLETE</E>
29/11/2022,10:48:48.702,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>44</HS></G><F>WFSGetInfo</F><P><N>*lppResult</N><OB><M><N>RequestID</N><U4>2268</U4></M><M><N>hService</N><U2>44</U2></M><M><N>hResult</N><U4>0</U4></M><M><N>Code</N><U4>408</U4></M><M><N>lpBuffer</N><PT>280E284D</PT></M></OB></P><RV><H>0</H></RV><SF>MgrApi.cpp</SF><SL>1394</SL>
29/11/2022,10:48:48.702,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><P><N>lpResult</N><OB><M><N>RequestID</N><U4>2268</U4></M><M><N>hService</N><U2>44</U2></M><M><N>hResult</N><U4>0</U4></M><M><N>Code</N><U4>408</U4></M><M><N>lpBuffer</N><PT>280E284D</PT></M></OB></P><SF>MgrApi.cpp</SF><SL>1230</SL>
29/11/2022,10:48:48.702,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><RV><H>0</H></RV><SF>MgrApi.cpp</SF><SL>1240</SL>
29/11/2022,10:48:48.703,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><P><N>lpResult</N><OB><M><N>RequestID</N><U4>2266</U4></M><M><N>hService</N><U2>49</U2></M><M><N>hResult</N><U4>0</U4></M><M><N>Code</N><U4>301</U4></M><M><N>lpBuffer</N><PT>08120D85</PT></M></OB></P><SF>MgrApi.cpp</SF><SL>1230</SL>
29/11/2022,10:48:48.703,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><RV><H>0</H></RV><SF>MgrApi.cpp</SF><SL>1240</SL>
29/11/2022,10:48:48.703,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><P><N>lpResult</N><OB><M><N>RequestID</N><U4>2267</U4></M><M><N>hService</N><U2>44</U2></M><M><N>hResult</N><U4>0</U4></M><M><N>Code</N><U4>401</U4></M><M><N>lpBuffer</N><PT>281523A5</PT></M></OB></P><SF>MgrApi.cpp</SF><SL>1230</SL>
29/11/2022,10:48:48.703,Mgr,Mgr,<PID>6324</PID><TID>6588</TID><G><HS>0</HS></G><F>WFSFreeResult</F><RV><H>0</H></RV><SF>MgrApi.cpp</SF><SL>1240</SL>
Doing a slight modification to your code, because ($_ -split ',')[0] would be only targeting the Date and not the Time, the following works properly for me outputting the line starting with:
29/11/2022,10:48:48.693,MDMSP,DC,<PID>6200...
I'm also using DateTime.TryParse to convert these strings into a DateTime instance, I'm honestly not sure comparing these strings as strings would work correctly, at least converting them to DateTime instances we're 100% sure the comparison will be correct.
Aside from that, as was pointed out in comments by other users, Set-Content currently has the path to output but no value as argument.
& {
$referenceTime = (Get-Date '2019/02/25,19:09:00.590').AddMinutes(-5)
$regexSearch = '\bSdata:\s*\[(\d{2})]'
$parsedDate = [ref] [datetime]::new(0)
switch -Regex -File 'C:\bla\bla\error.csv' {
$regexSearch {
$success = [datetime]::TryParseExact(
('{0},{1}' -f $_.Split(',', 3)[0, 1]),
'dd/MM/yyyy,HH:mm:ss.fff',
[cultureinfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::AssumeLocal,
$parsedDate
)
if($success -and $parsedDate.Value -gt $referenceTime) {
$_
}
}
}
} | Set-Content 'C:\bla\bla\errorCOLLECTION.csv'

Why is this do/until loop exiting with values starting 1?

Built a menu system which for the most part works fine but I've hit a weird validation error and I'm scratching my head as to why this function is escaping when you answer with "11" (or indeed any number starting with 1)
function Get-MenuSelection {
$totalOptions = 2
do {
$input = Read-Host "[1 <-> $totalOptions | (Q)uit - FIRST]"
while (!$input) {
$input = Read-Host "[1 <-> $totalOptions | (Q)uit - LOOP]"
}
} until ($input -lt $totalOptions -or $input -eq $totalOptions -or $input -eq "q")
Write-Host "exiting"
}
Get-MenuSelection
Output I'm getting:
./wtf.ps1
[1 <-> 2 | (Q)uit - FIRST]:
[1 <-> 2 | (Q)uit - LOOP]:
[1 <-> 2 | (Q)uit - LOOP]: test
[1 <-> 2 | (Q)uit - FIRST]: 22
[1 <-> 2 | (Q)uit - FIRST]: 9090
[1 <-> 2 | (Q)uit - FIRST]: 11
exiting
I'm clearly doing something wrong but just can't figure out what.
Solution
For those reading this some time in the future, I ended up with this - I chose to drop the 'q' options since it was just over-complicating the logic. Thanks to #AdminofThings and #mklement0 for the input. Appreciated.
function Get-MenuSelection {
param (
$output
)
[int]$totalOptions = $output.Count
do {
try { [int]$answer = Read-Host "Options: [1 <-> $totalOptions]" }
catch { }
if ($answer -eq "0" -or $answer -gt $totalOptions) {
Write-Host "Invalid input detected. Ctrl+C to quit."
}
} while ($answer -gt $totalOptions -or !$answer)
$returnedAnswer = "menu_$answer"
return $returnedAnswer
}
Since $input is an automatic/reserved variable, your code will not execute as intended. $input will likely result in an empty value during retrieval.
If we theoretically assume that $input is replaced by something that is not reserved, then a corresponding issue here is $input is a string and $totaloptions is an int. When PowerShell is faced with a comparison operation and both sides of the comparison don't match types, it will attempt to convert the righthand side (RHS) type to match the lefthand side (LHS). To get around this, you need to either cast $input as an [int] or bring $totaloptions to the LHS.
until ([int]$input -lt $totalOptions -or $input -eq $totalOptions -or $input -eq "q")
# OR
until ($totalOptions -gt $input -or $input -eq $totalOptions -or $input -eq "q")
An example of your situation:
#Unexpected Outcome
> [string]11 -lt [int]2
True
#Expected Outcome
> [int]11 -lt [int]2
False
#Expected Outcome
> [int]2 -gt [string]11
False

powershell v2 function, how to have a default value for the 1st argument w/o naming the 2nd?

the problem is quite simple. I have a function with two arguments and I would like to be able to call it without providing the first one when it has its default value, without naming the second one.
for example (this is not my real function but a simple example to show what I mean)
function foo
{ param([int]$int=-1,[string]$str="''")
$int
$str
}
I was hoping that forcing the type in the argument list will make PS bind the right value to the right argument but it seams that it's not the case
PS C:\> foo
-1
''
PS C:\> foo 1
1
''
PS C:\> foo 1 x
1
x
PS C:\> foo -str x
-1
x
PS C:\> foo x
foo : Impossible de traiter la transformation d'argument sur le paramètre «int». Impossible de convertir la valeur «x» en type «System.Int32». Erreur: «Le format de la chaîne
d'entrée est incorrect.»
Au caractère Ligne:1 : 5
+ foo x
+ ~
+ CategoryInfo : InvalidData : (:) [foo], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,foo
is what I want possible in PS ?
EDIT
here's a better example of what I'm trying to do
test.ps1
[cmdletbinding()]
param(
[parameter()]
[string[]]$___where=#('*')
)
function InvokeOn {
[cmdletbinding()]
param(
[string[]]
${___names}=#('*'),
[scriptblock]
${___block}
)
$___doExec = ($___names -contains '*')
foreach($___name in $___names) { $___doExec = ($___doExec -or ($___where -contains $___name)) }
if($___doExec) { &$___block }
}
$null = new-item alias:__LOG__ -value InvokeOn
__LOG__ c1 { Write-host '-- 1 --' }
__LOG__ c2 { Write-host '-- 2 --' }
__LOG__ c1,c2 { Write-host '-- 1 or 2 --' }
__LOG__ { Write-host 'always, defaulted' }
__LOG__ -___block { Write-host 'always, named' }
and a few run
PS C:\> .\test
always, named
PS C:\> .\test c1
-- 1 --
-- 1 or 2 --
always, named
PS C:\> .\test c2
-- 2 --
-- 1 or 2 --
always, named
PS C:\> .\test c2,c1
-- 1 --
-- 2 --
-- 1 or 2 --
always, named
as you can see, __LOG__ { Write-host 'always, defaulted' } never fires as PS binds the scriptblock the wrong parameter.
Parameters names are on purpose complex and should not even be known by the developer using the aliased function.
Swapping the parameters is not practical as the scriptblock may be long and even for short ones, make the case where __LOG__ will fire less readable.
SOLUTION
applying majkinetor idea, I modified my code this way
function InvokeOn {
[cmdletbinding()]
param(
[string[]]
${___names} = #('*'),
[scriptblock]
${___block}
)
if(!$PSBoundParameters.ContainsKey('___block')) { $___names,$___block = #('*'),[scriptblock]::create($___names[0]) }
$___doExec = ($___names -contains '*')
foreach($___name in $___names) { $___doExec = ($___doExec -or ($___where -contains $___name)) }
if($___doExec) { &$___block }
}
and now it works as expected :)
You could do something like:
function foo
{
param ($int=-1,[string]$str="''")
if ($int.gettype().Name -eq 'String') { $str = $int; $int = -1 }
$int
$str
}
Notice - $int must not have type.
I consider hiding parameter names from the user and evaluating parameters solely based on their type a Bad Idea™ and strongly recommend against doing this.
With that said, if you for some obscure reason you absolutely must have it this way, I'd drop named parameters entirely and use the $args automatic variable instead.
function foo {
# define default values for your "parameters"
$int = -1
$str = "''"
...
# evauluate $args and override default values
foreach ($arg in $args) {
switch ($arg.GetType().FullName) {
'System.Int32' { $int = $arg }
'System.String' { $str = $arg }
...
default { throw "Unrecognized type ${_}." }
}
}
# handle missing arguments if required
# Example:
# if ($args.Count -eq 0) { throw "No arguments provided." }
...
}

Powershell Array parameter from pipline

I'm trying to duplicate a typical powershell -Computername parameter that's available from the pipeline and as a normal parameter using CmdletBinding and ValueFromPipeline. My challenge is that I'm getting different results from specifying the parameter versus piping in values.
My code looks like this:
[CmdletBinding()]
param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)] [string[]]$ComputerName
)
BEGIN { "Begin script`n-----" }
PROCESS {
" `$ComputerName '$ComputerName'"
" `$_ '$_'"
" +++ Count: " + $ComputerName.Count
foreach($computer in $ComputerName) {
" `$computer '$computer'"
}
" -----"
}
END { "Complete" }
When I run this using a pipeline, I get this:
PS> (1, 2, 3) | .\BPEParamTest.ps1
Begin script
-----
$ComputerName '1'
$_ '1'
+++ Count: 1
$computer '1'
-----
$ComputerName '2'
$_ '2'
+++ Count: 1
$computer '2'
-----
$ComputerName '3'
$_ '3'
+++ Count: 1
$computer '3'
-----
Complete
However, when run with a parameter, I get different results:
PS> .\BPEParamTest.ps1 -ComputerName (1, 2, 3)
Begin script
-----
$ComputerName '1 2 3'
$_ ''
+++ Count: 3
$computer '1'
$computer '2'
$computer '3'
-----
Complete
I always use the following construction. This works for arrays in a parameter as well as from the pipeline:
[CmdletBinding()]
param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)] [string[]]$ComputerName
)
process {
foreach($computer in $computername){
#do the stuff
}
Full explanation: The process block is run once for each item in the pipeline, so that's how it handles lists on the pipeline (i.e. $computername is set to each item in turn). If you pass the values as a parameter, the $computername is set to the list which is why there's a loop.
.\BPEParamTest.ps1 -ComputerName (1, 2, 3) does not use the pipeline but rather a single input (a 3-element array). In contrast, (1, 2, 3) | .\BPEParamTest.ps1 uses the pipeline (3 separate inputs).
To work around this, you can check bound parameters and determine if there is pipeline input. Here is a short example:
function out-item {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$TRUE)]
$item
)
begin {
$PipelineInput = -not $PSBoundParameters.ContainsKey("item")
write-host "Pipeline input? $PipelineInput"
$n = 0
}
process {
if ($PipelineInput) {
$_
$n++
}
else {
$item | foreach-object {
$_
$n++
}
}
}
end {
write-host "Output $n item(s)"
}
}
With this function, both 1,2,3 | out-item and out-item 1,2,3 produce the same output (except for the "Pipeline input?" part).

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
}