124

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?

8 Answers 8

264

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.

10
  • 5
    like this one more for readability, especially for not dedicate powershell developers like me.
    – anIBMer
    Commented Aug 20, 2014 at 13:32
  • 2
    I agree this method is easier to read and therfore probably better for scripting.
    – leinad13
    Commented Sep 9, 2014 at 10:12
  • 2
    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. Commented Jan 3, 2015 at 3:12
  • 1
    @JamesQMurphy I don't see anything that supports your claim of the pipeline being faster, based on my tests: gist.github.com/VertigoRay/3bb0166d6a877839b420
    – VertigoRay
    Commented Aug 18, 2015 at 5:47
  • 4
    @JamesQMurphy 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!
    – VertigoRay
    Commented Aug 18, 2015 at 18:48
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
6
  • 1
    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?
    – SimonH
    Commented Oct 27, 2017 at 14:14
  • 6
    Why $hash.Item($_) instead of $hash[$_]?
    – alvarez
    Commented Nov 9, 2017 at 14:34
  • 1
    @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.aspx
    – chazbot7
    Commented Dec 28, 2017 at 17:28
  • 5
    You could even use just $hash.$_.
    – TNT
    Commented May 26, 2018 at 21:48
  • 2
    This approach does not work if hashtable contains key = 'Keys'. Commented Mar 19, 2019 at 6:35
10

You can also do this without a variable

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}
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" Commented Jan 27, 2016 at 16:46
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
2
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]);
}
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
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
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
    }
1
  • n.b. the command is ConvertFrom-Json (and the converse, ConvertTo-Json) -- just swap the dash placement.
    – atmarx
    Commented Oct 20, 2015 at 20:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.