Windows Powershell

Top1 References

Top2 First steps

Top2.1 Open PowerShell window via context menu

Assume you have stored your Powershell script files within some file folder, then you can open the Powershell command window using item "Open PowerShell window here" within context menu of this folder.

Open powershell window

If there is no appropriate context menu item on your system you can create one by using the following reg file to adapt the registry:

Windows Registry Editor Version 5.00 

[HKEY_CLASSES_ROOT\Directory\shell\PowerShell]
@="Open PowerShell window here" 

[HKEY_CLASSES_ROOT\Directory\shell\PowerShell\Command]
@="\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\"
    -NoExit -Command [Environment]::CurrentDirectory=(Set-Location
    -LiteralPath:'%L' -PassThru).ProviderPath\"" 
Please check version and path of your powershell executable. Store contents within file PowerShellFromHere.reg and double click on it.

When you finally have succeeded with clicking the menu item the PowerShell window will show an appropriate prompt for the location where it has been opened:

powershell command window

Within the command window you can directly enter PowerShell commands (e.g. "Get-ChildItem", probably better known under its shortcut "dir"). If you have some scripts of your own, you can start them and provide all needed arguments within command line.

Top2.2 Create and start your first script: Hello world

You can use a simple text editor (e.g. notepad) to write the well known "Hello World" program. In Powershell syntax it looks like:
# File: HelloWorld.ps1

write-host "Hello world!"
To start the script enter "./HelloWorld.ps1" within PowerShell window. As you might have expected the following output is generated:
PS D:\\Gerald\TEMP> ./HelloWorld.ps1
Hello world!
PS D:\\Gerald\TEMP>
If you have problems with starting a script on your machine, you might have to set the appropriate security policy within the PowerShell window:
PS D:\\Gerald\TEMP> Set-ExecutionPolicy unrestricted

Top2.3 Directly starting Powershell scripts, hotkeys

Start from explorer
For the only purpose of starting an already existing script, you can select item "Run with PowerShell" from context menu of the script file itself:

start powershell script

Start from CMD file
To control execution of PowerShell scripts you can also use CMD files. There you have the possibilities to suppress the comand window, to redirect output to a logfile and to pass arguments to the script:
:: File: CheckBuildResults.cmd

:: Start powershell script, suppress command window, but write
:: output to logfile
PowerShell.exe -WindowStyle Hidden ./CheckBuildResults.ps1
    >CheckBuildResults.run.log

:: Start powershell script and suppress command window
::PowerShell.exe -WindowStyle Hidden ./CheckBuildResults.ps1

:: Start powershell script within command window
::PowerShell.exe ./CheckBuildResults.ps1
Start with hotkey

You can start script execution with a selfdefined keystroke, a hotkey. To define a hotkey for your script perform the following steps:

  • Create a CMD file which starts your script
  • Create a shortcut to the CMD file and define your desired keystroke combination within properties of the shortcut
  • The keystroke combination gets operative when you move the shortcut to desktop or start menu.

Top2.4 Debugging with the PowerShell ISE

To debug a PowerShell script you can use the "PowerShell Integrated Scripting Environment (ISE)". The ISE is usually reachable via start menu (Programs / Windows Power Shell / Windows Power Shell ISE).

powershell ise

Within the ISE there are three sections:

  • script pane
    Here you can edit your script
  • output pane
    Within this section output from a running script or from a command entered within command pane is displayed here.
  • command pane
    This section represents a PowerShell command window, where you can directly enter commands or start scripts.
The most useful things within debugging are:
  • step over [F10], step into [F11], step out [Shift+F11]
  • set breakpoint (mouse context menu on script line / "Toggle breakpoint")
  • inspect variable values (simply pointing to variable name)
  • display call stack [Ctrl+Shift+D]

Top2.5 How to get info about shell commands

Within PowerShell there are numerous internal commands, the so called "cmdlets". You can get help abput their usage direct from the shell by typing at command prompt:
  • Get-Command
    displays a list of all available commands within the shell
  • Get-help *expression*
    displays info about all commands containing "expression" in their name
  • SomeCommand -? (e.g. get-childitem -?)
    displays all available options for the command
  • SomeCommand | Get-Member
    display info about the returned object type(s) with all members.
    For Get-ChildItem you get following infos (and much more): it returns one or more objects of type System.IO.DirectoryInfo and / or System.IO.FileInfo. Objects of type FileInfo have attribute PSIsContainer=false. Objects of type DirectoryInfo have attribute PSIsContainer=true.

    Get help

  • Search for cmdlets at Microsoft TechNet

Top3 Script syntax

Top3.1 Organization of script files

A PowerShell script file has extension .ps1 and may contain the following structure elements:
# File: Example.ps1

# Global variables and definitions
[int]$script:counter # script global counter

# User defined functions
function MyFirstFunction()
{
    # break statement into several lines:
    $sum = $x `
         + $y
}

# Main part of the script
# directly use script statements and call functions
...
You can use "#" to mark the rest of the line as a comment.

To break a longer statement for better readability into several lines you have to use a special line-continuation character: the trailing back-tick ` (yes this is not a spot of dust on your monitor, this is the character to use)

Top3.2 Variables

Top3.2.1 Basic data types

Variable names start with a "$". You are not forced to specify a specific variable type but it might be a good practice to do so. The most used types are the following:
[int]$counter     =    5  # 32 bit signed integer
[long]$numRecords =  100  # 64 bit signed integer
[double]$pi       = 3.14  # 64 bit floating-point number
[bool]$isFemale   = $true # boolean true/false
[string]$info     = "-"   # string of unicode characters

[char]$ch  # unicode character
[byte]$num # 8 bit number

Top3.2.2 Arrays

There is also an array type, but you cannot remove entries from a PowerShell array. For many practical usages prefer using a .NET ArrayList which is suitable for most cases:
# Example: Define an array of test names
$myDefinedTests = New-Object System.Collections.ArrayList
Working with such an ArrayList is simple:
# add an entry
$myDefinedTests.Add($testName)

#remove an entry
$myDefinedTests.Remove($someTest)

# check if an element is already contained
if ($myDefinedTests.Contains($someTest))
{
    ...
}

# get total number of contained elements
$numElements = $myDefinedTests.Count

# clear array
# (unwanted output from execution is suppressed via
# piping to null device)
$myDefinedTests.Clear() | Out-Null 

Top3.2.3 Create your own data structs

You can define your own complex data type by creating an appropriate object with dynamically assigned properties. For easier usage define your own factory function:
function CreateMyDataStruct([bool]$in_flag,[string]$in_info, [int]in_number))
{
    $newObject = New-Object Object

    $newCheckInfo | Add-Member NoteProperty isFemale $in_flag
    $newCheckInfo | Add-Member NoteProperty name $in_info
    $newCheckInfo | Add-Member NoteProperty age $in_number

    return $newObject
}
Using the data struct:
[System.Object]$person = CreateMyDataStruct $true "Mrs. Smith" 36

if (-not $person.isFemale)
{
    write-host $person.name "is of age" $person.age
}

Top3.3 Logical expressions

The most important logical operators
Operator Meaning
-and and
-or or
-not not
! not
The most important comparison operators
Operator Meaning
-eq is equal to
-ne is not equal to
-ge greater than or equal to
-gt greater than
-le less than or equal to
-lt less than
Usage
if (($num -gt 0) -and (-not ($num -eq 17)))
{
    # do something
}

Top3.4 Control structures

All structure elements are well known from other languages.

Top3.4.1 if-else-elseif

if ($condition)
{
}

if ($condition)
{
}
else
{
}

if ($condition)
{
}
elseif ($otherCondition)
{
}
else
{
}

Top3.4.2 switch

Simple comparison for equality
[int]$num = 7
switch ($num)
{
    1 {write-host "one"}
    2 {write-host "two"}
    default {write-host "other value"}
}
Check for complex conditions
switch ($num)
{
    {$_ -gt 2}
        {write-host "greater 2"}
    {($_ -gt 5) -and ($_ -lt 10)}
        {write-host "greater 5 and less 10"}
    default
        {write-host "something else"}
}
Output:
greater 2
greater 5 and less 10
  • default case is used if no other condition matches
  • if more than one condition applies all corresponding switches are executed
  • use "break" to leave the switch block at the first matching case:
    {$_ -gt 2}
            {write-host "greater 2"; break}
    
  • Hint: There are other possibilities like processing arrays and comparing strings with or without case sensitivity, using wildcards or regular expressions. For details see [Mastering PowerShell].

Top3.4.3 for and foreach

for ($i=0; $i -lt 7; $i++)
{
    ...
}
foreach ($element in $container)
{
    # do something with $element
}

Top3.4.4 while and do-while

while ($condition)
{
    # do something
}
do
{
    # do something
} while ($condition)
You can use "break" to leave a loop or "continue" to continue with the next loop cycle.

Top3.5 Writing functions

You can define your own function:
function GetSum([int]$in_x, [int]$in_y)
{
    [int]$sum = $in_x + $in_y
    return $sum
}
Calling your function:
[int}$someVal = 17
[int]$result = GetSum $someVal 4

# here $result has value 21
Attention
  • Arguments passed to a function are separated by blanks
  • There is no "calling bracket" as in other languages
  • Brackets are primarily used to evaluate expressions within the script
  • Passing more arguments than specified in the function's definition leads to ignoring the last arguments. There will be no warning or error to indicate this situation!
  • Passing less arguments than specified in the function's definition leads to using default values (e.g. 0) for missing arguments. There will be no warning or error to indicate this situation!
$result = GetSum $someVal
# result is 17, missing argument is replaced by 0

$result = GetSum $someVal 4 5 6
# result is 21, last arguments (5, 6) are ignored

$result = GetSum $someVal (4 + 5 + 6)
# result is 32, the expression within brackets is evaluated before
# it gets passed to the function as second argument
Return values
There is nothing like an out param which can be passed back to the caller. The only ways to pass information back are:
  • Use the function's return value
  • Change the value of global variables or variables defined in an enclosing scope (see next section)
Return statement is optional
The usage of a return statement within the function's body is only optional. A function simply returns all "things" written to output during execution. This means the return value can contain several values of different types. If more than one value is returned the values are wrapped into an array.
function GetSum([int]$in_x, [int]$in_y)
{
    # this is a message written to screen, and not a message to
    # output, i.e. this is not part of the returned values
    write-host "calculating..."
    
    [int]$sum = $in_x + $in_y
    

    # the follwing lines will return 3 different values
    [string]$info = "hello"
    $info         # pass to output
    3.14 * 2      # pass to output
    return $sum   # pass to output
}
Calling the function
[int]$someVal = 17

# here $result is no longer of type int, omitting type
# specification will allow implicit setting of appropriate
# type
$result = GetSum $someVal 4
write-host "Result=" $result
Output
calculating...
Result= hello 6,28 21
Recommendation
If you have no specific reasons prefer using the same programming style as in other languages, i.e. return always a single value via explicit return statement.

Top3.6 Visibilty of variables

Top3.6.1 Local and script global visibility

$myVar / $local:myVar
As default variables are defined with "local" scope. Access for read is possible within scope of definition and within all called functions. Changing of a variable's value is only possible within the scope of its definition.

Attention:
Trying to change a local variable within an subordinate scope results in creating a new variable within the called scope. Changes will not affect the original variable within the enclosing scope.

$script:myVar
Variables defined with scope "script" have both read and write access throughout the whole script. For write access you have to use the explicit scope notation.
Example
[int]$counterA         = 1  # local per default, readable within this script
[int]$script:counterB  = 1  # readable and writable within this script

function IncrementCounters ([int]$in_level)
{
    [string]$funcInfo = "IncrementCounters-" + $in_level + ": "

    # all variables can be read without specifying scope:
    
    write-host ""
    write-host ($funcInfo + "counterA=" + $counterA)    
    write-host ($funcInfo + "counterB=" + $counterB)    
    write-host ""

    write-host ($funcInfo + "now incrementing all counters...")
    
    # changing counterA causes creation of a new variable
    # within new scope to store the changed value!
    $counterA += 1
    
    # for changing value of script variable explicitly
    # use "script" specification otherwise
    # a new variable would be created
    $script:counterB += 1
    
    write-host ""
    write-host ($funcInfo + "counterA=" + $counterA)    
    write-host ($funcInfo + "counterB=" + $counterB)    
    write-host ""
    
    if ($in_level -eq 1)
    {
        IncrementCounters 2
        write-host ""
        write-host ($funcInfo + "counterA=" + $counterA)    
        write-host ($funcInfo + "counterB=" + $counterB)    
        write-host ""
    }
}

#----- main -----

write-host ""
write-host ("main: counterA=" + $counterA)    
write-host ("main: counterB=" + $counterB)    
write-host ""

IncrementCounters 1

write-host ""
write-host ("main: counterA=" + $counterA)    
write-host ("main: counterB=" + $counterB)    
write-host ""

Output
main: counterA=1
main: counterB=1


IncrementCounters-1: counterA=1
IncrementCounters-1: counterB=1

IncrementCounters-1: now incrementing all counters...

IncrementCounters-1: counterA=2
IncrementCounters-1: counterB=2


IncrementCounters-2: counterA=2
IncrementCounters-2: counterB=2

IncrementCounters-2: now incrementing all counters...

IncrementCounters-2: counterA=3
IncrementCounters-2: counterB=3


IncrementCounters-1: counterA=2 # scope level 1 is not changed!
IncrementCounters-1: counterB=3


main: counterA=1 # main scope is not changed!
main: counterB=3

Top3.6.2 Private visibility

$private:myVar
Private variables have read and write access only within scope of their definition. To pass a private variable for read access to a called function you have to use explicit function parameters.

Attention:
Trying to change a private variable within an subordinate scope results in creating a new variable within the called scope. Changes will not affect the original variable within the enclosing scope.

Example
[int]$private:counterC  = 1  # visible only within main scope

function ReadCounter ()
{
    # private variable value is not available here:
    write-host ("ReadCounter: counterC=" + $counterC)    
    write-host ("ReadCounter: private:counterC=" + $private:counterC)    
    write-host ""
}

function PassPrivateValue ([int]$in_someValue)
{
    write-host ("PassPrivateValue: in_someValue=" + $in_someValue)    
    $in_someValue += 5 # change is not visible outside the function
    write-host ("PassPrivateValue: in_someValue=" + $in_someValue)    
}

#----- main -----

write-host ("main: counterC=" + $counterC)    
write-host ""

# no read access
ReadCounter

# private variable is changeable only within scope of definition
$private:counterC += 1
$counterC += 1 # scope specification is not necessary

write-host ("main: counterC=" + $counterC)    
write-host ""

# for read access private variables can be explicitly passed to a function
PassPrivateValue $counterC

write-host ""
write-host ("main: counterC=" + $counterC)    
Output
main: counterC=1

ReadCounter: counterC= # value is not available
ReadCounter: private:counterC=

main: counterC=3

PassPrivateValue: in_someValue=3
PassPrivateValue: in_someValue=8 # changed value is not visible outside

main: counterC=3

Top3.7 Output formatting

Instruction to format a single value: "{argIndex[,alignment][:formatString]}"
  • argIndex relates to the zero based index within the list of arguments typically following the format expression
  • alignment specifies the width to use, a negative number means left justified output, positive means right justified output
  • Often used possibilities for formatString:
    • N3 : number with precision three, i.e. 3 digits after decimal point
    • P1 : build percentage value (e.g. 0.1546 is formatted to 15.5%)
    • ###.## : 2 digits after decimal point, but only if needed (e.g. 2 / 4.5 / 3.14)
    • ###.00 : always 2 digits after decimal point (e.g. 2.00 / 4.50 / 3.14)
    • 000.# : fill with leading zeros up to three digits (e.g. 002 / 004.5 / 003.1)

Format expression with several values: "any text {0..} any text {1..} .." - f $val1, $val2, ..

Example
# Formatting of double values,
# first column left justified with leading zeros, various precisions
# second column right justified with precision 2
$someValue  = 3.45678
$otherValue = 145.678
"{0,-10}{1,10:N2}" -f $someValue, $otherValue
"{0,-10:0#.00 }{1,10:N2}" -f $someValue, 8.1
"{0,-10:0#.000}{1,10:N2}" -f ($someValue*3), ($otherValue * 2)

write-host ""

# Percentage, first column right adjusted
"{0,5:P1}  {1,-20}" -f (10 / 200),"Small percentage"
"{0,5:P1}  {1,-20}" -f 0.9567,    "High percentage"
Output
3,45678       145,68
03,46           8,10
10,370        291,36

 5,0%  Small percentage    
95,7%  High percentage    

Top4 Useful code snippets

Top4.1 Control of verbosity: Writing text to stdout (WriteLine)

Function WriteLine allows to control the amount of information written to stdout.

Within the script you have to attach an "output level" to each text line. The principle here is: the more important the information is the smaller the attached output level is. Depending on the current value for the script variable $OUTPUT_LEVEL_STDOUT the output of the given text will be suppressed (if the script variable is less than the output level attached to the given text).

Configuration
[int]$OUTPUT_LEVEL_STDOUT = 1

# add your comments here (depending on your usage of WriteLine):
# 0 : no output
# 1 : basic output for regular usage of the script
# 2 : more detailed output
# 3 : trace information for debugging
Definition
# Write output to stdout.
# Depending on the configured output level call may be ignored.
function WriteLine ($in_outputLevel, [string]$in_info)
{
    if ($in_outputLevel -le $OUTPUT_LEVEL_STDOUT)
    {
        write-host $in_info
    }
}
Usage
[int]num = 5

WriteLine 1 "This is an important information"
WriteLine 3 ("DetailedInfo: num=" + $num)

Top4.2 Current time (CurTime)

Function CurTime returns the current time as a formatted string.
Definition
#Generate a formatted time stamp
function CurTime
{
    $t = (Get-Date -format T)
    return $t
}
Usage
#using built-in function write-host
write-host "The current time is" ((CurTime) + ". This is really late.")

#using helper WriteLine (see slightly different syntax for combining strings)
WriteLine 1 ("The current time is " + (CurTime) + ". This is really late.")

#Remark: Omitting the brackets around CurTime does not work. The rest of
# the line would be regarded as possible (but not needed) parameters for
# CurTime and therefore would be ignored completely
Output:
The current time is 13:10:10. This is really late.
The current time is 13:10:10. This is really late.

Top4.3 Converting from english to german date format (ParseAndTransformDate)

Function ParseAndTransformDate can be used for converting textual date formats.

The function expects an arbitrary string starting with the date, e.g. "2011/12/02...." or "2011-12-02....". For both cases the function returns the date in german format "02.12.2011". Additional string contents after the date are ignored.

Definition
# Transform date from "2011/12/02...." to "02.12.2011"
function ParseAndTransformDate ([string]$in_date)
{
    [string]$date = $in_date.Substring(8,2) + "."
    $date +=  $in_date.Substring(5,2) + "."  + $in_date.Substring(0,4)
    return $date;         
}

Top4.4 Sending an Email (SendEmailTo)

There are many use cases where you want to notify yourself or some other person automatically via an email message about a specific problem. Example: a script running in background or on an other work station detects an error situation and automatically informs the presumed guilty or responsible person.

As you can automatically generate the contents for both email subject and message body the receiver can be provided with sufficient context information about the problem.

The sending of an email has to be organized with use of the mail server existing in your environment. Depending on the needed security and authentication procedures you have to specify more or less authentication data.

Within office environments there are often the following situations:

  • Sending to your or someone else's "office address" via your company's mail server. Probably you have to use your Windows logon account to legitimate for sending an email.
  • Sending to a private email address via an external mail provider needing a specific logon account
Configuration for office mail server
[string]$MY_EMAIL_ADDRESS               = "Somebody@SomeCompany.com"
[string]$MAIL_SMTP_SERVER               = "SomeCompany.smtp.server"
[int]   $MAIL_PORT                      = 567
[bool]  $MAIL_USE_WINDOWS_LOGON_ACCOUNT = $true # true if mail logon is required
[bool]  $MAIL_USE_EXTERNAL_MAIL_ACCOUNT = $false
Configuration for private mail account
$MY_EMAIL_ADDRESS               = "mail@gerald-fahrnholz.de"
$MAIL_SMTP_SERVER               = "mail.gerald-fahrnholz.de"
$MAIL_PORT                      = 25
$MAIL_USE_WINDOWS_LOGON_ACCOUNT = $false
$MAIL_USE_EXTERNAL_MAIL_ACCOUNT = $true
$MAIL_EXTERNAL_USER             = "YourUserName"
$MAIL_EXTERNAL_PASSWORD         = "YourPassword"
Helper functions
The functions SendEmailToMyself and SendEmailTo encaspulate the details of logon and send procedures.
function SendEmailToMyself ($subject, $info)
{
    SendEmailTo $MY_EMAIL_ADDRESS $subject $info
}

#----------------------------------------------------------

function SendEmailTo ($to, $subject, $info)
{
    WriteLine 3 "Sending email..."

    $from    = $MY_EMAIL_ADDRESS
    $body    = $info

    WriteLine 3 ("To         : " + $to)
    WriteLine 3 ("SmtpServer : " + $MAIL_SMTP_SERVER)

    if ($MAIL_USE_WINDOWS_LOGON_ACCOUNT)
    {
        WriteLine 3 ("Using windows login for authentication")
        $SMTPClient = New-Object Net.Mail.SmtpClient($MAIL_SMTP_SERVER, $MAIL_PORT) 
        $SMTPClient.UseDefaultCredentials = $true 
        # $SMTPClient.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network
        $SMTPClient.EnableSsl = $false
        $SMTPClient.Send($from, $to, $subject, $body)
    }
    elseif ($MAIL_USE_EXTERNAL_MAIL_ACCOUNT)
    {
        WriteLine 3 ("Using external mail account for user " + $MAIL_EXTERNAL_USER)
        $SMTPClient = New-Object Net.Mail.SmtpClient($MAIL_SMTP_SERVER, $MAIL_PORT) 
        $SMTPClient.EnableSsl = $false
        $SMTPClient.Credentials = New-Object System.Net.
            NetworkCredential($MAIL_EXTERNAL_USER, $MAIL_EXTERNAL_PASSWORD); 
        $SMTPClient.Send($from, $to, $subject, $body)
    }
    else # simple mail without authentication
    {
        Send-MailMessage -To $to -Subject $subject -From $from
            -Body $body -SmtpServer $MAIL_SMTP_SERVER
    }
   
    WriteLine 3 "...email was sent"
}
Usage
After you have properly configured your mail connection you can send an email from any location within your script. You have to specify only the text for email subject and message body.

[string]title = "..."
[string]info = GetDetailedInfoFromSomewhere()

SendEmailToMyself ($title + " - (automated mail info)") $info

Top4.5 Notification via systemtray balloon (AlertInSystray)

Important messages can be displayed within status bar (systemtray) as a bubble of text (balloon). Depending on the configuration and version of your Windows system such a message will be displayed only for a limited time period and will disappear automatically or it will persist for an unlimited time until the user closes the balloon by clicking on it or until the script programmatically deletes it.

Within a PowerShell script you can process the closed event of the balloon. But to do so you would need an event loop within your script. To limit programming efforts and nevertheless have a practicable solution for simple sequential scripts in the following a simpler mechanism for closing balloons is presented.

Configuration
Creat a script variable, representing the balloon (NotifyIcon):
# Prepare usage of system tray balloon
[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null
[System.Windows.Forms.NotifyIcon]$script:balloon = $null
Helper functions
The helper function OpenBalloon opens a new balloon and displays the given text.
function OpenBalloon ($title, $info)
{
    # get the icon from the running process
    $path = Get-Process -id $pid | Select-Object -ExpandProperty Path
    $icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)

    # open balloon
    $script:balloon = New-Object System.Windows.Forms.NotifyIcon
    $script:balloon.Icon = $icon
    $script:balloon.BalloonTipIcon = 'Info'
    $script:balloon.BalloonTipTitle = $title
    $script:balloon.BalloonTipText = $info
    $script:balloon.Visible = $true

    WriteLine 3 "Open balloon..."
    $script:balloon.ShowBalloonTip(60) # timeout value did not work!

    # simple alternative method:
    # wait for some time then delete balloon and proceed with script
    # Start-Sleep -Seconds 30
    # $script:balloon.dispose()

    WriteLine 3 "...proceed script execution while balloon is open"
}
The function AlertInSystemTray closes an eventually still existing balloon and displays the given text within a new balloon. In this way you are sure that a running script only displays the latest message and the user has not to close many outdated balloons one by one.
function AlertInSystemTray ([string]$in_title, [string]$in_info)
{
    # Remark:
    # This script runs sequentially and does not support any event loop.
    # Therefore we do not call register-objectevent for the closed event
    # of the balloon. Instead we will dispose the old balloon each time
    # before we open a new balloon for the next message
    if ($script:balloon)
    {
        # in any case close the balloon which may still be open from last message
        WriteLine 3 "Deleting balloon"
        $script:balloon.dispose()
        $script:balloon = $null
    }

    if ($in_info -ne "")
    {
        # open new balloon only when there is something to report
        OpenBalloon $in_title $in_info
    }
}
Usage
From any location within your script you can refresh the balloon contents by specifying a string for new title and new text. If the balloon has already disappeared a new balloon will be opened.
[string]$title = "..."
[string]$info = GetDetailedInfoFromSomewhere()

AlertInSystemTray $title $info

Top4.6 Display a message box (OpenMsgBox)

You can display important informations within a simple message box opened directly from your script. Besides displaying informations to the user there is also the possibility to let the user choose between some alternatives about subsequent script execution.

PowerShell scripts may often run without a user watching for the progress of the script. There may be good reasons to continue script execution also in the case the user does not confirm the message box or answer the request. For these cases there is the possibility to limit the time period for displaying the message box. When the time period has elapsed the message box will close automatically and the script execution continues.

Configuration
You can define a repeat counter by setting a corresponding control variable. The message box will be opened and closed the given number of times. This may attract the user's attention even for the case where the message box is opened behind the current active application window. In any case there will be a window icon appearing and disappearing within the sytem tray.
[int]$MSGBOX_MAX_REPEAT  = 3  # if user does not confirm the
                              # msgbox with "Yes" the msg box will be
                              # closed and reopened the given number of times
Helper functions
Function OpenMsgBox encapsulates the display of a simple textual message. The function contains the possibility of repeatedly opening the message box and of returning when some time period has elapsed.

Within the implementation body of the function you can adjust the period of time for visibilty and invisibility.

function OpenMsgBox ($title, $info)
{
    # Configure messages and waiting times
    $message                     = $info
    $maxDisplayTimeInSeconds     = 15 # if user does not confirm,
                                      # close message box automatically
    $repeatAlertAfterSeconds     =  4 # if user does not confirm,
                                      # repeat alert after N seconds
    $delayedReminderWaitSeconds  = 60 # delay alert for another N seconds if user
                                      # clicks "NO"

    # Create a single instance of scripting host to open message boxes
    $ws = new-object -comobject wscript.shell
    $repeatCounter = $MSGBOX_MAX_REPEAT
    do
    {
        WriteLine 3 ("MsgBox " + (CurTime) + " Open MsgBox...")

        # Open message box
        $constYesNoButton = 4
        $buttonClick = $ws.popup($message,$maxDisplayTimeInSeconds,
                                 $title,$constYesNoButton)
        $repeatCounter -= 1;
        
        # Process anwser
        $constYesButton = 6
        $constNoButton = 7
        $constTimeout = -1
        if ($buttonClick -eq $constYesButton)
        {
            # we are finished, terminate execution
            WriteLine 3 ("MsgBox " + (CurTime) + " ... user has confirmed")
            $waitSeconds = 0
        }
        else # timeout or user has clicked NO
        {
            if ($buttonClick -eq $constTimeout)
            {
                WriteLine 3 ("MsgBox " + (CurTime)
                   + " ... user did not react, repeat alert after a short while")
                $waitSeconds = $repeatAlertAfterSeconds
            }
            elseif ($buttonClick -eq $constNoButton)
            {
                WriteLine 3 ("MsgBox " + (CurTime)
                   + " ... user wants to prolong waiting time")
                $waitSeconds = $delayedReminderWaitSeconds
            }
            else
            {
                WriteLine 3 ("MsgBox " + (CurTime)
                    + " ... unexpected answer, repeat alert immediately")
                $waitSeconds = 2
            }
        }
        
        if (($waitSeconds -gt 0) -and (($repeatCounter -gt 0)))
        {
            WriteLine 3 ("MsgBox " + (CurTime) + " waiting another "
                + $waitSeconds + " seconds...")
            Start-Sleep -Seconds $waitSeconds
        }
        
        if (($waitSeconds -gt 0) -and ($repeatCounter -eq 0))
        {
            WriteLine 3 ("MsgBox " + (CurTime)
                 + " ... giving up, user is sleeping")
        }
        
    } while (($waitSeconds -gt 0) -and ($repeatCounter -gt 0))
}

Usage
From any location within your script you can open a message box by specifying a string for title and text. Script execution will continue when the user has confirmed the message box or when the configured time period has elapsed.
[string]$title = "..."
[string]$info = GetDetailedInfoFromSomewhere()

OpenMsgBox $title $info

For more infos about message boxes see Popup method within Windows scripting host (Microsoft MSDN library)

Top4.7 Configurable notify function (Alert)

Often it may be useful to send informations to the user by using several ways of notification (in the hope the user will notice at least one of them).

Popular ways of notification are:

  • Sending an email
  • Displaying a balloon within system tray
  • Opening a message box
Configuration
With simple setting of corresponding control variables you can select one or several notification channels.
# Activate one or more alert targets to get informed about important events.
# If at least one alert target is active you could call this script from
# cmd file with option "-WindowStyle Hidden" to make the cmd shell completely
# invisible                                          
[bool]$ALERT_AS_SYSTRAY_BALLOON   = $true
[bool]$ALERT_AS_MESSAGE_BOX       = $false
[bool]$ALERT_AS_EMAIL             = $true
Helper functions
Function Alert encapsulates the display of a simple textual information consiting of short topic text and a longer message text. The information will be propagated via the configured ways of notification.
# Alerts the user about the given info text. If text is empty
# call will be ignored.
function Alert ([string]$in_title, [string]$in_info)
{
    # ----- Open balloon within system tray -----

    if ($ALERT_AS_SYSTRAY_BALLOON)
    {
        AlertInSystemTray $title $info
    }

    # ----- Send email -----

    if ($ALERT_AS_EMAIL)
    {
        if ($in_info -ne "")
        {
            SendEmailToMyself ($in_title + " - (automated mail info)") $in_info
        }
    }
    
    # ----- Open message box -----

    if ($ALERT_AS_MESSAGE_BOX)
    {
        if ($in_info -ne "")
        {
            OpenMsgBox $in_title $in_info
        }
    }
}
Usage
From any location within your script you can send a textual notification.
[string]title = "..."
[string]info = GetDetailedInfoFromSomewhere()

Alert $title $info

Top4.8 Download web contents

With a simple function call you can download a complete web page into a regular string which you can write to stdout or parse for relevant pieces of information.
$web = New-Object System.Net.WebClient
[string]$webAddress ="https://www.gerald-fahrnholz.de/small_pages/links.htm"
[string]$pageContents = $web.DownloadString($webAddress)

# write to stdout
write-host $pageContents

# or parse the returned string...

Top4.9 File system: Find the N newest subdirectories

You can use pipe lining to specify several properties for file system objects you are looking for. In the following example we are interested in the newest N subdirectories of a directory.
[string]$parentDirToExamine = "D:\Test"
[int]$numRelevantEntries    = 3

# Step 1: get all elements of parent directory
# Remark: explicitly define array type otherwise you may 
# get only a single element and not an array which may
# prevent you from examining the number of elements within
# the array
[System.Object[]]$subDirs = get-childitem $parentDirToExamine |

    # Step 2: get directory elements which are folders
    where {$_.PsIsContainer} |

    # Step 3: sort them with descending LastWriteTime,
    # i.e. newest entry is the first
    sort-object -property LastWriteTime -descending |

    # Step 4: from this ordered set we are only interested
    # on the first N elements (parent dir may contain less entries)
    select-object -first $numRelevantEntries

# write found results to stdout
write-host "Num entries found: " + $subDirs.Count)
foreach ($subDir in $subDirs)
{
    write-host $subDir.Name
}

Top4.10 File system: Find bitmap files within directory

Search a directory for contained bitmap files:
[string]$dirPath = "C:\Temp"

$allFiles = get-childitem $dirPath
$bmpFiles = $allFiles | where {$_.extension -eq ".bmp"}

Top4.11 Windows registry: Change wallpaper

You can programmatically change the current wallpaper to any picture file in bitmap format:
# get current wallpaper
$oldWallpaper = Get-ItemProperty -path 'HKCU:\Control Panel\Desktop\'`
    -name Wallpaper
write-host "Current wallpaper: " $oldWallpaper.Wallpaper

# path to picture in bitmap format
[string]$newBmpFile = "C:\TEMP\SomePicture.bmp"

# Set new wallpaper by changing values within registry
Set-ItemProperty -path 'HKCU:\Control Panel\Desktop\' -name Wallpaper `
    -value $newBmpFile -Force
# display centered and stretched
Set-ItemProperty -path 'HKCU:\Control Panel\Desktop\' `
    -name TileWallpaper -value "0"
Set-ItemProperty -path 'HKCU:\Control Panel\Desktop\' `
    -name WallpaperStyle -value "2"

# activate changed registry settings
rundll32.exe user32.dll,UpdatePerUserSystemParameters

Top4.12 Polling script execution

There are use cases where you have to repeatedly check for some changes on your system (e.g. check if new compile or test results are available). For some cases you may subscribe to notification functions on your operating system. But this may be difficult to program. Furthermore remote systems you want to access from your script may not offer active notifications.

The simple concept of time controlled polling may help in all these situations:

# repeat the script execution every 2 minutes
# specifying 0 means: script is executed only once
[int]$REPEAT_CHECK_AFTER_SECONDS = 120

do
{
    # perform some checks (e.g. call some functions)
    ...

    if ($REPEAT_CHECK_AFTER_SECONDS -gt 0)
    {
        # after some sleep time the check for the latest
        # results will be repeated
        Start-Sleep -Seconds $REPEAT_CHECK_AFTER_SECONDS
        }
    }
    
} while ($REPEAT_CHECK_AFTER_SECONDS -gt 0)

Top5 Sample scripts

Top5.1 Automatic change of wallpaper

Download of script: ChangeWallpaper (zip file)

You can specify a change interval and a bitmap directory as command arguments. The script randomly sets a bitmap from the given directory as desktop wallpaper. If the given change interval is 0 then the wallpaper is set only once and the script terminates. Otherwise the wallpaper will be set repeatedly.

Typical code snippet
See sections accessing windows registry and find bitmap files.

Top5.2 Reminder: Tea is ready

Download of script: Tea-Reminder (zip file)

Enthusiastic programmers may run into the risk of forgetting to remove the teabag out of their cup. Here is the solution for this problem: a script which reminds you by a popup message when tea is ready. You can specify your desired preparation time via command line parameter. By clicking a corresponding button you can prolong the waiting time.
Of course you can adjust the script to remind you to other important things of daily life.

Typical code snippet
See section Open message box.

Top5.3 Retrieving system information

Download of script: GetSysInfo (zip file)

The script collects informations about your computer system. Basically the information is collected via "Get-Wmiobject" and "System.IO.DriveInfo".

Example output
COMPUTER SYSTEM

HostName             : SIRIUS
Owner                : Gerald Fahrnholz
Domain               : ARBEITSGRUPPE
Manufacturer         : Dell Inc.
Model                : Inspiron 9300
OS name              : Microsoft Windows XP Professional
OS Service Pack      : 3
OS version           : 5.1.2600
OS serial number     : 76497-OEM-0011903-00102
OS directory         : C:\WINDOWS\system32


PROCESSOR and MEMORY

Class                : Win32_Processor
Manufacturer         : GenuineIntel
Name                 : Intel(R) Pentium(R) M processor 2.00GHz
Description          : x86 Family 6 Model 13 Stepping 8
Number of cores      : 1
L2 cache size        : 2 MB
Max clock speed      : 1994 MHz
Physical memory      : 2 GB


DISK SPACE

Drive  DiskType   VolumeName    Free  FreeSpace  TotalSize  Format

C:     local                   25,1%   23,36 GB   93,15 GB    NTFS
E:     removable  MYUSBSTICK    4,4%    0,02 GB    0,48 GB     FAT
Typical code snippet
# get processor name
$processor = Get-WmiObject win32_processor
"Processor name   :"  $processor.Name.Trim(' ')
"Number of cores  :"  $processor.NumberOfCores

Top5.4 Killing unwanted processes

Download of script: Killer (zip file)

The script checks if any process from a configurable set of process names is running and tries to kill all instances of them.

The execution can be configured for one time execution or repeated (time controlled) execution to capture also unwanted processes which are started later in time.

Attention: the demo script deletes all instances of notepad.exe on your system. Therefore save all open text documents before starting script execution.

Command line syntax
PS C:\UserData\Gerald\SW\PowerShell>./Killer -?

Killer.ps1 - commandline syntax

-checkEverySec 

  repeat the check for unwanted processes when the given time interval
  has elapsed; default time interval is 30 seconds; a time interval
  of 0 means single execution;

processname_1 processName_2 ...

  a list of (unwanted) processes to be deleted, the names are separated
  by blanks; you have to specify at least one process name

-? or -help

  show these infos and exit from script execution
Typical code snippet
[string[]]$processesToDelete = GetArrayOfProcessnamesFromSomewhere

# option "SilentlyContinue" tolerates search for not existing processes
get-process $processesToDelete -ErrorAction SilentlyContinue `
    | foreach-object {$_.kill()}

Top5.5 Watching processes (CPU load, memory consumption)

Download of script: ProcessWatcher (zip file)

The script checks CPU load and working set usage. To identify the processes which make heavy use of CPU and memory the top N processes are shown within two distinct rankings.

Example output
PorcessWatcher-Output

Command line syntax
ProcessWatcher-CmdLineArgs
Typical code snippet
$processor        = Get-WmiObject win32_processor
$operatingSystem  = get-wmiobject win32_OperatingSystem
$runningProcesses = get-process

foreach($process in $runningProcesses)
{
    [double]$procTotalCpuTime = $process.CPU
    [double]�procWorkingSet   = $process.Workingset64
}

$totalCpuLoad = $processor.LoadPercentage

# total physical memory usable by the operating system
$totalPhysicalMemory = $operatingSystem.TotalVisibleMemorySize

# still free physical memory usable by the operating system
$freePhysicalMemory  = $operatingSystem.FreePhysicalMemory
Remark
The operating system may not be able to use all of the installed memory HW, i.e. "$operatingSystem.TotalVisibleMemorySize" may be smaller than property "TotalPhysicalMemory" from "get-wmiobject win32_ComputerSystem".

Top5.6 Automated check of build results

Download of script: CheckBuildResults (zip file)

In modern sw development "continous integration" should be a well known phrase. In practice it means that all team members continuously add their changed source code to the central repository. To ensure code stability there is a "continuous integration server" (e.g. CruiseControl) which continously builds the source code. Even execution of unit tests can be integrated into the build process.

CruiseControl offers build results in the form of intranet web pages which can be read by all developers. The developer can be notified about the failure of a project via a notification icon within system tray.

What the developper should do in case of an signalled error:
Browse the CruiseControl web page, look for the project he has worked on, click to the page with detailed build results, check if he has checked in a bad piece of code and probably fix it. In many cases this is a blind alarm because the error may be caused by someone else or by some side effects on the CruiseControl server.

And here comes the help of the script CheckBuildResults.ps1. The following features are supported:

  • Periodically checking the build results on the CruiseControl web pages for a configurable set of projects you are interested in
  • Automatically extracting information about the detected errors and writing them to stdout
  • Ignoring uninteresting problems on build server (lack of disk space, cruise control failures, planned project failures when a preceding project has failed)
  • Checking test reports and writing failed test applications to stdout
  • informing about relevant failures through email, balloon messages or message boxes
Example for extracted error informations:
10.05.2012 19:15:32 Build    : OK
10.05.2012 19:15:32 UnitTest : OK
10.05.2012 20:14:36 Build    : COMPILE_ERROR : InfraStructure MainGui

    --- COMPILE_ERROR --- Infrastructure 

    d:\SomePath\InfraStructure\UserList.h(41): error C2661:
       'Access::CheckAuthentication' : no overloaded function takes 5 arguments

    --- COMPILE_ERROR --- MainGui

    MainWindow.cpp(1): fatal error C1083: Cannot open precompiled header file:
        'D:\SomePath\MouseTracking.pch':
        No such file or directory

11.05.2012 08:52:06 Build    : OK
11.05.2012 09:11:52 UnitTest : OK
11.05.2012 10:27:49 Build    : OK
11.05.2012 09:11:52 UnitTest : TEST_EXEC_ERROR : TestAccounts TestReaction

    --- TEST_EXEC_ERROR --- TestAccounts

    !!Error at location
    TestAccounts.cpp (173):  (call location of error)
    Expected :   date: 19.03.2012
    Found    :   date: 18.03.2012

    First error in:
    Test 40 / Z1 Logging to event log

    --- TEST_EXEC_ERROR --- TestReaction

   [TimedExec]: 11.05.2012 09:03:51   FAILURE (TIMEOUT_EXE_TERMINATION)
       TestReaction.exe
       StartTime: 11.05.2012 09:02:21   Timeout of 90 sec elapsed

Necessary adjustments
You have to make some adaptions to your specific environment. Simply search for term "Your" within the script file and replace the following definitions:
  • set your email address and mail server
  • set server name of your CruiseControl server
  • set the path where the CruiseControl web pages are stored
  • set the path where the test protocols are stored