Hey, Scripting Guy! Question

Hey Scripting Guy! I am working on a Windows PowerShell script, and I would like to pause the script for three minutes and then resume it. I know I can use the Start-Sleep cmdlet to halt script execution, but I would like to give a visual indication that the script is waiting, and display information on the screen every second that would list how much time is remaining. Is this going to be too hard to do? Should I abandon my attempts at providing user feedback and just go with the Start-Sleep cmdlet?

-- BD

Hey, Scripting Guy! Answer

Hello BD,

Microsoft Scripting Guy Ed Wilson here. One should never give up hope—otherwise there would be nothing to hope for. Having written thousands of scripts for all kinds of users, I have found that it is a good practice to at least provide the option to get feedback from the user. This is true even for me—if I click something and I do not see something take place, I am likely to click it again and again. If I still don’t see something happen, I am likely to reboot my computer (although this has not been my experience since I installed Windows 7). At a minimum this feedback should be something that says something like this:

Script is working … this may take some time … please wait …

I wrote one script that would write to the registry the amount of time the operation took. The next time the script ran, it would retrieve the information from the registry. The prompt looked something like this:

Beginning process … When this script last ran on 1/5/2009 it took 3:35 seconds to complete.
Please wait …

When the script completed, it displayed the amount of time the script ran and updated the values in the registry. Writing the processing time to the registry is not what you inquired about, although it is a cool technique. What will help you is an egg timer script. I seem to remember something like that from the 2009 Summer Scripting Games. Hold on while I go check.

This week we will be reviewing some of the scripts that were submitted during the recently held 2009 Summer Scripting Games. The description of the 2009 Summer Scripting Games details all of the events. Each of the events was answered by a globally recognized expert in the field. There were some cool prizes and winners were recognized from around the world. Additionally, just like at the "real Olympics" because there was a lot going on, an "if you get lost page" was created. Communication with participants was maintained via Twitter, Facebook, and a special forum. (The special forum has been taken down, but Twitter and Facebook are still used to communicate with Hey, Scripting Guy! fans). We will be focusing on solutions that used Windows PowerShell. We have several good introduction to Windows PowerShell Hey, Scripting Guy! articles that you will find helpful.

The 2009 Summer Scripting Games Beginner Event 10 involved counting down time in an egg timer fashion. BigAlfonso submitted the ScriptingGamesBeginnerEvent10.ps1 script. The complete script, seen here, features a three-minute countdown timer.

ScriptingGamesBeginnerEvent10.ps1

for ($time=180;$time -ge 0;$time--)
{
$min = [math]::floor([int] $time / [int] 60)
$sec = $time % 60
$strmin = $min.tostring()
if ($sec -lt 10)
{$strsec = "0" + $sec.tostring()}
else
{$strsec = $sec.tostring()}
write-host $strmin":"$strsec
start-sleep 1
}
write-host "Time's up!!!"
write-host ("`a"*4)

The script begins with a for statement that uses the double-minus operator. A for statement in Windows PowerShell is a looping construction that can be used to walk through a collection, to repeat an operation multiple times, walk through an array, or work with groups of items. It is a useful construction. For a good introduction to using the for statement check out the “Mind-Numbing, Repetitive Tasks” Hey, Scripting Guy! article. In that article, I discussed several ways of using the for statement. One thing, however, that I did not talk about was backtracking. Just as we have the ++ operator that means to take the current value and add one to it, we also have a -- operator that means to take the current number and decrement it by one. This is seen here:

for($i=5; $i -ge 0 ; $i--)
{ $i }

When the above construction is run, it produces the output seen here:

5
4
3
2
1
0

The -- operator is the perfect tool to use when counting backward, such as when creating an egg timer. The key is making sure your condition is correct. When counting upward, we often want to do something as long as a condition is less than or equal to something. Here we want the condition to prevail as long as the time is greater than zero. This is seen here:

for ($time=180;$time -ge 0;$time--)

One unusual feature of the BigAlfonso script is the use of the [math]::floor() method. A math floor has nothing to do with where you went when you studied differential equations. The floor method of the System.Math .NET Framework class returns the largest integer less than or equal to the specified number. The behavior of this method follows IEEE Standard 754, section 4. This kind of rounding is sometimes called rounding toward negative infinity. The opposite behavior is the [math]::Ceiling() method that is used to return the smallest integer greater than or equal to the specified number. Rounding to the smallest integer is sometimes called rounding toward positive infinity. An example of using the floor method is seen here:

PS C:\> [math]::floor(180/60)
3
PS C:\> [math]::floor(179/60)
2
PS C:\> [math]::floor(119/60)
1
PS C:\>

The other interesting part of the BigAlfonso script is the use of the modulo operator. The modulo operator returns what is left over after completing a division operation. For example, 5 modulo 2 would return 1 because 2 goes into 5 twice and leaves one left over. Here are additional examples of modulo division that are more applicable to the egg timer scenario:

PS C:\> 180 % 60
0
PS C:\> 179 % 60
59
PS C:\> 119 % 60
59
PS C:\>

The floor method and the modulo operator are seen here:

$min = [math]::floor([int] $time / [int] 60)
$sec = $time % 60

The next thing the script does is convert the number of minutes into a string and assign it to another variable. In Windows PowerShell, it is easy to convert a number to a string. This is seen here where a type constraint is used to ensure the number 5 is an integer. The number is then converted to a string by using the tostring method and the gettype method is used to display the data type that is returned. Not surprisingly, the data type is a string:

PS C:\> ([int]5).tostring().GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object

In the script, the number of minutes that are returned from the floor method is converted to a string with the code seen here:

$strmin = $min.tostring()

The step of converting the number to a string is not required and can be left out. This is because when the number is placed into quotation marks, it automatically gets converted to a string. This will be seen later.

BigAlfonso next uses an if…else construction to determine whether or not to pad the number of seconds. If the number is less than 10 seconds, padding is required; if the number is 10 seconds or greater, no padding is required. This is seen here:

if ($sec -lt 10)
{$strsec = "0" + $sec.tostring()}
else
{$strsec = $sec.tostring()}

Two things remain: Produce output and pause for a second between loops. The Write-Host cmdlet is used to write to the console host, and the Start-Sleep cmdlet is used to pause the script for 1 second. The backtick a (“`a”) produces an alert (beep) sound to the system speaker when the script is run in the Windows PowerShell console, but it only writes a [] in the output when the script is run inside the Windows PowerShell ISE. I love being able to multiple things. If you want to display three a’s, you can use the multiplication technique as seen here:

PS C:\> "a"*3
aaa
PS C:\>

The output section of the script is seen here:

write-host $strmin":"$strsec
start-sleep 1
}
write-host "Time's up!!!"
write-host ("`a"*4)

When the script is run, it produces the output seen here:

Image of the output produced by the script

 

To make the countdown timer a bit more useful, you can turn it into a function. The ScriptingGamesBeginnerEvent10Function.ps1 script is essentially the same as the script submitted by BigAlfonso. To turn it into a function, the function keyword is used along with a couple of curly brackets {} for the script block. A couple of minor changes were indicated. The first was to remove the explicit conversion of the integers to strings as mentioned in the earlier discussion. The second thing that was done was to remove the use of the Write-Host cmdlet. Write-Host is not required and most times is to be avoided because it destroys the object that it emits to the console. This is seen here where the “a” on the Windows PowerShell console returns a system.string object. Write-Host does not return anything:

PS C:\> "a" | gm


   TypeName: System.String

Name             MemberType            Definition
----             ----------            ----------
Clone            Method                System.Object Clone()
CompareTo        Method                int CompareTo(System.Object value), int CompareTo(string strB)
Contains         Method                bool Contains(string value)
CopyTo           Method                System.Void CopyTo(int sourceIndex, char[] destination, int destinationIndex,..
EndsWith         Method                bool EndsWith(string value), bool EndsWith(string value, System.StringCompari..
Equals           Method                bool Equals(System.Object obj), bool Equals(string value), bool Equals(string..
GetEnumerator    Method                System.CharEnumerator GetEnumerator()
GetHashCode      Method                int GetHashCode()
GetType          Method                type GetType()
GetTypeCode      Method                System.TypeCode GetTypeCode()
IndexOf          Method                int IndexOf(char value), int IndexOf(char value, int startIndex), int IndexOf..
IndexOfAny       Method                int IndexOfAny(char[] anyOf), int IndexOfAny(char[] anyOf, int startIndex), i..
Insert           Method                string Insert(int startIndex, string value)
IsNormalized     Method                bool IsNormalized(), bool IsNormalized(System.Text.NormalizationForm normaliz..
LastIndexOf      Method                int LastIndexOf(char value), int LastIndexOf(char value, int startIndex), int..
LastIndexOfAny   Method                int LastIndexOfAny(char[] anyOf), int LastIndexOfAny(char[] anyOf, int startI..
Normalize        Method                string Normalize(), string Normalize(System.Text.NormalizationForm normalizat..
PadLeft          Method                string PadLeft(int totalWidth), string PadLeft(int totalWidth, char paddingChar
PadRight         Method                string PadRight(int totalWidth), string PadRight(int totalWidth, char padding..
Remove           Method                string Remove(int startIndex, int count), string Remove(int startIndex)
Replace          Method                string Replace(char oldChar, char newChar), string Replace(string oldValue, s..
Split            Method                string[] Split(Params char[] separator), string[] Split(char[] separator, int..
StartsWith       Method                bool StartsWith(string value), bool StartsWith(string value, System.StringCom..
Substring        Method                string Substring(int startIndex), string Substring(int startIndex, int length)
ToCharArray      Method                char[] ToCharArray(), char[] ToCharArray(int startIndex, int length)
ToLower          Method                string ToLower(), string ToLower(System.Globalization.CultureInfo culture)
ToLowerInvariant Method                string ToLowerInvariant()
ToString         Method                string ToString(), string ToString(System.IFormatProvider provider)
ToUpper          Method                string ToUpper(), string ToUpper(System.Globalization.CultureInfo culture)
ToUpperInvariant Method                string ToUpperInvariant()
Trim             Method                string Trim(Params char[] trimChars), string Trim()
TrimEnd          Method                string TrimEnd(Params char[] trimChars)
TrimStart        Method                string TrimStart(Params char[] trimChars)
Chars            ParameterizedProperty char Chars(int index) {get;}
Length           Property              System.Int32 Length {get;}


PS C:\> Write-host "a" | gm
a
Get-Member : No object has been specified to the get-member cmdlet.
At line:1 char:20
+ Write-host "a" | gm <<<<
    + CategoryInfo          : CloseError: (:) [Get-Member], InvalidOperationException
    + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand

 A function in Windows PowerShell automatically returns an object to the calling command. Write-Host short-circuits that operation. The last thing I did in the Get-CountDownTime function was clear screen between displaying the numbers. I debated whether to do that, but in the end decided to do so. The Complete ScriptingGamesBeginnerEvent10Function.ps1 with a small piece of code that illustrates calling the function is seen here.

ScriptingGamesBeginnerEvent10Function.ps1

Function Get-CountDownTime([int]$seconds)
{
 for ($time=$seconds;$time -ge 0;$time--)
 {
  $min = [math]::floor([int]$time/[int]60)
  $sec = $time % 60
   if ($sec -lt 10)
    {$sec = "0$sec"}
  [string]"$min`:$sec"
  Start-Sleep -Seconds 1
  Clear-Host
 }
} #end function Get-CountDownTime
# *** Entry Point to script ***
 get-CountDownTime 180

NOTE: the above works on Windows PowerShell 2.0. On Windows PowerShell 1.0 you will need to modify this line: [string]"$min`:$sec"

to this:

[string]"$($min)`:$sec"

 

BigAlfonso—great contribution! Thanks for participating in the 2009 Summer Scripting Games. BD, we thank you for your question about pausing script execution by creating an egg timer.

If you want to be the first to know what is happening on the Script Center, follow us on Twitter or Facebook. If you need assistance with a script, you can post questions to the Official Scripting Guys Forum, or send an e-mail to scripter@microsoft.com. This draws to a close our wrap-up of the 2009 Summer Scripting Games. We start a new topic next week. Have a great weekend, and we will see you Monday. Until then, take care.

Ed Wilson and Craig Liebendorfer, Scripting Guys