We will now dive into the specifics of the PowerShell syntax.
A semicolon is typically a mandatory statement terminator in many programming and scripting languages. PowerShell considers both a newline and semicolon as statement terminators, although using the newline is more common. The caveat for using the newline is that the previous line must be a complete statement before it gets executed.
The backtick (`
) is a peculiar character in PowerShell, and it has a double meaning. You can typically find this character on your keyboard above the left Tab key, and it is on the same key as the tilde ~
symbol.
The backtick is the escape character in PowerShell. Some of the common characters that need to be escaped are as follows:
Escaped character |
Description |
---|---|
|
Newline |
|
Carriage return |
|
Single quote |
|
Double quote |
|
Null |
PowerShell also uses the backtick (`
) as a line continuation character.
Note
A technical reviewer, Chrissy LeMaire, points out an interesting fact about the backtick. It is used as a line continuation character because it actually escapes the newline. Interesting tidbit!
You may find yourself writing a long chain of commands and may want to put different parts of the command into different lines to make the code more readable. If you do so, you need to make sure to put a backtick at the end of each line you are continuing; otherwise, PowerShell treats the newline as a statement terminator. You also need to make sure that there are not any extra spaces after the backtick:
Invoke-Sqlcmd ` -Query $query ` -ServerInstance $instanceName ` -Database $dbName
Variables are placeholders for values. Variables in PowerShell start with a dollar ($
) sign:
$a=10
By default, variables are loosely and dynamically typed, which means that the variable assumes the data type based on the value of the content:
$a=10 $a.GetType() #Int32 $a="Hello" $a.GetType() #String $a=Get-Date $a.GetType() #DateTime
Note how the data type changes based on the value we assign to the variable. You can, however, create strongly typed variables:
[int]$a=10 $a.GetType() #Int32
When we have a strongly typed variable, we can no longer just haphazardly assign it any value. If we do, we will get an error:
$a="Hello" <# Error Cannot convert value "Hello" to type "System.Int32". Error: "Input string was not in a correct format." At line:3char:1 + $a = "Hello" + ~~~~~~~~~~~~ + CategoryInfo : MetadataError: (:) [], ArgumentTransformationMetadataException + FullyQualifiedErrorId : RuntimeException #>
We have also mentioned in the previous section that PowerShell is object oriented. Variables in PowerShell are automatically created as objects. Depending on the data type, variables are packaged with their own attributes and methods. To explore what properties and methods are available with a data type, use the Get-Member
cmdlet:
There may be times when you need to create a string variable that will contain multiple lines of code. You should create these as here-string
.
A here-string
data type is a string that often contains large blocks of text. It starts with @"
and must end with a line that contains only "@
. For here-string
terminating characters, make sure this is placed in its own line and there are no other characters and no spaces before or after it:
$query=@" INSERT INTO SampleXML (FileName,XMLStuff,FileExtension) VALUES('$xmlfile','$xml','$fileextension') "@
When working with strings, you need to remember that using a double quote evaluates enclosed variables, that is, variables are replaced with their values. For example, run the following code snippet:
$today= Get-Date Write-Host "Today is $today" #result #Today is 07/04/2015 19:48:24
This behavior may sometimes cause issues especially if you need to use multiple variables in continuation; as in the following case, where we want to combine $name
, underscore (_
), $ts
, and .txt
to create a timestamped filename:
$name = "belle"
$ts = Get-Date -Format yyyy-MMM-dd
$filename = ""$name_$ts.txt""
$filename = "$($name)_$($ts).txt"
Write-Host $filename
This will give an incorrect result because it will look for $name_
and $ts
, but since it cannot find $name_
, the final filename that we get is 2015-Jul-04.txt
and not belle_2015-Jul-04.txt
.
To resolve this issue, we can use any of the following to ensure proper interpolation:
$filename = "$($name)_$($ts).txt" Write-Host $filename $filename = "${name}_${ts}.txt" Write-Host $filename $filename = "{0}_{1}.txt" -f $name, $ts Write-Host $filename
A single quote, on the other hand, preserves the actual variable name and does not interpolate the value:
$today= Get-Date Write-Host 'Today is $today' #result #Today is $today
You can also store actual commands in a string. However, this is treated as a string unless you prepend it with an ampersand (&
), which is PowerShell's invoke or call operator:
$cmd="Get-Process" $cmd#just displays Get-Process, treated as string &$cmd#actually executes Get-Process
The operators used in PowerShell may not be readily familiar to you even if you have already done some programming before. This is because the operators in PowerShell do not use the common operator symbols.
PowerShell |
Traditional |
Meaning |
---|---|---|
|
|
Equal to |
|
|
Not equal to |
|
Match using regex; searches anywhere in a string | |
|
Check whether the match exists in the collection or array. | |
|
Wildcard match asterisk (*) for zero or more characters question mark (?) for any single character | |
|
Case-sensitive wildcard match | |
|
|
Negation |
|
|
Less than |
|
|
Less than or equal to |
|
|
Greater than |
|
|
Greater than or equal to |
|
|
Logical and |
|
|
Logical or |
|
|
Bitwise or |
|
|
Bitwise and |
|
|
Exclusive or |
Note that many operators perform case-insensitive string comparisons by default. If you want to do case-sensitive matching, prepend with c
. For example, –ceq
, -clike
, and -cnotlike
.
Often, we will need to display or log messages as our scripts execute. PowerShell provides a few cmdlets to help us accomplish this:
Get-Command -Name "*Write*" -CommandTypeCmdlet
This will give a list of our Write-
related cmdlets:
Cmdlet |
Description |
---|---|
|
This displays a debug message on the console. It is typically used with |
|
This displays a nonterminating error message on the console. |
|
This writes a message to the Windows event log. |
|
This displays a string message on the host. |
|
This writes an object to the pipeline. |
|
This displays a progress bar. |
|
This displays a verbose message on the console. It is typically used with |
|
This displays a warning message on the console. |
Although some of these cmdlets seem similar, there are some fundamental differences. For example, Write-Host
and Write-Output
seem to display the same messages on the screen. The Write-Host
cmdlet, however, simply displays a string, but Write-Output
writes objects that have properties that can be queried and can eventually be used in the pipeline.
We use Write-Verbose
a fair bit in the recipes in this book. Write-Verbose
does not automatically display messages on the host. It relies on the $VerbosePreference
setting. By default, $VerbosePreference
is set to SilentlyContinue
, but it can also be set to Continue
, which allows us to display messages used with Write-Verbose
on the screen:
$VerbosePreference = ""Continue""
$folderName = ""C:\BLOB Files\""
#using PowerShell V2 style Where-Object syntax
Get-ChildItem $folderName |
Where-Object {$_.PSIsContainer -eq $false} |
ForEach-Object {
$blobFile = $_
Write-Verbose ""Importing file $($blobFile.FullName)...""
}
$VerbosePreference = "SilentlyContinue"
This is an elegant way of turning all messages on or off, without needing to change the script.
Comments are important in any programming or scripting language. Comments are often used to document logic and sometimes a chain of changes to the script.
Single-line comments start with a hash sign (#
):
#this is a single line comment
Block comments start with <#
and end with #>
:
<# this is a block comment #>
PowerShell also supports what's called Comment-Based Help. This feature allows you to put a special comment block at the start of your script, or in the beginning of your function, that allows the script or function to be looked up using Get-Help
. A sample of this type of comment block is as follows:
<# .SYNOPSIS Creates a full database backup .DESCRIPTION Creates a full database backup using specified instance name and database name This will place the backup file to the default backup directory of the instance .PARAMETER instanceName instance where database to be backed up resides .PARAMETER databaseName database to be backed up .EXAMPLE PS C:\PowerShell> .\Backup-Database.ps1 -instanceName"QUERYWORKS\SQL01" -databaseName"pubs" .EXAMPLE PS C:\PowerShell> .\Backup-Database.ps1 -instance "QUERYWORKS\SQL01" -database "pubs" .NOTES To get help: Get-Help .\Backup-Database.ps1 .LINK http://msdn.microsoft.com/en-us/library/hh245198.aspx #>
To look up the help, you can simply type a Get-Help
cmdlet followed by the script filename or function name:
PS>Get-Help .\Backup-Database.ps1
PowerShell also has some special variables. These special variables do not need to be created ahead of time; they are already available. Some of the special variables are as follows:
Special variable |
Description |
---|---|
|
Current pipeline object |
|
Arguments passed to a function |
|
An array that stores all errors |
|
User's home directory |
|
Host information |
|
Regex matches |
|
Path to a profile, if available |
| |
|
PowerShell Scripting Environment object |
|
Process ID (PID) of a PowerShell process |
|
Present Working Directory |
|
Boolean true |
|
Boolean false |
|
Null value |
A few more special characters worth noting down are explained in the following table, which you may find in many scripts you work with:
Character |
Name |
Description |
---|---|---|
|
Pipe |
Command chaining; the output from one command to input to another |
|
Backtick |
An escape or continuation character |
|
At sign |
An array |
|
Hash sign |
A comment |
|
Square brackets |
For indexes and strongly typed variables |
|
Parentheses |
For array members; for calling functions |
|
Ampersand |
A call operator |
|
Start or asterisk |
A wildcard character |
|
Percent |
An alias for |
|
Question mark |
An alias for |
|
Plus |
Addition; a string concatenation operator |
PowerShell supports conditional statements using if-else
or switch
statements. These two constructs allow you to check for a condition, and consequently execute different blocks of code if the condition is met or not. Note that PowerShell treats null/empty as false.
Let's take a look at an example of an if
-else
block:
$answer=Read-Host"Which course are you taking?" if ($answer-eq"COMP 4677") { Write-Host"That's SQL Server Administration" } elseif ($answer-eq"COMP 4678") { Write-Host"That's SQL Server Development" } else { Write-Host"That's another course" }
Note
Note that the elseif
and else
blocks are optional. They don't need to be defined if you do not have a separate code to execute if the condition is not met.
An equivalent switch
block can be written for the preceding code:
$answer=Read-Host"Which course are you taking?" switch ($answer) { "COMP 4677" { Write-Host"That's SQL Server Administration" } "COMP 4678" { Write-Host"That's SQL Server Development" } default { Write-Host"That's another course" } }
Note that these two constructs can be functionally equivalent for simple comparisons. The choice to use one over the other hinges on preference and readability. If there are many choices, the switch can definitely make the code more readable.
Regular expressions, more commonly referred to as regex, specify a string pattern to match. Regex can be extremely powerful and is often used when dealing with massive amounts of text. The area of bioinformatics, for example, tends to rely heavily on regular expressions for gene pattern matching.
Regex can also be quite confusing especially for beginners. It has its own set of patterns and wildcards, and it is up to you to put these together to ensure that you are matching what you need to be matched.
Note
Refer to the Testing regular expressions recipe in Chapter 11, Helpful PowerShell Snippets.
Arrays are collections of items. Often, we find ourselves in situations where we need to store a group of items, either for further processing or for exporting:
#ways to create an array $myArray= @() #empty $myArray=1,2,3,4,5 $myArray= @(1,2,3,4,5) #array of processes consuming >30% CPU $myArray= (Get-Process|Where-Object CPU -gt30 )
Arrays can either be a fixed size or not. Fixed-size arrays are instantiated with a fixed number of items. Some of the typical methods such as Add
or Remove
cannot be used with fixed-size arrays:
$myArray= @() $myArray+= 1,2,3,4,5 $myArray+=6,7,8 $myArray.Add(9) #error because array is fixed size
Removing an item from a fixed array is a little bit tricky. Although arrays have the Remove
and RemoveAt
methods to remove entries based on a value and index, respectively, we cannot use these with fixed-size arrays. To remove an item from a fixed-size array, we will need to reassign the new set of values to the array variable:
#remove 6 $myArray=$myArray-ne6 #remove 7 $myArray=$myArray-ne7
To create a dynamic-sized array, you will need to declare the array as an array list and add items using the Add
method. This also supports removing items from the list using the Remove
method:
$myArray=New-ObjectSystem.Collections.ArrayList $myArray.Add(1) $myArray.Add(2) $myArray.Add(3) $myArray.Remove(2)
We can use indexes to retrieve information from the array:
#retrieve first item $myArray[0] #retrieve first 3 items $myArray[0..2]
We can also retrieve information based on some comparison or condition:
#retrieving anything > 3 $myArray-gt3
A hash table is also a collection. This is different from an array, however, because hashes are collections of key-value pairs. Hashes are also called associative arrays or hash tables:
#simple hash $simplehash= @{ "BCIT"="BC Institute of Technology" "CST"="Computer Systems Technology" "CIT"="Computer Information Technology" } $simplehash.Count #hash containing process IDs and names $hash= @{} Get-Process| Foreach-Object { $hash.Add($_.Id,$_.Name) } $hash.GetType()
To access items in a hash, we can refer to the hash table variable and retrieve information based on the stored key:
$simplehash["BCIT"] $simplehash.BCIT
A loop allows you to repeatedly execute block(s) of code based on some condition. There are different types of loop support in PowerShell. For all intents and purposes, you may use all of these types, but it's always useful to be aware of what's available and doable.
This is a while
loop, where the condition is tested at the beginning of the block:
$i=1 while($i-le5) { #code block $i $i++ }
There is also support for the do while
loop, where the condition is tested at the bottom of the block:
$i=1 do { #code block $i $i++ } while($i-le5)
The for
loop allows you to loop a specified number of times, based on a counter that you create in the for
header:
for($i=1; $i-le5; $i++) { $i }
There is yet another type of loop, a foreach
loop. This loop is a little bit different because it works with arrays or collections. It allows a block of code to be executed for each item in a collection:
$backupcmds=Get-Command -Name "*Backup*" -CommandTypeCmdlet foreach($backupcmdin$backupcmds) { $backupcmd|Get-Member }
If you're a developer, this code might look very familiar to you. In PowerShell, however, you can use pipelining to make your code more concise:
Get-Command -Name "*Backup*" -CommandTypeCmdlet| Foreach-Object { #recall that $_ is the current pipeline object $_|Get-Member }
Use the foreach
loop operator when the collection of objects is small enough so that it can be loaded into memory. For example, an array of 20 string values.
Use the ForEach-Object
cmdlet when you want to pass only one object at a time through the pipeline, minimizing the memory usage. For example, a directory containing 10,000 files.
When developing functions or scripts, it is important to think beyond just the functionality you are trying to achieve. You also want to handle exceptions, or errors, when they happen. We all want our scripts to gracefully exit if something goes wrong, rather than displaying some rather intimidating or cryptic error messages.
Developers in the house will be familiar with the concept of try
/catch
/finally
. This is a construct that allows us to put the code we want to run in one block (try
), exception handling code in another (catch
), and any must-execute housekeeping blocks in a final block (finally
):
$dividend =20 $divisor =0 try { $result = $dividend/$divisor } catch { Write-Host ("======"*20) Write-Host"Exception $error[0]" Write-Host ("======"*20) } finally { Write-Host"Housekeeping block" Write-Host"Must execute by hook or by crook" }