PowerShell in Depth, Second Edition (2015)
Part 3. PowerShell scripting and automation
The chapters in this part of the book have a single goal: repeatability. Using PowerShell’s scripting language, along with associated technologies like workflow, you can begin to create reusable tools that automate key tasks and processes in your environment.
Chapter 19. PowerShell’s scripting language
This chapter covers
· Logical conditions
· Loops
· Branching
· Code formatting
Although we firmly maintain that PowerShell isn’t a scripting language, it does—like many command-line shells—contain a scripting language. This language can prove useful when you want to automate complex, multipart processes that may require different actions to be taken for different scenarios. PowerShell’s language is definitely simple, consisting of less than two dozen commands usually referred to as keywords, but it’s more than adequate for most jobs.
Note
The list of keywords, plus their definitions and references to more information, can be found in the help file about_Language_Keywords.
The ability to use cmdlets, functions, and .NET negates the pure language deficiencies. The language’s syntax is loosely modeled on C#, which lends it a strong resemblance to other C-based languages such as PHP, C++, and Java.
19.1. Defining conditions
As with most languages, the heart of PowerShell’s scripting capabilities is based on conditions. You define a condition that keeps a loop repeating or define a condition that causes your script to execute some particular command that it’d otherwise ignore.
The conditions you specify will usually be contained within parentheses and will often use PowerShell’s various comparison operators to achieve either a True or False result. Referred to as Boolean values, these True/False results tell PowerShell’s various scripting language elements whether to take some action, keep executing a command, and so on.
For example, all of the following conditions—expressions is the proper term for them—evaluate to True, which in PowerShell is represented with the built-in variable $True:
(5 –eq 5)
((5 –gt 0) –or (10 –lt 100))
('this' –like '*hi*')
All of the following conditions evaluate to False, which PowerShell represents by using $False:
(5 –lt 1)
((5 –gt 0) –and (10 –gt 100))
('this' –notlike '*hi*')
As you dive into PowerShell’s scripting language, the punctuation becomes pretty important. For now, keep in mind that we’re not using parentheses in a new way here. All of these expressions evaluate to either True or False; PowerShell executes the expressions first because they’re contained in parentheses (remember your algebra lessons!). The resulting True or False is then utilized by the scripting keyword to loop, branch, and so on.
19.2. Loops: For, Do, While, Until
PowerShell supports several types of loop. All of these loop constructs share a single purpose: to continue executing one or more commands until some expression is either True or False. These loops differ only in how they achieve that purpose. The choice of loop is important, because some loops will execute once even if the conditions are immediately met and others will skip the loop entirely. You may want one or the other behavior to occur depending on your processing scenario.
19.2.1. The For loop
The For loop is the simplest and is designed to execute one or more commands a specific number of times. Here’s the basic syntax:
For (starting-state ; repeat-condition ; iteration-action) {
Do something
}
The For loop is unusual in that it doesn’t take a single expression within parentheses, which is what—as you’ll see—the other loops use. Instead, its parentheses contain three distinct components, separated by semicolons:
· starting-state, which is where you usually define a variable and assign it a starting value.
· repeat-condition, which is where you usually compare that variable to a given value. As long as your comparison is True, the loop will repeat again.
· iteration-action, which is some action that PowerShell takes after executing the loop each time. This is where you usually increment or decrement the variable. The change doesn’t have to be an increment of 1. The counter can be incremented by 2, –3, or whatever you need.
This loop is a lot easier to see in an example:
For ($i=0; $i –lt 10; $i++) {
Write-Host $i
}
This code will output the numbers 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9. After outputting 9, the iteration-action will be executed again, incrementing $i to 10. At that point, $i is no longer less than 10, so the repeat-condition will return False and the loop won’t execute an 11th time.
In the ISE or in scripts it’s possible—and legal in PowerShell—to put the conditions on multiple lines separated by carriage returns:
For (
$i=0
$i –lt 10
$i++) {
Write-Host $i
}
The results are identical to those obtained in the previous example. The drawback to this approach is that the code can’t be copied out and run in the console as easily; it’s also not as easy to read and understand. Using the form with semicolons is a better approach when you’re working interactively, which is also the way you’re most likely to find it in shared scripts and published material.
The starting value of the counter can be set outside the condition expression:
$i=0
For ($i; $i –lt 10; $i++) {
Write-Host $i
}
But be careful, because if the condition is met, the loop won’t execute. This next example doesn’t produce any output because the condition is tested at the top of the loop and immediately produces a value of False:
$i=10
For ($i; $i –lt 10; $i++) {
Write-Host $i
}
Note that it’s perfectly legal to change the value of the variable—such as $i in our example—within the loop. If doing so results in the repeat-condition being False at the end of the loop iteration, then the loop won’t execute again.
Loop counters
Have you ever wondered why $i is used for the counter in examples of For loops?
It goes all the way back to mainframe days and the FORTRAN language. FORTRAN was one of the first computer languages that was readable—that wasn’t binary or assembler-level code. Any variable starting with the letters I–N was by default treated as an integer. Simple counters were defined as I, J, K, and so on.
The concept stuck across the industry, and we now use $i as our counter in PowerShell.
19.2.2. The other loops
The other three keywords you’ll see used in simple conditional loops are Do, While, and Until. These are designed to execute until some condition is True or False, although unlike For, these constructs don’t take care of changing that condition for you. The following listing shows the three forms you can use.
Listing 19.1. Loops
Note
Although it’s legal to use the While keyword at the beginning of the loop or, in combination with Do, at the end of the loop, it isn’t legal to use Until that way. The Until keyword can be used only at the end of a loop, with Do at the beginning of that loop.
For each of these loops, we’ve set a starting condition of 0 for the variable $i. Within each loop, we increment $i by 1. The loops’ expressions determine how many times each will execute. Some important details:
· In the Do-While loop, the commands in the loop will always execute at least one time because the condition isn’t checked until the end. They’ll continue executing as long as the While expression—that is, $i is less than 10—results in True.
· With the Do-Until loop, the contents of the loop will also execute at least one time. It isn’t until the end of the loop that the Until keyword is checked, causing the loop to repeat if the expression is False.
· The While loop is different in that the contents of the loop aren’t guaranteed to execute at all. They’ll execute only if the While expression is True to begin with (we made sure it would be by setting $i = 0 in advance), and the loop will continue to execute while that expression results in True.
Try changing the starting values of $i to see how the loop structures respond.
Tip
The key lesson with loops is that if the condition is tested at the end of the loop, that loop will execute at least once. If the test is at the start of the loop, there are no guarantees that the loop will execute at all. The appropriate structure to use depends on the condition you’re checking.
19.3. ForEach
This scripting construct has the exact same purpose as the ForEach-Object cmdlet. That cmdlet has an alias, ForEach, which is easy to confuse with the ForEach scripting construct because—well, because they have the exact same name. PowerShell looks at the context of the command line to figure out if you mean “foreach” as an alias or as a scripting keyword. Here’s an example of both the cmdlet and the scripting construct being used to do the exact same thing:
Get-Service –name B* | ForEach { $_.Pause() }
$services = Get-Service –name B*
ForEach ($service in $services) {
$service.Pause()
}
Here are the details of how this scripting construct works:
· You provide two variables, separated by the keyword In, within parentheses. The second variable is expected to hold one or more objects that you populate in advance. The first variable is one that you make up; it must contain one of those objects at a time. If you come from a VBScript background, this sort of thing should look familiar.
· It’s common practice to give the second variable a plural name and the first one the singular version of that plural name. This convention isn’t required, though. You could’ve put ForEach ($fred in $rockville), and provided that $rockville contained some objects, PowerShell would’ve been happy. But stick to giving variables meaningful names and you’ll be much happier.
· PowerShell automatically takes one object at a time from the second variable and puts it into the first. Then, within the construct, you use the first variable to refer to that one object and do something with it. In the example, you executed its Pause method. Don’t use $_ within the scripting construct as you do with the ForEach-Object cmdlet.
Someday, you might be unsure whether you should be using the cmdlet or the scripting construct. In theory, the cmdlet approach—piping something to ForEach-Object—could use less overall memory in some situations. But we’ve also seen the cmdlet approach run considerably slower with some large sets of objects. Your mileage may vary. If the processing in the loop is complex, especially if multiple pipelines are involved (using $_ or $psitem to indicate the object on the pipeline), it may be less confusing to use the keyword to differentiate the pipelines.
You also might consider your overall need, especially if you wanted to pipe to another cmdlet or function:
PS C:\> foreach ($service in $services) {
>> $service | select Name,DisplayName,Status
>>} | Sort Status
>>
An empty pipe element is not allowed.
At line:3 char:4
+ } | <<<< Sort Status
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : EmptyPipeElement
PowerShell complains because there’s nothing to come out the other side for the ForEach construct. But something like this example will work:
PS C:\> $services | foreach {
>> $_ | select Name,DisplayName,Status
>> } | Sort Status
>>
Name DisplayName Status
---- ----------- ------
Browser Computer Browser Stopped
BDESVC BitLocker Drive Encrypt... Stopped
bthserv Bluetooth Support Service Running
BFE Base Filtering Engine Running
BITS Background Intelligent ... Running
Another common consideration is whether you’ll need to reference the collection of objects just once or multiple times—if you need access to the collection several times, it may be more efficient to use the script construct so that you don’t incur the overhead of refreshing the data. The last point to make is that many of the scripts you’ll find on the web are conversions from VBScript, which had to use this approach of iterating over a collection of objects. It’s always worthwhile to stop and think for a second in order to determine the best approach to solve your problem.
What we’ll commit to is this: Don’t use either approach if you don’t have to. For example, our service-pausing example would be much better written this way:
Get-Service –name B* | Suspend-Service
And our sort example is better written like this:
Get-Service b* | Sort Status | select Name,DisplayName,Status
If you don’t have to manually enumerate through objects, don’t. Using ForEach—either the cmdlet/alias or the scripting construct—is sometimes an indication that you’re doing something you shouldn’t be doing. That’s not true in all cases—but it’s worth considering each usage to make sure you’re not doing something that PowerShell would be willing to do for you. But don’t get hung up on this issue; many cmdlets don’t accept pipeline input on the parameters you may need to use, so ForEach becomes a necessity. It’s sometimes more important to get the job done than to track down the ultimate right way of doing something.
This is also a good time for us to remind you of the power of parentheses. We sort of fibbed when we said that the ForEach scripting construct requires two variables. Technically, it needs only one—the first one. The second position must contain a collection of objects, which can be either in a variable—as in our examples so far—or from the result of a parenthetical expression, like this:
foreach ($service in (Get-Service –name B*)) {
$service.pause()
}
This version is a bit harder to read, perhaps, but it’s also totally legal and a means of eliminating the need to store the results of the Get-Service command in a variable before working with those results. The inner parentheses (remember your algebra) are evaluated first and produce a collection of service objects that are iterated over.
19.4. Break and Continue
You can use two special keywords—Break and Continue—within the contents of a loop, that is, within the curly bracketed section of a loop:
· Break will exit the loop immediately. It exits only one level. For example, if you have Loop A nested within Loop B and the contents of Loop B include Break, then Loop B will exit but Loop A will continue executing. Break has no effect on the If construct, but it does have an effect on the Switch construct, as you’ll see shortly.
· Continue will immediately skip to the end of the current loop and decide (based on how the loop was written) whether to execute another iteration.
These keywords are useful for prematurely exiting a loop, either if you’ve determined that there’s no need to continue executing or to skip over a bunch of code and get to the next iteration.
For example, suppose you’ve retrieved a bunch of Active Directory user objects and put them into the variable $users. You want to check their Department property, and if it’s “Accounting,” you want to disable the account (hah, that’ll teach those bean counters). Once you’ve done that to five accounts, though, you don’t want to do any more (no sense in upsetting too many folks). You also don’t want to disable any account that has a “Title” attribute of “CFO” (you’re mean, not stupid). Here’s one way to do that (again assuming that $users is already populated):
$disabled = 0
ForEach ($user in $users) {
If ($user.department –eq 'accounting') {
If ($user.title –eq 'cfo' {
Continue
}
$user | Disable-ADAccount
$disabled++
}
If ($disabled –eq 5) {
Break
}
}
Granted, this might not be the most efficient way to code this particular task, but it’s a good example of how Continue and Break work. If the current user’s title is “CFO,” Continue will skip to the end of the ForEach loop—bypassing your disabling and everything else and continuing on with the next potential victim. Once $disabled equals 5, you’ll just abort, exiting the ForEach loop entirely.
19.5. If . . . ElseIf . . . Else
Enough about loops for a moment. Let’s look now at a logical construct, which is used to evaluate one or more expressions and execute some commands if those expressions are True. Here’s the most complex form of the construct:
If ($this –eq $that) {
Get-Service
} ElseIf ($those –gt $these) {
Get-Process
} ElseIf ($him –eq $her) {
Get-EventLog –LogName Security
} Else {
Restart-Computer –ComputerName localhost
}
Here are the important details:
· The If keyword is mandatory, as is the curly bracketed section following it. The other sections—the two ElseIf sections and the Else section—are optional.
· You can have as many ElseIf sections as you want.
· You can have an Else section regardless of whether there are any ElseIf sections.
· Only the first section whose expression results in True will execute. For example, if $this equals $that, Get-Service will run and the entire remainder of the construct will be disregarded.
You can put anything you like inside the parentheses, provided that it evaluates to True or False. That said, you won’t always need to make a comparison. For example, suppose you have a variable, $mood, that contains an object. That object has a property, Good, that contains either Trueor False. Given that supposition, the following code is completely legal:
If ($mood.Good) {
Get-Service
} else {
Restart-Computer –computername localhost
}
The entire contents of the parentheses will evaluate to True or False because that’s what the Good property will contain—there’s no need to do this:
If ($mood.Good –eq $True) {
Get-Service
} else {
Restart-Computer –computername localhost
}
That example is completely legal, though, and PowerShell will treat these two exactly the same. It’s unusual to see an explicit comparison to $True or $False—don’t expect to see experienced folks doing that. This holds true for the conditions in Do-While, Do-Until, and While loops as well.
A common use for this technique is testing connectivity to a remote machine. This code
$computer = "server02"
Test-Connection -ComputerName $computer -Count 1 -Quiet
will return True or False. So the test becomes
$computer = "server02"
If (Test-Connection -ComputerName $computer -Count 1 –Quiet){
#do something to that machine
}
A final point to make on If is that the comparison expression can be made up of multiple components to produce a more complex test. At this point, you need to bring in the logical operators –and and –or to help you with this processing:
$procs = Get-Process | select Name, Handles, WS
foreach ($proc in $procs){
if (($proc.Handles -gt 200) -and ($proc.WS -gt 50000000)) {
$proc
}
}
"`n`n`n"
foreach ($proc in $procs){
if (($proc.Handles -gt 200) -or ($proc.WS -gt 50000000)) {
$proc
}
}
The variable $procs is used to hold the process information. You use foreach to iterate over the collection of processes twice. Constructing the code in this way ensures that you have the same data each time you perform some processing.
In the first foreach, you test to determine whether the Handles property is greater than 200 and that the WS property is greater than 50,000,000 (these are arbitrary values). If both of these conditions are True, you output the details. In the second case, you output the details if eitherHandles is greater than 200 or WS is greater than 50,000,000—only one of them has to be True. The `n`n`n throws three blank lines to break the display and show the two sets of output. Notice that we put parentheses around each condition. You don’t have to do this, but we recommend you do so because it makes the code easier to read and, more importantly, easier to debug if you have problems.
19.6. Switch
The Switch construct is a bit like a really big list of If...ElseIf statements that all compare one specific thing to a bunch of possible values. For example, let’s say you have a printer object of some kind stored in the variable $printer. That object has a Status property, which contains a number. You want to translate that number to an English equivalent, perhaps for display to an end user. You want the translated status to go into the variable $status. One way to do that might be like this (we’re totally making up what these numbers mean—this is just an example):
If ($printer.status –eq 0) {
$status = "OK"
} elseif ($printer.status –eq 1) {
$status = "Out of Paper"
} elseif ($printer.status –eq 2) {
$status = "Out of Ink"
} elseif ($printer.status –eq 3) {
$status = "Input tray jammed"
} elseif ($printer.status –eq 4) {
$status = "Output tray jammed"
} elseif ($printer.status –eq 5) {
$status = "Cover open"
} elseif ($printer.status –eq 6) {
$status = "Printer Offline"
} elseif ($printer.status –eq 7) {
$status = "Printer on Fire!!!"
} else {
$status = "Unknown"
}
There’s nothing wrong whatsoever with that approach, but the Switch construct offers a more visually efficient way (that also involves less typing and is easier to maintain) of doing the same thing:
Switch ($printer.status) {
0 {$status = "OK"}
1 {$status = "Out of paper"}
2 {$status = "Out of Ink"}
3 {$status = "Input tray jammed"}
4 {$status = "Output tray jammed"}
5 {$status = "Cover open"}
6 {$status = "Printer Offline"}
7 {$status = "Printer on Fire!!"}
Default {$status = "Unknown"}
The details:
· The Switch statement’s parentheses don’t contain a comparison; instead, they contain the thing you want to examine. In this case it’s the $printer.status property.
· The following statements each contain one possible value, along with a block that says what to do if the thing being examined matches that value.
· The Default block is executed if none of the other statements match. We recommend that you always have a Default block, even if it exists only to catch errors.
· Unlike If...ElseIf...Else, every single possible value statement that matches will execute.
That last point might seem redundant. After all, $printer.status isn’t going to be both 0 and 7 at the same time, right? Well, this is where some of Switch’s interesting variations come into play. Let’s suppose instead that you’re looking at a server name, contained in $servername. You want to display a different message based on what kind of server it is. If the server name contains “DC,” for example, then it’s a domain controller; if it contains “LAS,” then it’s located in Las Vegas. Obviously, multiple conditions could be true, and you might want to do a wildcard match:
$message = ""
Switch –wildcard ($servername) {
"DC*" {
$message += "Domain Controller"
}
"FS*" {
$message += "File Server"
}
"*LAS" {
$message += " Las Vegas"
}
"*LAX" {
$message += " Los Angeles"
}
}
Because of the way you’ve positioned the wildcard character (*), you’re expecting the server role (DC or FS) at the beginning of the name and the location (LAS or LAX) at the end. So if $servername contains “DC01LAS”, then $message will contain “Domain Controller Las Vegas”.
There might be times when multiple matches are possible but you don’t want them all to execute. In that case, just add the Break keyword to the appropriate spot. Once you do so, the Switch construct will exit entirely. For example, suppose you don’t care about a domain controller’s location but you do care about a file server. You might make the following modification:
$message = ""
Switch –wildcard ($servername) {
"DC*" {
$message += "Domain Controller"
break
}
"FS*" {
$message += "File Server"
}
"*LAS" {
$message += " Las Vegas"
}
"*LAX" {
$message += " Los Angeles"
}
}
$message might contain “Domain Controller” or “File Server Las Vegas”, but it’d never contain “Domain Controller Las Vegas”.
The conditions we’ve seen so far in the Switch construct have simple, direct comparisons. It’s also possible to build some logic into the comparison, for instance:
foreach ($proc in (Get-Process)){
switch ($proc.Handles){
{$_ -gt 1500}{Write-Host "$($proc.Name) has very high handle count"
break }
{$_ -gt 1200}{Write-Host "$($proc.Name) has high handle count"
break }
{$_ -gt 800}{Write-Host "$($proc.Name) has medium-high handle count"
break }
{$_ -gt 500}{Write-Host "$($proc.Name) has medium handle count"
break }
{$_ -gt 250}{Write-Host "$($proc.Name) has low handle count"
break }
{$_ -lt 100}{Write-Host "$($proc.Name) has very low handle count"
break }
}
}
The code iterates through the set of processes and determines whether the Handles property meets one of the criteria. The logic for the test is a script block in curly braces {}. The important point is that $_ is used to represent the value of $proc.Handles in the tests. Using break is imperative in these scenarios, because a process that has more than 1,500 handles also has more than 1,200. This way, you only get the highest result as you test from high to low, and you use break to jump out of the testing once you have a match. It was a deliberate decision to not put adefault block on this example.
Switch has four total options:
· -Wildcard, which you’ve seen and which tells Switch that its possible matches include wildcard characters.
· -CaseSensitive, which forces string comparisons to be case sensitive (usually they’re case insensitive).
· -RegEx, which tells Switch that the potential matches are regular expressions, and whatever’s being compared will be evaluated against those regular expressions as if you were using the –match operator.
· -Exact, which disables –Wildcard and –RegEx and forces string matches to be exact. Ignored for nonstring comparisons.
19.7. Mastering the punctuation
With those scripting constructs out of the way, let’s take a brief tangent to examine the specifics of script construct formatting.
First, PowerShell is a little picky about the placement of the parenthetical bit in relation to the scripting keyword: You should always leave a space between the keyword and the opening parentheses, and it’s considered a good practice to leave only one space. You should also leave a space or a carriage return between the closing parenthesis and the opening curly bracket. You don’t have to leave a space after the opening parenthesis or before the closing parenthesis—but it doesn’t hurt to do so, and the extra whitespace can make your scripts a bit easier on the eyes.
PowerShell isn’t at all picky about the location of the curly brackets. As long as nothing but whitespace comes between the closing parentheses and the opening curly bracket, you’re all set. But all the cool kids will make fun of you if you don’t take the time to make your script easy to read. There are two generally accepted ways of formatting the curly brackets:
For ($i=0; $i –lt 10; $i++) {
Write-Host $i
}
For ($i=0; $i –lt 10; $i++)
{
Write-Host $i
}
The only difference is where the opening curly bracket is placed. In the first example, it’s just after the closing parenthesis (with a not required but good-looking space in between them). In the second example, it’s on a line by itself. In both cases, the contents of the curly brackets are indented—usually with a tab character or four spaces (we sometimes use fewer in this book just to help longer lines fit on the page). The closing curly bracket in both examples is indented to the same level as the scripting keyword, which in this case is For. In the second example, the opening curly bracket is indented to that level as well.
This may seem like incredible nitpicking, but it isn’t. Consider the following example:
For ($i=0;$i –lt 9;$i++){
Write-Host $i
For ($x=10;$i –gt 5;$i--){
If ($x –eq $i) { break }}}}
Try to run that example and you’ll get an error message. The error doesn’t specifically come from the formatting—PowerShell doesn’t care about how visually appealing a script is—but the error is a lot harder to spot because of the formatting. Let’s look at the same example, this time with proper formatting:
For ( $i=0; $i –lt 9; $i++){
Write-Host $i
For ( $x=10; $i –gt 5; $i--){
If ($x –eq $i) {
break
}
}
}}
Wait, what’s that extra closing brace (}) doing at the end? That’s the error! It’s a lot easier to see now, because the opening and closing braces are much more visually balanced. You can see that the last line starts with a closing brace and its indentation level of zero matches it with the scripting keyword—For—that’s indented to that same level. There’s already a closing brace for the second For, and for the If, and those are clearly identifiable by their indentation levels. The extra brace is just that—extra—and removing it will make the code run without error. The version of ISE in PowerShell v3 and v4 will attempt to show you any mismatches between opening and closing braces, parentheses, and brackets. The visual clue is a little hard to spot; it’s a wiggly underline where the editor thinks the problem lies. Other editors such as PowerShell Plus or PowerGUI have similar mechanisms to aid correct code construction.
Tip
If your code gets complex with lots of opening and closing braces, put a comment by the closing brace to remind you which construct it’s closing, like this:
#your code...
} #close ForEach computer
We’re not going to go so far as to suggest that bad things will happen if you don’t format your code neatly. But sometimes strange things happen, and you certainly don’t want to be the one to blame, do you? What’s more, if you ever run into trouble and end up having to ask someone else for help, you’ll find the PowerShell community a lot more willing to help if they can make sense of your code—and proper formatting helps.
Tip
Many commercial script editors, including some free ones as well as paid offerings, provide features that can reformat your code. Such features usually indent the contents of curly brackets properly, and this is a great way to beautify some ugly chunk of code that you’ve pulled off of the internet. We also recommend that if you’re developing PowerShell scripts and modules as part of a team, you develop a standardized formatting style to ensure uniformity.
19.8. Summary
PowerShell’s scripting language is a sort of glue. By itself, it isn’t very useful. Combined with some useful materials, such as commands that do actual work, this glue becomes very powerful. With it, a handful of commands can be turned into complex, automated processes that make decisions, repeat specific actions, and more. It’s almost impossible to write a script of any complexity without needing some of these constructs. It may not mean you’re programming, exactly, but it’s as close as most folks get within PowerShell.