Dealing with PowerShell Hash Table Quirks

Doctor Scripto

Summary: Microsoft Scripting Guy Ed Wilson shows how to deal with two Windows PowerShell hash table quirks.

 

Microsoft Scripting Guy Ed Wilson here. Our week in Ottawa draws to a close. We leave for Montreal today, and are really excited about the Windows PowerShell people we will meet while we are there. I enjoyed working on my Windows PowerShell Quiz scripts this week. The point of the articles was not so much about creating a quiz engine, but the fact that it offered a good exercise for working on function design and with hash tables.

Today, I want to focus in on two aspects of hash tables that came up this week while I was writing the scripts. The first aspect I want talk about is piping a hash table to other cmdlets. This also comes into play when supplying a hash table to a cmdlet as an inputobject. As an example, I will use the hash table created in yesterday’s post, Easily Create a PowerShell Hash Table.

Here is the DemoHashtableWithProcesses.ps1 script that creates a hash table from process information:

$hash = $null

$hash = @{}

$proc = get-process | Sort-Object -Property name -Unique

 

foreach ($p in $proc)

{

 $hash.add($p.name,$p.id)

}

$hash

 

The $hash variable contains a hash table with a number of key value pairs in it. The count property tells me how many items are in the hash table. When I pipe the hash table to the Get-Random cmdlet and tell the Get-Random cmdlet to return one random key value pair, the results are confusing. Here is the command I am talking about:

$hash | Get-Random -Count 1

Image of command and associated output

As seen in the previous figure, all the key value pairings from the hash table are returned. I was expecting a single, randomly selected pair. I see this same problem when using the Sort-Object cmdlet. For example, when I type the following command, I expect to see the processes sorted by name:

$hash | Sort-Object -Property name

But as shown in the following figure, the sort is not working.

Image of sort not working

The problem extends itself even to the Where-Object. The following command returns nothing, even though there is a process with a value of 6108:

$hash | Where-Object { $_.value -eq 6108}

I solved this problem earlier in the week in Create a PowerShell Quiz Script post by getting a collection of keys, walking through the keys, and using the item method to retrieve the associated value.

The applicable line of code is shown here:

Function New-Question

{

 Param(

  [hashtable]$Puzzle

 )

  Foreach ($p in $puzzle.KEYS)

   {

    $rtn = Read-host “What is the cmdlet name $($puzzle.item($P))”

    If($puzzle.contains($rtn))

     { “Correct $($puzzle.item($P)) equals $p” }

    ELSE

     {“Sorry. $rtn is not right. $($puzzle.item($P)) is $p” }

    } #end foreach $P

}
#end function New-Question

Though this methodology works just fine for the Windows PowerShell Quiz script, it is too much trouble to do for a simple pipeline operation. There needs to be an easier way to walk through a hash table. And there is! The secret is to use the getEnumerator method from the hashtable object. If I want to choose a random key value pair from a hash table, I call the getenumerator method prior to passing it to the Get-Random cmdlet. The command is shown here:

$hash.getenumerator() | Get-Random -Count 1

The GetEnumerator method works the same way with the Where-Object cmdlet. The command is shown here:

$hash.GetEnumerator() | Where-Object { $_.value -eq 6108}

It also works with the Sort-Object cmdlet, as shown here:

$hash.GetEnumerator() | Sort-Object -Property name

All three of these commands and their associated output are shown in the following figure.

Image of commands and associated output

 

The second topic I want to talk about came up while I was writing Create a PowerShell Quiz by Reading a Text File.

All the examples of using the ConvertFrom-StringData cmdlet illustrate using a Here-String or similar hardcoded string data to create a hash table. This technique will not work for me because I wanted to read a text file. My first attempt generated the error shown here:

PS C:\> ConvertFrom-StringData C:\fso\Questions.txt

ConvertFrom-StringData : Data line ‘C:\fso\Questions.txt’ is not in ‘name=value’ format.

At line:1 char:23

+ ConvertFrom-StringData <<<< C:\fso\Questions.txt

    + CategoryInfo          : InvalidOperation: (:) [ConvertFrom-StringData], PSInvalidOperationException

    + FullyQualifiedErrorId : InvalidOperation,Microsoft.PowerShell.Commands.ConvertFromStringDataCommand

The error basically says that my input file is not in name=value format. But as shown in the following figure, that is not true.

Image showing error message is not true

So, I thought I needed to read the content of the file first. This time I got the error shown here:

PS C:\> ConvertFrom-StringData (Get-content C:\fso\Questions.txt)

ConvertFrom-StringData : Cannot convert ‘System.Object[]’ to the type ‘System.String’ required by parameter ‘StringData

‘. Specified method is not supported.

At line:1 char:23

+ ConvertFrom-StringData <<<<  (Get-content C:\fso\Questions.txt)

    + CategoryInfo          : InvalidArgument: (:) [ConvertFrom-StringData], ParameterBindingException

    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.ConvertFromStringDataCommand

Next, I got the idea to pipe the information to the cmdlet. When I did this, it appeared I had hit upon a successful combination:

PS
C:\> Get-Content C:\fso\Questions.txt | ConvertFrom-StringData

 

Name                           Value

—-                           —–

Canberra                       Australia

Berlin                         Germany

Ottawa                         Canada

I then tried to use my hash table. So I stored the hash table in a variable, and attempted to access the keys property, as shown here:

PS C:\> $hash = Get-Content C:\fso\Questions.txt | ConvertFrom-StringData

PS C:\> $hash.keys

Nothing came back. There are no keys? So, I examined the $hash variable by using the Get-Member  cmdlet (gm is an alias). The results of this exploration are shown here:

PS C:\> $hash | gm

 

   TypeName: System.Collections.Hashtable

 

Name                           MemberType                  Definition

Add                              Method                            System.Void Add(System.Object key, System.Object value)

Clear                             Method                           System.Void Clear()

Clone                            Method                            System.Object Clone()

Contains                        Method                            bool Contains(System.Object key)

ContainsKey                   Method                           bool ContainsKey(System.Object key)

ContainsValue                Method                           bool ContainsValue(System.Object value)

CopyTo                         Method                         System.Void CopyTo(array array, int arrayIndex)

Equals                           Method                         bool Equals(System.Object obj)

GetEnumerator              Method                         System.Collections.IDictionaryEnumerator GetEnumerator()

GetHashCode                Method                         int GetHashCode()

GetObjectData               Method                         System.Void GetObjectData(System.Runtime.Serialization.SerializationInfo inf…

GetType                        Method                         type GetType()

OnDeserialization           Method                         System.Void OnDeserialization(System.Object sender)

Remove                         Method                         System.Void Remove(System.Object key)

ToString                        Method                         string ToString()

Item                              ParameterizedProperty   System.Object Item(System.Object key) {get;set;}

Count                            Property                        System.Int32 Count {get;}

IsFixedSize                     Property                        System.Boolean IsFixedSize {get;}

IsReadOnly                    Property                        System.Boolean IsReadOnly {get;}

IsSynchronized               Property                        System.Boolean IsSynchronized {get;}

Keys                              Property                        System.Collections.ICollection Keys {get;}

SyncRoot                       Property                        System.Object SyncRoot {get;}

Values                           Property                        System.Collections.ICollection Values {get;}

 

Well, it looks like it is a hash table. So how about looking at the values property? The results are shown here—nothing. Next, I use the count property, and it tells me I have three items in the hash table.

PS C:\> $hash.values

PS C:\> $hash.count

3

This looks really weird. Then I had an idea: I wonder if somehow I obtained an array. I index into the array, and sure enough, I have an array of hash tables. This is shown here:

PS C:\> $hash[0]

Name                           Value

Canberra                       Australia

 

PS C:\> $hash[1]

Name                           Value

Berlin                            Germany

 

PS C:\> $hash[2]

Name                           Value

Ottawa                          Canada

I have an array of hash tables because of the way that Get-Content returns information. It returns an array. One element for each line of the file is a behavior that is normally fine. But in this example, the behavior causes problems. The easy way around this is to use the ReadAlltext static method from the io.file .NET Framework class. This technique is shown here:

PS C:\> ConvertFrom-StringData ([io.file]::ReadAllText(“C:\fso\Questions.txt”))

Name                           Value

Berlin                            Germany

Canberra                       Australia

Ottawa                          Canada

 

That’s it for today. Join me tomorrow for more Windows PowerShell goodness. See you then.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson, Microsoft Scripting Guy

 

 

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Hoffmann, Travis 0

    Great article. One brief comment – you can use (get-content -raw) instead of readalltext .net method to address the get-content array problem. To be fair I’m not sure what version the parameter was introduced in relative to this writing.

Feedback usabilla icon