36

Editor's note: This question has a complicated history, but boils down to this:
* To learn how to enumerate the entries of a hashtable by its key-value pairs, see the accepted answer.
* To learn how to filter a hashtable by a collection of key values, see the other answer.


I think I fell into the X Y problem again, my initial question was about Filtering a hash table. I discovered it's easier to filter before creating the hash table. Question answered, right?

Nope, the Y problem was looping each Key and using the Values which @briantist helped me with.

My goal is to loop over the key names, which are timestamps, and schedule a task using the key name as the task name and trigger.

I'm creating a hash table from a CSV file using Group-Object -AsHashTable -AsString -edit, it's worth mentioning here that filtering the CSV before creating the HashTable only makes things easier down the Pipeline or script.

As an example:

Import-CSV (ls -path D:\ -Filter source*.csv | sort LastWriteTime | Select -Last 1).FullName |
 where {$_.TimeCorrected -ne 'ManualRebootServer'} |
 group TimeCorrected -AsHashTable -AsString

I'm trying to loop over the key names and able to display the key names using:

$var = Import-Csv csv123.csv | Group-Object Value1 -AsHashTable -AsString

foreach ($key in $var.Keys){"The key name is $key"}

#Create a scheduled task named and triggered based on the HashTable keyname
#test test test
foreach ($key in $var.keys){IF($key -ne 'ManualRebootServer'){"Register-ScheduledJob"}}

I'm just not sure how to get the values from the keys I am interested in.

I've found the following works, but only when I enter a Key name manually. I'm just unsure how to combine both loops.

($val.GetEnumerator() | Where {$_.key -eq '06-11-16 18:00'} | ForEach-Object { $_.value }).Server
Community
  • 1
  • 1
user4317867
  • 2,397
  • 4
  • 31
  • 57
  • Just to offer an explanation, the filtering (at least in PowerShell) is always best accomplished before or to the left of the Pipeline operator `|` to make scripts more efficient. – user4317867 Jun 05 '16 at 16:01
  • 2
    Indeed; to put it differently: Using provider-native filtering with the `-Filter` parameter (if available) is usually much faster than passing unfiltered output through the pipeline in order to let the `Where-Object` cmdlet perform the filtering later. – mklement0 Jun 05 '16 at 17:52

2 Answers2

65

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
}
Ram
  • 1,115
  • 8
  • 20
briantist
  • 45,546
  • 6
  • 82
  • 127
  • I've tried the code in the first block, using an IF statement because there is a key I want to skip. This works `$value = $val[$key].server` but I've got further filtering work that needs to be done after this. Could it be as simple as putting the `$value` into an array and filter after the fact? – user4317867 Jun 04 '16 at 23:24
  • 1
    @user4317867 it would help if you would be more explicit in what you're ultimately trying to accomplish, as I can only speculate based on what you've said so far. "Further filtering work" can mean a lot of things. – briantist Jun 04 '16 at 23:29
  • Apologies, I put a half day into this question! It's worth pointing out to filter the CSV before creating the hashtable, eg `Import-CSV file.csv | where {$_.Column -ne 'String'}` to make things easier down the line! – user4317867 Jun 04 '16 at 23:54
  • 1
    Neatly summarizes the 2 idioms to master. It's a thousand pities Powershell doesn't have a less cumbersome .GetEnumerator(). – BobHy Dec 06 '18 at 14:44
15

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).

mklement0
  • 382,024
  • 64
  • 607
  • 775