Writing PowerShell Scripts

Copyright © 2016 Paul Scott. All rights reserved.

PowerShell scripts are far more powerful and concise than equivalent JScript or VBScript via WSH, although to the novice they might seem equally vexing. Hopefully, this basic tutorial will take away some of the frustration and provide a good foundation for getting started with this wonderful tool.

Scripting Documentation

Core About Topics
Core Cmdlets
.Net Framework

Before You Begin

The Windows PowerShell default execution policy prevents any scripts from running. In order to run scripts you'll have to type the following command at a PowerShell prompt:

Set-ExecutionPolicy RemoteSigned

It is only necessary to run that command once as the execution policy is remembered until you change it again.

Note that Windows 7 comes with PowerShell 2, which does not support many of the newer, more powerful features of the .Net Framework. You should probably install the latest (non-Beta) version of PowerShell and its minimum required .Net Framework. Generally, whatever version of Windows you're using, check that you have the most current version of PowerShell and the .Net Framework. To get version information, type the following command at a PowerShell prompt:

$PSVersionTable

When you write PowerShell scripts, use .ps1 as the file extension, regardless of which version of PowerShell you have installed.

Help

Within a PowerShell command window you can use the help and get-help commands to get additional help. The help command can be used for general searches. The get-help command can produce finer grained documentation. Use help get-help for more information. Almost everything you need to know about PowerShell is available through the built-in help. For help on the .Net Frameworks, though, you'll have to use other sources, particularly the .Net Frameworks link above.

Comments

Single line comments begin with the # character. Everything after the # is ignored.

Multi-line comments (block comments) start with <# and end with #>.

Flow Control Statements

Powershell has all the expected flow control statements seen in other languages:

break  continue  do  for  foreach  function  if  return  switch  trap  throw  try-catch-finally  while

These work pretty much like you'd expect them to. Read the doc to familiarize yourself with any arcane syntax.

Variables

There are three different types of variables: user created variables, predefined variables, and preference variables.

User variables are created by the user's script. They can have global, script, or local scope.

Predefined variables—referred to as automatic variables in the doc—are created and modified by Windows PowerShell as necessary to maintain state. They are read-only from the perspective of the user's script. You should familiarize yourself with these predefined variables as they can come in quite handy.

Preference variables are created by Windows PowerShell and can be modified by the user's script to change the behavior of the PowerShell environment.

All variables are alpha-numeric strings that begin with a dollar sign ($), such as $path, or $name. Variable names are not case-sensitive, and can contain blanks and special characters, although it's recommended that you stick with simple alpha-numeric characters.

Generally, variables are loosely typed, which simplifies script writing. However, you can use type attributes to restrict the type and PowerShell will try to convert to the indicated type if it can.

Use the command help about_variables at a PowerShell prompt to get more information about variables.

Operators

Arithmetic Operators

Use arithmetic operators (+, -, *, /, %) to calculate values in a command or expression. With these operators, you can add, subtract, multiply, or divide values, and calculate the remainder (modulus) of a division operation.

Assignment Operators

Use assignment operators (=, +=, -=, *=, /=, %=) to assign one or more values to variables, to change the values in a variable, and to append values to variables.

Comparison Operators

Use comparison operators (-eq, -ne, -gt, -lt, -le, -ge) to compare values and test conditions. For example, you can compare two string values to determine whether they are equal. Note that these operators might be different than what you're used to seeing in other languages.

The comparison operators include the match operators (-match, -notmatch), which find patterns by using regular expressions; the replace operator (-replace), which uses regular expressions to change input values; the like operators (-like, -notlike), which find patterns using wildcard characters (*); and the containment operators (-in, -notin, -contains, -notcontains), which determine whether a test value appears in a reference set.

They also include the bitwise operators (-bAND, -bOR, -bXOR, -bNOT) to manipulate the bit patterns in values.

Logical Operators

Use logical operators (-and, -or, -xor, -not, !) to connect conditional statements into a single complex conditional.

Redirection Operators

Use redirection operators (>, >>, 2>, 2>>, and 2>&1) to send the output of a command or expression to a text file. The redirection operators work like the Out-File cmdlet (without parameters) but they also let you redirect error output to specified files. You can also use the Tee-Object cmdlet to redirect output.

Split and Join Operators

The -split and -join operators divide and combine substrings. The -split operator splits a string into substrings. The -join operator concatenates multiple strings into a single string.

Type Operators

Use the type operators (-is, -isnot, -as) to find or change the .NET Framework type of an object.

Unary Operators

Use unary operators to increment or decrement variables or object properties and to set integers to positive or negative numbers. For example, to increment the variable $a from 9 to 10, you type $a++.

Special Operators

Use special operators to perform tasks that cannot be performed by the other types of operators. For example, special operators allow you to perform operations such as running commands and changing a value's data type.

@( )Array operator returns the result of one or more statements as an array. If there is only one item, the array has only one member. For example, $myarray = @(1).
&Call operator runs a command, script, or script block. This is one of a number of ways to run a program.
[ ]Cast operator converts or limits objects to the specified type. If the objects cannot be converted, Windows PowerShell generates an error. Note that [ ] is also used in other contexts, including array indexing and static class references (covered later).
,Comma operator. As a binary operator, the comma creates an array. For example, $myarray = 1, 2, 3. As a unary operator, the comma creates an array with one member; place the comma before the member. For example, $myarray = ,1.
.Dot sourcing operator runs a script in the current scope so that any functions, aliases, and variables that the script creates are added to the current scope. This is similar to . in the bash shell.
-fFormat operator formats strings by using the format method of string objects.
[ ]Index operator selects objects from indexed collections, such as arrays and hash tables. Array indexes are zero-based. For arrays (only), you can also use negative indexes to get the last values. Hash tables are indexed by key value.
|Pipeline operator sends the output of the command that precedes it to the command that follows it. When the output includes more than one object (a collection), the pipeline operator sends the objects one at a time.
.Property dereference operator accesses the properties and methods of an object. For example, (get-process PowerShell).kill().
..Range operator represents the sequential integers in an integer array, given an upper and lower boundary. For example, foreach ($a in 1..$max) { write-host $a }.
::Static member operator calls the static properties operator and methods of a .NET Framework class. For example, [System.Io.Directory]::GetDirectories("C:\").
$( )Subexpression operator returns the result of one or more statements. For a single result, returns a scalar. For multiple results, returns an array. This is similar to $(( )) in the bash shell.

Accessing .Net Framework Classes

PowerShell lets you access all of the .Net Framework from within your scripts. Think about how powerful that is. Given sufficient time and effort, it is possible to write an entire GUI application as a PowerShell script!

While a GUI application is possible, it's more likely you'll want to use the power of .Net Frameworks to get simple tasks done. To do that, you'll need to know a little about the PowerShell syntax that let's us access the frameworks.

But first, let's review the .Net Frameworks from a programming standpoint. The frameworks are a set of classes contained within namespaces. The namespaces are just that: names. Like System, System.IO, and System.Net. Within those namespaces are classes that represent libraries of methods (useful functions) and their related properties, such as System.IO.File, or System.Net.Mail.MailMessage. How can you tell the difference between a namespace (not useful by itself) and a class (containing useful functionality)? Answer: read the .Net Framework documentation for the name you're interested in.

Now, classes are obviously useful since they contain code we can call to do work. But classes come in two types—static classes, and object classes—and PowerShell uses a different syntax for each.

Static Classes

Static classes are singleton classes that have no constructors. That is, there is ever only one of each of them, and it already exists. Typically, these classes will contain both methods and properties—just like object classes. However they will not have any constructors. An Enum is a special case of a static class.

A static class is accessed with the [ ] construct, and its methods and properties (or Enum members) are accessed with the :: operator.

For example, to get the path to the MyDocuments folder of the current user, you would access the GetFolderPath() method of the Environment class in the System namespace, as such:

[System.Environment]::GetFolderPath("MyDocuments")

Note that PowerShell will always prepend "System" if it's missing, so that you could have written:

[Environment]::GetFolderPath("MyDocuments")

It's important to point out that while we're passing a string to GetFolderPath() there is no method by that name that takes a string argument, yet the code works! That's because GetFolderPath() accepts an Enum—which is really just a mapping of numbers to strings—so the string we pass is automatically cast into the appropriate Enum value. We could have used:

[Environment]::GetFolderPath([Environment.SpecialFolder]::MyDocuments)

but "MyDocuments" is much easier. Remember: you can use a string where an Enum parameter is expected.

Object Classes

Whereas static classes already exist and only one is allowed, an object class must be created using one of its constructor methods, and multiple objects (instances) can be created. Object classes use a completely different syntax than static classes. You must use New-Object to create an instance:

$mesg = New-Object System.Net.Mail.MailMessage

Note that MailMessage is both the class name and the class constructor name. Constructors always have the same name as their class.

If the constructor has arguments, then you pass them as an array to New-Object:

$mesg = New-Object System.Net.Mail.MailMessage -ArgumentList ("Joe User <joe@example.com>", "Jane User <jane@example.com>")

Note that this is an example of using the comma (,) operator to create an array. Cool, huh?

Once you have created an object, you can access its properties and methods with the dot (.) operator:

$smtp.UseDefaultCredentials = 1 # set property
$smtp.Send($mesg)               # invoke method

Strings

All strings in PowerShell are instances of the System.String class. As such, you can invoke any method or property of the String class on a literal string or a $variable string reference. For example:

"red".CompareTo($s)
$s.CompareTo("red")

Note that these two forms are not equivalent because the CompareTo() returns -1, 0, or 1 to indicate the magnitude of the comparison. Also, they are distinctly different from:

"red" -eq $s

which returns the ''value'' of either $true or $false.

Console Input, Output, and Redirection

You can use Read-Host to read from the console (with optional prompt) and Write-Host to write to the console. Type help Read-Host or help Write-Host at a PowerShell prompt for more information.

Keep in mind that Write-Host is often superfluous, as the value of any expression that isn't assigned to a variable or redirected to a file will be written to the console. For example, the following are equivalent:

$home
Write-Host $home

Similarly, writing the value of an expression to a file can be as simple as these two examples show:

$home > "home.txt" 
Get-ChildItem -path variable: > "D:\variables.txt"

To capture only the error output to a file, use the redirection operator 2>:

Get-ChildItem -path variable: 2> "D:\variables.txt"

To capture both the standard output and error output to a file, append 2>&1 to the end of the command:

Get-ChildItem -path variable: > "D:\variables.txt" 2>&1

Cmdlets

While accessing the .Net Frameworks gives us unprecedented power, you might be wondering if some of the more common tasks that access multiple framework classes haven't already been pre-built as useful tools, and if so how do you access them? Well, they have; and PowerShell delivers them by the boatload as Cmdlets.

To get a list of all available Cmdlets, type get-help -category cmdlet | more at a PowerShell prompt.

To get help on a particular Cmdlet, such as curl, type get-help curl | more at a PowerShell prompt.

Example Programs

Sending Mail

The following PowerShell script will send the contents of the file .gitconfig in the sender's home directory via e-mail:

$mesg = New-Object System.Net.Mail.MailMessage
$mesg.From = "PowerShell 4 <ps4@example.com>"
$mesg.To.Add("Joe User <joe@example.com>")
$mesg.Subject = "Your .gitconfig file contents"
$mesg.Body = (get-content $home\.gitconfig) -join "`n"
$mesg.BodyTransferEncoding = "SevenBit"

$smtp = New-Object System.Net.Mail.SmtpClient
$smtp.Port = 587 # submission port
$smtp.Host = "mail.example.com"
$smtp.EnableSsl = 1
$smtp.UseDefaultCredentials = 1
$smtp.Send($mesg)

Process sub-directories

The following PowerShell script will get a sorted list of all directories (projects) under the main git container, run "git gc" on each project (garbage collection), mail the results (log) to a user with the subject line containing the highest exit code seen, and write the log to a file in the user's "My Documents" directory.

# Weekly git Cleanup

try {
    Push-Location

    $maxrc = 0
    $log = ""
    $from = "Windows Task Scheduler <wts@example.com>"
    $to = "Jane User <jane@example.com>"

    $LogFile = -join ([Environment]::GetFolderPath("MyDocuments").TrimEnd(@("\")), "\git_gc.log")
    Remove-Item -Path $LogFile > $null 2>&1

    $list = New-Object System.Collections.ArrayList
    foreach ( $n in @([Io.Directory]::GetDirectories("D:\Git")) ) {
        $n = $list.Add($n.TrimEnd(@("\")))
    }
    $list.Sort()

    foreach ( $n in $list ) {
        Set-Location -Path $n
        $out = Invoke-Expression "cmd.exe /c git gc --quiet 2>&1"
        $rc = $LastExitCode
        $log = -join ($log, "`n", $n.Substring($n.LastIndexOf("\")+1), "`n")
        if ( $out ) {
            $log = -join ($log, $out, "`n")
        }
        $log = -join ($log, "rc = ", $rc, "`n")
        if ( $rc -gt $maxrc ) {
            $maxrc = $rc
        }
    }

    $smtp = New-Object System.Net.Mail.SmtpClient
    $smtp.Port = 587 # submission port
    $smtp.Host = "mail.example.com"
    $smtp.EnableSsl = 1
    $smtp.UseDefaultCredentials = 1
    $smtp.Send($from, $to, -join ("Weekly git cleanup (", $maxrc, ")"), $log )
}

catch { }

finally {
   $log > $LogFile
   Pop-Location
}