Looping through a hash, or using an array in PowerShell
Asked Answered
S

8

123

I'm using this (simplified) chunk of code to extract a set of tables from SQL Server with BCP.

$OutputDirectory = 'c:\junk\'
$ServerOption =   "-SServerName"
$TargetDatabase = "Content.dbo."

$ExtractTables = @(
    "Page"
    , "ChecklistItemCategory"
    , "ChecklistItem"
)

for ($i=0; $i -le $ExtractTables.Length – 1; $i++)  {
    $InputFullTableName = "$TargetDatabase$($ExtractTables[$i])"
    $OutputFullFileName = "$OutputDirectory$($ExtractTables[$i])"
    bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
}

It works great, but now some of the tables need to be extracted via views, and some don't. So I need a data structure something like this:

"Page"                      "vExtractPage"
, "ChecklistItemCategory"   "ChecklistItemCategory"
, "ChecklistItem"           "vExtractChecklistItem"

I was looking at hashes, but I'm not finding anything on how to loop through a hash. What would be the right thing to do here? Perhaps just use an array, but with both values, separated by space?

Or am I missing something obvious?

Shaylynn answered 26/1, 2012 at 7:45 Comment(0)
A
118

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:

$hash = @{
    a = 1
    b = 2
    c = 3
}
$hash.Keys | % { "key = $_ , value = " + $hash.Item($_) }

Output:

key = c , value = 3
key = a , value = 1
key = b , value = 2
Asepsis answered 26/1, 2012 at 8:47 Comment(6)
How can I enumerate the hash in another order? E.g. when I want to print its content in ascending order (here a ... b ... c). Is it even possible?Cannes
Why $hash.Item($_) instead of $hash[$_]?Predispose
@LPrc use the Sort-Object method to do that. This article does a pretty good explanation of it: technet.microsoft.com/en-us/library/ee692803.aspxDemetrius
You could even use just $hash.$_.Bassesalpes
IMO @TNT's comment is the best practise anwer. When $_ is not of type string, only Dot Annotation will not raise ParserError. This should be edited into accepted answer.Amoroso
This approach does not work if hashtable contains key = 'Keys'.Fadil
D
261

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

PS> $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:

foreach ($h in $hash.GetEnumerator()) {
    Write-Host "$($h.Name): $($h.Value)"
}

Output:

c: 3
b: 2
a: 1

Option 2: Keys

The Keys method would be done as shown:

foreach ($h in $hash.Keys) {
    Write-Host "${h}: $($hash.$h)"
}

Output:

c: 3
b: 2
a: 1

Additional information

Be careful sorting your hashtable...

Sort-Object may change it to an array:

PS> $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.

Dogcatcher answered 23/4, 2013 at 17:53 Comment(10)
like this one more for readability, especially for not dedicate powershell developers like me.Mitre
I agree this method is easier to read and therfore probably better for scripting.Mae
Readability aside, there is one difference between VertigoRay's solution and Andy's solution. %{} is an alias for ForEach-Object, which is different than the foreach statement here. ForEach-Object makes use of the pipeline, which can be much faster if you're already working with a pipeline. The foreach statement does not; it's a simple loop.Kuebbing
@Kuebbing I don't see anything that supports your claim of the pipeline being faster, based on my tests: gist.github.com/VertigoRay/3bb0166d6a877839b420Dogcatcher
@Dogcatcher In your test, you are piping $hash.Keys, which is essentially just an IEnumerable, so your results are going to be nearly similar to the pure enumeration case. The pipeline can be much faster, but you will only see that behavior where the pipeline contains results from items running in parallel (like from jobs, or from other servers via Invoke-Command).Kuebbing
@Kuebbing I copy and pasted the answer provided by @andy-arismendi from above; it is the popular pipeline solution for this question. Your statement, which peaked my interest, was that ForEach-Object (aka: %{}) is faster than foreach; I showed that is not faster. I wanted to demonstrate if your point was valid or not; because I, like most people, like my code to run as fast as possible. Now, your argument is changing to running jobs in parallel (potentially using multiple computers/servers) ... which of course is faster than running a job in series from a single thread. Cheers!Dogcatcher
@Kuebbing I would also think powershell follows the traditionnal advantage that shells give you when you pipe data streams between several processes: you get natural parallelism from the simple fact that each pipeline stage is an independent process (of course, a buffer may drain and in the end the whole pipeline goes as slow as its slowest process). That's different from having a pipeline process that decides on its own to spawn multiple threads, which is entirely dependent of the implementation details of the process itself.Posey
Option 2 does not work if hashtable contains key = 'Keys'.Fadil
"Foreach" as control flow is not "Powershell-like". It might be more readable for others but not for Powershell developers. Powershell uses pipes whenever it's possible.Teran
PowerShell was written for easy adoption by novices. Pipes streamline commands in the CLI, but do not improve readability/adoption by novices if used excessively in scripts. It's an argument that will wage on, but one fact remains: real PowerShell developers code in C# and make DLLs for module imports.Dogcatcher
A
118

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:

$hash = @{
    a = 1
    b = 2
    c = 3
}
$hash.Keys | % { "key = $_ , value = " + $hash.Item($_) }

Output:

key = c , value = 3
key = a , value = 1
key = b , value = 2
Asepsis answered 26/1, 2012 at 8:47 Comment(6)
How can I enumerate the hash in another order? E.g. when I want to print its content in ascending order (here a ... b ... c). Is it even possible?Cannes
Why $hash.Item($_) instead of $hash[$_]?Predispose
@LPrc use the Sort-Object method to do that. This article does a pretty good explanation of it: technet.microsoft.com/en-us/library/ee692803.aspxDemetrius
You could even use just $hash.$_.Bassesalpes
IMO @TNT's comment is the best practise anwer. When $_ is not of type string, only Dot Annotation will not raise ParserError. This should be edited into accepted answer.Amoroso
This approach does not work if hashtable contains key = 'Keys'.Fadil
G
10

You can also do this without a variable

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}
Gass answered 28/10, 2014 at 7:15 Comment(1)
This gets me a " ForEach-Object : Cannot bind parameter 'Process'. Cannot convert the "getEnumerator" value of type "System.String" to type "System.Management.Automation.ScriptBlock"Witchy
G
9

I prefer this variant on the enumerator method with a pipeline, because you don't have to refer to the hash table in the foreach (tested in PowerShell 5):

$hash = @{
    'a' = 3
    'b' = 2
    'c' = 1
}
$hash.getEnumerator() | foreach {
    Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}

Output:

Key = c and Value = 1
Key = b and Value = 2
Key = a and Value = 3

Now, this has not been deliberately sorted on value, the enumerator simply returns the objects in reverse order.

But since this is a pipeline, I now can sort the objects received from the enumerator on value:

$hash.getEnumerator() | sort-object -Property value -Desc | foreach {
  Write-Host ("Key = " + $_.key + " and Value = " + $_.value);
}

Output:

Key = a and Value = 3
Key = b and Value = 2
Key = c and Value = 1
Gownsman answered 14/2, 2019 at 15:23 Comment(2)
The enumerator can return the values in "any" order, as it simply iterates the underlying implementation. In this case it happens to appear be reverse order. For a counter-example: $x = @{a = 1; z = 2}; $x.q = 3 is "ordered" as q,a,z here.Gona
"Ordered dictionaries differ from hash tables in that the keys always appear in the order in which you list them. The order of keys in a hash table is not determined - the [ordered] attribute triggers selection of the different implementation.Gona
C
7

About looping through a hash:

$Q = @{"ONE"="1";"TWO"="2";"THREE"="3"}
$Q.GETENUMERATOR() | % { $_.VALUE }
1
3
2

$Q.GETENUMERATOR() | % { $_.key }
ONE
THREE
TWO
Cabstand answered 26/1, 2012 at 8:3 Comment(0)
B
7

Here is another quick way, just using the key as an index into the hash table to get the value:

$hash = @{
    'a' = 1;
    'b' = 2;
    'c' = 3
};

foreach($key in $hash.keys) {
    Write-Host ("Key = " + $key + " and Value = " + $hash[$key]);
}
Borborygmus answered 20/10, 2016 at 19:35 Comment(0)
R
4

A short traverse could be given too using the sub-expression operator $( ), which returns the result of one or more statements.

$hash = @{ a = 1; b = 2; c = 3}

forEach($y in $hash.Keys){
    Write-Host "$y -> $($hash[$y])"
}

Result:

a -> 1
b -> 2
c -> 3
Ramage answered 21/2, 2021 at 7:39 Comment(0)
G
0

If you're using PowerShell v3, you can use JSON instead of a hashtable, and convert it to an object with Convert-FromJson:

@'
[
    {
        FileName = "Page";
        ObjectName = "vExtractPage";
    },
    {
        ObjectName = "ChecklistItemCategory";
    },
    {
        ObjectName = "ChecklistItem";
    },
]
'@ | 
    Convert-FromJson |
    ForEach-Object {
        $InputFullTableName = '{0}{1}' -f $TargetDatabase,$_.ObjectName

        # In strict mode, you can't reference a property that doesn't exist, 
        #so check if it has an explicit filename firest.
        $outputFileName = $_.ObjectName
        if( $_ | Get-Member FileName )
        {
            $outputFileName = $_.FileName
        }
        $OutputFullFileName = Join-Path $OutputDirectory $outputFileName

        bcp $InputFullTableName out $OutputFullFileName -T -c $ServerOption
    }
Grivation answered 4/7, 2013 at 13:38 Comment(1)
n.b. the command is ConvertFrom-Json (and the converse, ConvertTo-Json) -- just swap the dash placement.Chalmer

© 2022 - 2024 — McMap. All rights reserved.