19 You call this scripting?

So far, you could’ve accomplished everything in this book by using PowerShell’s command-line interface. You haven’t had to write a single script. That’s a big deal for us, because we see a lot of administrators initially shying away from scripting, perceiving it (rightly) as a kind of programming, and feeling (correctly) that learning it can sometimes take more time than it’s worth. Hopefully, you’ve seen how much you can accomplish in PowerShell without having to become a programmer.

But at this point, you may also be starting to feel that constantly retyping the same commands is going to become pretty tedious. You’re right, so in this chapter we’re going to dive into PowerShell scripting—but we’re still not going to be programming. Instead, we’re going to focus on scripts as little more than a way of saving our fingers from unnecessary retyping.

19.1 Not programming, more like batch files

Most system administrators have, at one point or another, created a command-line batch file (which usually has a .bat, .cmd, or .sh filename extension). These are nothing more than simple text files (that you can edit with a text editor, such as vi) containing a list of commands to be executed in a specific order. Technically, you call those commands a script, because like a Hollywood script, they tell the performer (your computer) exactly what to do and say, and in what order to do and say it. But batch files rarely look like programming, in part because the cmd.exe shell has a limited language that doesn’t permit incredibly complicated scripts.

PowerShell scripts work similarly to Bash or sh scripts. List the commands that you want run, and the shell will execute those commands in the order specified. You can create a script by copying a command from the host window and pasting it into a text editor. We expect you’ll be happier writing scripts with the VS Code PowerShell extension, or with a third-party editor of your choice.

VS Code, in fact, makes scripting practically indistinguishable from using the shell interactively. When using VS Code, you type the command or commands you want to run, and then click the Run button in the toolbar to execute those commands. Click Save, and you’ve created a script without having to copy and paste anything at all.

Heads UP Just a reminder that this chapter is very Windows focused as far as the examples are concerned.

19.2 Making commands repeatable

The idea behind PowerShell scripts is, first and foremost, to make it easier to run a given command over and over, without having to manually retype it every time. That being the case, we need to come up with a command that you’ll want to run over and over again, and use that as an example throughout this chapter. We want to make this decently complex, so we’ll start with something from CIM and add in some filtering, sorting, and other stuff.

At this point, we’re going to switch to using VS Code instead of the normal console window, because VS Code will make it easier for us to migrate our command into a script. Frankly, VS Code makes it easier to type complex commands, because you get a full-screen editor instead of working on a single line within the console host. Here’s our command:

Get-CimInstance -class Win32_LogicalDisk -computername localhost `
-filter "drivetype=3" | Sort-Object -property DeviceID |
Format-Table -property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB)';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

Tip Remember, you can use name instead of label, and either can be abbreviated to a single character, n or l. But it’s easy for a lowercase L to look like the number 1, so be careful!

Figure 19.1 shows how we enter this into VS Code. Notice that we select the two-pane layout by using the toolbar button on the far right of the layout choices. Also notice that we format our command so that each physical line ends in either a pipe character or a comma. By doing so, we’re forcing the shell to recognize these multiple lines as a single, one-line command. You could do the same thing in the console host, but this formatting is especially effective because it makes the command a lot easier to read. Also notice that we use full cmdlet names and parameter names and that we specify every parameter name rather than using positional parameters. All of that will make our script easier to read and follow, either for someone else or in the future when we might have forgotten our original intent.

Figure 19.1 Entering and running a command in VS Code using the two-pane layout

We run the command by clicking the Run toolbar icon (you could also press F5) to test it, and our output shows that it’s working perfectly. Here’s a neat trick in VS Code: you can highlight a portion of your command and press F8 to run just the highlighted portion. Because we’ve formatted the command so that there’s one distinct command per physical line, that makes it easy for us to test our command bit by bit. We could highlight and run the first line independently. If we were satisfied with the output, we could highlight the first and second lines and run them. If that worked as expected, we could run the whole command.

At this point, we can save the command—and we can start calling it a script now. We’ll save it as Get-DiskInventory.ps1. We like giving scripts cmdlet-style verb-noun names. You can see how this script is starting to look and work a lot like a cmdlet, so it makes sense to give it a cmdlet-style name.

19.3 Parameterizing commands

When you think about running a command over and over, you might realize that some portion of the command will have to change from time to time. For example, suppose you want to give Get-DiskInventory.ps1 to some of your colleagues, who might be less experienced in using PowerShell. It’s a complex, hard-to-type command, and they might appreciate having it bundled into an easier-to-run script. But, as written, the script runs only against the local computer. You can certainly imagine that some of your colleagues might want to get a disk inventory from one or more remote computers instead.

One option is to have them open the script and change the -computername parameter’s value. But it’s entirely possible that they wouldn’t be comfortable doing so, and there’s a chance they’ll change something else and break the script entirely. It would be better to provide a formal way for them to pass in a different computer name (or a set of names). At this stage, you need to identify the things that might need to change when the command is run, and replace those things with variables.

We’ll set the computer name variable to a static value for now so that we can still test the script. Here’s our revised script.

Listing 19.1 Get-DiskInventory.ps1, with a parameterized command (Windows only)

$computername = 'localhost                      
Get-CimInstance -class Win32_LogicalDisk `      
 -computername  $computername `                 
 -filter "drivetype=3" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB)';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

Sets a new variable

Breaks the line with a backtick

Uses a variable

We do three things here, two of which are functional and one of which is purely cosmetic:

  • We add a variable, $computername, and set it equal to localhost. We’ve noticed that most PowerShell commands that accept a computer name use the parameter name -computername, and we want to duplicate that convention, which is why we chose the variable name that we did.

  • We replace the value for the -computername parameter with our variable. Right now, the script should run exactly the same as it did before (and we tested to make sure it does), because we put localhost into the $computername variable.

  • We add a backtick after the -computername parameter and its value. This escapes, or takes away, the special meaning of the carriage return at the end of the line. That tells PowerShell that the next physical line is part of this same command. You don’t need to do that when the line ends in a pipe character or a comma, but in order to fit the code within this book, we needed to break the line before the pipe character. This will work only if the backtick character is the last thing on the line!

Listing 19.2 Get-FilePath.ps1, with a parameterized command (cross-platform)

$filePath = '/usr/bin/'                           
get-childitem -path $filepath | get-filehash |    
Sort-Object hash | Select-Object -first 10

Sets a new variable

Breaks the line after a pipe and uses a variable

We do three things here, two of which are functional and one of which is purely cosmetic:

  • We add a variable, $filepath, and set it equal to /usr/bin. We’ve noticed that the Get-ChildItem command accepts a path parameter name -path, and we want to duplicate that convention, which is why we chose the variable name that we did.

  • We replace the value for the -path parameter with our variable. Right now, the script should run exactly the same as it did before (and we tested to make sure it does), because we left the path parameter blank, and it runs in the current working directory.

  • If you need to break up your command into multiple lines, the best way to do this is to put a line break after the pipe symbol. PowerShell knows that if there is nothing next to the pipe symbol, then the next line of code will be a continuation of the previous line. This can be very helpful if you have a very long pipeline.

Get-Process | Sort-Object        
 
Get-Process |                    
Sort-Object
 
Get-Process `                    
 | Sort-Object                  Starting the line with a pipeline symbol

Shows our original command

Breaks the command at a pipe

Breaks the command with a backtick

TIP After you make any changes, run your script to validate it is still working. We always do that after making any kind of change to ensure we haven’t introduced a random typo or other error.

19.4 Creating a parameterized script

Now that we’ve identified the elements of the script that might change from time to time, we need to provide a way for someone else to specify new values for those elements. We need to take that hardcoded $computername variable and turn it into an input parameter. PowerShell makes this easy.

Listing 19.3 Get-DiskInventory.ps1, with an input parameter

param (
  $computername = 'localhost'      
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=3" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

Uses a param block

All we did was add a Param() block around our variable declaration. This defines $computername as a parameter and specifies that localhost is the default value to be used if the script is run without a computer name being specified. You don’t have to provide a default value, but we like to do so when there’s a reasonable value that we can think of.

All parameters declared in this fashion are both named and positional, meaning that we can now run the script from the command line in any of these ways:

PS C:> .Get-DiskInventory.ps1 SRV-02
PS C:> .Get-DiskInventory.ps1 -computername SRV02
PS C:> .Get-DiskInventory.ps1 -comp SRV02

In the first instance, we use the parameter positionally, providing a value but not the parameter name. In the second and third instances, we specify the parameter name, but in the third instance we abbreviate that name in keeping with PowerShell’s normal rules for parameter name abbreviation. Note that in all three cases, we have to specify a path (., which is the current folder) to the script, because the shell won’t automatically search the current directory to find the script.

You can specify as many parameters as you need to by separating them with commas. For example, suppose that we want to also parameterize the filter criteria. Right now, it’s retrieving only logical disks of type 3, which represents fixed disks. We could change that to a parameter, as in the following listing.

Listing 19.4 Get-DiskInventory.ps1, with an additional parameter

param (
  $computername = 'localhost',
  $drivetype = 3                                         
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=$drivetype" |                        
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

Specifies an additional parameter

Uses a parameter

Notice that we take advantage of PowerShell’s ability to replace variables with their values inside double quotation marks (you learned about that trick in chapter 16). We can run this script in any of the three original ways, although we could also omit either parameter if we wanted to use the default value for it. Here are some permutations:

PS C:> .Get-DiskInventory.ps1 SRV1 3
PS C:> .Get-DiskInventory.ps1 -ComputerName SRV1 -drive 3
PS C:> .Get-DiskInventory.ps1 SRV1
PS C:> .Get-DiskInventory.ps1 -drive 3

In the first instance, we specify both parameters positionally, in the order in which they’re declared within the Param() block. In the second case, we specify abbreviated parameter names for both. The third time, we omit -drivetype entirely, using the default value of 3. In the last instance, we leave off -computername, using the default value of localhost.

19.5 Documenting your script

Only a truly mean person would create a useful script and not tell anyone how to use it. Fortunately, PowerShell makes it easy to add help into your script, using comments. You’re welcome to add typical programming-style comments to your scripts, but if you’re using full cmdlet and parameter names, sometimes your script’s operation will be obvious. By using a special comment syntax, however, you can provide help that mimics PowerShell’s own help files. This listing shows what we’ve added to our script.

Listing 19.5 Adding help to Get-DiskInventory.ps1

<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses CIM to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -computername SRV02 -drivetype 3
#>
param (
  $computername = 'localhost',
  $drivetype = 3
)
Get-CimInstance -class Win32_LogicalDisk -computername $computername `
 -filter "drivetype=$drivetype" |
 Sort-Object -property DeviceID |
 Format-Table -property DeviceID,
     @{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
     @{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
     @{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}

PowerShell ignores anything on a line that follows a # symbol, meaning that # designates a line as a comment. We can also use the <# #> block comment syntax instead, because we have several lines of comments and want to avoid starting each line with a separate # character.

Now we can drop to the normal console host and ask for help by running help .Get-DiskInventory.ps1 (again, you have to provide a path because this is a script and not a built-in cmdlet). Figure 19.2 shows the results, which proves that PowerShell is reading those comments and creating a standard help display.

Figure 19.2 Viewing the help by using the normal help command

We can even run help .Get-DiskInventory -full to get full help, including parameter information in our example.

These special comments are called comment-based help. There are several keywords in addition to .DESCRIPTION, .SYNOPSIS, and the others we’ve used. For a full list, run help about_comment_based_help in PowerShell.

19.6 One script, one pipeline

We normally tell folks that anything in a script will run exactly as if you manually typed it into the shell, or if you copied the script to the clipboard and pasted it into the shell. That’s not entirely true, though. Consider this simple script:

Get-Process
Get-UpTime

Just two commands. But what happens if you were to type those commands into the shell manually, pressing Enter after each?

Try it Now Run these commands on your own to see the results. They create fairly long output that won’t fit well within this book or even in a screenshot.

When you run the commands individually, you’re creating a new pipeline for each command. At the end of each pipeline, PowerShell looks to see what needs to be formatted and creates the tables that you undoubtedly saw. The key here is that each command runs in a separate pipeline. Figure 19.3 illustrates this: two completely separate commands, two individual pipelines, two formatting processes, and two different-looking sets of results.

Figure 19.3 Two commands, two pipelines, and two sets of output in a single console window

You may think we’re crazy for taking so much time to explain something that probably seems obvious, but it’s important. Here’s what happens when you run those two commands individually:

  1. You run Get-Process.

  2. The command places Process objects into the pipeline.

  3. The pipeline ends in Out-Default, which picks up the objects.

  4. Out-Default passes the objects to Out-Host, which calls on the formatting system to produce text output (you learned about this in chapter 11).

  5. The text output appears on the screen.

  6. You run Get-UpTime.

  7. The command places TimeSpan objects into the pipeline.

  8. The pipeline ends in Out-Default, which picks up the objects.

  9. Out-Default passes the objects to Out-Host, which calls on the formatting system to produce text output.

  10. The text output appears on the screen.

So you’re now looking at a screen that contains the results from two commands. We want you to put those two commands into a script file. Name it Test.ps1 or something simple. Before you run the script, though, copy those two commands onto the clipboard. In your editor, you can highlight both lines of text and press Ctrl-C to get them onto the clipboard.

With those commands on the clipboard, go to the PowerShell console host and press Enter. That pastes the commands from the clipboard into the shell. They should execute exactly the same way, because the carriage returns also get pasted. Once again, you’re running two distinct commands in two separate pipelines.

Now go back to your editor and run the script. Different results, right? Why is that?

In PowerShell, every command executes within a single pipeline, and that includes scripts. Within a script, any command that produces pipeline output will be writing to a single pipeline: the one that the script itself is running in. Take a look at figure 19.4.

Figure 19.4 All commands within a script run within that script’s single pipeline.

We’ll try to explain what happens:

  1. The script runs Get-Process.

  2. The command places Process objects into the pipeline.

  3. The script runs Get-UpTime.

  4. The command places TimeSpan objects into the pipeline.

  5. The pipeline ends in Out-Default, which picks up both kinds of objects.

  6. Out-Default passes the objects to Out-Host, which calls on the formatting system to produce text output.

  7. Because the Process objects are first, the shell’s formatting system selects a format appropriate to processes. That’s why they look normal. But then the shell runs into the TimeSpan objects. It can’t produce a whole new table at this point, so it winds up producing a list.

  8. The text output appears on the screen.

This different output occurs because the script writes two kinds of objects to a single pipeline. This is the important difference between putting commands into a script and running them manually: within a script, you have only one pipeline to work with. Normally, your scripts should strive to output only one kind of object so that PowerShell can produce sensible text output.

19.7 A quick look at scope

The last topic we need to visit is scope. Scopes are a form of container for certain types of PowerShell elements, primarily aliases, variables, and functions.

The shell itself is the top-level scope and is called the global scope. When you run a script, a new scope is created around that script, and it’s called the script scope. The script scope is a subsidiary—or a child—of the global scope. Functions also get their own private scope, which we will cover later in the book.

Figure 19.5 illustrates these scope relationships, with the global scope containing its children, and those containing their own children, and so forth.

Figure 19.5 Global, script, and function (private) scopes

A scope lasts only as long as needed to execute whatever is in the scope. The global scope exists only while PowerShell is running, a script scope exists only while that script is running, and so forth. When whatever it is stops running, the scope vanishes, taking everything inside with it. PowerShell has specific—and sometimes confusing—rules for scoped elements, such as aliases, variables, and functions, but the main rule is this: If you try to access a scoped element, PowerShell sees whether it exists within the current scope. If it doesn’t, PowerShell sees whether it exists in the current scope’s parent. It continues going up the relationship tree until it gets to the global scope.

TIP To get the proper results, it’s important that you follow these steps carefully and precisely.

Let’s see this in action. Follow these steps:

  1. Close any PowerShell or PowerShell editor windows you may have open so that you can start from scratch.

  2. Open a new PowerShell window and a new VS Code window.

  3. In VS Code, create a script that contains one line: Write $x.

  4. Save the script as C:Scope.ps1.

  5. In the regular PowerShell window, run the script with C:Scope.ps1. You shouldn’t see any output. When the script runs, a new scope is created for it. The $x variable doesn’t exist in that scope, so PowerShell goes to the parent scope—the global scope—to see whether $x exists there. It doesn’t exist there, either, so PowerShell decides that $x is empty and writes that (meaning, nothing) as the output.

  6. In the normal PowerShell window, run $x = 4. Then run C:Scope.ps1 again. This time, you should see 4 as the output. The variable $x still isn’t defined in the script scope, but PowerShell is able to find it in the global scope, so the script uses that value.

  7. In VS Code, add $x = 10 to the top of the script (before the existing Write command), and save the script.

  8. In the normal PowerShell window, run C:Scope.ps1 again. This time, you’ll see 10 as the output. That’s because $x is defined within the script scope, and the shell doesn’t need to look in the global scope. Now run $x in the shell. You’ll see 4, proving that the value of $x within the script scope doesn’t affect the value of $x within the global scope.

One important concept here is that when a scope defines a variable, alias, or function, that scope loses access to any variables, aliases, or functions having the same name in a parent scope. The locally defined element will always be the one PowerShell uses. For example, if you put New-Alias Dir Get-Service into a script, then within that script the alias Dir will run Get-Service instead of the usual Get-ChildItem. (In reality, the shell probably won’t let you do that, because it protects the built-in aliases from being redefined.) By defining the alias within the script’s scope, you prevent the shell from going to the parent scope and finding the normal, default Dir. Of course, the script’s redefinition of Dir will last only for the execution of that script, and the default Dir defined in the global scope will remain unaffected.

It’s easy to let this scope stuff confuse you. You can avoid confusion by never relying on anything that’s in any scope other than the current one. So before you try to access a variable within a script, make sure you’ve already assigned it a value within that same scope. Parameters in a Param() block are one way to do that, and there are many other ways to put values and objects into a variable.

19.8 Lab

Note For this lab, you need any computer running Windows 10 or Server 2019 with PowerShell v7 or later.

The following command is for you to add to a script. You should first identify any elements that should be parameterized, such as the computer name. Your final script should define the parameter, and you should create comment-based help within the script. Run your script to test it, and use the Help command to make sure your comment-based help works properly. Don’t forget to read the help files referenced within this chapter for more information. Here’s the command:

Get-CimInstance -classname Win32_LogicalDisk -filter "drivetype=3" |
Where { ($_.FreeSpace / $_.Size) -lt .1 } |
Select -Property DeviceID,FreeSpace,Size

Here’s a hint: At least two pieces of information need to be parameterized. This command is intended to list all drives that have less than a given amount of free disk space. Obviously, you won’t always want to target localhost (we’re using the PowerShell equivalent of %computername% in our example), and you might not want 10% (that is, .1) to be your free-space threshold. You could also choose to parameterize the drive type (which is 3 here), but for this lab, leave that hardcoded with the value 3.

19.9 Lab answer

<#
.Synopsis
Get drives based on percentage free space
.Description
This command will get all local drives that have less than the specified 
 percentage of free space available.
.Parameter Computername
The name of the computer to check. The default is localhost.
.Parameter MinimumPercentFree
The minimum percent free diskspace. This is the threshold. The default value 
 is 10. Enter a number between 1 and 100.
.Example
PS C:> Get-DiskSize -minimum 20
Find all disks on the local computer with less than 20% free space.
.Example
PS C:> Get-DiskSize -Computername SRV02 -minimum 25
Find all local disks on SRV02 with less than 25% free space.
#>
Param (
    $Computername = 'localhost',
    $MinimumPercentFree = 10
)
#Convert minimum percent free
$minpercent = $MinimumPercentFree / 100
Get-CimInstance -classname Win32_LogicalDisk –computername $computername `
    -filter "drivetype=3" |
Where { $_.FreeSpace / $_.Size –lt $minpercent } |
Select –Property DeviceID, FreeSpace, Size
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.61.179