121

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?

Paul Carlton
  • 2,785
  • 2
  • 24
  • 42
Sylvia
  • 2,578
  • 9
  • 30
  • 37

8 Answers8

256

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.

VertigoRay
  • 5,935
  • 6
  • 39
  • 48
  • 5
    like this one more for readability, especially for not dedicate powershell developers like me. – anIBMer Aug 20 '14 at 13:32
  • 2
    I agree this method is easier to read and therfore probably better for scripting. – leinad13 Sep 09 '14 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. – JamesQMurphy Jan 03 '15 at 03:12
  • 1
    @JamesQMurphy I don't see anything that supports your claim of the pipeline being faster, based on my tests: https://gist.github.com/VertigoRay/3bb0166d6a877839b420 – VertigoRay Aug 18 '15 at 05:47
  • @VertigoRay 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). – JamesQMurphy Aug 18 '15 at 17:13
  • 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 Aug 18 '15 at 18:48
  • 1
    @JamesQMurphy 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. – Johan Boulé Jul 27 '18 at 16:57
  • 1
    __Option 2__ does not work if hashtable contains key = 'Keys'. – Boris Nikitin Mar 19 '19 at 06:37
  • "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. – Mehrdad Mirreza Feb 17 '20 at 09:37
  • 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. – VertigoRay Apr 24 '20 at 15:13
117

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
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • 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 Oct 27 '17 at 14:14
  • 6
    Why `$hash.Item($_)` instead of `$hash[$_]`? – alvarez Nov 09 '17 at 14:34
  • 1
    @LPrc use the Sort-Object method to do that. This article does a pretty good explanation of it: https://technet.microsoft.com/en-us/library/ee692803.aspx – chazbot7 Dec 28 '17 at 17:28
  • 5
    You could even use just `$hash.$_`. – TNT May 26 '18 at 21:48
  • 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. – DoubleOZ Dec 31 '18 at 16:07
  • 2
    This approach does not work if hashtable contains key = 'Keys'. – Boris Nikitin Mar 19 '19 at 06:35
10

You can also do this without a variable

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}
Zombo
  • 1
  • 62
  • 391
  • 407
  • 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" – luis.espinal Jan 27 '16 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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Eelco L.
  • 714
  • 7
  • 7
  • 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. – user2864740 Jul 10 '19 at 19:40
  • 2
    ["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](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables?view=powershell-6) - the `[ordered]` attribute triggers selection of the different implementation. – user2864740 Jul 10 '19 at 19:42
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]);
}
user1161625
  • 642
  • 1
  • 8
  • 13
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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
CB.
  • 58,865
  • 9
  • 159
  • 159
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
Pepe Alvarez
  • 1,218
  • 1
  • 9
  • 15
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
    }
Aaron Jensen
  • 25,861
  • 15
  • 82
  • 91
  • n.b. the command is ConvertFrom-Json (and the converse, ConvertTo-Json) -- just swap the dash placement. – atmarx Oct 20 '15 at 20:14