Create a PowerShell Quiz Script

Create a PowerShell Quiz Script

  • Comments 2
  • Likes

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.

Image of created input box

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.

Image of created command-line prompt

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.

Image of illustration of concept

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

 

 

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Hi Ed,

    this is another wonderful example for the usage of hashtables!

    And it's even fun to use it here :-)

    There is one error in your script, that could be easily corrected:

    In function "New-CmdletPuzzle" the loop

     foreach($l in $rndchar)

     {

      $cmdletP = $cmdlet

      $cmdletP = $cmdletP.Replace($l,"_")

     }# end foreach l

    assigns the real name of the cmdlet to the variable $cmdletP over and over again.

    This won't incrementally add the changes already made to $cmdletP in the last

    iteration of the loop to the current value of $cmdletP because its' value is overwritten

    by the original value of $cmdlet when the next iteration starts.

    Solution: Just move the assignment "$cmdletP = $cmdlet" before the foreach loop!

    ( Otherwise you will always be able to guess the right cmdlet if only one letter is

    changed to "_" ... that's too easy :-)

    Another slight modification I would propose is storing the hash values in lowercase e.g. characters and transform the user input to lowercase before comparing it to the solution. This doesn't require my solution to match the case of each letter of the cmdlet ... I

    ( 'm lazy ... or maybe too old to remember the details ... )

    Klaus.

  • @Klaus Shulte you are right! I have made the change to the code in the article. Thanks!