125

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

272

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.

Sign up to request clarification or add additional context in comments.

10 Comments

like this one more for readability, especially for not dedicate powershell developers like me.
I agree this method is easier to read and therfore probably better for scripting.
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 I don't see anything that supports your claim of the pipeline being faster, based on my tests: gist.github.com/VertigoRay/3bb0166d6a877839b420
@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!
|
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 Comments

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?
Why $hash.Item($_) instead of $hash[$_]?
@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
You could even use just $hash.$_.
This approach does not work if hashtable contains key = 'Keys'.
|
11

You can also do this without a variable

@{
  'foo' = 222
  'bar' = 333
  'baz' = 444
  'qux' = 555
} | % getEnumerator | % {
  $_.key
  $_.value
}

1 Comment

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"
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 Comments

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.
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]);
}

Comments

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

Comments

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

Comments

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 Comment

n.b. the command is ConvertFrom-Json (and the converse, ConvertTo-Json) -- just swap the dash placement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.