You're creating a hashtable, whose entries cannot be accessed with positional indices (0, 1, ...); instead what you specify inside [...] is an entry key.
Therefore, use the .Keys property for enumeration:
$test = @{set01 = '0'; set02 = '1'}
foreach (
test.Keys) {
$key
# To access the entry's *value*, use $test[
test[$key]
}
The result is:
set02
set01
Note how the keys were enumerated in a different order than they were defined, because [hashtable] instances do not guarantee any particular enumeration order.
In PSv3+ you can use an ordered hashtable to remedy that, which would also allow you to use positional indexing - but note that positional indexing then returns the entry values, not the keys:
$test = [ordered] @{set01 = '0'; set02 = '1'}
for (
i -lt $test.Count; $i++) {
$test[$i]
}
Note: While you could use $test.Keys[$i] to access the keys, as in Dandre's answer, direct enumeration of the keys with foreach ( is less verbose and faster; the only good reason to use index-based iteration with test.Keys)
$test.Keys[$i] would be if index $i weren't just a helper variable, but were required for operation, such as for outputting a string that reflects both a key and its index.
The above therefore yields:
0
1
Note that the for syntax is not only verbose, it actually performs worse than foreach, and in case you do need explicit indices, use foreach with the indices generated via the range operator (..), which is not only faster but also arguably more readable than the for loop:
$test = [ordered] @{set01 = '0'; set02 = '1'}
foreach (
test.Count-1)) {
$test[$i]
}
While it is less memory-efficient, because the array of indices must be created as a whole up front, that is usually not a concern.
Answer from mklement0 on Stack OverflowWhy isn't .GetEnumerator() treated as array by default on hashtables/dictionaries?
powershell - How can I enumerate a hashtable as key-value pairs / filter a hashtable by a collection of key values - Stack Overflow
dictionary - Looping through a hash, or using an array in PowerShell - Stack Overflow
powershell - Why I can use both `Name` and `Key` to get the key in `$hashTable.GetEnumerator()`? - Stack Overflow
You're creating a hashtable, whose entries cannot be accessed with positional indices (0, 1, ...); instead what you specify inside [...] is an entry key.
Therefore, use the .Keys property for enumeration:
$test = @{set01 = '0'; set02 = '1'}
foreach (
test.Keys) {
$key
# To access the entry's *value*, use $test[
test[$key]
}
The result is:
set02
set01
Note how the keys were enumerated in a different order than they were defined, because [hashtable] instances do not guarantee any particular enumeration order.
In PSv3+ you can use an ordered hashtable to remedy that, which would also allow you to use positional indexing - but note that positional indexing then returns the entry values, not the keys:
$test = [ordered] @{set01 = '0'; set02 = '1'}
for (
i -lt $test.Count; $i++) {
$test[$i]
}
Note: While you could use $test.Keys[$i] to access the keys, as in Dandre's answer, direct enumeration of the keys with foreach ( is less verbose and faster; the only good reason to use index-based iteration with test.Keys)
$test.Keys[$i] would be if index $i weren't just a helper variable, but were required for operation, such as for outputting a string that reflects both a key and its index.
The above therefore yields:
0
1
Note that the for syntax is not only verbose, it actually performs worse than foreach, and in case you do need explicit indices, use foreach with the indices generated via the range operator (..), which is not only faster but also arguably more readable than the for loop:
$test = [ordered] @{set01 = '0'; set02 = '1'}
foreach (
test.Count-1)) {
$test[$i]
}
While it is less memory-efficient, because the array of indices must be created as a whole up front, that is usually not a concern.
Agreeing with @mklement0.
If you want the traditional for-loop you could do the following using the Keys property.
$test = @{set01 = '0'; set02 = '1'}
for (
i -lt $test.Count; $i++) {
Write-Host $test.Keys[$i]
}
This will output: set02 set01
Just wondering why the GetEnumerator() method isn't treated as an array by default.
If it is forced using @() you can access indexes as any other array or if you use Select-Object, but trying against the raw output results in the entire hashtable/dictionary being returned:
10:09:09 PS C:\> $test = [ordered]@{"prop1" = "val1" ; "prop2" = "val"}
10:09:14 PS C:\> ($test.GetEnumerator())[0]
Name Value
---- -----
prop1 val1
prop2 val
10:09:25 PS C:\> @($test.GetEnumerator())[0]
Name Value
---- -----
prop1 val1
10:09:27 PS C:\> $test.GetEnumerator() | Select-Object -First 1
Name Value
---- -----
prop1 val1
10:09:36 PS C:\>
Anyone know why this is?
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).
Shorthand is not preferred for scripts; it is less readable. The %{} operator is considered shorthand. Here's how it should be done in a script for readability and reusability:
Variable Setup
CopyPS> $hash = @{
a = 1
b = 2
c = 3
}
PS> $hash
Name Value
---- -----
c 3
b 2
a 1
Option 1: GetEnumerator()
Note: personal preference; syntax is easier to read
The GetEnumerator() method would be done as shown:
Copyforeach ($h in $hash.GetEnumerator()) {
Write-Host "$($h.Name): $($h.Value)"
}
Output:
Copyc: 3
b: 2
a: 1
Option 2: Keys
The Keys method would be done as shown:
Copyforeach ($h in $hash.Keys) {
Write-Host "${h}: $($hash.$h)"
}
Output:
Copyc: 3
b: 2
a: 1
Additional information
Be careful sorting your hashtable...
Sort-Object may change it to an array:
CopyPS> $hash.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Hashtable System.Object
PS> $hash = $hash.GetEnumerator() | Sort-Object Name
PS> $hash.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
This and other PowerShell looping are available on my blog.
Christian's answer works well and shows how you can loop through each hash table item using the GetEnumerator method. You can also loop through using the keys property. Here is an example how:
Copy$hash = @{
a = 1
b = 2
c = 3
}
$hash.Keys | % { "key = $_ , value = " + $hash.Item($_) }
Output:
Copykey = c , value = 3
key = a , value = 1
key = b , value = 2
Note: This answer applies equally to situations where an API directly returns an enumerator object.
Using .GetEnumerator() returns an enumerator[1] for the key-value pairs, which is not the same as the results of the enumeration.
- An enumerator must be iterated over via its
.MoveNext()method in order to perform actual enumeration. However, you can let PowerShell do this for you, as shown below.
To get the desired behavior, force enumeration via @(...), the array sub-expression operator, and use the results:
# Note the use of @(...), which collects the enumerated objects
# in an [object[]] array.
# Get an array of key-value pairs.
$KeyPairs = @($ht.GetEnumerator())
# Get the first key-value pair.
@($ht.GetEnumerator())[0]
Note:
In the pipeline it is PowerShell itself that performs the enumeration of an enumerator object, which is why something like
$ht.GetEnumerator() | ForEach-Object { <# work with each key-value pair #> }does work.In the pipeline, hashtables / dictionaries, which are technically also collections, are not enumerated by default, unlike list-like collections such as arrays.[2] That is, hashtables / dictionaries are sent as a whole through the pipeline by default, which is why a call to
.GetEnumerator()is needed to return an enumerator for their entries (key-value pairs), which the pipeline then enumerates.
As for what you tried:
$KeyPairs # Nothing is returned as if $KeyPairs lost its value
Because $KeyPairs contains an enumerator, it is done enumerating after the first enumeration that output to the pipeline (the display) implicitly performed, and therefore there's nothing left to enumerate on re-invocation - unless you call $KeyPairs.Reset() first.
However, note that not every enumerator is guaranteed to support .Reset() for repeating an enumeration - some enumerators invariably perform one-time-only enumerations.
($ht.GetEnumerator())[0] # !! DOESN'T WORK
An enumerator cannot be indexed into.
PowerShell treats it like a single object (which it is) and falls back to its own indexing, where it allows even single objects (scalars) to be indexed for the sake of unified handling of collections and scalars; in that case,
[0]is an effective no-op, simply returning the single object itself (similar to how(42)[0]and(42)[-1]are the same as42)
[1] Specifically, .GetEnumerator() returns an object that implements the System.Collections.IDictionaryEnumerator interface.
[2] See the bottom section of this answer for which types PowerShell does and doesn't automatically enumerate in the pipeline.
$KeyPairs in your example $KeyPairs = $ht.GetEnumerator() is a misleading name, you do not have Key / Value pairs allocated in that variable, what you have is an object of the type HashtableEnumerator, a type that implements the IDictionaryEnumerator Interface, this interface, basically is a "contract" that specifies how a hash table can and should be enumerated. One of the specifics of the implementation is that the instance can be enumerated once and, if you want to enumerate it again, you must call its .Reset() method. This, as far as I know, is applicable for any type implementing the IEnumerator interace (base interface for IDictionaryEnumerator).
PS ..\pwsh> $ht = @{'Key1' = 'Value1'; 'Key2' = 'Value2' }
PS ..\pwsh> $enum = $ht.GetEnumerator()
PS ..\pwsh> $enum
Name Value
---- -----
Key1 Value1
Key2 Value2
PS ..\pwsh> $enum.Reset()
PS ..\pwsh> $enum
Name Value
---- -----
Key1 Value1
Key2 Value2
As for the second question:
The hashtable enumerator seems to only support the
Select-Option, not the index.
That is correct, enumerators don't implement IList interface nor the implement an indexer.
If you want to use indexing you must convert that HashtableEnumerator into a collection of DictionaryEntry using @($ht.GetEnumerator()), in which case, using a hashtable in the first place wouldn't make sense, it would lose its purpose.
Or, a better approach would be to use an OrderedDictionary, this type supports accessing by index and by key and is as fast as a normal hashtable while performing look-ups:
$dict = [ordered]@{'Key1' = 'Value1'; 'Key2' = 'Value2' }
$dict[0] # Value1
$dict.Keys[0] # Key1
# or, for a key / value pair:
[System.Collections.DictionaryEntry]::new($dict.Keys[0], $dict[0])
# Name Value
# ---- -----
# Key1 Value1