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
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.
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.
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.
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