How can I enumerate a hashtable as key-value pairs / filter a hashtable by a collection of key values
You have some options here.
Enumerating through keys:
foreach ($key in $var.Keys) {
$value = $var[$key]
# or
$value = $var.$key
}
Enumerating key-value pairs (which you've discovered, but may not be using effectively):
foreach ($kvp in $var.GetEnumerator()) {
$key = $kvp.Key
$val = $kvp.Value
}
To complement briantist's helpful answer by focusing on filtering a hashtable by an array of key values (PSv3+ syntax):
# Sample hashtable.
$ht = @{ one = 1; two = 2; three = 3 }
# Filter it by an array of key values; applying .GetEnumerator() yields an array
# of [System.Collections.DictionaryEntry] instances, which have
# a .Key property and a .Value property.
$ht.GetEnumerator() | ? Key -in 'one', 'two'
# Similarly, the *output* - even though it *looks* like a hashtable -
# is a regular PS *array* ([Object[]]) containing [System.Collections.DictionaryEntry]
# entries (2 in this case).
$arrFilteredEntries = $ht.GetEnumerator() | ? Key -in 'one', 'two'
$arrFilteredEntries.GetType().Name # -> Object[]
To further process the matching key-value pairs, simply pipe to %
(ForEach-Object
) and access $_.Key
and $_.Value
(value):
$ht.GetEnumerator() | ? Key -in 'one', 'two' |
% { "Value for key '$($_.Key)': $($_.Value)" }
The equivalent command using a more efficient foreach
loop instead of the pipeline:
foreach ($key in $ht.Keys) {
if ($key -in 'one', 'two') { "Value for key '$($key)': $($ht.$key)" }
}
Note: In PSv2:
* operator -in
is not supported, but you can use -contains
instead with the operands swapped:
'one', 'two' -contains $key
* in the pipeline, use Where-Object { 'one', 'two' -contains $_.Key }
With the sample hashtable, this yields:
Value for key 'two': 2
Value for key 'one': 1
Note how the key order in the output differs from the definition order; in PSv3+, you can create ordered hashtables ([ordered] @{ ... }
) to preserve the definition order.
The key-filtering technique used above is not limited to filtering by literal key arrays; any (string) collection will do as the RHS of the -in
operand, such as the .Keys
collection of a different hashtable:
# Sample input hashtable.
$htInput = @{ one = 1; two = 2; three = 3 }
# Hashtable by whose keys the input hashtable should be filtered.
# Note that the entries' *values* are irrelevant here.
$htFilterKeys = @{ one = $null; two = $null }
# Perform filtering.
$htInput.GetEnumerator() | ? Key -in $htFilterKeys.Keys |
% { "Value for key '$($_.Key)': $($_.Value)" }
# `foreach` loop equivalent:
foreach ($key in $htInput.Keys) {
if ($key -in $htFilterKeys.Keys) { "Value for key '$($key)': $($htInput.$key)" }
}
The result is the same as in the example with the static filter-keys array.
Finally, if you want to filter a hashtable in place or create a new hashtable with only the filtered entries:
# *In-place* Updating of the hashtable.
# Remove entries other than the ones matching the specified keys.
# Note: The @(...) around $ht.Keys is needed to clone the keys collection before
# enumeration, so that you don't get an error about modifying a collection
# while it is being enumerated.
foreach ($key in @($ht.Keys)) {
if ($key -notin 'one', 'two') { $ht.Remove($key) }
}
# Create a *new* hashtable with only the filtered entries.
# By accessing the original's .Keys collection, the need for @(...) is obviated.
$htNew = $ht.Clone()
foreach ($key in $ht.Keys) {
if ($key -notin 'one', 'two') { $htNew.Remove($key) }
}
As an aside:
The default output format for [System.Collections.DictionaryEntry]
(and thus hashtables ([System.Collections.Hashtable]
) uses column name Name
rather than Key
; Name
is defined as an alias property of Key
added by PowerShell (it is not part of the [System.Collections.DictionaryEntry]
.NET type definition; verify with
@{ one = 1 }.GetEnumerator() | Get-Member
).