Summary: Learn how to use hash tables and create a Windows PowerShell quiz script.
Microsoft Scripting Guy Ed Wilson here. For the past several days, I have been building a Windows PowerShell cmdlet name quiz. On the first day, I looked at replacing random letters in a string. Next I moved the code into a function and dded parameter validation to limit the values that can be supplied to the function. In this way I was able to prevent a divide-by-zero error that could arise depending on what someone supplied from the command line. This is actually a great way to do error handling—prevent the error from arising in the first place. Today, I am going to add the question and answer feature for the Windows PowerShell cmdlet name game.
The complete New-CmdletPuzzleQuiz.ps1 script is shown here.
function New-CmdletPuzzle
{
Param(
[Parameter(Position=0,
HelpMessage="A number between 2 and 7")]
[alias("Level")]
[ValidateRange(2,7)]
[int]$difficulty = 4
) #end param
$array = @()
$hash = New-Object hashtable
$array = Get-Command -CommandType cmdlet |
ForEach-Object { $_.name.tostring() }
Foreach($cmdlet in $array)
{
$rndChar = get-random -InputObject ($cmdlet.tochararray()) `
-count ($cmdlet.length/$difficulty)
$cmdletP = $cmdlet #moved from inside foreach loop
foreach($l in $rndchar)
{
#moved code to outside Foreach loop
$cmdletP = $cmdletP.Replace($l,"_")
}# end foreach l
$hash.add($cmdlet,$cmdletP)
} #end foreach cmdlet
$hash
}
#end function New-CmdletPuzzle
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
#
*** Entry point to script ***
$puzzle = New-CmdletPuzzle
New-Question -puzzle $puzzle
The first thing I do in the New-CmdletPuzzleQuiz.ps1 script is use the New-CmdletPuzzle function from yesterday’s script. That function creates a hash table with cmdlet names as the key value and cmdlet names with missing letters as the value. It then returns the hash table to he calling code.
The new function I wrote for today is the New-Question function. It appears 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
The input to the New-Question function is the hash table returned by the New-CmdletPuzzle function, but any question/answer type of hash table would actually work. For example, in the following script, QuestionsAndAnswere.ps1, I add a function that creates a hash table of questions about capitals and their associated countries. The only change I needed to make to the New-Question function was to remove the phrase, “What is the cmdlet name” from the Read-Host command
This actually points to a major design issue—hard coded literals often cause code reuse issues. In this example, if I had a variable to hold the prompt string, and I passed the string when calling the function, it would be easier to reuse the function. The two evaluation strings (“Correct…equals...” and “Sorry…Is not right…is…”) are pretty generic and make sense (albeit a bit stilted) in most cases. A better approach there would be to use custom correct and incorrect strings, and pass them when calling the function. The more abstract a function becomes, the greater the reuse capabilities. The complete QuestionsAndAnswers.ps1 script is shown here.
QuestionsAndAnswers.ps1
Function New-Puzzle
{
@{
"What is the capital of Australia" = "Canberra"
"What is the capital of Canada" = "Ottawa"
"What is the capital of Germany" = "Berlin"
}
}
Function New-Question
{
Param(
[hashtable]$Puzzle
)
Foreach ($p in $puzzle.KEYS)
{
$rtn = Read-host "$($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
#
*** Entry point to script ***
$puzzle = New-Puzzle
New-Question -puzzle $puzzle
In the New-Question function, I use a [hashtable] type constraint to ensure the input parameter is a hash table. This code is shown here:
Function New-Question
{
Param(
[hashtable]$Puzzle
)
When I have the input hash table, I use the foreach language statement to walk through the collection of hash table keys. I obtain the hash table keys by using the keys property from the hashtable object. I use the variable $p to represent a single key (the enumerator) in the collection as I work my way through the collection. This portion of the foreach loop is shown here:
Foreach ($p in $puzzle.KEYS)
{
I needed a way to receive input from the user of the script, and I decided that using the Read-Host cmdlet was the easiest for this application. The user types the answer to the question, and I store the answer in the $rtn variable. The Read-Host cmdlet creates an input box when run from the Windows PowerShell ISE. This box is shown in the following figure.
When the script runs from the Windows PowerShell console, the Read-Host cmdlet generates a command-line prompt. This prompt is shown in the following figure.
To display the cmdlet name with the random letters removed, I use the actual cmdlet name I received from the collection of keys. The variable $p contains the actual cmdlet name. When working with a hash table, the item method uses a key to retrieve the data stored in the value that is associated with the key. This concept is illustrated in the following figure.
This line of code is shown here:
$rtn = Read-host "What is the cmdlet name $($puzzle.item($P))"
When I have the user input, it is time to see if the input matches the actual cmdlet name. I do this by using the contains method from the hashtable object. If the input matches, I display a line that states the user is correct; if it does not match, the else condition matches. The contains operator here is case sensitive. Therefore, New-Object does not match New-Object. The if portion of the script is shown here:
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
The entry point to the script is pretty simple. I call the New-CmdletPuzzle function and store the returned hash table in the $puzzle variable. I then pass the $puzzle variable containing the hash table to the New-Question function.
That’s it for today. Join me tomorrow when I will add the capability to determine the number of questions that make up a quiz, and return a score for the quiz.
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