Dealing with PowerShell Hash Table Quirks

Dealing with PowerShell Hash Table Quirks

  • Comments 12
  • Likes

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

 

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Here is an interesting real-world use for a hash table.

    When we return values from Active Directory using the [adsisearcher] or the DirectorySeracher class the values all come 'wrapped' as an array or collection.  This makes them sort of inconvenient to use.

    Here is how you can easily leverage a hash to regenerate the properties as a normal set of properties along with how to use the searchers with a custom set of properties.

    gallery.technet.microsoft.com/.../Extract-arbitrary-list-of-6f59d3b4

  • @jv that is a really cool script, and you are right it is a good way to deal with return values wrapped in an array. Thank you for your comment -- and for your script.

  • @Ed

    As you are showing us in detail, hashes are g4reat tools to use in many areas.  I can't wait to see where you take this next.  Or maybe somewher completely different.

    Keep up the great blogs.

  • @JV I am changing the subject next week :-) but I will come back to Hash Tables -- in Windows PowerShell they are very important for many different reasons. I am working on an article right now involving WMI, but the syntax takes a Hash table. A PowerShell user needs to be able to immediately type a hash table without even thinking to be truely effective.

  • #Ed - sounds like a cliffhanger from the old days.

  • @jv you mean like, "stay tuned for these exciting messages..."

  • Hi Ed,

    hash tables are great and this practical example (and the try and error approach to solve the riddle of the returned array of hasttables:-) are stuff that we need to know and use in daily work.

    Sometimes it is not obvious why powershell doesn't seem to "do the right thing" ... that what we expect to get and I often had to "debug" the data structures which seemed to be a bit strange ( at first and second sight). I'm afraid we all need a bit more practice to be able to handle this "without even thinking" ...

    A side note ... repeated from last week:     "If($puzzle.contains($rtn))" should be "if ($P -eq $rtn)" :-)

    @jv: You are right ... !

    Klaus.

  • I like your blog it is great job.

    <a href="kundedata.dk/.../a>

  • Since Get-Contents returns an object per line, its result, an array of lines, is not quite suited to be piped directly to ConvertFrom-StringData since the latter expects a string. To remedy this we can use Out-String:

    $hash$ = gc file.txt | Out-String | ConvertFrom-StringData

  • I don't know who this is written for, there are big sections with little or no explination.  I don't get the refference to the part "too much trouble to do for a simple pipeline operation."  I don't know how that helps us figure out how to learn this stuff.  

    The section after that leave me to believe the best we can do is guess what the output was and if that would match the imput requirments...  Isn't there a way to look in the help filles and see type of service and the output and look at the help files of the other side of the pipe and see what the input requirments are?

    I don't know what I am talking about yet, but I would think you would want to teach us how to follow a consistant path to fine the right way to find the matches between the pieces.

    Function New-Question

    {

    Param(

    Function New-Question

    {

    Param(

     [hashtable]$Puzzle

    What is a Function and what is a Param?

    What is  [hashtable]$Puzzle?

  • Hello,

    today, reading your post I got the idea to use the "-raw" in the command "Get-Content":

    Remove-Variable -Name hash -EA 0

    $hash = ConvertFrom-StringData ( Get-Content .\x.txt -Raw )

    $hash.GetType()

    To my surprise, it turned out that it works! We get the same results as when using "[io.file]::ReadAllText()" method.

    I'm proud of myself :-)