Free Essay

What Is Powershell

In:

Submitted By navaruban
Words 186534
Pages 747
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

1

MEAP Edition Manning Early Access Program

Copyright © 2010 Manning Publications For more information on this and other Manning titles go to www.manning.com

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

2

Table of Contents

Part 1 LEARNING POWERSHELL 1

1 Welcome to PowerShell 2 Foundations of PowerShell 3 Working with types 4 Operators and expressions 5 Advanced operators and variables 6 Flow control in scripts 7 PowerShell Functions 8 Advanced functions and scripts 9 Using and Authoring Modules 10 Module Manifests and Metadata 11 Metaprogramming with ScriptBlocks and Dynamic Code 12 Remoting and Background Jobs 13 Remoting: Configuring Applications and Services 14 Errors and exceptions 15 The PowerShell ISE and Debugger

Part 2 USING POWERSHELL
16 Working with paths , text, and XML 17 Getting fancy—.NET and WinForms 18 Windows objects: COM, WMI and WSMan 19 Security, security, security

appendix A Comparing PowerShell to other languages appendix B Admin examples appendix C The PowerShell grammar appendix D Additional PowerShell Topic

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

3

1
Welcome to PowerShell

Space is big. Really big! You just won’t believe how vastly hugely mindbogglingly big it is. I mean you may think it’s a long way down the road to the chemist, but that’s just peanuts compared to space! Don’t Panic. —Douglas Adams, The Hitchhiker’s Guide to the Galaxy

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

4 Welcome to Windows PowerShell, the new command and scripting language from Microsoft. We begin this chapter with two quotes from The Hitchhiker’s Guide to the Galaxy. What do they have to do with a new scripting language? In essence, where a program solves a particular problem or problems, a programming language can solve any problem, at least in theory. That’s the “big, really big” part. The “Don’t Panic” bit is, well—don’t panic. While PowerShell is new and different, it has been designed to leverage what you already know, making it easier to learn. It’s also designed to allow you to learn it a bit at a time. Starting at the beginning, here’s the traditional “Hello world” program in PowerShell. "Hello world." As you can see, no panic needed. But “Hello world” by itself is not really very interesting. Here’s something a bit more complicated: dir $env:windir\*.log | select-string -List error | format-table path,linenumber –auto Although this is more complex, you can probably still figure out what it does. It searches all the log files in the Windows directory, looking for the string “error”, then prints the full name of the matching file and the matching line number. “Useful, but not very special,” you might think, because you can easily do this using cmd.exe on Windows or bash on UNIX. So what about the “big, really big” thing? Well, how about this example? ([xml](new-object net.webclient).DownloadString( "http://blogs.msdn.com/powershell/rss.aspx" )).rss.channel.item | format-table title,link Now we’re getting somewhere. This script downloads the RSS feed from the PowerShell team weblog, and then displays the title and a link for each blog entry.

NOTE
RSS stands for Really Simple Syndication. This is a mechanism that allows programs to download web logs automatically so they can be read more conveniently than in the browser.

By the way, you weren’t really expected to figure this example out yet. If you did, you can move to the head of the class! Finally, one last example: [void][reflection.assembly]::LoadWithPartialName( "System.Windows.Forms") $form = new-object Windows.Forms.Form $form.Text = "My First Form" $button = new-object Windows.Forms.Button $button.text="Push Me!" $button.Dock="fill" $button.add_click({$form.close()}) $form.controls.add($button) $form.Add_Shown({$form.Activate()}) $form.ShowDialog() This script uses the Windows Forms library (WinForms) to build a graphical user interface (GUI) that has a single button displaying the text “Push Me”. The window this script creates is shown in figure 1.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

5

Figure 1,1 When you run the code from the example, this window will be displayed. If you don’t see it, it may be hidden behind another window.

When you click the button, it closes the form and exits the script. With this you go from

"Hello world" to a GUI application in less than two pages.
Now let’s come back down to earth for minute. The intent of chapter 1 is to set the stage for understanding PowerShell—what it is, what it isn’t and, almost as important—why we made the decisions we made in designing the PowerShell language. Chapter 1 covers the goals of the project along with some of the major issues we faced in trying to achieve those goals. By the end of the chapter you should have a solid base from which to start learning and using PowerShell to solve real-world problems. Of course all theory and no practice is boring, so the chapter concludes with a number of small examples to give you a feel for PowerShell. But first, a philosophical digression: while under development, the codename for this project was Monad. The name Monad comes from The Monadology by Gottfried Wilhelm Leibniz, one of the inventors of calculus. Here is how Leibniz defined the Monad: “The Monad, of which we shall here speak, is nothing but a simple substance, which enters into compounds. By ‘simple’ is meant ‘without parts.’” From The Monadology by Gottfried Wilhelm Leibniz (translated by Robert Latta) In The Monadology, Leibniz described a world of irreducible components from which all things could be composed. This captures the spirit of the project: to create a toolkit of simple pieces that you compose to create complex solutions.

1.1 What is PowerShell?
What is PowerShell and why was it created? As we said, PowerShell is the new commandline/scripting environment from Microsoft. The overall goal for this project was to provide the best shell scripting environment possible for Microsoft Windows. This statement has two parts, and they are equally important, as the goal was not just to produce a good generic shell ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

6 environment, but rather to produce one designed specifically for the Windows environment. While drawing heavily from existing command-line shell and scripting languages, the PowerShell language and runtime were designed from scratch to be an optimal environment for the modern Windows operating system. Historically, the Windows command line has been weak. This is mainly the result of the early focus in Microsoft on computing for the average user, who is neither particularly technical nor particularly interested in computers. Most of the development effort for Windows was put into improving the graphical environment for the non-technical user, rather than creating an environment for the computer professional. Although this was certainly an enormously successful commercial strategy for Microsoft, it has left some segments of the community under-served. In the next couple of sections, we’ll go over some of the other environmental forces that led to the creation of PowerShell. By environmental forces, we mean the various business pressures and practical requirements that needed to be satisfied. But first we’ll refine our definitions of shell and scripting.

1.1.1 Shells, command-lines, and scripting languages
In the previous section, we called PowerShell a command-line shell. You may be asking, what is a shell? And how is that different from a command interpreter? What about scripting languages? If you can script in a shell language, doesn’t that make it a scripting language? In answering these questions, let’s start with shells. Defining what a shell is can be a bit tricky, especially at Microsoft, since pretty much everything at Microsoft has something called a shell. Windows Explorer is a shell. Visual Studio has a component called the shell. Heck - even the Xbox has something they call a shell. Historically, the term shell describes the piece of software that sits over an operating system’s core functionality. This core functionality is known as the operating system kernel (shell... kernel... get it?). A shell is the piece of software that lets you access the functionality provided by the operating system. Windows Explorer is properly called a shell because it lets you access the functionality of a Windows system. For our purposes, though, we’re more interested in the traditional text-based environment where the user types a command and receives a response. In other words, a shell is a command-line interpreter. The two terms can be used for the most part interchangeably. SCRIPTING LANGUAGES VS. SHELLS If this is the case, then what is scripting and why are scripting languages not shells? To some extent, there isn’t really a difference. Many scripting languages have a mode in which they take commands from the user and then execute those commands to return results. This mode of operation is called a Read-Evaluate-Print loop or REP loop. Not all scripting languages have these interactive loops, but many do. In what way is a scripting language with a REP loop not a shell? The difference is mainly in the user experience. A proper command-line shell is also a proper user interface. As such, a command line has to provide a number of features to make the user’s experience pleasant and customizable. The features that improve the user’s experience include aliases (shortcuts for hard-to-type commands), wildcard matching so you don’t have to type out full names, and the ability to start other programs without having to do ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

7 anything special such as calling a function to start the program. Finally, command-line shells provide mechanisms for examining, editing, and re-executing previously typed commands. These mechanisms are called command history. If scripting languages can be shells, can shells be scripting languages? The answer is, emphatically, yes. With each generation, the UNIX shell languages have grown more and more powerful. It’s entirely possible to write substantial applications in a modern shell language, such as bash or zsh. Scripting languages characteristically have an advantage over shell languages, in that they provide mechanisms to help you develop larger scripts by letting you break a script into components or modules. Scripting languages typically provide more sophisticated features for debugging your scripts. Next, scripting language runtimes are implemented in a way that makes their code execution more efficient, so that scripts written in these languages execute more quickly than they would in the corresponding shell script runtime. Finally, scripting language syntax is oriented more toward writing an application than toward interactively issuing commands. In the end, there really is no hard and fast distinction between a shell language and a scripting language. Some of the features that make a good scripting language result in a poor shell user experience. Conversely, some of the features that make for a good interactive shell experience can interfere with scripting. Since PowerShell’s goal is to be both a good scripting language and a good interactive shell, balancing the trade-offs between user-experience and scripting authoring was one of the major language design challenges.

1.1.2 Why a new shell? Why now?
In the early part of this decade, Microsoft commissioned a study to identify areas where it could improve its offerings in the server space. Server management, and particularly command-line management of Windows systems, was called out as a critical area for improvement. While some might say that this is like discovering that water is wet, the important point is that people cared about the problem. When comparing the command-line manageability of a Windows system to a UNIX system, Windows was found to be limited, and this was a genuine pain point with customers. There are a number of reasons for the historically weak Windows command line. First, as mentioned previously, limited effort had been put into improving the command line. Since the average desktop user doesn’t care about the command line, it wasn’t considered important. Secondly, when writing graphical user interfaces, you need to access whatever you’re managing through programmer-style interfaces called Application Programmer Interfaces (APIs). APIs are almost universally binary (especially on Windows), and binary interfaces are not command-line friendly. MANAGING WINDOWS THROUGH OBJECTS Another factor that drove the need for a new shell model is that, as Windows acquired more and more subsystems and features, the number of issues you had to think about when managing a system increased dramatically. To deal with this increase in complexity, the manageable elements were factored into structured data objects. This collection of management objects is known internally at Microsoft as the Windows management surface. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

8

AUTHOR'S NOTE
Microsoft wasn't the only company that was running into issues due to increased complexity. Pretty much everyone in the industry was having this problem. This led to the Desktop Management Taskforce, an industry organization, creating a standard for management objects called the Common Information Model (CIM). Microsoft's implementation of this standard is called the Windows Management Instrumentation (WMI). Chapter 16 covers PowerShell's support for WMI.

While this factoring addressed overall complexity and worked well for graphical interfaces, it made it much harder to work with using a traditional text-based shell environment. Finally, as the power of the PC increased, Windows began to move off the desktop and into the corporate data center. In the corporate data center, you have a large number of servers to manage, and the graphical point-and-click management approach that worked well for one machine doesn’t scale. All these elements combined to make it clear that Microsoft could no longer ignore the command line.

1.1.3 The last mile problem
Why do we care about command-line management and automation? Because it helps to solve the Information Technology professional’s version of the last mile problem. The last mile problem is a classical problem that comes from the telecommunications industry. It goes like this: the telecom industry can effectively amortize its infrastructure costs across all its customers until it gets to the last mile where the service is finally run to an individual location. Installing service across this last mile can’t be amortized because it serves only a single location. Also, what’s involved in servicing any particular location can vary significantly. Servicing a rural farmhouse is different and significantly more expensive than running service to a house on a city street. In the Information Technology (IT) industry, the last mile problem is figuring out how to manage each IT installation effectively and economically. Even a small IT environment has a wide variety of equipment and applications. One approach to solving this is through consulting: IT vendors provide consultants who build custom last-mile solutions for each end-user. This, of course, has problems with recurring costs and scalability (it’s great for the vendor, though). A better solution for end-users is to empower them to solve their own last mile problems. We do this by providing a toolkit to enable end-users to build their own custom solutions. This toolkit can’t merely be the same tools used to build the overall infrastructure as the level of detail required is too great. Instead, you need a set of tools with a higher level of abstraction. This is where PowerShell comes in—its higher-level abstractions allow you to connect the various bits of your IT environment together more quickly and with less effort. Now that we understand the environmental forces that led to the creation of PowerShell, the need for command-line automation in a distributed object-based operating environment, let’s look at the form the solution took.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

9

1.2 Soul of a new language
The title of this section was adapted from Tracey Kidder’s Soul of a New Machine, one of the best non-technical technical books ever written. Kidder's book described how Data General developed a new 32-bit minicomputer, the Eclipse, in a single year. At that time, 32-bit minicomputers were not just new computers; they represented a whole new class of computers. It was a bold, ambitious project; many considered it crazy. Likewise, the PowerShell project is not just about creating a new shell language. We are developing a new class of object-based shell languages. And we’ve been told more than a few times that we were crazy. In this section, we’re going to cover some of the technological forces that shaped the development of PowerShell. A unique set of customer requirements in tandem with the arrival of the new .NET wave of tools at Microsoft led to this revolution in shell languages.

1.2.1 Learning from history
In section 1.1.2, we described why Microsoft needed to improve the command line. Now let’s talk about how we decided to improve it. In particular, let’s talk about why we created a new language. This is certainly one of the most common questions people ask about PowerShell (right after “What, are you guys nuts?”). People ask “why not just use one of the UNIX shells?” or “why not extend the existing Windows command line?” In practice, we did start with an existing shell language. We began with the shell grammar for the POSIX standard shell defined in IEEE Specification 1003.2. The POSIX shell is a mature command-line environment available on a huge variety of platforms including Microsoft Windows. It’s based on a subset of the UNIX Korn shell, which is itself a superset of the original Bourne shell. Starting with the POSIX shell gave us a well-specified and stable base. Then we had to consider how to accommodate the differences that properly supporting the Windows environment would entail. We wanted to have a shell optimized for the Windows environment in the same way that the UNIX shells are optimized for this UNIX environment. To begin with, traditional shells deal only with strings. Even numeric operations work by turning a string into a number, performing the operation, and then turning it back into a string. Given that a core goal for PowerShell was to preserve the structure of the Windows data types, we couldn’t simply use the POSIX shell language as is. This factor impacted the language design more than any other. Next, we wanted to support a more conventional scripting experience where, for example, expressions could be used as you would normally use them in a scripting language such as VBScript, Perl, or Python. With a more natural expression syntax, it would be easier to work with the Windows management objects. Now we just had to decide how to make those objects available to the shell.

1.2.2 Leveraging .NET
One of the biggest challenges in developing any computer language is deciding how to represent data in that language. For PowerShell, the key decision was to leverage the .NET object model. .NET is a unifying object representation that is being used across all of the groups at Microsoft. It is a hugely ambitious project that has taken years to come to fruition. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

10 By having this common data model, all the components in Windows can share and understand each other’s data. One of .NET’s most interesting features for PowerShell is that the .NET object model is selfdescribing. By this, we mean that the object itself contains the information that describes the object’s structure. This is important for an interactive environment, as you need to be able to look at an object and see what you can do with it. For example, if PowerShell receives an event object from the system event log, the user can simply inspect the object to see that it has a data stamp indicating when the event was generated. Traditional text-based shells facilitate inspection because everything is text. Text is great— what you see is what you get. Unfortunately, what you see is all you get. You can’t pull off many interesting tricks with text until you turn it into something else. For example, if you want to find out the total size of a set of files, you can get a directory listing, which looks something like the following: 02/26/2004 10:58 02/26/2004 10:59 02/26/2004 10:59 02/26/2004 11:00 PM PM PM PM 45,452 47,808 48,256 50,681 Q810833.log Q811493.log Q811630.log Q814033.log

You can see where the file size is in this text, but it isn’t useful as is. You have to extract the sequence of characters starting at column 32 (or is it 33?) until column 39, remove the comma, and then turn those characters into numbers. Even removing the comma might be tricky, because the thousands separator can change depending on the current cultural settings on the computer. In other words, it may not be a comma—it may be a period. Or it may not be present at all. It would be easier if you could just ask for the size of the files as a number in the first place. This is what .NET brings to PowerShell: self-describing data that can be easily inspected and manipulated without having to convert it to text until you really need to. Choosing to use the .NET object model also brings an additional benefit, in that it allows PowerShell to directly use the extensive libraries that are part of the .NET framework. This brings to PowerShell a breadth of coverage rarely found in a new language. Here’s a simple example that shows the kinds of things .NET brings to the environment. Say we want to find out what day of the week December 13, 1974 was. We can do this in PowerShell as follows: PS (1) > (get-date "December 13, 1974").DayOfWeek Friday In this example, the get-date command returns a .NET DateTime object, which has a property that will calculate the day of the week corresponding to that date. The PowerShell team didn’t need to create a library of date and time manipulation routines for PowerShell—we got them for free by building on top of .NET. And the same DateTime objects are used throughout the system. For example, say we want to find out which of two files is newer. In a text-based shell, we’d have to get a string that contains the time each file was updated, covert those strings into numbers somehow, and then compare them. In PowerShell, we can simply do: PS (6) > (dir data.txt).lastwritetime -gt >> (dir hello.ps1).lastwritetime >> True

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

11 We use the dir command to get the file information objects and then simply compare the last write time of each file. No string parsing is needed. Now that we’re all sold on the wonders of objects and .NET (I’m expecting my check from the Microsoft marketing folks real soon), let’s make sure we’re all talking about the same thing when we use words like object, member, method, and instance. The next section discusses the basics of object-oriented programming.

1.3 Brushing up on objects
Since the PowerShell environment uses objects in almost everything it does, it’s worth running through a quick refresher on objects and how they're used in programming. If you’re comfortable with this material, feel free to skip most of this section, but do please read the section on objects and PowerShell. There is no shortage of “learned debate” (also known as bitter feuding) about what objects are and what object-oriented programming is all about. For our purposes, we’ll use the simplest definition. An object is a unit that contains both data (properties) and the information on how to use that data (methods). Take a light bulb object as a simple example. This object would contain data describing its state—whether it’s off or on. It would also contain the mechanisms or methods needed to change the on/off state. Non-object-oriented approaches to programming typically put the data in one place, perhaps a table of numbers where 0 is off and 1 is on, and then provide a separate library of routines to change this state. To change its state, the programmer would have to tell these routines where the value representing a particular light bulb was. This could be complicated and is certainly error prone. With objects, because both the data and the methods are packaged as a whole, the user can work with objects in a more direct and therefore simpler manner, allowing many errors to be avoided.

1.3.1 Reviewing object-oriented programming
That’s the basics of what objects are. Now what is object-oriented programming? Well, it deals mainly with how you build objects. Where do the data elements come from? Where do the behaviors come from? Most object systems determine the object’s capabilities through its type. In the light bulb example, the type of the object is (surprise) LightBulb. The type of the object determines what properties the object has (for example, IsOn) and what methods it has (for example, TurnOn and TurnOff). Essentially, an object’s type is the blueprint or pattern for what an object looks like and how you use it. The type LightBulb would say that that it has one data element—IsOn—and two methods—TurnOn() and TurnOff(). Types are frequently further divided into two subsets:   Types that have an actual implementation of TurnOn() and TurnOff(). These are typically called classes. Types that only describe what the members of the type should look like but not how they work. These are called interfaces.

The pattern IsOn/TurnOn()/TurnOff() could be an interface implemented by a variety of classes such as LightBulb, KitchenSinkTap, or Television. All these objects have the same basic pattern for being turned on and off. From a programmer’s perspective, if they all ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

12 have the same interface (that is, the same mechanism for being turned on and off), once you know how to turn one of these objects on or off, you can use any type of object that has that interface. Types are typically arranged in hierarchies with the idea that they should reflect logical taxonomies of objects. This taxonomy is made up of classes and subclasses. An example taxonomy is shown in figure 1.2.

Mystery Novel Fiction Historical Short-Story Collection

Book

NonFiction

History

Figure 1.2 This diagram shows how books can be organized in a hierarchy of classes, just as object types can be organized into classes.

In this taxonomy, Book is the parent class, Fiction and Non-fiction are subclasses of Book, and so on. While taxonomies organize data effectively, designing a good taxonomy is hard. Frequently, the best arrangement is not immediately obvious. In figure 1.2, it might be better to organize by subject matter first, instead of the Novel/Short-story Collection grouping. In the scientific world, people spend entire careers categorizing items. Since it’s hard to categorize well, people also arrange instances of objects into collections by containment instead of by type. A library contains books, but it isn’t itself a book. A library also contains other things that aren’t books, such as chairs and tables. If at some point you decide to recategorize all of the books in a library, it doesn’t affect what building people visit to get a book. It only changes how you find a book once you reach that building. On the other hand, if the library moves to a new location, you have to learn where it is. Once inside the building, however, your method for looking up books hasn’t changed. This is usually called a has-a relationship—a library has-a bunch of books. Now let’s see how these concepts are used in the PowerShell environment.

1.3.2 Objects in PowerShell
We’ve said that PowerShell is an object-based shell as opposed to an object-oriented language. What do we mean by object-based? In object-based scripting, you typically use objects somebody else has already defined for you. While it’s possible to build your own objects in ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

13 PowerShell, it isn’t something that you need to worry about—at least not for most basic PowerShell tasks. Returning to the LightBulb example, PowerShell would probably use the LightBulb class like this: $lb = get-lightbulb –room bedroom $lb.TurnOff() Don’t worry about the details of the syntax for now—we’ll cover that later. The key point is that you usually get an object “foo” by saying: get-foo –option1 –option2 bar rather than saying something like: new foo() as you would in an object-oriented language. PowerShell commands, called cmdlets, use verb-noun pairs like Get-Date. The Get-* verb is used universally in the system to get at objects. Note that we didn’t have to worry about whether LightBulb is a class or an interface, or care about where in the object hierarchy it comes from. You can get all of the information about the member properties of an object though the get-member command (see the pattern?), which will tell you all about an object’s properties. But enough talk! By far the best way to understand PowerShell is to use it. In the next section, we’ll get you up and going with PowerShell, and quickly tour through the basics of the environment.

1.4 Up and running with PowerShell
In this section, we’ll look at the things you need to know to get going with PowerShell as quickly as possible. This is a brief introduction intended to provide a taste of what PowerShell can do and how it works. We begin with how to download and install PowerShell and how to start the interpreter once it’s installed. Then we’ll cover the basic format of commands, command-line editing, and how to use command completion with the Tab key to speed up command entry. Once you’re up and running, we’ll look at what you can do with PowerShell.

NOTE
The PowerShell documentation package also includes a short Getting Started guide that will include up-to-date installation information and instructions. You may want to take a look at this as well.

1.4.1 Installing PowerShell
How you get PowerShell depends on what operating system you're using. If you're using Windows 7 or Windows Server 2008 R2, you have nothing to do - it's already there. system. If you're using Windows Server 2008, PowerShell was included with this operating system but as an optional component which will need to be turned on before you can use it. For earlier Microsoft operating systems, you’ll have to download and install the PowerShell package on ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 All Microsoft operating systems beginning with Windows 7 include PowerShell as part of the

Licensed to Andrew M. Tearle

14 your computer. For details about supported platforms, etc., go to the PowerShell page on the Microsoft website: http://microsoft.com/powershell This page contains links to the appropriate installers as well as documentation packages and other relevant materials. Alternatively, you can go to Microsoft Update and search for the installer there. Once you’ve located the installer, follow the instructions to install the package.

1.4.2 Starting PowerShell
Now let's look at how we start PowerShell running. PowerShell follows a model found in many modern interactive environments. It's actually composed of two main parts: 1. The PowerShell engine which interprets the commands 2. A host application that passes commands from the user to the engine. While there is only one PowerShell engine, there can be many hosts, including hosts written by third-parties like Quest Software's PowerGUI. In version 1 of PowerShell, Microsoft only provided one basic PowerShell host based on the old-fashioned Windows console. Version 2 introduced a much more modern host environment, called the PowerShell Integrated Scripting Environment (PowerShell ISE). We'll look at both of these hosts in the next few sections.

1.4.3 The PowerShell console host
To start an interactive PowerShell session using the console host go to: Start -> Programs -> Windows PowerShell -> Windows PowerShell PowerShell will start and you’ll see a screen like that shown in figure 1.3:

Figure 1.3 When you start an interactive PowerShell session, the first thing you see is the PowerShell logo and then the prompt. As soon as you see the prompt, you can begin entering commands.

This window looks a lot like the old Windows command window (except that it's blue and yellow instead of black and white.) Now type the first command most people type: “dir”. This produces a listing of the files on your system, as shown in figure 1.4.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

15

Figure 1.4 At the prompt, type “dir” and press the Enter key. PowerShell will then execute the dir command and display a list of files in the current directory.

As you would expect, the dir command prints a listing of the current directory to standard output.

NOTE
Let’s stop for a second and talk about the conventions we’re going to use in examples. Since PowerShell is an interactive environment, we’ll show a lot of example commands as the user would type them, followed by the responses the system generates. Before the command text, there will be a prompt string that looks like “PS (2) > ”. Following the prompt, the actual command will be displayed in bold font. PowerShell’s responses will follow on the next few lines. Since PowerShell doesn’t display anything in front of the output lines, you can distinguish output from commands by looking for the prompt string. These conventions are illustrated in figure 1.5.

First Prompt

User enters “1+2+3+4”

PowerShell Outputs the Result 10

PS (1) > 1+2+3+4 10 PS (2) >

Next Prompt

Figure 1.5 This diagram illustrates the conventions we’re using for showing examples in this book. The text ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

16 that the user types is shown in bold. Prompts and other output from the interpreter are shown in normal weight text. COMMAND EDITING IN THE CONSOLE Typing in commands is all well and good but you also want to be able to edit and rerun commands. Command-line editing works the same way in the PowerShell console window as it does for cmd.exe. The available editing features and keystrokes are listed in table 1.1.

Table 1.1 Command editing features

Keyboard Sequence Left/Right Arrows

Editing Operation Move the editing cursor left and right through the current command line.

Ctrl-Left Arrow, Ctrl-Right Arrow

Holding the control (CTRL) key down while pressing the left and right arrow keys will move the editing cursor through the current command line one word at a time, instead of one character at a time.
Moves the editing cursor to the beginning of the current command line. Moves the editing cursor to the end of the current command line Moves up and down through the command history.

Home

End

Up/Down Arrows

Insert Key

Toggles between character insert and character overwrite modes. Deletes the character under the cursor.

Delete Key

Backspace Key

Deletes the character to the left of the cursor.

These key sequences let you create and edit commands effectively at the command line. In fact, they aren’t really part of PowerShell at all. These command-line editing features are part of the Windows console subsystem, so they are the same across all console applications. Users of cmd.exe or any modern UNIX shell will also expect to be able to do command completion. Since this component is common to both host environments, we'll cover how it works in its own section. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

17 Now let's leap into the 21st Century and look at a modern shell environment - the PowerShell ISE.

1.4.4 The PowerShell Integrated Scripting Environment
Starting with version 2, PowerShell includes a modern integrated environment for working with PowerShell - the Integrated Scripting Environment (or ISE). To start the PowerShell ISE go to: Start -> Programs -> Windows PowerShell -> Windows PowerShellISE PowerShell will start and you’ll see a screen like that shown in figure 1.4:

Figure 1.4 The PowerShell Integrated Scripting Environment

You can see that, by default, the window is divided into three parts: the text entry area at the bottom, the output window in the middle and an editor at the top. As we did in the console window, let's run the dir command. Type dir into the bottom pane and hit enter. The command will disappear from the bottom pane, appear in the middle pane followed by the output of the command as shown in figure 1.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

18

Figure 1.5 This figure shows running the dir command in the PowerShell ISE. The command is entered in the bottom pane and the result of the command is shown in the output pane in the middle.

Because the ISE is a real Windows application, it follows all of the Windows Common User Access (CUA) guidelines. The left and right arrows work as expected. The up and down arrows will move you through the history of commands that you've entered. Something that requires special mention is how Ctrl-C works. By default, this is the key sequence for copying into the clipboard in Windows. It is also, however, the way to interrupt a running command in most shells. As a result, the ISE has to treat Ctrl-C in a special way. When there is something selected, Ctrl-C copies the selection. If there is a command running and there is no selection, then the running command will be interrupted. There is also another way to interrupt a running command. You may have noticed the two buttons immediately above the command entry pane - the ones that look like the play and stop buttons on a media player. As one might expect, the green "play" button will run a command just like if you hit enter. If there is a command running, the "play" button is disabled (grayed out) and the red "stop" button is enabled. Clicking on this button will stop the currently running command. USING THE EDITOR PANE The topmost pane in the ISE is a script editor that understands the PowerShell language. This editor will do syntax highlighting as you type in script text. It will also let you select a region ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

19 and either hit the "play" button above the pane or press the F8 key to execute the part of the script you want to test out. If there is nothing selected in the window, then the whole script will be run. If you are editing a script file, the ISE will ask if you want to save the file before running it. Another nice feature of the ISE editor is the ability to have multiple files open at once, each in individual tabs as shown in figure 1.6. Figure 1.6 Multiple tabs in the ISE editor

Figure 1.6 This figure shows using multiple tabs in the ISE editor. Each new file that is opened gets its own tab. Files can be opened from the file menu or by using the 'psedit' command in the command window as shown.

And finally, as well as multiple editor tabs, the ISE also allows you to have multiple session tabs as shown in figure 1.7. In this figure you can see that there are 4 session tabs and within each tab, there can be multiple editor tabs. This makes the ISE a very powerful way to organize your work and easily multitask between different activities.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

20

Figure 1.7 This figure shows how multiple session tabs are displayed in the ISE. Note that each session tab has its own set of editor tabs.

These are the basic concepts in the ISE. But the ISE is not just a tool for writing, testing and debugging PowerShell scripts. It's also scriptable by PowerShell. This means that you can use scripts to manipulate the contents of buffers, create new tabs and menu items, etc. This allows you to use the ISE as part of your application in much the same way as the EMACS editor was a component of custom applications. There are some limitations to this in the first version of the ISE - we didn't have time to do everything we wanted (there's never enough time) but the result is still very powerful. We'll see more of this later on.

NOTE:
Ok - so why is this an 'ISE' instead of an 'IDE' like Visual Studio? The big difference is that the ISE is intended for interactive use of PowerShell, not just the creation of PowerShell applications. One of the biggest differences between the two approaches is the lack of a project system in the ISE.

1.4.5 Command completion
One of the most useful editing features in PowerShell is command completion, also called tabcompletion. While cmd.exe does have tab-completion, PowerShell’s implementation is significantly more powerful. Command completion allows you to partially enter a command, then hit the Tab key and have PowerShell try to fill in the rest of the command. By default, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

21 PowerShell will do tab completion against the file system, so if you type a partial file name and then hit Tab, the system matches what you’ve typed against the files in the current directory and returns the first matching file name. Hitting Tab again takes you to the next match, and so on. PowerShell also supplies the powerful capability of tab-completion on wild cards (see chapter 4 for information on PowerShell wild cards). This means that you can type: PS (1) > cd c:\pro*files and the command is expanded to: PS (2) > cd 'C:\Program Files' PowerShell will also do tab-completion on partial cmdlet names. If you enter a cmdlet name up to the dash and then hit the Tab key, the system will step through the matching cmdlet names. So far, this isn’t much more interesting than what cmd.exe provide. What is significantly different is that PowerShell also does completion on parameter names. If you enter a command followed by a partial parameter name and hit Tab, the system will step through all of the possible parameters for that command. PowerShell also does tab-completion on variables. If you type a partial variable name and then hit the Tab key, PowerShell will complete the name of the variable. And finally, PowerShell does completion on properties in variables. If you’ve used the Microsoft Visual Studio development environment, you’ve probably seen the Intellisense feature. Property completion is kind of a limited Intellisense capability at the command line. If you type something like: PS (1) > $a="abcde" PS (2) > $a.len The system expands the property name to: PS (2) > $a.Length Again, the first Tab returns the first matching property or method. If the match is a method, an open parenthesis is displayed: PS (3) > $a.sub which produces: PS (3) > $a.Substring( Note that the system corrects the capitalization for the method or property name to match how it was actually defined. This doesn’t really impact how things work. PowerShell is caseinsensitive by default whenever it has to match against something. (There are operators that allow you to do case-sensitive matching, which are discussed in chapter 3). Version 2 of PowerShell introduced an additional tab-completion feature (suggested by a PowerShell user no less). PowerShell remembers each command you type. You can access previous commands using the arrow keys or show them using the Get-History command. A new feature was added to allow you to do tab completion against the command history. To recall the first command containing the string "abc", type the # sign, followed by the pattern of the command you want to find and then hit the Tab key:

PS (4) > #abc
This will expand the command line to PS (4) > $a="abcde" You can also select a command from the history by number (this is why we have numbers in the prompt). To do this, type the # sign, then the number of the command to run followed by tab ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

22 PS (5) > #2 And this should expand to PS (5) > $a="abcde"

NOTE:
The PowerShell tab completion mechanism is user extendable. While the path completion mechanism is built into the executable, features such as parameter and property completion are implemented through a shell function that users can examine and modify. The name of this function is TabExpansion. Chapter 7 describes how to write and manipulate PowerShell functions.

1.5 Dude! Where’s my code?
Ok enough talk, let's see some more example code! First, we'll revisit the dir example we saw earlier. This time, instead of simply displaying the directory listing, we'll save it into a file using output redirection just like in other shell environments. In the following example, we'll use dir to get information about a file named "somefile.txt" in the root of the C: drive. Using redirection, we direct the output into a new file "c:\foo.txt" and then use the type command to display what was saved. This looks like PS (2) > dir c:\somefile.txt > c:\foo.txt PS (3) > type c:\foo.txt Directory: Microsoft.PowerShell.Core\FileSystem::C:\ Mode ----a--PS (4) > LastWriteTime ------------11/17/2004 3:32 AM Length Name ------ ---0 somefile.txt

As you can see, commands work more or less as you’d expect.

AUTHOR'S NOTE
Ok - nobody really has a file named "somefile.txt" in the root of their C drive (except me). For the purpose of this example, just choose any file that does exist and the example will work fine, though obviously the output will be different.

Let’s go over some other things that should be familiar to you.

1.5.1 Navigation and Basic Operations
The PowerShell commands for working with the file system should be pretty familiar to most users. You navigate around the file system with the cd command. Files are copied with the

copy or cp commands, moved with the move and mv commands and removed with the del or rm commands. Why two of each command you might be asking? One set are the names familiar to cmd.exe/DOS users and the other are familiar to UNIX users. In practice they are actually aliases for the same command, designed to make it easier for people to get going with PowerShell. One thing to keep in mind however, is that while the commands are similar they are not exactly the same as either of the other two systems. You can use the Get-Help ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

23 command to get help about these commands. Here is the output of Get-Help for the dir command. PS (1) > get-help dir NAME Get-ChildItem SYNOPSIS Gets the items and child items in one or more specified locations.

SYNTAX Get-ChildItem [-Exclude ] [-Force] [-Include ] [-Name] [-Recurse] [[-Path] ] [[Filter] ] [] Get-ChildItem [-Exclude ] [-Force] [-Include ] [-Name] [-Recurse] [[-Filter] ] [-L iteralPath] []

DETAILED DESCRIPTION The Get-Childitem cmdlet gets the items in one or more specified locations. If the item is a container, it get s the items inside the container, known as child items. You can use the Recurse parameter to get items in all child containers. A location can be a file system location, such as a dir ectory, or a location exposed by another provider, such as a registry hive or a certificate store.

RELATED LINKS About_Providers Get-Item Get-Alias Get-Location Get-Process REMARKS To see the examples, type: "get-help Get-ChildItem -exa mples". For more information, type: "get-help Get-ChildItem -de tailed". For technical information, type: "get-help Get-ChildIte m -full". The PowerShell help subsystem contains information about all of the commands provided with the system and is a great way to explore what's available. You can even use wildcard characters to search through the help topics (version 2 and later). Of course this is the simple text output. The PowerShell ISE also includes help in the richer windows format and will even let you select an item then hit F1 to view the help for the item. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

24

1.5.2 Basic expressions and variables
In addition to running commands, PowerShell can also evaluate expressions. In effect, it operates as a kind of calculator. Let’s evaluate a simple expression: PS (4) > 2+2 4 Notice that as soon as you typed the expression, the result was calculated and displayed. It wasn’t necessary to use any kind of print statement to display the result. It is important to remember that whenever an expression is evaluated, the result of the expression is output, not discarded. We’ll explore the implications of this in later sections. Here are few more examples of PowerShell expressions examples: PS (5) > (2+2)*3 12 PS (6) > (2+2)*6/2 12 PS (7) > 22/7 3.14285714285714 You can see from these examples that PowerShell supports most of the basic arithmetic operations you’d expect, including floating point.

NOTE
PowerShell supports single and double precision floating point, as well as the .NET decimal type. See chapter 3 for more details.

Since we’ve already shown how to save the output of a command into a file using the redirection operator, let’s do the same thing with expressions: PS (8) > (2+2)*3/7 1.71428571428571 PS (9) > (2+2)*3/7 > c:\foo.txt PS (10) > type c:\foo.txt 1.71428571428571 Saving expressions into files is useful; saving them in variables is more useful: PS (11) > $n = (2+2)*3 PS (12) > $n 12 PS (13) > $n / 7 1.71428571428571 Variables can also be used to store the output of commands: PS (14) > $files = dir PS (15) > $files[1] Directory: Microsoft.PowerShell.Core\FileSystem::C:\Document s and Settings\brucepay

Mode ---d----

LastWriteTime ------------4/25/2006 10:32 PM

Length Name ------ ---Desktop

In this example, we extracted the second element of the collection of file information objects returned by the dir command. We were able to do this because we saved the output of the dir command as an array of objects in the $files variable. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

25

NOTE:
Note that collections in PowerShell start at 0, not at 1. This is a characteristic we’ve inherited from the .NET Common Language Runtime specification. This is why $files[1] is actually extracting the second element, not the first.

PowerShell has a very rich set of operators which are covered in detail in chapters 4 and 5.

1.5.3 Processing data
As we’ve seen in the preceding sections, we can run commands to get information and then store it in files and variables. Now let’s do some processing on that data. First we’ll look at how to sort objects and how to extract properties from those objects. Then we’ll look at using the PowerShell flow control statements to write scripts that use conditionals and loops to do more sophisticated processing. SORTING OBJECTS First let’s sort a list of files. Here’s the initial list, which by default is sorted by name. PS (16) > cd c:\files PS (17) > dir Directory: Microsoft.PowerShell.Core\FileSystem::C:\files Mode ----a---a---a---a--LastWriteTime ------------4/25/2006 10:55 PM 4/25/2006 10:51 PM 4/25/2006 10:56 PM 4/25/2006 10:54 PM Length -----98 42 102 66 Name ---a.txt b.txt c.txt d.txt

The output of this shows the basic properties on the file system objects sorted by the name of the file. Now, let’s run it through the sort utility: PS (18) > dir | sort Directory: Microsoft.PowerShell.Core\FileSystem::C:\files Mode ----a---a---a---a--LastWriteTime ------------4/25/2006 10:55 PM 4/25/2006 10:51 PM 4/25/2006 10:56 PM 4/25/2006 10:54 PM Length -----98 42 102 66 Name ---a.txt b.txt c.txt d.txt

Granted, it’s not very interesting. Sorting an already sorted list by the same property gives you the same result. Let’s do something a bit more interesting. Let’s sort by name in descending order: PS (19) > dir | sort -descending

Directory: Microsoft.PowerShell.Core\FileSystem::C:\files Mode ----a---a---a---a--LastWriteTime ------------4/25/2006 10:54 PM 4/25/2006 10:56 PM 4/25/2006 10:51 PM 4/25/2006 10:55 PM Length -----66 102 42 98 Name ---d.txt c.txt b.txt a.txt

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

26 So there you have it—files sorted by name in reverse order. Now let’s sort by something other than the name of the file. Let’s sort by file length. You may remember from an earlier section how hard it would be to sort by file length if the output were just text.

NOTE:
In fact, on a UNIX system, this sort command looks like:

ls -l | sort -n -k 5 which, while pithy, is pretty opaque. Here’s what it’s doing. The -n option tells the sort function that you want to do a numeric sort. -k tells you which field you want to sort on. (The sort utility considers space-separated bits of text to be fields.) In the output of the ls -l command, the field containing the length of the file is at offset 5, as shown in the following:

-rw-r--r--rw-r--r--

1 brucepay 1 brucepay

brucepay brucepay

5754 Feb 19 204 Aug 19

2005 index.html 12:50 page1.htm

We need to set things up this way because ls produces unstructured strings. We have to tell sort how to parse those strings before it can sort them.

In PowerShell, when we use the Sort-Object cmdlet, we don’t have to tell it to sort numerically—it already knows the type of the field, and we can specify the sort key by property name instead of a numeric field offset. PS (20) > dir | sort -property length Directory: Microsoft.PowerShell.Core\FileSystem::C:\files Mode ----a---a---a---a--LastWriteTime ------------4/25/2006 10:51 PM 4/25/2006 10:54 PM 4/25/2006 10:55 PM 4/25/2006 10:56 PM Length -----42 66 98 102 Name ---b.txt d.txt a.txt c.txt

In this example, we’re working with the output as objects; that is, things having a set of distinct characteristics accessible by name. SELECTING PROPERTIES FROM AN OBJECT In the meantime, let’s introduce a new cmdlet—Select-Object. This cmdlet allows you to either select some of the objects piped into it or select some properties of each object piped into it. Say we want to get the largest file in a directory and put it into a variable: PS (21) > $a = dir | sort -property length -descending | >> select-object -first 1 >> PS (22) > $a Directory: Microsoft.PowerShell.Core\FileSystem::C:\files ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

27 Mode ----a--LastWriteTime ------------4/25/2006 10:56 PM Length Name ------ ---102 c.txt

From this we can see that the largest file is c.txt.

NOTE
Note the secondary prompt “>>” in the previous example. The first line of the command ended in a pipe symbol. The PowerShell interpreter noticed this, saw that the command was incomplete, and prompted for additional text to complete the command. Once the command is complete, you type a second blank line to send the command to the interpreter.

Now say we want only the name of the directory containing the file and not all of the other properties of the object. We can also do this with Select-Object. As with the sort cmdlet, Select-Object also takes a -property parameter (you’ll see this frequently in the PowerShell environment—commands are consistent in their use of parameters). PS (23) > $a = dir | sort -property length -descending | >> select-object -first 1 -property directory >> PS (24) > $a Directory --------C:\files We now have an object with a single property. PROCESSING WITH THE FOREACH-OBJECT CMDLET The final simplification is to get just the value itself. Let’s introduce a new cmdlet that lets you do arbitrary processing on each object in a pipeline. The Foreach-Object cmdlet executes a block of statements for each object in the pipeline. PS (25) > $a = dir | sort -property length -descending | >> select-object -first 1 | >> foreach-object { $_.DirectoryName } >> PS (26) > $a C:\files This shows that we can get an arbitrary property out of an object, and then do arbitrary processing on that information using the Foreach-Object command. Combining those features, here’s an example that adds up the lengths of all of the objects in a directory. PS (27) > $total = 0 PS (28) > dir | foreach-object {$total += $_.length } PS (29) > $total 308 In this example, we initialize the variable $total to 0, then add to it the length of each file returned by the dir command and finally display the total. PROCESSING OTHER KINDS OF DATA One of the great strengths of the PowerShell approach is that once you learn a pattern for solving a problem, you can use this same pattern over and over again. For example, say we want to find the largest three files in a directory. The command line might look like this: PS (1) > dir | sort -desc length | select -first 3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

28 Directory: Microsoft.PowerShell.Core\FileSystem::C:\files Mode ----a---a---a--LastWriteTime ------------4/25/2006 10:56 PM 4/25/2006 10:55 PM 4/25/2006 10:54 PM Length -----102 98 66 Name ---c.txt a.txt d.txt

We ran the dir command to get the list of file information objects, sorted them in descending order by length, and then selected the first three results to get the three largest files. Now let’s tackle a different problem. We want to find the three processes on the system with the largest working set size. Here’s what this command line looks like: PS (2) > get-process | sort -desc ws | select -first 3 Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- -------------- ----------- ----------1294 43 51096 81776 367 11.48 3156 OUTLOOK 893 25 55260 73340 196 79.33 5124 iexplore 2092 64 42676 54080 214 187.23 988 svchost This time we run Get-Process to get the data and sort on the working set instead of the file size. Otherwise the pattern is identical to the previous example. This command pattern can be applied over and over again. For example, to get the three largest mailboxes on an Exchange mail server, the command might look like: get-mailboxstatistics | sort –desc property to filter on. Even when we don’t have a specific command for the data we’re looking for and have to use other facilities such as WMI, we can continue to apply the pattern. Say we want to find the three drives on the system that have the most free space. To do this we need to get some data from WMI. Not surprisingly, the command for this is Get-WmiObject. Here’s how we’d use this command: PS (4) > get-wmiobject win32_logicaldisk | >> sort -desc freespace | select -first 3 | >> format-table -autosize deviceid, freespace >> deviceid freespace ---------------C: 97778954240 T: 31173663232 D: 932118528 Once again, the pattern is almost identical. The Get-WmiObject command returns a set of objects from WMI. We pipe these objects into sort and sort on the freespace property, then use Select-Object to extract the first three. TotalItemSize | select –first 3

Again the pattern is repeated except for the Get-MailboxStatistics command and the

NOTE:
Because of this ability to apply a command pattern over and over, most of the examples in this book are deliberately generic. The intent is to highlight the pattern of the solution rather than show a specific example. Once you understand the basic patterns, you can effectively adapt them to solve a multitude of other problems.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

29

1.5.4 Flow control statements
Pipelines are great, but sometimes you need more control over the flow of your script. PowerShell has the usual script flow control statements found in most programming languages. These include the basic if statements, a very powerful switch statement, and various loops like a while loop, for and foreach loops and so on. Here's an example showing use of the

while and if statements.
PS (1) > $i=0 PS (2) > while ($i++ -lt 10) { if ($i % 2) {"$i is odd"}} 1 is odd 3 is odd 5 is odd 7 is odd 9 is odd PS (3) > In this example, we’re using the while loop to count through a range of numbers, printing out only the odd numbers. In the body of the while loop, we have an if statement that tests to see whether the current number is odd, and then writes out a message if it is. We can do the same thing using the foreach statement and the range operator (..), but much more succinctly: PS (3) > foreach ($i in 1..10) { if ($i % 2) {"$i is odd"}} 1 is odd 3 is odd 5 is odd 7 is odd 9 is odd The foreach statement iterates over a collection of objects and the range operator is a way to generate a sequence of numbers. The two combine to make looping over a sequence of numbers very clean. Of course, since the range operator generates a sequence of numbers and numbers are objects like everything else in PowerShell, you can implement this using pipelines and the

ForEach-Object cmdlet:
PS (5) > 1..10 | foreach { if ($_ % 2) {"$_ is odd"}} 1 is odd 3 is odd 5 is odd 7 is odd 9 is odd These examples only scratch the surface of what you can do with the PowerShell flow control statements (just wait 'till you see the switch statement!) The complete set of control structures is covered in detail in chapter 6 with lots of examples.

1.5.5 Scripts and Functions
What good is a scripting language if you can't package commands into scripts? PowerShell lets you do this by simply putting your commands into a text file with a ".ps1" extension and then running that command. You can even have parameters in your scripts. Put the following text into a hello.ps1 param($name = "bub") "Hello $name, how are you?" ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

30 Notice that the param keyword is used to define a parameter called $name. The parameter is given a default value "bub". Now we can run this script from the PowerShell prompt by typing the name as ".\hello". We need the ".\" to tell PowerShell to get the command from the current directory (chapter 13 explains why this is needed.). The first time we run this script, we won't specify any arguments. PS (1) > .\hello Hello bub, how are you? and we see that the default value was used in the response. Let's run it again but this time we'll specify an argument. PS (2) > .\hello Bruce Hello Bruce, how are you? Now the argument is in the output instead of the default value. Of course sometimes we just want to have subroutines in our code. PowerShell address this need through functions. Let's turn our hello script into a function. Here's what it looks like: PS (3) > function hello { >> param($name = "bub") >> "Hello $name, how are you" >> } >> The body of the function is exactly the same as the script. The only thing we've added is the

function keyword, the name of the function and braces around the body of the function. Now let's run it, first with no arguments like we did with the script PS (4) > hello Hello bub, how are you and then with an argument: PS (5) > hello Bruce Hello Bruce, how are you Obviously the function operates in the same way as the script except that PowerShell didn't have to load it from a disk file so it's a bit faster to call. Scripts and functions are covered in detail in chapter 7.

1.5.6 Remoting and the Universal Execution Model
In the previous sections we've see the kinds of things we can do with PowerShell on a single computer. But the computing industry has long since moved beyond a one-computer world. Being able to manage groups of computers, without having to physically visit each one is critical in the modern information technology world. To address this, PowerShell Version 2 introduced remote execution capabilities (remoting) and the Universal Execution Model - a fancy term that really just means that if it works locally, then it should work remotely.

AUTHOR'S NOTE
At this point you should be asking "if this is so important why wasn't it in version 1"? In fact it was planned from shortly before day 1 of the PowerShell project. But something universal, secure and simple is, in fact, very hard. - to make

The core of PowerShell remoting is the Invoke-Command command, aliased to icm. This command allows you to invoke a block of PowerShell script on the current computer, on a ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

31 remote computer or on a thousand remote computers. Let's see some of this in action. Our example scenario will be to check the version of the Microsoft Management Console host program installed on a computer. We might need to do this because we want to install an MMC extension (called a snapin) on a set of machines and this snapin requires a minimum version of the MMC host. We can do this locally by using the Get-Command command (gcm) to retrieve information about mmc.exe - the executable for the MMC host. PS (1) > (gcm mmc.exe).FileVersionInfo.ProductVersion 6.1.7069.0 This is a pretty simple one-line command and it shows us that we have version 6.1 of mmc.exe installed on the local machine. Now let's run it on a remote machine. (We'll assume that our target machine names are stored in the variables $m1 and $m2.) Let's check the version on the first machine. We'll run the same command as before, but this time we'll enclose it in braces as an argument to icm. We also give the icm command the name of the host to execute on. PS {2) > icm $m1 { >> (gcm mmc.exe).FileVersionInfo.ProductVersion >> } >> 6.0.6000.16386 Oops - this machine has an older version of mmc.exe. OK - let's try machine 2. We run exactly the same command but pass in the name of the second machine this time. PS {3) > icm $m2 { >> (gcm mmc.exe).FileVersionInfo.ProductVersion >> } >> 6.1.7069.0 and this machine is up-to-date. At this point we've addressed the need to physically go to each machine but we're still executing the commands one at a time. Let's fix that too by putting the machines to check into a list. We'll use an array variable for this example however this list could come from a file, an Excel spreadsheet, a database or a web service. Let's run the command with the machine list: PS {4) > $mlist = $m1,$m2 PS {5) > icm $mlist { >> (gcm mmc.exe).FileVersionInfo.ProductVersion >> } >> 6.0.6000.16386 6.1.7069.0 and we same the same results as last time, but as a list instead of one at a time. In practice, most of our machines are probably up to date, so we really only care about the ones that don't have the correct version of the software. We can use the where command to filter out the machines we don't care about. PS {6) > icm $mlist { >> (gcm mmc.exe).FileVersionInfo.ProductVersion >> } | where { $_ -notmatch '6\.1' } >> 6.0.6000.16386 This returns the list of machines that have out-of-date mmc.exe versions. There's still a problem however - we see the version number but not the computer's name. Obviously we'll need this too if we're going to update those machines. To address this, PowerShell remoting ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

32 automatically adds the name of the originating computer to each received object using a special property called "PSComputerName". Now let's jump a head a bit and see how much effort it would be to produce a nice table of computer names and version numbers. We'll run the remote command again, use where to filter the results, extract the fields we want to display using the select command and finally format the report using the Format-Table command. For the purpose of this example, we'll add the machine lists together so we know we'll get two records in our report. Here's what the command looks like: PS {7) > icm ($mlist + $mlist) { >> (gcm mmc.exe).FileVersionInfo.ProductVersion } | >> where { $_ -notmatch '6\.1' } | >> select @{n="Computer"; e={$_.PSComputerName}}, >> @{n="MMC Version"; e={$_}} | >> format-table -auto >> Computer -------brucepaydev07 brucepaydev07 MMC Version ----------6.0.6000.16386 6.0.6000.16386

While the command may look a bit scary, we were able to produce our report with very little effort. And the techniques we used for formatting the report can be used over and over again. This example shows how easily PowerShell remoting can be used to expand the reach of your scripts to cover 10, hundreds or thousands of computers all at once. But sometimes you just want to work with a single remote machine interactively. Let's see how to do that. The Invoke-Command command is the way to programmatically execute PowerShell commands on a remote machine. When we want to connect to a machine so we can interact with it on a 1-1 basis, we use the Enter-PSSession command. This command allows you to start an interactive 1-1 session with a remote computer. Running Enter-PSSession looks like: PS (11) > enter-pssession server01 [server01]: PS > (gcm mmc.exe).FileVersionInfo.ProductVersion 6.0.6000.16386 [brucepaydev07]: PS > get-date Sunday, May 03, 2009 7:40:08 PM [server01]: PS > exit PS (12) > As shown in the example, when we connect to the remote computer, our prompt changes to indicate that we are working remotely. Otherwise, once connected, we can pretty much interact with the remote computer the same way we would with a local machine. When we're done, we exit the remote session with the exit command and this pops us back to the local session. This brief introduction cover some very powerful techniques but we've only begun to cover all of the things remoting let's you do. At this point, we'll end of our “Cook’s tour” of PowerShell. We’ve only breezed over the features and capabilities of the environment. There are many other areas of PowerShell that we haven’t covered here, especially in Version 2 which introduced advanced functions and scripts, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

33 modules, transactions, eventing support and many more features. In the subsequent chapters, we’ll cover each of the elements discussed here in detail and a whole lot more.

1.6 Summary
This chapter covered what PowerShell is and, just as important, why it is. We also took a whirlwind tour through some simple examples of using PowerShell interactively. Here are the key points that were covered:  PowerShell is the new command-line/scripting environment from Microsoft Corporation. Since its introduction with Windows Server 2008, PowerShell has rapidly moved to the center of Microsoft server and application management technologies. Many of the most important server products now use PowerShell as their management layer including Exchange, Active Directory and SQL Server.  The Microsoft Windows management model is primarily object-based, through .NET, COM and WMI. This required us to take a novel approach to command-line scripting, incorporating object-oriented concepts into a command-line shell. PowerShell uses the .NET object model as the base for its type system but can also access other object types like WMI. PowerShell is an interactive environment with two different host applications - the console host powershell.exe and the graphical host powershell_ise.exe. It can also be "hosted" in other applications to act as the scripting engine for those applications. Shell operations like navigation and file manipulation in PowerShell are very similar to what you're used to in other shells. The way to get help about things in PowerShell is to use the Get-Help command (or selecting text and pressing F1 in the ISE). PowerShell is a very sophisticated with a full range of calculation, scripting and text processing capabilities. PowerShell Version 2 introduced a comprehensive set of remoting features to allow you to do scripted automation of large collections of computers.



   

In the following chapters, we’ll look at each of the PowerShell features we showed you here in much more detail. Stay tuned!

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

34

2
Foundations of PowerShell

“Begin at the beginning,” the king said “and then go on till you come to the end, then stop.” —Lewis Carroll, Alice in Wonderland Vizzini: Inconceivable! Inigo: You keep on using that word. I do not think it means what you think it means. —William Goldman, The Princess Bride

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

35 In this chapter, we introduce the foundational concepts underlying the PowerShell language and its environment. We'll cover language details that are specific to PowerShell and look at how the interpreter parses the commands we type. This chapter also covers the different types of commands we'll encounter along with the the anatomy and operation of the pipeline itself. We'll look at the basic syntax for expressions and commands including how to add comments to our scripts and how arguments and parameters are handled. Finally we'll close the chapter with a discussion of how the formatting and output subsystem works in PowerShell. The chapter presents many examples that are not completely explained. If you don’t understand everything when you read the examples, don’t worry—we’ll revisit the material in later chapters. In this chapter, we just want to cover the core concepts, and then focus on the details in subsequent chapters.

2.1 Getting a sense of the PowerShell language
Before digging too far into PowerShell concepts and terminology, let’s capture some first impressions of the language: what does the PowerShell language look and feel like? Birdwatchers have to learn how to distinguish hundreds of different species of fast-moving little brown birds (or LBBs as they’re known). To understand how they do this, I consulted with my wife, the "Master Birder". (The only bird I can identify is a chicken, preferably stuffed and roasted). Birdwatchers use something called the GISS principle which stands for General Impression, Size, and Shape. It’s that set of characteristics that allow us to determine what we've seen even though we had only a very brief or distant glance. Take a look at the silhouettes shown in figure 2.1. The figure shows the relative sizes of four birds and highlights the characteristic shape of each one. This is more than enough information to recognize each bird.

Figure 2.1 This figure illustrates the G.I.S.S. principle—the general impression, size, and shape of some common birds. Even without any detail, the basic shape and size is enough for most people to identify these birds. This same principle can be applied when learning programming languages; a sense of the overall shape of the language allows you to identify common coding patterns in the language.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

36 What does this have to do with computers (other than to prove we aren’t the only ones who make up strange acronyms)? In essence, the GISS principle also works well with programming languages. The GISS of the PowerShell syntax is that it’s like any of the C programming language descendents with specific differences such as the fact that variables are distinguished by a leading dollar (“$”) sign.

AUTHOR'S NOTE
PowerShell uses the “at” symbol (“@”) in a few places, has $_ as a default variable, and uses “&” as the function call operator. These elements lead people to say that PowerShell looks like Perl. In practise, at one point, we did use Perl as a root language, and these elements stem from the period. Later on, the syntax was changed to align more with C#, but we kept these elements because they worked well. In Perl terminology, they contributed significantly to the “whipupitude quotient” of the language. In fact, the language that PowerShell looks most like is PHP. (This wasn’t deliberate. It’s a case of parallel evolution—great minds thinking alike and all that.) But don’t let this fool you; semantically, PowerShell and PHP are quite different.

2.2 The core concepts
The core PowerShell language is based on the IEEE standard POSIX 1003.2 grammar for the Korn shell. This standard was chosen because of its maturity and long history as a successful basis for modern shells like bash and zsh. We deviated from this standard where necessary to address the specific needs of an object-based shell. We also deviated in areas where we wanted to make it easier to write sophisticated scripts. Originally, Perl idioms were appropriated for some of the advanced scripting concepts such as arrays and hash tables. As the project progressed, it became clear that aligning PowerShell syntax with C# was more appropriate. If nothing else, this would facilitate migrating code between PowerShell and C#. The major value this brings is that PowerShell code can be migrated to C# when necessary for performance improvements, and C# examples can be easily converted to PowerShell. This second point is important, since the more examples you have in a new language, the better off you are.

2.2.1 Command concepts and terminology
As with any piece of new technology, PowerShell has its own terminology, although we’ve tried to stick to existing terms as much as we could. Consequently, much of the terminology used in PowerShell will be familiar if you’ve used other shells. However, because PowerShell is a new kind of shell, there are a number of terms that are different and a few new terms we just made up. In this section, we’ll go over the PowerShell-specific concepts and terminology for command types and command syntax.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

37

2.2.2 Commands and cmdlets
Commands are the fundamental part of any shell language; they’re what you type to get things done. As we saw in the previous chapter, a simple command looks like this: command –parameter1 –parameter2 argument1 argument2 A more detailed illustration of the anatomy of this command is shown in figure 2.2. This figure calls out all the individual elements of the command.

Command Name

Parameter with Argument

command -parameter1 -parameter2 arg1 arg2

Switch Parameter

Positional Argument

Figure 2.2 This figure shows the anatomy of a basic command. It begins with the name of the command, followed by some number of parameters. These may be switch parameters that take no arguments, regular parameters that do take arguments, or positional parameters where the matching parameter is inferred by the argument’s position on the command line.

All commands are broken down into the command name, the parameters specified to the command, and the arguments to those parameters.

AUTHOR'S NOTE
The distinction between “parameter” and “argument” may seem a bit strange from a programmer’s perspective. However, if you’re used to languages such as Python or Visual Basic that allow for keyword parameters, PowerShell parameters correspond to the keywords, and arguments correspond to the values.

The first element in the command is the name of the command to be executed. The PowerShell interpreter looks at this name and figures out what actually has to be done. It must figure out not only which command to run but which kind of command to run. In PowerShell, there are four different categories of commands: cmdlets, shell function commands, script commands, and native Windows commands. (We’ll cover the different categories in detail in the following sections.) Following the command name comes zero or more parameters and/or arguments. A parameter starts with a dash, followed by the name of the parameter. An argument, on the other hand, is the value that will be associated with or bound to a specific parameter. Let’s look at an example: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

38 PS (1) > write-output -inputobject Hello Hello In this example, the command is Write-Output, the parameter is -inputobject, and the argument is Hello. What about the positional parameters mentioned in figure 2.1? When a PowerShell command is created, the author of that command specifies information which allows PowerShell to determine which parameter to bind an argument to, even if the parameter name itself is missing. For example, the Write-Output command has been defined so that the first parameter is -inputobject. This lets us write PS (2) > write-output Hello Hello instead of having to actually specify -inputobject. The piece of the PowerShell interpreter that figures all of this out is called the parameter binder. In fact, the parameter binder is smart—it doesn’t require that you specify the full name of a parameter as long as you specify enough for it to uniquely distinguish what you meant. This means that you can write any of the following: PS (3) > write-output -input Hello Hello PS (4) > write-output -IN Hello Hello PS (5) > write-output -i Hello Hello and the parameter binder still does the right thing. (Notice that it doesn’t matter whether you use uppercase or lowercase letters, either.) So what else does the parameter binder do? It’s in charge of figuring out how to match the types of arguments to the types of parameters. Remember that PowerShell is an object-based shell. Everything in Power-Shell has a type. For this to work seamlessly, PowerShell has to use a fairly complex type conversion system to correctly put things together, a subject that is covered in chapter 3. When you type a command at the command line, you’re really typing strings. What happens if the command requires a different type of object? The parameter binder uses the type converter to try to convert that string into the correct type for the parameter. Here’s a simple example. Let’s use the Get-Process command to get the process with the process Id 0. Instead of passing it the number 0, we’ll put the argument in quotes to force the argument to be a string. This means that the -id parameter, which requires a number, will be passed a string instead. PS (7) > get-process -id "0" Handles ------0 NPM(K) -----0 PM(K) ----0 WS(K) VM(M) ----- ----28 0 CPU(s) -----Id ProcessName -- ----------0 Idle

When trying to run this command, the parameter binder detects that -id needs a number, not a string, so it takes the string “0” and tries to convert it into a number. If this succeeds, the command continues as we see in the example. What happens if it can’t be converted? Let’s try it: PS (8) > get-process -id abc Get-Process : Cannot bind parameter 'Id'. Cannot convert value "abc" to type "System.Int32". Error: "Input string was not in a correct fo rmat." At line:1 char:16 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

39 + get-process -id PS (9) > .\foo.txt PS (2) > You get the prompt back more or less immediately, and your default text editor will pop up (probably notepad.exe since that’s the default). The program to launch is determined by the file associations that are defined as part of the Windows environment.

AUTHOR'S NOTE
In PowerShell, unlike in cmd.exe, you have to prefix a command with ./ or .\ if you want to run it out of the current directory. This is part of PowerShell’s “Secure by Design” philosophy. This particular security feature was adopted to prevent “Trojan horse” attacks where the user is lured into a directory and then told to run an innocuous command such as

notepad.exe. Instead of running the system notepad.exe, they end up running a hostile program that the attacker has placed in that directory and named notepad.exe.
Chapter 13 covers the security features of the PowerShell environment in detail.

So what about when you specify the editor explicitly? PS (2) > notepad foo.txt PS (3) > The same thing happens—the command returns immediately. But what about when you run the command in the middle of a pipeline? PS (3) > notepad foo.txt | sort-object PS (4) > Now PowerShell waits for the command to exit before giving you back the prompt. This can be handy when you want to insert something such as a graphical form editor in the middle of a script to do some processing. This is also the easiest way to make PowerShell wait for a process to exit. Finally, let’s run the edit.com program. This is the old console-based full screen editor included with MS-DOS and Windows since about DOS 4.0. (Of course this also works with other console editors—vi, emacs, and so forth.) PS (6) > edit.com ./foo.txt PS (7) > As you would expect, the editor starts up, taking over the console window. You can edit the file and then exit the editor and return to PowerShell, all as one would expect. As you can see, the behavior of native commands depends on the type of native command, as well as where it appears in the pipeline. A useful thing to remember is that the PowerShell interpreter itself is a native command:

powershell.exe. This means that you can call PowerShell from within PowerShell. When you do this, a second PowerShell process is created. In practice there is nothing very unusual about this - this basically how all shells work. PowerShell just doesn't have to do it very often making it much faster than conventional shell languages. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

44 The ability to run a child PowerShell process is particularly useful if you want to have isolation in portions of your script. A separate process means that the child script can't impact the caller's environment. In fact this feature is useful enough that PowerShell has special handling for this case allowing you to embed the script to run in-line. If you want to run a fragment of script in a child process you can do this by passing the block of script to the child process delimited by braces. Here is an example of this. PS {1) > powershell { get-process *ss } | Format-Table name, handles Name ---csrss lsass smss Handles ------1077 1272 28

There are two things to note in this example. The script code in the braces can be any PowerShell code and it will be passed through to the new PowerShell process. The special handling takes care of encoding the script in such a way that it is passed properly to the child process. The other thing to note is that, when PowerShell is executed this way, the output of the process is serialized objects - the basic structure of the output is preserved - and so can be passed into other commands. We'll look at this serialization in detail when we look at remoting - the ability to run PowerShell scripts on a remote computer - in chapter XX. Now that we’ve covered all four PowerShell command types, let’s get back to looking at the PowerShell syntax. Notice that a lot of what we've looked at so far is a bit verbose. This makes it easy to read which is great for script maintenance, but it looks like it would be a pain to type on the command line. PowerShell addresses these two conflicting goals - readability and writability - with the concept of elastic syntax. Elastic syntax allows you to expand and collapse how much you need to type to suit your purpose. We'll see how this works in the next section.

2.3 Aliases and elastic syntax
We haven’t really talked about aliases yet or how they’re used to achieve an elastic syntax in PowerShell. Since this concept is important in the PowerShell environment, we need to spend some time on it. The cmdlet Verb-Noun syntax, while regular, is, as we noted, also verbose. You may have noticed that in most of our examples we’re using commands such as dir and type. The trick behind all this is aliases. The dir command is really Get-ChildItem and the type command is really Get-Content. In fact, you can see this by using the Get-Command command: PS (1) > get-command dir CommandType Name Definition ----------------------Alias dir Get-ChildItem This tells you that the command is an alias for Get-ChildItem. To get information about the

Get-ChildItem command, you then do
PS (2) > get-command get-childitem CommandType Name -------------Cmdlet Get-ChildItem Definition ---------Get-ChildItem [[-P...

which truncates the information at the width of the console window. To see all of the information, pipe the output of Get-Command into fl: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

45 PS (3) > get-command get-childitem | fl Name : Get-ChildItem CommandType : Cmdlet Definition : Get-ChildItem [[-Path] ] [[-Filter] ] [-Include ] [-Exclude ] [-Recurse] [-Force] [-Name] [-Verbo se] [-Debug] [-ErrorAction ] [-ErrorVariable ] [-OutVariable ] [-OutBuffer ] Get-ChildItem [-LiteralPath] [[-Fi lter] ] [-Include ] [-Exclu de ] [-Recurse] [-Force] [-Name] [Verbose] [-Debug] [-ErrorAction ] [-ErrorVariable ] [-OutVariabl e ] [-OutBuffer ] : : : C:\WINDOWS\assembly\GAC_MSIL\Microsoft.PowerS hell.Commands.Management\1.0.0.0__31bf3856ad3 64e35\Microsoft.PowerShell.Commands.Managemen t.dll HelpFile : Microsoft.PowerShell.Commands.Management.dllHelp.xml ParameterSets : {Items, LiteralItems} ImplementingType : Microsoft.PowerShell.Commands.GetChildItemCom mand Verb : Get Noun : ChildItem This shows you the full detailed information about this cmdlet. But wait—what’s the fl command? Again we can use Get-Command to find out: PS (4) > get-command fl CommandType Name -------------Alias fl Path AssemblyInfo DLL

Definition ---------Format-List

PowerShell comes with a large set of predefined aliases. There are two basic categories of aliases—transitional aliases and convenience aliases. By transitional aliases, we mean a set of aliases that map PowerShell commands to commands that people are used to using in other shells, specifically cmd.exe and the UNIX shells. For the cmd.exe user, PowerShell defines dir,

type, copy, and so on. For the UNIX user, PowerShell defines ls, cat, cp, and so forth.
These aliases allow some basic level of functionality for new users right away. The other set of aliases are the convenience aliases. These aliases are derived from the names of the cmdlets they map to. So Get-Command becomes gcm, Get-ChildItem becomes gci, Invoke-Item becomes ii, and so on. For a list of the defined aliases, just type Get-Alias at the command line. You can use the Set-Alias command (whose alias is sal by the way) to define your own aliases.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

46

AUTHOR'S NOTE
Aliases in PowerShell are limited to aliasing the command name only. Unlike other systems such as ksh, bash, or zsh, PowerShell aliases cannot take parameters. if you need to do something more sophisticated than simple command name translations, you’ll have to use shell functions or scripts.

This is all well and good, but what does it have to do with elastics? Glad you asked! The idea is that PowerShell can be terse when needed and descriptive when appropriate. The syntax is concise for simple cases and can be stretched like an elastic band for larger problems. This is important in a language that is both a command-line tool and a scripting language. The vast majority of “scripts” that you will write in PowerShell will be no more than a few lines long. In other words, they’ll be a string of commands that you’ll type on the command line and then never use again. To be effective in this environment, the syntax needs to be very concise. This is where aliases like fl come in—they allow you to write concise command lines. When you’re scripting, however, it is best to use the long name of the command. This is because sooner or later, you’ll have to read the script you wrote (or—worse—someone else will). Would you rather read something that looks like this? gcm|?{$_.parametersets.Count -gt 3}|fl name or this? get-command | where-object {$_.parametersets.count -gt 3} | format-list name I’d certainly rather read the latter. (As always—we’ll cover the details of these examples later on.)

AUTHOR'S NOTE
PowerShell has two (or more) names for many of the same commands. Some people find this unsettling—they prefer having only one way of doing things. In fact this “only one way to do it” principle is also true for PowerShell, but with a significant variation: we want to have one best way of doing something for each particular scenario or situation. Fundamentally this is what computers are all about; at their simplest, everything is just a bunch of bits. To be practical, you start from the simple bits and build out solutions that are more appropriate for the problem you’re trying to solve. Along the way, you create an intermediate-sized component that may be reused to solve other problems. This is the approach that PowerShell uses: a series of components at different levels of complexity intended to address a wide range of problem classes. Not every problem is a nail, so having more tools than a hammer is a good idea even if requires a bit more learning.

2.3.1 Parameter Aliases
There is a second type of alias used in PowerShell: parameter aliases. Unlike command aliases which can be created by end-users, parameter aliases are created by the author of a cmdlet, script or function. (We'll see how to do this when we look at advanced function creation in chapter 7.) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

47 A parameter alias is just a shorter name for a parameter. But wait a second. Earlier I said that you just needed enough of the parameter name to distinguish it from other command parameters. Isn't this enough for convenience and elasticity? So why do we need parameter aliases? The reason we need these aliases has to do with script versioning. The easiest way to understand versioning is to look at an example. Say you have a script that calls a cmdlet Process-Message. This cmdlet has a parameter

-reply. We write our script specifying just Process-Message -re
We run the script and it works fine. Now, a few months later we install an enhanced version of the Process-Message command. This new version introduces a new parameter -receive. Now just specifying -re is no longer sufficient. If you run the old script with the new cmdlet it will fail with an ambiguous parameter message. In other words, the script is broken. So how do we fix this with parameter aliases? The first thing to know is that PowerShell will always pick the parameter that exactly matches a parameter name or alias over a partial match. By providing parameter aliases, we can meet our goal for pithiness without also making scripts subject to versioning issues. (Of course we do recommend always using the full parameter name for production scripts or scripts you want to share. Readability is always more important in that scenario.) Now that we’ve covered the core concepts of how commands are processed, let’s step back a bit and look at PowerShell language processing overall. PowerShell has a small number of important syntactic rules that you should learn. Once you understand these rules, your ability to read. write and debug PowerShell scripts will increase tremendously.

2.4 Parsing and PowerShell
In this section, we’ll cover the details of how PowerShell scripts are parsed. Before the PowerShell interpreter can execute the commands you type, it first has to parse the command text and turn it into something the computer can execute as shown in figure 2.3.

3+2

Parser +

The parser converts this to an internal representation Execution engine evaluates the internal represenation Engine 5

The user types an expression which is passed to the parser

3

2

Figure 2.3 This figure the flow of processing in the PowerShell interpreter where an expression is transformed then executed to produce a result. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

48 More formally, parsing is the process of turning human-readable source code into a form the computer understands. This is one area of computer science that actually deserves both of these the words—computer and science. Science in this case means formal language theory, which is a branch of mathematics. And since it’s mathematics, discussing it usually requires a collection of Greek letters. We’ll keep things a bit simpler here.A piece of script text is broken up into tokens by the tokenizer (or lexical analyzer if you want to be more technical). A token is a particular type of symbol in the programming language, for example a number, a keyword, or variable. Once the raw text has been broken into a stream of tokens, these tokens are processed into structures in the language through syntactic analysis. In syntactic analysis, the stream of tokens is processed according to the grammatical rules of the language. In normal programming languages, this process is straightforward—a token always has the same meaning. A sequence of digits is always a number; an expression is always an expression, and so on. For example the sequence 3+2 would always be an addition expression, and "Hello world" would always be a constant string. Unfortunately, this isn’t the case in shell languages. Sometimes you can’t tell what a token is except through its context. In the next section, we go into more detail on why this is and how the PowerShell interpreter parses a script.

2.4.1 How PowerShell parses
For PowerShell to be successful as a shell, it cannot require that everything be quoted. PowerShell would fail if it required people to continually type cd ".." or copy "foo.txt" "bar.txt" On the other hand, people have a strong idea of how expressions should work: 2 is the number 2, not a string “2”. Consequently, PowerShell has some rather complicated parsing rules. The next three sections will cover these rules. We’ll cover how quoting is handled, the two major parsing modes, and the special rules around newlines and statement termination.

2.4.2 Quoting
Quoting is the mechanism used to turn a token that has special meaning to the PowerShell interpreter into a simple string value. For example, the Write-Output cmdlet has a parameter InputObject. But what if we want to actually use the string –InputObject as an argument, as mentioned earlier? To do this, we have to quote it; that is, we surround it in single or double quotes. The result looks like this: PS (2) > write-output '-inputobject' -inputobject What would happen if we hadn’t put the argument in quotes? Let’s find out: PS (3) > write-output -inputobject Write-Output : Missing an argument for parameter 'InputObject'. Specify a parameter of type 'System.Management.Automation.PSObje ct[]' and try again. At line:1 char:25 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

49 + write-output -inputobject pwd Path ---C:\Program Files What happens if we don’t use the quotes? Again, let’s try it and find out: PS (6) > cd c:\program files Set-Location : A parameter cannot be found that matches paramete r name 'files'. At line:1 char:3 + cd 2 + >> 2 >> 4 PS (2) > the sequence “2 +” is incomplete so the interpreter prompts you to enter more text. (This is indicated by the nest prompt characters >>.) On the other hand, in the next sequence PS (2) > 2 2 PS (3) > + 2 2 PS (4) > The number 2 by itself is a complete expression, so the interpreter goes ahead and evaluates it. Likewise, “+ 2” is a complete expression and is also evaluated (+ in this case is treated as the unary plus operator). From this, you can see that if the newline comes after the plus operator, the interpreter will treat the two lines as a single expression. If the newline comes before the plus operator, it will treat the two lines as two individual expressions. Most of the time, this mechanism works the way you expect, but sometimes you can receive some unanticipated results. Take a look at the following example: PS (22) > $b = ( 2 >> + 2 ) >> Missing closing ')' in expression. At line:2 char:1 + + 2) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

54 It is parsed as $b = (2 + 2) because a trailing "+" operator is only valid as part of a binary operator expression. Since the sequence $b = ( 2 + can’t be a syntactically complete statement, the newline is treated as whitespace. On the other hand, consider the text > $b = (2 > + 2) In this case, 2 is a syntactically complete statement, so the newline is now treated as a line terminator. In effect, the sequence is parsed like $b = (2 statements. Since the syntax for a parenthetical expression is ( ) you get a syntax error—the interpreter is looking for a closing parenthesis as soon as it has a complete expression. Contrast this with using a subexpression instead of just the parentheses: >> $b = $( >> 2 >> +2 >> ) >> PS (24) > $b 2 2 Here the expression is valid because the syntax for subexpressions is

; +2 ); that is, two complete

$( )
But how do you deal with the case when you do need to extend a line that isn’t extensible by itself? This is another place where you can use the backtick escape character. If the last character in the line is a backtick then the newline will be treated as simple breaking space instead of as a new line: PS (1) > write-output ` >> -inputobject ` >> "Hello world" >> Hello world PS (2) > Finally, one thing that surprises some people is that strings are not terminated by a newline character. Strings can carry over multiple lines until a matching, closing quote is encountered: PS (1) > write-output "Hello >> there >> how are >> you?" >> Hello there how are you? PS (2) > In this example, you could see a string that extended across multiple lines. When that string was displayed, the newlines were preserved in the string. The handling of end-of-line characters in PowerShell is another of the tradeoffs we had to make for PowerShell to be useful as a shell. Although the handling of end-of-line characters is a bit strange compared to non-shell languages, the overall result is easy for most people to get used to. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

55

2.4.5 Comment Syntax in PowerShell
Every computer language has some mechanism for annotating code with expository comments. Like many other shells and scripting languages, PowerShell comments begin with a ‘number sign’ (‘#”) symbol and continue to the end of the line. The ‘#’ character must be at the beginning of a token for it to start a comment. Here’s an example that illustrates what this means: PS (1) > echo hi#there hi#there In this example, the number sign is in the middle of the token “hi#there” and so is not treated as starting of a comment. In the next example, there is a space before the number sign. PS (2) > echo hi #there hi Now the ‘#’ it is treated as starting a comment and the following text is not displayed. Of course it can be preceded by characters other than a space and still start a comment. It can be preceded by any statement-terminating or expression-terminating character like a bracket, brace or semicolon as shown in the next couple of examples: PS (3) > (echo hi)#there hi PS (4) > echo hi;#there hi In both of these examples, the # symbol indicates the start of a comment. Finally, you need to take into account whether you are in expression mode or command mode. In command mode, as shown in the next example, the + symbol is included in the token “hi+#there”. PS (5) > echo hi+#there hi+#there But in expression mode, it’s parsed as its own token. Now the "#” indicates the start of a comment and the overall expression results in an error. PS (6) > "hi"+#there You must provide a value expression on the right-hand side of the '+' operator. At line:1 char:6 + "hi"+ get-command out-* | ft name

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

64 Name ---Out-Default Out-File Out-GridView Out-Host Out-Null Out-Printer Out-String Here we have a somewhat broader range of choices. We’ve already talked about Out-Default. The next one we’ll talk about is Out-Null. This is a simple outputter; anything sent to Out-Null is simply discarded. This is useful when you don’t care about the output for a command; you want the side effect of running the command. For example, the mkdir command outputs a listing of the directory it just created. PS (1) > mkdir foo Directory: Microsoft.PowerShell.Core\FileSystem::C:\Temp Mode ---d---LastWriteTime ------------6/25/2006 8:50 PM Length Name ------ ---foo

If you don’t want to see this output, pipe it to Out-Null. First remove the directory we created, then create the directory. PS (2) > rmdir foo PS (3) > mkdir foo | out-null PS (4) > get-item foo Directory: Microsoft.PowerShell.Core\FileSystem::C:\Temp

Mode LastWriteTime Length Name --------------------- ---d---6/25/2006 8:50 PM foo And finally, since we didn’t get the message, we verify that the directory was actually created.

AUTHOR'S NOTE
For the I/O redirection fans in the audience; piping to Out-Null is essentially equivalent to doing redirecting to $null. So

mkdir foo | out-null is equivalent to

mkdir foo > $null Next we have Out-File. Instead of sending the output to the screen, this sends it to a file.
(This command is also used by I/O redirection when doing output to a file.) In addition to writing the formatted output, Out-File has several flags that control how this output is written. These flags include the ability to append to a file instead of replacing it, to force writing to read-only files, and to choose the output encodings for the file. This last item is the trickiest one. You can choose from a number of the text encodings supported by Windows. Since I can

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

65 never remember what they all are, here’s a trick: enter the command with an encoding that you know doesn’t exist:

PS (9) > out-file -encoding blah Out-File : Cannot validate argument "blah" because it does not belong to the set "unicode, utf7, utf8, utf32, ascii, b igendianunicode, default, oem". At line:1 char:19 + out-file -encoding 2 + 3.0 + "4" 9 PS (2) > (2 + 3.0 + "4").GetType().FullName System.Double As you can see from the result, everything was widened to a double-precision floating point number. (Widening means converting to a representation that can handle larger or wider numbers: a [long] is wider than an [int], and so forth.) Now let’s be a bit trickier. Let’s put the floating point number into quotes this time. PS (3) > 2 + "3.0" + 4 9 PS (4) > (2 + "3.0" + 4).GetType().FullName System.Double Once again the system determines that the expression has to be done in floating point.

NOTE
The .NET single-precision floating point representation isn’t typically used unless you request it. In PowerShell, there usually isn’t a performance benefit for using single precision, so there is no reason to use this less precise representation.

Now let’s look at some simple examples that involve only integers. As you would expect, all these operations result in integers as long as the result can be represented as an integer. PS (5) > (3 + 4) 7 PS (6) > (3 + 4).GetType().FullName System.Int32 PS (7) > (3 * 4).GetType().FullName System.Int32 Let’s try an example using the division operator: PS (8) > 6/3 2 PS (9) > (6/3).GetType().FullName System.Int32 Since 6 is divisible by 3, the result of this division is also an integer. But what happens if the divisor isn’t a factor? Let’s try it and see. PS (10) > 6/4 1.5 PS (11) > (6/4).GetType().FullName System.Double The result is now a [double]. The system noticed that there would be a loss of information if the operation were performed with integers, so it’s executed using doubles instead. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

72 Finally, let’s try some examples using scientific notation. Let’s add an integer to a large decimal. PS (10) > 1e300 1E+300 PS (11) > 1e300 + 12 1E+300 The operation executed with the result being a double. In effect, adding an integer to a number of this magnitude means that the integer is ignored. This sort of loss is considered “OK” by the system. But there is another numeric type that is designed to be precise: System.Decimal. Normally you only use this type when you really care about the precision of the result. Let’s try the previous example, adding a decimal instead of an integer. PS (12) > 1e300 + 12d Cannot convert "1E+300" to "System.Decimal". Error: "Value was either too large or too small for a Decimal." At line:1 char:8 + 1e300 + 'This is a string in single quotes' This is a string in single quotes PS (3) > Literal strings can contain any character including newlines, with the exception of an unquoted closing quote character. In double-quoted strings, to embed the closing quote character, you have to either quote it with the backtick character or double it up. In other words, two adjacent quotes become a single literal quote in the string. In single-quoted strings, doubling up the quote is the only way to embed a literal quote in the string. This is one area where there is an important difference between single- and double-quoted strings. In single-quote strings, the backtick is not special. This means that it can’t be used for embedding special characters such as newlines or escaping quotes.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

76 Like the UNIX shells, PowerShell supports variable substitutions. These variable

substitutions or expansions are only done in double-quoted strings (which is why these are sometimes called expandable strings).

NOTE
Arguments to commands are treated as though they were in double quotes, so variables will be expanded in that situation as well. We’ll see examples of this later on.

Let’s look at an example of string expansion: PS (1) > $foo = "FOO" PS (2) > "This is a string in double quotes: $foo" This is a string in double quotes: FOO PS (3) > 'This is a string in single quotes: $foo' This is a string in single quotes: $foo PS (4) > In the preceding lines, you can see that $foo in the double-quoted string was replaced by the contents of the variable "FOO", but not in the single-quoted case. SUBEXPRESSION EXPANSION IN STRINGS Expandable strings can also include arbitrary expressions by using the subexpression notation. A subexpression is a fragment of PowerShell script code that is replaced by the value resulting from the evaluation of that code. Here are some examples of subexpression expansion in strings. PS (1) > 2+2 is 4 PS (2) > PS (3) > 3 * 2 is PS (4) > "2+2 is $(2+2)" $x=3 "$x * 2 is $($x * 2)" 6

The expression in the $( ... ) sequence in the string is replaced by the result of evaluating the expression. $(2+2) is replaced by 4 and so on. USING COMPLEX SUBEXPRESSIONS IN STRINGS So far, these examples show only simple embedded expression. In fact, subexpressions allow statement lists—a series of PowerShell statements separated by semicolons—to be embedded. Here’s an example where the subexpression contains three simple statements. First let’s just execute the three simple statements: PS (1) > 1;2;3 # three statements 1 2 3 Now let’s execute the same set of statements in a subexpression expansion: PS (2) > "Expanding three statements in a string: $(1; 2; 3)" Expanding three statements in a string: 1 2 3 PS (3) > The result shows the output of the three statements concatenated together, space--separated, and inserted into the result string. Here’s another example of using a for statement in a subexpression expansion. PS (1) > "Numbers 1 thru 10: $(for ($i=1; $i -le 10; $i++) { $i })." Numbers 1 thru 10: 1 2 3 4 5 6 7 8 9 10. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

77 PS (2) > The output of all the iterations for the loop are gathered up, turned into a string with one value separated from the next by a space, and then substituted into overall string. As you can see, this can be quite powerful. Using a subexpression in a string is one way to quickly generate formatted results when presenting data. STRING EXPANSION CONSIDERATIONS PowerShell expands strings when an assignment is executed. It doesn’t re-evaluate those strings when the variable is used later on. This is an important point. Let’s look at two examples that will make this clear. In these examples, we’ll use the post-increment operator

++, which adds one to a variable, and the range operator, which expands to a sequence of numbers. Let’s take a look. In the first example, we initialize $x to 0 and then assign a string with an expansion that increments $x to a variable $a. Next we’ll output $a three times to see what happens to the value of $x.
PS (1) > $x=0 PS (2) > $a = "x is $($x++; $x)" PS (4) > 1..3 | foreach {$a} x is 1 x is 1 x is 1 As you can see, $x was incremented once when $a was assigned, but didn’t change on subsequent references. Now let’s inline the string literal into the body of the loop and see what happens. PS (5) > 1..3 | foreach {"x is $($x++; $x)"} x is 1 x is 2 x is 3 This time around, we can see that $x is being incremented each time. To reiterate, string literal expansion is done only when the literal is assigned.

NOTE
There actually is a way to force a string to be re-expanded if you really need to do it. You can do this by calling

$ExecutionContext.InvokeCommand.ExpandString( 'a is $a' )
This method will return a new string with all of the variables expanded.

HERE-STRING LITERALS Getting back to the discussion of literal string notation, there is one more form of string literal, called a here-string. A here-string is used to embed large chunks of text inline in a script. This can be powerful when you’re generating output for another program. Here’s an example that assigns a here-string to the variable $a. PS (1) > $a = @" >> Line one >> Line two >> Line three >> "@ >> ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

78 PS (2) > $a Line one Line two Line three

AUTHOR'S NOTE
Here's a note for C# users. There is a lexical element in C# that looks a lot like PowerShell here-strings. In practice, the C# feature is most like PowerShell’s single-quoted strings. In PowerShell, a here-string begins at the end of the line and the terminating sequence must be at the beginning of the line that terminates the here-string. In C#, the string terminates at the first closing quote that isn’t doubled up.

When $a is displayed, it contains all of the lines that were entered. Now you’re probably saying, “Wait a minute—you told me I can do the same thing with a regular string. What makes here-strings so special?” It has to do with how quoting is handled. Here-strings have special quoting rules. Here-strings start with @ and end with @. The

are important because the here-string quote sequences won’t be treated as quotes without the newlines. The content of the here-string is all of the lines between the beginning and ending quotes, but not the lines the quotes are on. Because of the fancy opening and closing quote sequences, other special characters such as quotes that would cause problems in regular strings are fine here. This makes it easy to generate string data without having quoting errors. Here is a more elaborate example PS (1) > $a = @" >> One is "1" >> Two is '2' >> Three is $(2+1) >> The date is "$(get-date)" >> "@ + "A trailing line" >> PS (2) > $a One is "1" Two is '2' Three is 3 The date is "1/8/2006 9:59:16 PM"A trailing line PS (3) > On line 1, the here-string is assigned to the variable $a. The contents of the here-string start on line 2. which has a string containing double quotes. Line 3 has a string with single quotes. Line 4 has an embedded expression, and line 5 calls the Get-Date cmdlet in a subexpression to embed the current date into the string. Finally, line 6 appends some trailing text to the whole string. When you look at the output of the variable shown in lines 9-12, you see that the quotes are all preserved and the expansions are shown in place. Here-strings come in single and double-quoted versions just like regular strings, with the significant difference being that variables and subexpressions are not expanded in the singlequoted varient as shown here: PS (1) > $a=123 PS (2) > @" ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

79 >> a is $a >> "@ >> a is 123 In the double-quoted here-string, the variable $a is expanded, but in the single-quoted here string: PS (3) > @' >> a is $a >> '@ >> a is $a PS (4) > it isn't. The single-quoted version is best for embedding large blocks of literal text where you don't what to have to deal with individually quoting '$' everywhere. We'll see how useful this can be when we look at the Add-Type cmdlet in chapter 9. That should be enough about strings for now. Let's move on to numbers and numeric literals. This will finally let us express that "10 megabytes" value we wanted to compare against earlier.

3.2.2 Numbers and numeric literals
As mentioned earlier, PowerShell supports all the basic .NET numeric types and performs conversions to and from the different types as needed. Table 3.2 lists these numeric types.

Table 3.2 Numeric Literals
Example Numeric Literal .NET Full Type Name Short Type Name

1 0x1FE4 10000000000 10l 1.1 1e3 There is no singleprecision numeric literal but you can use a cast: [float] 1.3 1d 1.123d

System.Int32

[int]

System.Int64

[long]

System.Double System.Single

[double] [single] or [float]

System.Decimal

[decimal]

Now that we know the basic numeric types, we need to understand how are literals of each type are specified. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

80

SPECIFYING NUMERIC LITERALS In general, you don’t specify a literal having a particular type; the system will figure out the best way to represent the number. By default, an integer will be used. If the literal is too large for a 32-bit integer, a 64-bit integer will be used instead. If it’s still too big or if it contains a decimal point, a System.Double will be used. (System.Single - single precision floating point is not used for numeric literals as it offers no advantages and just complicates the process.) The one case where you do want to tell the system that you’re requesting a specific type is with the System.Decimal type. These are specified by placing a “d” at the end of the number with no intervening space, as shown: PS (1) > ( 123 ).gettype().fullname System.Int32 PS (2) > ( 123d ).gettype().fullname System.Decimal PS (3) > ( 123.456 ).gettype().fullname System.Double PS (4) > ( 123.456d ).gettype().fullname System.Decimal You can see that in each case where there is a trailing “d”, the literal results in a [decimal] value being created. (If there is a space between the number and the “d”, you’ll just get an error.) THE MULTIPLIER SUFFIXES Of course, plain numbers are fine for most applications, but in the system administration world, there are many special values that you want to be able to conveniently represent, namely those powers of two—kilobytes, megabytes, gigabytes, terabytes and petabytes (terabyte and petabyte suffices aren't available in PowerShell V1). PowerShell provides a set of multiplier suffixes for common sizes to help with this, as listed in table 3.3. These suffixes allow you to easily express common very large numbers.

Table 3.3 The numeric multiplier suffixes supported in PowerShell. Suffixes marked V2 only are only available in version 2 or PowerShell
Multiplier Suffix kb or KB Multiplication Factor 1024 Example 1KB Equivalent Value 1024 .NET Type System.Int32

kb or KB mb or MB mb or MB

1024 1024*1024 1024*1024

2.2kb 1Mb 2.2mb

2252.8 1048576 2306867.2

System.Double System.Int32 System.Double

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

81 gb or GB 1024*1024*1024 1Gb 1073741824 System.Int32

gb or GB

1024*1024*1024

2.14gb

2297807503.36

System.Double

tb or TB (V2 only) tb or TB (V2 only) pb or PB (V2 only) pb or PB (V2 only)

1024*1024*1024* 1024 1024*1024*1024* 1024 1024*1024*1024* 1024*1024 1024*1024*1024* 1024*1024

1tb

1099511627776

System.Int64

2.14TB

2352954883440.64

System.Double

1pb

1125899906842624

System.Int64

2.14PB

2.40942580064322E+15

System.Int64

NOTE
Here's a note about standards. Yes, the PowerShell team is aware that these notations are not consistent with the IEC recommendations (kibabyte, and so on). Since the point of this notation is convenience and most people in the IT space are more comfortable with Kb than with Ki, we choose to err on the side of comfort over conformance in this one case. Sorry. This particular issue generated easily the second most heated debate on the PowerShell internal and external beta tester lists. We’ll cover the most heated debate later when we get to the comparison operators.

HEXADECIMAL LITERALS The last item we cover in this section is hexadecimal literals. When working with computers, it’s obviously useful to be able to specify hex literals. PowerShell uses the same notation as C, C#, and so on; namely preceding the number with the sequence “0x” and allowing the letters A-F as the extra digits. As always, the notation is case-insensitive as shown in the following examples. PS (1) > 0x10 16 PS (2) > 0x55 85 PS (3) > 0x123456789abcdef 81985529216486895 PS (4) > 0xDeadBeef -559038737 Now that we’ve covered the “basic” literals, strings and numbers, let’s move on to the more interesting and less common ones. This is one of the areas where the power of scripting ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

82 languages really shines through. These literals let you express complex configuration data, inline in your script, in a very clear and direct fashion. This, in turn, means that you don't have use an external data language like XML or .INI files to encode this configuration data. PowerShell lets you express this information in PowerShell itself.

3.3 Collections: dictionaries and hashtables
Perhaps the most flexible data type in PowerShell is the hashtable. This data type lets you map a set of keys to a set of values. For example, we may have a hashtable that maps “red” to 1, “green” to 2, and “yellow” to 4.

NOTE
A dictionary is the general term for a data structure that maps keys to values. In the .NET world, this takes the form of an interface (System.Collections.IDictionary) that describes how a collection should do this mapping. A hashtable is a specific implementation of that interface. While the PowerShell hashtable literal syntax only creates instances of

System.Collections.Hashtable, scripts that you write will work properly with any object that implements IDictionary.

3.3.1 Creating and inspecting hashtables
In PowerShell, you use hash literals to create a hashtable inline in a script. Here is a simple example: PS (26) > $user = @{ FirstName = "John"; LastName = "Smith"; >> PhoneNumber = "555-1212" } PS (27) > $user Key --LastName FirstName PhoneNumber Value ----Smith John 555-1212

This example created a hashtable that contained three key-value pairs. The hashtable starts with the token “@{” and ends with “}”. Inside the delimiters, you define a set of key-value pairs where the key and value are separated by an equals sign “=”. Formally, the syntax for a hash literal is = '@{' '=' '=' ] * '}' = ';' | [

Now that we’ve created a hashtable, let’s see how we can use it. PowerShell allows you to access members in a hashtable in two ways—through property notation and through array notation. Here’s what the property notation looks like:

PS (3) > $user.firstname John PS (4) > $user.lastname Smith ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

83 This notation lets you treat a hashtable like an object. This access method is intended to facilitate the use of hashtables as a kind of lightweight data record. Now let’s look at using the array notation.

PS (5) > $user["firstname"] John PS (6) > $user["firstname","lastname"] John Smith Property notation works pretty much the way you’d expect; you specify a property name and get the corresponding value back. Array notation, on the other hand, is more interesting. In the second command in the example, we provided two keys and got two values back. Here’s an example that shows some additional features of the underlying hashtable object. The underlying object for PowerShell hashtables is the .NET type

System.Collections.Hashtable. This type has a number of properties and methods that you can use. One of these properties is the keys property. This property will give you a list of all of the keys in the hashtable. PS (7) > $user.keys LastName FirstName PhoneNumber In the array access notation, you can use keys to get a list of all of the values in the table. PS (8) > $user[$user.keys] Smith John 555-1212

NOTE
A more efficient way to get all of the values from a hashtable is to use the Values property. The point of this example is to demonstrate how you can use multiple indexes to retrieve the values based on a subset of the keys.

You might have noticed that the keys property didn’t return the keys in alphabetical order. This is because of the way hashtables work; i.e., keys are randomly distributed in the table to speed up access. If you do need to get the values in alphabetical order, here’s how you can do it: PS (10) > $user.keys | sort-object FirstName LastName PhoneNumber The Sort-Object (or just sort) cmdlet sorts the keys into alphabetical order and returns a list. Let’s use this list to index the table. PS (11) > $user[[string[]] ($user.keys | sort)] John Smith ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

84 555-1212 You’ll notice something funny about the last example: we had to cast or convert the sorted list into an array of strings. This is because the hashtable keys mechanism expects strings, not objects, as keys. There’s much more on casts later in this chapter. A DIGRESSION: SORTING, ENUMERATING AND HASHTABLES Let's digress for a second and address a question that comes up sometimes when people, From the .NET perspective, they are an enumerable especially .NET programmers, first encounter hashtables in PowerShell. The question is "are hashtables a collection or a scalar". is important, collection just like arrays except they contain a collection of key/value pairs. However, and this PowerShell treats hashtables like a scalar object . It does this because, in scripting languages, hashtables are commonly used as on-the-fly structures or data records. Using hashtables this way, you don't have to predefine the fields in a record - you just make them up as you go. If PowerShell treated hashtables as an enumerable collection by default, this wouldn't be possible because every time you passed one of these "records" into a pipeline, it would be broken up into a stream of individual key/value pairs and the integrity of the original table would be lost. This causes the most problems for people when they use hashtables in the foreach statement. In a .NET language like C#, the foreach statement iterates over all of the pairs. In PowerShell, the foreach the loop will run only once because the hashtable is not considered an enumerable, at least not by default. So, if you do want to iterate over the pairs, you'll need to call the GetEnumerator() method yourself. This looks like:

PS (12) > $h = @{a=1; b=2; c=3} PS (13) > foreach ($pair in $h.GetEnumerator()) >> { >> $pair.key + " is " + $pair.value >> } >> a is 1 b is 2 c is 3 Each iteration, the next pair is assigned to $pair and processing continues.
A significant part of the reason this behavior confuses people is that when PowerShell displays a hashtable, it uses enumeration to list the key/value pairs as part of the presentation. The result is that there is no visible different between when you call GetEnumerator() in the

foreach loop and when you don't. Let's look at this. First, the no GetEnumerator() case:
PS (14) > Name ---a b c foreach ($pair in $h) { $pair } Value ----1 2 3

Now call GetEnumerator() in the loop: PS (15) > foreach ($pair in $h.GetEnumerator()) { $pair } Name ---Value -----

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

85 a b c 1 2 3

As we can see, the output is identical in both cases. This is desirable in the sense that it's a good way to present a hashtable and doesn't require effort on the user to do this. On the other hand it masks the details of what's really going on. As always, it is difficult to serve all audiences perfectly. Another aspect of the hashtable collection question is that people want to be able to "sort" a hashtable the way you'd sort a list of numbers. In the case of a hashtable, this usually means that the user wants to be able to control the order in which keys will be retrieved from the hashtable. Unfortunately this can't work because the default hashtable object that PowerShell uses has no way to store any particular key ordering in the table. The keys are just stored in random order as we saw earlier in this section. If you want to have an ordered dictionary, you'll have to use a different type of object such as:

[Collections.Generic.SortedDictionary[object,object]]
This is a sorted generic dictionary (we'll get to type literals and generics later in this chapter.) And now, back to our regularly scheduled topic.

3.2.2 Modifying and manipulating hashtables
Now let’s look at adding, changing, and removing elements from the hashtable. First let’s add the date and the city where the user lives to the $user table. PS (1) > $user.date = get-date PS (2) > $user Key Value ------LastName Smith date 1/15/2006 12:01:10 PM FirstName John PhoneNumber 555-1212 PS (3) > $user["city"] = "Seattle" PS (4) > $user Key Value ------city Seattle LastName Smith date 1/15/2006 12:01:10 PM FirstName John PhoneNumber 555-1212 A simple assignment using either the property or array accessor notation allows you to add an element to a hashtable. Now let’s say we got the city wrong—John really lives in Detroit. Let’s fix that. PS (5) > $user.city = "Detroit" PS (6) > $user Key Value ------city Detroit LastName Smith date 1/15/2006 12:01:10 PM FirstName John PhoneNumber 555-1212 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

86 As this example shows, simple assignment is the way to update an element. Finally, we don’t really want this element, so let’s remove it from the table with the remove() method. PS (7) > $user.remove("city") PS (8) > $user Key Value ------LastName Smith date 1/15/2006 12:01:10 PM FirstName John PhoneNumber 555-1212 The hashtable no longer contains the element. If you want to create an empty hashtable, use @{ } with no member specifications between the braces. This creates an empty table that you can then add members to incrementally. PS (1) > $newHashTable = @{} PS (2) > $newHashTable PS (3) > $newHashTable.one =1 PS (4) > $newHashTable.two = 2 PS (5) > $newHashTable Key --two one members are created on assignment. Value ----2 1

In the example, there were no members initially; we added two by making assignments. The

3.2.3 Hashtables as reference types
Hashtables are reference types, so if you create a hashtable, assign it to a variable $foo, and assign $foo to another variable $bar, you will have two variables that point to or reference the same object. Consequently, any changes that are made to one variable will affect the other, because they’re pointing to the same object. Let’s try this out. Create a new hashtable and assign PS (2) >> a = >> b = >> c = >> } >> PS (3) Key --a b c it to $foo. > $foo = @{ 1 2 3

> $foo Value ----1 2 3

Now assign $foo to $bar and verify that it matches $foo as we expect. PS (4) > $bar = $foo PS (5) > $bar Key --a b c

Value ----1 2 3

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

87 Next assign a new value to the element "a" in $foo. PS (6) > $foo.a = "Hi there" PS (7) > $foo.a Hi there And let’s look at what happened to $bar: PS (8) > $bar.a Hi there PS (9) > $bar Key Value ------a Hi there b 2 c 3 The change that was made to $foo has been reflected in $bar. Now if we want to make a copy of the hashtable instead of just copying the reference, we can use the Clone() method on the object to do this: PS (1) > $foo=@{a=1; b=2; c=3} PS (2) > $bar = $foo.Clone() Now let's change the "a" member in the table. PS (3) > $foo.a = "Hello" and verify that the table in $foo hash changed. PS (4) > $foo.a Hello but the hashtable in $bar has not PS (5) > $bar.a 1 since it is a copy not a reference. This technique can be useful if we're creating a number of tables that are mostly the same. We can create a "template" table, make copies and then change the pieces we need to. There is still more to know about hashtables and how they work with operators, but we’ll cover that in chapters 4 and 5. For now, we’ll move on to the next data type.

3.4 Collections: arrays and sequences
In the previous section, we talked about hashtables and hash literals. Now let’s talk about the PowerShell syntax for arrays and array literals. Most programming languages have some kind of array literal notation similar to the PowerShell hash literal notation, where there is a beginning character sequence followed by a list of values, followed by a closing character sequence. Here’s how array literals are defined in PowerShell: They’re not. There is no array literal notation in PowerShell. Yes, you read that correctly. There is no array literal notation in PowerShell. So how exactly does this work? How do you define an inline array in a PowerShell script? Here’s how to do it: instead of having array literals, there is a set of operations that create collections as needed. In fact, collections of objects are created and discarded transparently throughout PowerShell. If you need an array, one will be created for you. If you need a singleton (or scalar) value, the collection will be unwrapped as needed.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

88

3.4.1 Collecting pipeline output as an array
The most common operation resulting in an array in PowerShell is collecting the output from a pipeline. When you run a pipeline that emits a sequence of objects and assign that output to a variable, it automatically collects the elements into an array, specifically into a .NET object of type [object[]]. But what about building a simple array in an expression? The simplest way to do this is to use the comma operator (“,”). For example, at the command line, type 1,2,3 and you’ll have created a sequence of numbers. (See chapter 5 for more information about using the comma operator.) When you assign that sequence to a variable, it is stored as an array. Let’s assign these three numbers to a variable $a and look at the type of the result. PS (1) > $a = 1,2,3 PS (2) > $a.gettype().fullname System.Object[] As in the pipeline case, the result is stored in an array of type [object[]].

3.4.2 Array indexing
Let’s explore some of the operations that can be performed on arrays. As is commonly the case, getting and setting elements of the array (array indexing) is done with square brackets. The length of an array can be retrieved with the Length property. PS (3) > $a.length 3 PS (4) > $a[0] 1 Note that arrays in PowerShell are origin-zero; that is, the first element in the array is at index 0, not index 1. As the example showed, the first element of $a is in $a[0]. As with hashtables, changes are made to an array by assigning new values to indexes in the array. In the following example, we’ll assign new values to the first and third elements in $a. PS (5) > $a[0] = 3.1415 PS (6) > $a 3.1415 2 3 PS (7) > $a[2] = "Hi there" PS (8) > $a 3.1415 2 Hi there PS (9) > Looking at the output, we can see that elements of the array have been changed. Simple assignment updates the element at the specified index.

3.4.3 Polymorphism in arrays
Another important thing to note from the previous example is that arrays are polymorphic by default. By polymorphic we mean that you can store any type of object in an array. (A VBScript user would call these variant arrays). When we created the array, we assigned only integers to it. In the subsequent assignments, we assigned a floating-point number and a string. The ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

89 original array was capable of storing any kind of object. In formal terms, PowerShell arrays are polymorphic by default (though it is possible to create type-constrained arrays). Earlier we saw how to get the length of an array. What happens when we try to assign to an element past the end of the array? The next example illustrates this.

PS (9) > $a.length 3 PS (10) > $a[4] = 22 Array assignment failed because index '4' was out of range. At line:1 char:4 + $a[4 $a.length 5 PS (13) > $a[4] 33 PS (14) > So the length of the array in $a is now five. The addition operation did add elements. Here’s how this works: First PowerShell creates a new array large enough to hold the total number of elements. Then it copies the contents of the original array into the new one. Finally it copies the new elements into the end of the array. We didn’t add any elements to the original array after all. Instead we created a new, larger one.

3.4.4 Arrays as reference types
This copying behavior has some interesting consequences. Let’s explore this further. First create a simple array and look at the value. We’ll use string expansion here so that the values in the variable are all displayed on one line. PS (1) > $a=1,2,3 PS (2) > "$a" 1 2 3 Now assign $a to a new variable $b and check that $a and $b have the same elements. PS (3) > $b = $a PS (4) > "$b" 1 2 3 Next change the first element in $a. PS (5) > $a[0] = "Changed" PS (6) > "$a" Changed 2 3 Yes, the first element in $a was changed. But what about $b? Let’s examine it now. PS (7) > "$b" Changed 2 3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

90 It was also changed. As with hashtables, array assignment is done by reference. When we assigned $a to $b, $b we got a copy of the reference to the array instead of a copy of contents of the array. Let’s add a new element to $b. PS (8) > $b += 4 PS (9) > "$b" Changed 2 3 4

$b is now four elements long. As we’ve just discussed, due to the way array concatenation works, $b contains a copy of the contents of the array instead of a reference. If we change $a now, it won’t affect $b. Let’s verify that: PS (10) > $a[0] = "Changed again" PS (11) > "$a" Changed again 2 3 PS (12) > "$b" Changed 2 3 4 We see that PS (13) PS (14) Changed 1 2 3 4 PS (15)

$b was in fact not changed. Conversely, changing $b should have no effect on $a.
> $b[0] = 1 > "$a"; "$b" again 2 3 >

Again, there was no change. To reiterate, arrays in PowerShell, like arrays in other .NET languages, are reference types, not value types. When you assign them to a variable, you get another reference to the array, not another copy of the array.

3.4.5 Singleton arrays and empty arrays
Returning to array literals, we saw how to use the comma operator to build up an array containing more than one element. You also use the comma operator as a prefix operator to create PS 1 PS 1 PS an array containing only one element. The next example shows this: (1) > , 1 (2) > (, 1).length (3) >

In the example we made an array containing a single element “1”. How about empty arrays? The comma operator always takes an argument to work on. Even using $null as an argument to the comma operator will result in a one-element array containing the $null reference. Empty arrays are created through a special form of sub-expression notation that uses the “@” symbol instead of the “$” sign to start the expression. Here’s what it looks like: PS (3) > @() PS (4) > @().length 0 PS (5) > In the preceding example, we created an array of length 0. In fact, this notation is more general. It takes the result of the expression it encloses and ensures that it is always returned as an array. If the expression returns $null or a scalar value, it will be wrapped in a oneelement array. Given this behavior, the other solution to creating an array with one element is: PS (1) > @(1) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

91 1 PS (2) > @(1).length 1 That is, you place the value you want in the array in @( ... ) and you get an array back. This notation is used when you don’t know if the command you’re calling is going to return an array or not. By executing the command in this way, you are guaranteed to get an array back. Note that if what you’re returning is already an array, it won’t be wrapped in a new array. Compare this to the use of the comma operator. PS (1) > 1,2,3 1 2 3 PS (2) > (1,2,3).Length 3 PS (3) > ( , (1,2,3) ).Length 1 PS (4) > ( @( 1,2,3 ) ).Length 3 On line 1 of the example, we created a regular array. On line 5, we get the length and we see that it’s 3. Next on line 7, we apply the prefix operator to the array and then get the length. The result now is only 1. This is because the unary comma operator always wraps its arguments in a new array. Finally, on line 9, we use the @( ... ) notation and then get the length. This time it remains three. The @( ... ) sequence doesn’t wrap unless the object is not an array. Now let's look at the last type of literal: the type literal. Because object types are so important in PowerShell (just like real life), we need to be able to express types in a script. Remember with numbers, when we wanted to say "get me all of the files larger than 10 megabytes" we needed numeric literals? The same concept applies to types. If we want to be able to say "get me all of the objects of a particular type" we need to be able to express that type in the script.

3.5 Type literals
In earlier sections, we showed a number of things that looked like [type]. These are the type literals. In PowerShell, you use type literals a variety of ways. You use them to specify a particular type. They can be used as operators in a cast (a operation that converts an object from one type to another), as part of a type-constrained variable declaration (see chapter 4), or as an object itself. Here’s an example of a cast using a type literal: $i = [int] "123" In this example, we are casting or converting a string into a number, specifically an instance of primitive .NET type System.Int32. In fact, we could use the longer .NET type name to accomplish the same thing: $i = [System.Int32] "123" Now let's look at something a bit more sophisticated. If we wanted to make this into an array of integers, we would do $i = [int[]][object[]] "123" In this example, we’re not just casting the basic type, we’re also changing it from a scalar object to an array. Notice that we had to do this in two steps. In the first step, we converted it into a collection but without changing the element type. In the second step, we convert the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

92 types of the individual elements. This follows the general type converter rule that no more than one conversion will be performed in a single step. This rule makes it much easier to predict what any given conversion will do.

AUTHOR'S NOTE
However, in this particular case, converting a scalar value into an array is so common that we added support doing this directly in PowerShell V2. In V2, you can simply say

$i = [int[]] "123"

3.5.1 Type name aliases
Obviously, the shorter type name (or type alias as it’s known) is more convenient. Table 3.4 lists all of the type aliases defined in PowerShell and the .NET types they correspond to. It also indicates which version of PowerShell the alias is available in. (Another change that was made in version 2 is that there are no longer separate aliases for arrays of the base type. As a result, these aren't shown in the table as they were in the first version of the book.) Anything in the

System.Management.Automation namespace is specific to PowerShell and will be covered in later chapters in this book. The other types are core .NET types and are covered in the Microsoft Developers Network (MSDN) documentation.

AUTHOR'S NOTE
When PowerShell resolves a type name, first it checks the type name alias table, then it checks to see if a type exists whose full name matches the string specified and finally it prepends the type with "system." and checks to see if a type exists that matches the new string. This means that things that are in the System namespace look like they might be aliased. For example, the type System.IntPtr can be referred to as [intpr] even though it's not in the alias table. For the most part, this is transparent. The one time it does matter is if, for some reason. a type was added to the system that lives in the top-level name space. In this case, [intptr] would refer to the new type and we'd have to use

[system.intptr] to refer to the system type. This should never happen of course because types should always be in names spaces.

Table 3.4 PowerShell type aliases and their corresponding .NET types
Type Alias [int] [long] Corresponding .NET Type System.Int32 System.Int64 Version V1 & 2 V1 & 2

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

93

[string] [char] [bool] [byte] [double] [decimal] [float] [single] [regex] [array] [xml] [scriptblock] [switch] [hashtable] [ref] [type] [psobject]

System.String System.Char System.Boolean System.Byte System.Double System.Decimal System.Single System.Single System.Text.RegularExpressions.Regex System.Array System.Xml.XmlDocument System.Management.Automation.ScriptBlock System.Management.Automation.SwitchParameter System.Collections.Hashtable System.Management.Automation.PSReference System.Type System.Management.Automation.PSObject

V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V2 V2 V2 V2

[pscustomobject] System.Management.Automation.PSObject [psmoduleinfo] [powershell] System.Management.Automation.PSModuleInfo System.Management.Automation.PowerShell

[runspacefactory] System.Management.Runspaces.RunspaceFactory

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

94

[runspace] [ipaddress] [wmi] [wmisearcher] [wmiclass] [adsi] [adsisearcher]

System.Management.Automation.Runspaces.Runspace V2 System.Net.IPAddress System.Management.ManagementObject System.Management.ManagementClass System.Management.ManagementClass System.DirectoryServices.DirectoryEntry System.DirectoryServices.DirectorySearcher V2 V1 & 2 V1 & 2 V1 & 2 V1 & 2 V1 & 2

3.5.2 Generic Type Literals
There is a special kind of type in .NET called a generic type. The idea with a generic type is that you can say something like "a list of strings" instead of just "a list". And while you could do this without generics, you'd have to create a specific type for type of list. With generics, you create one generic list type (hence the name) and then parameterize it with the type it can contain.

AUTHOR'S NOTE
Generic type literal support was added in V2. In V1, while it was possible to express a type literal, it was very painful. We'll see how to do this later on in chapter XX.

Here's

an

example

that

shows

the

type

literal

for

a

generic

list

(System.Collections.Generic.List) of integers: PS (1) > [system.collections.generic.list[int]] | ft -auto IsPublic IsSerial Name BaseType -------- -------- ----------True True List`1 System.Object If you look at the type literal, it's pretty easy to see how the collection element type is expressed: [int] . This is essentially a nested type literal where the type parameter is enclosed in nested square brackets. Let's create an instance of this type: PS (2) > $l = new-object system.collections.generic.list[int] and now we can add some elements to it, PS (3) > $l.add(1) PS (4) > $l.add(2) get the count of elements added and list the elements PS (5) > $l.count 2 PS (6) > $l 1 2 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

95 Try and add something that isn't an integer: PS (7) > $l.add("hello") Cannot convert argument "0", with value: "hello", for "Add" to t ype "System.Int32": "Cannot convert value "hello" to type "Syste m.Int32". Error: "Input string was not in a correct format."" at line:1 char:7 $l.add [system.collections.generic.dictionary[string,int]] | ft -auto

IsPublic IsSerial Name BaseType -------- -------- ----------True True Dictionary`2 System.Object where the two type parameters are separated by a comma inside the square brackets. Now let’s take a trip into the “too-much-information” zone and look in detail at the process PowerShell uses to perform all of these type conversions. On first reading, you'll probably want to skim this section but read it in detail at later on when you're more comfortable with PowerShell. This is a "spinach" section - you may not like it, but it's good for you. The primary uses for type literals are in performing type conversions and invoking static methods. We’ll look both of these uses in the next two sections.

3.5.3 Accessing static members with type literals
As mentioned, a common use for type literals is for accessing static methods on .NET classes. This will also be covered later on in detail, but here’s a quick taste. You can use the Get-Member cmdlet to look at the members on an object. To look at the static members, use the -Static flag as shown: PS (1) > [string] | get-member -static TypeName: System.String Name ---Compare CompareOrdinal Concat Copy Equals Format MemberType ---------Method Method Method Method Method Method Definition ---------static System.Int32 Compare(String... static System.Int32 CompareOrdinal... static System.String Concat(Object... static System.String Copy(String str) static System.Boolean Equals(Strin... static System.String Format(String...

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

96 Intern IsInterned IsNullOrEmpty Join op_Equality op_Inequality ReferenceEquals Empty Method Method Method Method Method Method Method Property static static static static static static static static System.String Intern(String... System.String IsInterned(St... System.Boolean IsNullOrEmpt... System.String Join(String s... System.Boolean op_Equality(... System.Boolean op_Inequalit... System.Boolean ReferenceEqu... System.String Empty {get;set;}

This will dump out all of the static members on the .NET System.String class. If you want to call one of these methods, you need to use the “::” operator. Let’s use the join method to join an array of string. First create the array: PS (2) > $s = "one","two","three" Then use the join method to join all of the pieces into a single string with plus signs in between: PS (3) > [string]::join(' + ', $s) one + two + three PS (4) > EXAMPLE: USING ADVANCED MATH FUNCTIONS A good example of the power of static methods is the [math] class. This class— [System.Math] —is a pure static class. This means that you can’t actually create an instance of it—you can only use the static methods it provides. Again, let’s use the Get-Member cmdlet to look at the methods. Here’s a truncated listing of the output you would see: PS (1) > [math] | get-member -static TypeName: System.Math Name ---Abs Acos Asin Atan Atan2 : : Sqrt Tan Tanh Truncate E PI Method Method Method Method Property Property static static static static static static System.Double Sqrt(Double d) System.Double Tan(Double a) System.Double Tanh(Double v... System.Decimal Truncate(Dec... System.Double E {get;} System.Double PI {get;} MemberType ---------Method Method Method Method Method Definition ---------static System.Single static System.Double static System.Double static System.Double static System.Double

Abs(Single va... Acos(Double d) Asin(Double d) Atan(Double d) Atan2(Double ...

As you can see, it contains a lot of useful methods and properties. For example, it contains useful constants like Pi and e as static properties: PS (2) > [math]::Pi 3.14159265358979 PS (3) > [math]::e 2.71828182845905 PS (4) > There are also all of the trigonometric functions: PS (4) > [math]::sin(22) -0.00885130929040388 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

97 PS (5) > [math]::cos(22) -0.999960826394637 PS (6) > As we’ve said, types in PowerShell provide tremendous power and breadth of capabilities. In many cases, before rolling your own solution, it’s worth browsing the Microsoft Developers Network (MSDN) documentation on the .NET libraries to see if there is something you can use to solve your problems. Now that we’ve seen the types, let’s look at how PowerShell does type conversions.

3.6 Type conversions
In the previous section, we introduced type literals and the major datatypes used in PowerShell. But how do all of these types work together? This is a critical question we had to address in designing PowerShell. In shell languages, there is usually only string data, so you never have to worry about things being of the wrong type. So how could we achieve this “typeless” behavior in PowerShell? The answer was a comprehensive system for handling type conversions automatically. Automatic type conversion is the “secret sauce” that allows a strongly typed language like PowerShell to behave like a "typeless" command-line shell. Without a comprehensive type conversion system to map the output of one command to the input type required by another command, PowerShell would be nearly impossible to use as a shell. In the next few sections, we’ll go through an overview of how the type conversion system works, then look at the conversion algorithm in detail. Finally we’ll look at some of the special conversion rules that only apply when binding cmdlet parameters.

3.6.1 How type conversion works
Type conversions are used any time an attempt is made to use an object of one type in a context that requires another type (such as adding a string to a number). Here’s a good example: in the previous chapter, we talked about how parameters are bound to cmdlets. The parameter binder uses the type conversion system heavily when trying to bind incoming objects to a particular parameter. If the user has supplied a string and the cmdlet requires a number, the system will quietly convert the source object to the destination type as long as it’s not a destructive conversion. A destructive conversion is one where the sense of the original object has been lost or distorted in some significant way. With numbers, this typically means a loss of precision. The type conversion facility is also surfaced directly to the shell user through cast operations in the PowerShell language, as we mentioned in the previous section. In PowerShell, you use types to accomplish many things that you’d do with methods or functions in other languages. You use type literals as operators to convert (or cast) one type of object to another. Here’s a simple example: PS (1) > [int] "0x25" 37 PS (2) > In this example, a string representing a hexadecimal number is converted into a number by using a cast operation. A token specifying the name of a type in square brackets can be used ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

98 as a unary operator that will try to convert its argument into the desired type. These type cast operations can be composed—that is—several casts can be chained together. Here’s an example of that type of composition. To get the ordinal value for a char, you can do : PS (2) > [int] [char]"a" 97 Notice that we first cast the string into a char and then into an int. This is necessary because the simple conversion would try to parse the entire string as a number. This only works for a string containing exactly one character, however. If you want to convert an entire string, you need to use array types. Here’s what that looks like: PS (3) > [int[]] [char[]] "Hello world" 72 101 108 108 111 32 119 111 114 108 100 The string was split into an array of characters, then that array of characters was converted into an array of integers, and finally displayed as a list of decimal numbers. If you wanted to see those numbers in hex, you’d have to use the –f format operator and a format specifier string: PS (4) > "0x{0:x}" -f [int] [char] "a" 0x61 And next, if you want to make a round trip, string to char to int to char to string you can do: PS (6) > [string][char][int] ("0x{0:x}" -f [int] [char] "a") a Finally, here’s a somewhat extreme example (for 2001 fans). We’ll take the string “HAL” and increment each of the characters in the string by one. Let’s try it out. PS (7) > $s = "HAL" PS (8) > $OFS=""; [string] [char[]] ( [int[]] [char[]] $s | >> foreach {$_+1} ) >> IBM Creepy, but cool (or just weird if you’re not a 2001 fan)! Moving closer to home, we know that the Windows NT kernel was designed by the same person who designed the VMS operating system. Let’s prove that Windows NT (WNT) is just VMS plus one. Here we go: PS (9) > $s = "VMS" PS (10) > $OFS=""; [string] [char[]] ( [int[]] [char[]] $s | >> foreach {$_+1} ) >> WNT One final issue you may be wondering about: what is the $OFS (Output Field Separator) variable doing in the example? When PowerShell converts arrays to strings, it takes each array element, converts that element into a string, and then concatenates all the pieces together. Since this would be an unreadable mess, it inserts a separator between each element. That ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

99 separator is specified using the $OFS variable. It can be set to anything you want, even the empty string. Here’s an interesting example. Say we want to add the numbers from 1 to 10. Let’s put the numbers into an array: PS (1) > $data = 1,2,3,4,5,6,7,8,9,10 Now convert them to a string: PS (2) > [string] $data 1 2 3 4 5 6 7 8 9 10 As an aside, variable expansion in strings goes through the same mechanism as the type converter, so you’ll get the same result: PS (3) > "$data" 1 2 3 4 5 6 7 8 9 10 Now change $OFS to be the plus operator (“+”), and then display the data. PS (4) > $OFS='+' PS (5) > "$data" 1+2+3+4+5+6+7+8+9+10 Previously, the fields had been separated by spaces. Now they’re separated by plus operators. This is almost what we need. We just have to find a way to execute this string. PowerShell provides ability through the Invoke-Expression cmdlet. Here’s how it works. PS (6) > invoke-expression "$data" 55 PS (7) > Ta-da! Note that this is not an efficient way to add a bunch of numbers. The looping constructs in the language are a much better way of doing this.

3.6.2 PowerShell’s type-conversion algorithm
In this section, we’ll cover the steps in the conversion process in painful detail—much more than you’ll generally need to know in your day-to-day work. However, if you really want to be an expert on PowerShell, this stuff’s for you.

NOTE
Type conversion is one of the areas of the PowerShell project that grew “organically”. In other words, we sat down, wrote a slew of specifications, threw them out, and ended up doing something completely different. This is one of the joys of this type of work. Nice clean theory falls apart when you put it in front of real people. The type conversion algorithm as it exists today is the result of feedback from many of the early adopters both inside Microsoft as well as outside. The PowerShell community helped us tremendously in this area.

In general, the PowerShell type conversions are separated into two major buckets: PowerShell Language Standard Conversions. These are built-in conversions performed by the engine itself. They are always processed first and consequently cannot be overridden. This set of conversions is largely guided by the historical behavior of shell and scripting languages, and is not part of the normal .NET type conversion system.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

100

3.6.3 .NET-based custom converters
This class of converters uses (and abuses in some cases) existing .NET mechanisms for doing type conversion. Table 3.5 lists the set of built-in language conversions that PowerShell uses. The conversion process always starts with an object of a particular type and tries to produce a representation of that object in the requested target type. The conversions are applied in the order shown in table 3.5. Only one conversion is applied at a time. The PowerShell engine does not automatically chain conversions.

Table 3.5 The PowerShell language standard conversions
Converting From to Target Type Result Description ““ (empty string) `0’ (string containing a single character 0) The object corresponding to 0 for the corresponding numeric type.

$null

[string] [char]
Any kind of number

[bool] [PSObject]
Any other type of object Derived Class Anything Base Class

$false $null $null

The original object is returned unchanged. The object is discarded.

[void]

Anything

[string]

The PowerShell internal string converter is used.

Anything

[xml]

The original object is first converted into a string and then into an XML Document object.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

101

Array of type [X]

Array of type [Y] PowerShell creates a new array of the target type, then copies and converts each element in the source array into an instance for the target array type. Array of type [Y] Creates an array containing one element and then places the singleton object into the array, converting if necessary.

Non-array (singleton) object

System.Collections.IDictionary [Hashtable]

A new instance of System.Collections.Hashtable is created, and then the members of the source IDictionary are copied into the new object.

[string] [string]

[char[]] [regex]

Converts the string to an array of characters. Constructs a new instance of a .NET regular expression object. Converts the string into a number using the smallest representation available that can accurately represent that number. If the string is not purely convertible (i.e., only contains numeric information) then an error is raised.

[string]

Number

[int]

System.Enum

Converts the integer to the corresponding enumeration member if it exists. If it doesn’t, a conversion error is generated.

If none of the built-in PowerShell language-specific conversions could be applied successfully then the .NET custom converters are tried. Again, these converters are tried in order until a candidate is found that will produce the required target type. This candidate conversion is applied. If the candidate conversion throws an exception (that is, a matching converter is found but it fails during the conversion process) then no further attempt to convert this object will be made and the overall conversion process will be considered to have failed. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

102

AUTHOR'S NOTE
Understanding these conversions depend upon a fair knowledge of the .NET type conversion mechanisms. You’ll need to refer to additional documentation if you want to understand everything in table 3.6. On the other hand, with the .NET docs, you can see exactly what steps are being applied in the type conversion process.

Custom converters are executed in the order described in table 3.6.

Table 3.6 Custom Type Conversions
Converter type Description

PSTypeConverter A PSTypeConverter can be associated with a particular type using the TypeConverterAttribute or the tag in the types.ps1xml file. If the value to convert has a PSTypeConverter that can convert to the target type, then it is called. If the target type has a

PSTypeConverter that can convert from values to convert, then it is called.
The PSTypeConverter allows a single type converter to work for N different classes. For example, an enum type converter can convert a string to any enum (there doesn’t need to be separate type to convert each enum). Refer to the PowerShell SDK documentation for complete details on this converter.

TypeConverter

This is a CLR defined type that can be associated with a particular type using the TypeConverterAttribute or the tag in the types file. If the value to convert has a TypeConverter that can convert to the target type then it is called. If the target type has a TypeConverter that can convert from the source value, then it is called. Note: The CLR TypeConverter does not allow a single type converter to work for N different classes. Refer to the PowerShell SDK documents and the Microsoft .NET framework documentation for details on the TypeConverter class.

Parse() Method

If the value to convert is a string, and the target type has a Parse() method, then that Parse() method is called. Parse() is a well-known method name in the CLR world and is commonly implemented to allow conversion of strings to other types. If the target type has a constructor that takes a single parameter matching the type of the value to convert, then this constructor is used to create a new object of the desired type. If the value to convert has an implicit cast operator that converts to the target type, then it is called. Conversely, if the target type has an implicit cast operator that converts from value to convert’s type, then that is called.

Constructors

Implicit Cast Operator

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

103

Explicit Cast Operator

If the value to convert has an explicit cast operator that converts to the target type then it is called. Alternatively, if the target type has an explicit cast operator that converts from value to convert’s type then that is called.

IConvertable

System.Convert.ChangeType is then called.

This section covered the set of type conversions that PowerShell will apply in expressions. In the parameter binder, however, are a few extra steps that are applied first.

3.6.4 Special type conversions in parameter binding
In this final section, we’ll go over the extra type conversion rules that are used in parameter binding that haven’t already been covered. If these steps are tried and are not successful, the parameter binder goes on to call the normal PowerShell type converter code.

NOTE
If at any time there is a failure doing the type conversion, an exception will be thrown.

Here are the extra steps: 3. If there is no argument for the parameter, then the parameter type must be either a [bool] or the special PowerShell type SwitchParameter; otherwise a parameter binding exception is thrown. If the parameter type is a [bool], it is set to true. If the parameter type is a SwitchParameter, it is set to SwitchParameter.Present. 4. If the argument value is null and the parameter type is [bool], it is set to false. If the argument value is null and the parameter type is SwitchParameter, it is set to SwitchParameter.Present. Null can be bound to any other type, so it just passes through. 5. If the argument type is the same as the parameter type, the argument value is used without any type conversion. 6. If the parameter type is [object], the current argument value is used without any coercion. 7. If the parameter type is a [bool], then we use the PowerShell Boolean IsTrue() method to determine whether the argument value should set the parameter to true or false. 8. If the parameter type is a collection, then the argument type must be encoded into the appropriate collection type. Note, we will encode a scalar argument type or a collection argument type to a target collection parameter type. We will not encode a collection argument type into a scalar parameter type (unless that type is System.Object or PSObject). 9. If the argument type is a scalar, then we create a collection of the parameter type (currently only arrays and IList are supported) of length 1 and set the argument value as the only value in the collection. If needed, the argument type is converted to the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

104 element type for the collection using the same type coercion process this section describes 10.If the argument type is a collection, we create a collection of the parameter type with length equal to the number of values contained in the argument value. Each value is then coerced to the appropriate element type for the new collection using the recursive application of this algorithm. 11.If none of these steps worked, use the conversion in table 3.6. If those fail, then the overall parameter binding attempt fails. Once again, this is a level of detail that you don’t usually need to consider, but it’s useful to know it’s available when you need it. SCRIPTBLOCK PARAMETERS And finally, there is one last aspect of the parameter binder type converter to cover and this is a feature called scriptblock parameters. First - a bit of a preview of things to come. PowerShell has something called a scriptblock. A scriptblock is a small fragment of code that you can pass around as an object itself. This is a very powerful concept and we'll cover scriptblocks in great detail in later chapters, but for now we're just going to look at them in the context of parameter binding. Here's how scriptblock parameters work. Normally, when you pipe two cmdlets together, the second cmdlet receives values directly from the first cmdlet. Scriptblock parameters (you could also call them "computed parameters") allow you to insert a piece of script to perform a calculation or transformation in the middle of the pipelined operation. This calculation can do pretty much anything we want since a scriptblock can contain any element of PowerShell script. Here's an example that shows how this works. We want to take a collection of XML files and rename them to be text files. We could write a loop to do the processing but scriptblock parameters greatly simplify this problem. To rename each file, we'll use the Rename-Item cmdlet. This cmdlet takes two parameters - the current name of the file and the new name. We'll use a scriptblock parameter as an argument to the -NewName parameter to generate the new file name. This scriptblock will use the -replace operator to replace the ".xml" file extension with the desired ".txt". Here's what the command line to perform this task looks like: dir *.xml | Rename-Item -Path {$_.name} ` -NewName { $_.name -replace '\.xml$', '.txt' } -whatif The original path for -Path is just the current name of the file. The -NewName is the filename with the extension replaced. The -WhatIf parameter will let you see what the command will do before actually moving anything. Once you are happy that the correct operations are being performed, just remove the -WhatIf and the renaming will proceed. ScriptBlock parameters can be used with any pipelined parameter as long as the type of that parameter is not [object] or [scriptblock]. In these cases, the scriptblock is passed as the actual parameter instead of using it to calculate a new value. We'll see why this is important when we look at the Where-Object and ForEach-Object cmdlets later on.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

105 You now know everything you need to know about how types work on PowerShell. Well, not quite everything. In the next two chapters, we’ll discuss how the PowerShell operators build on this basic type foundation. But for now, we’re through!

3.7 Summary
A solid understanding of the PowerShell type system will allow you to use PowerShell most effectively. By taking advantage of the built-in type system and conversions, you can accomplish startlingly complex tasks with little code. In this chapter, we covered the following topics:       The PowerShell type system, how it works, and how you can use it. The basic PowerShell types and how they are represented in PowerShell script (literals). Some of the more advanced types—hashtables and arrays. The use of type literals in type casts and as a way to call static methods. PowerShell version 2 added support for generic type literals that greatly simplify working with generic types. The type conversion process for language conversions, the pre-conversion steps that are used by the parameter binder, and the relationship between the PowerShell types and the underlying .NET types. Scriptblock parameters allow you to calculate new values for pipelined parameters instead of having to write a loop to do this. We'll look at scriptblocks in detail in chapter 9.



©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

106

4
Operators and expressions

Operators, Mr. Rico! Millions of them! —Robert A. Heinlein, Starship Troopers, (paraphrased)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

107 So far, we've covered the basics, and we've covered the type system in considerable depth. Now let's look at how we can combine all this stuff and get some real work done. As in any language, objects are combined with operators to produce expressions. When these expressions are evaluated, the operators perform their operations on objects giving us (hopefully) useful results. This chapter covers the set of basic operators in PowerShell and how they're used in expressions. The operators we're going to cover in this chapter are shown in figure 4.1.

Arithmetic operators + * / %

Assignment operators = += -= *= /= %=

Comparison operators

Containment operator -contains -notcontains

-eq -ne -gt -ge -lt -le

Pattern matching and text operators -like -notlike -match -notmatch -replace -split -join

Logical and bitwise operators

-and -or -not -xor -band -bor -bnot -bxor

Figure 4.1 This diagram shows the broad groups of operators we'll cover in this chapter.

As you can see, PowerShell has operators. Lots of operators—the full complement you would expect in a conventional programming language and several more. In addition, PowerShell operators are typically more powerful than the corresponding operators in conventional languages such as C or C++. So, if you invest the time to learn what the PowerShell operators are and how they work, in a single line of code you will be able to accomplish tasks that would normally take a significant amount of programming. Here’s an example of the kind of thing that can be done using just the PowerShell operators. Say we have a file old.txt with the following text in it: Hello there. My car is red. Your car is blue. His car is orange and hers is gray. Bob's car is blue too. Goodbye. Our task is to copy this content to a new file, making certain changes. In the new file, the word “is” should be replaced with “was”, but only when it’s in front of the words “red” or “blue”. In most languages, this would require a fairly complex program. In PowerShell, it takes exactly one line. Here’s the “script”. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

108 ${c:old.txt} -replace 'is (red|blue)','was $1' > new.txt It uses the -replace operator along with output redirection and variable namespaces. The -

replace operator is described later in this chapter. Redirection and variable namespaces are features for working with files that are covered in chapter 5. After running this script, the content of new.txt looks like: Hello there. My car was red. Your car was blue. His car is orange and hers is gray. Bob's car was blue too. Goodbye.

NOTE
For the impatient reader, the notation ${c:old.txt} says: return the contents of the file “old.txt” from the current working directory on the C: drive. In contrast, ${c:\old.txt} says get the file “old.txt” from the root of the C: drive.

As you can see, only the second and fourth lines have been changed as desired. The phrases “is red” and “is blue” have been changed to “was red” and “was blue”. The “is orange” and “is gray” phrases weren’t changed. From this example, you can also see that it’s possible to do quite a bit of work just with the operators. One of the characteristics that makes PowerShell operators powerful is the fact that they are polymorphic. This simply means that they work on more than one type of object. While this is generally true in other object-based languages, in those languages the type of the object defines the behavior of the operator. For example, the Integer class would define an operator for adding a number to a class.

NOTE
If you’re a C# or Visual Basic user, here’s something you might want to know. In “conventional” NET languages, the operator symbols are mapped to a specific method name on a class called op_. For example, in C#, the plus operator “+” maps to the method op_Addition(). While PowerShell is a .NET language, it takes a different approach that is more consistent with dynamic scripting languages as we’ll see in the following sections.

In PowerShell, the interpreter primarily defines the behavior of the operators, at least for common data types. Type-based polymorphic methods are only used as a backup. By common types, we mean strings, numbers, hashtables, and arrays. This allows PowerShell to provide more consistent behavior over this range of common objects and also to provide higher-level behaviors than are provided by the objects themselves, especially when dealing with collections. We’ll cover these special behaviors in the sections for each class of operator. (The following sections have many examples, but the best way to learn this is to try the examples in PowerShell yourself.) Now let’s get going and start looking at the operators.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

109

4.1 Arithmetic operators
First we’ll cover the basic arithmetic operators shown in figure 4.2
Arithmetic operators + * / %

Figure 4.2 This diagram shows the arithmetic operators in PowerShell that will be covered in this section.

The polymorphic behavior of these operators was touched on briefly in chapter 3, where the various type conversions were covered. The operators themselves are listed with examples in table 4.1.

Table 4.1 The basic arithmetic operators In PowerShell

Operator +

Description Add two values together.

Example 2+4 “Hi “ + “there” 1,2,3 + 4,5,6

Result 6 “Hi There” 1,2,3,4,5,6 8 “aaa” 1,2,1,2 4 3 1.75 3

*

Multiply 2 values.

2*4 “a” * 3 1,2 * 2

/

Subtract one value from another. Divide two values.

6-2 6/2 7/4

%

Return the remainder from a division operation.

7%4

In terms of behavior, the most interesting operators are + and *. We’ll cover these operators in detail in the next two sections.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

110

4.1.1 The addition operator
As mentioned previously, PowerShell itself defines the behavior of the + and * operators for numbers, strings, arrays, and hashtables. Adding or multiplying two numbers produces a numeric result following the numeric widening rules. Adding two strings performs string concatenation, resulting in a new string, and adding two arrays joins the two arrays (array catenation), producing a new array. The interesting part occurs when you mix operand types. In this situation, the type of the left operand determines how the operation will proceed. We’ll look at how this works with addition first.

NOTE
The “left-hand” rule for arithmetic operators: The type of the left-hand operand determines the type of the overall operation. This is an important rule to remember.

If the left operand is a number, PowerShell will try to convert the right operand to a number. Here’s an example. In the following expression, the operand on the left is a number and the operand on the right is the string “123”. PS (1) > 2 + "123" 125 Since the operand on the left is a number, according to the conversion rule, the operand “123” must be converted into a number. Once the conversion is complete, the numeric addition operation proceeds and produces the result 125 as shown. Conversely, in the next example, when a string is on the left side: PS (2) > "2" + 123 2123 the operand on the right (the number 123) is converted to a string and appended to “2” to produce a new string “2123”. If the right operand can’t be converted into the type of the left operand then a type conversion error will be raised, as we see in the next example: PS (3) > 2 + "abc" Cannot convert "abc" to "System.Int32". Error: "Input string was not in a correct format." At line:1 char:4 + 2 + $a.gettype().fullname System.Int32[] Now let’s do some assignments. First assign an integer. PS (3) > $a[0] = 10 This works without error. Next try it with a string that can be converted into an integer. We’ll use the hex string mentioned earlier. PS (4) > $a[0] = "0xabc" This also works fine. Finally, let’s try assigning a non-numeric string to the array element. PS (5) > $a[0] = "hello" Array assignment to [0] failed: Cannot convert "hello" to "System.Int32". Error: "Input string was not in a correct format.". At line:1 char:4 + $a[0 $a hello 2 3 4 hello This time the assignment succeeds without error. What happened here? Let’s look at the type of the array now. PS (9) > $a.gettype().fullname System.Object[] When the new, larger array was created to hold the combined elements, it was created as type

[object[]], which is not type-constrained. Since it can hold any type of object, the assignment proceeded without error. Finally, let’s look at how addition works with hashtables. Similar to arrays, addition of hashtables creates a new hashtable and copies the elements of the original tables into the new one. The left elements are copied first, then the elements from the right operand are copied. (This only works if both operands are hashtables.) If there are any collisions, that is, if the keys of any of the elements in the right operand match the keys of any element in the left operand, then an error will occur saying that the key already exists in the hashtable. (This was an implementation decision, by the way; we could have had the new element overwrite the old one, but the consensus was that generating an error message is usually the better thing to do.)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

112 PS PS PS PS (1) (2) (3) (4) > > > > $left=@{a=1;b=2;c=3} $right=@{d=4;e=5} $new = $left + $right $new Value ----4 1 2 5 3

Key --d a b e c

The new hashtable is of type System.Collections.Hashtable: PS (5) > $new.GetType().FullName System.Collections.Hashtable The table is created in such a way that the strings that are used as keys are compared in a case-insensitive way. This completes our discussion of the behavior of the addition operator. We covered how it works with numbers, strings, hashtables, and arrays. Now that we’re finished with addition, let’s move on to the multiplication operator.

4.1.2 The multiplication operator
As with addition, PowerShell defines multiplication behavior for numbers, strings, and arrays. (We don’t do anything special for hashtables for multiplication.) Multiplying numbers works as expected and follows the widening rules discussed in chapter 3. In fact, the only legal righthand operand for multiplication is a number. If the operand on the left is a string then that string is repeated the number of times specified in the left operand. Let’s try this out. We’ll multiply the string “abc” by 1, 2, then 3: PS (1) > "abc" * 1 abc PS (2) > "abc" * 2 abcabc PS (3) > "abc" * 3 abcabcabc The results are “abc”, “abcabc”, and “abcabcabc”, respectively. What about multiplying by zero? PS (4) > "abc" * 0 PS (5) > The result appears to be nothing—but which “nothing”—spaces, empty string, or null? The way things are displayed, you can’t tell by looking. Here’s how to check. First check the type of the result: PS (5) > ("abc" * 0).gettype().fullname System.String We see that it’s a string, not $null. But it could still be spaces, so we need to check the length: PS (6) > ("abc" * 0).length 0 And, since the length is zero, we can tell that it is in fact an empty string. Now let’s look at how multiplication works with arrays. Since multiplication applied to strings repeats the string, logically you would expect that multiplication applied to arrays ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

113 should repeat the array, which is exactly what it does. Let’s look at some examples of this. First create an array with three elements: PS (1) > $a=1,2,3 PS (2) > $a.length 3 Now multiply it by 2: PS (3) > $a = $a * 2 PS (4) > $a.length 6 The length of the new array is 6. Looking at the contents of the array (using variable expansion in strings to save space) we see that it is “1 2 3 1 2 3”—the original array doubled. PS (5) > "$a" 1 2 3 1 2 3 Now multiply the new array by three: PS (6) > $a = $a * 3 And check that the length is now 18. PS (7) > $a.length 18 It is, so looking at the contents: PS (8) > "$a" 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 we see that it is six repetitions of the original three elements. As with addition, first a new larger array is created during multiplication, then the component elements are copied into it. This has the same issue that addition had, where the new array is created without type constraints. Even if the original array could only hold numbers, the new array can hold any type of object.

4.1.3 Subtraction, division, and the modulus operator
Addition and multiplication are the most interesting of the arithmetic operators in terms of polymorphic behavior, but let’s go over the remaining operators. Subtraction, division, and the modulus (%) operators are only defined for numbers by PowerShell. (Modulus returns the remainder from a division operation.) Again, as with all numeric computations, the widening rules for numbers are obeyed. Since, for the basic scalar types (i.e. strings and numbers), these operations are only defined for numbers, so if either operand is a number (not just the left-hand operand) then an attempt will be made to convert the other operand into a number as well, as PS (1) 30.75 PS (2) 30.75 PS (3) shown in the following: > "123" / 4 > 123 / "4" >

In the first example, the string “123” is converted into a number. In the second example, the string “4” will be converted into a number.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

114

NOTE
Here is an important characteristic about how division works in PowerShell that you should keep in mind. Integer division underflows into floating point (technically System.Double). This means that 5 divided by 4 in PowerShell results in 1.25 instead of 1 as it would in C#. If you want to round the decimal part to the nearest integer, simply cast the result into [int]. You also need to be aware that PowerShell uses what’s called “Banker’s Rounding” when converting floating point numbers into integers. Banker’s rounding rounds .5 up sometimes, and down sometimes. The convention is to round to the nearest even number, so that both 1.5 and 2.5 round to 2, and 3.5 and 4.5 both round to 4.

If neither operand is a number, the operation is undefined and you’ll get an error as shown: PS (3) > "123" / "4" Method invocation failed because [System.String] doesn't contain a method named 'op_Division'. At line:1 char:8 + "123" / tillxmas Days : 321 http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

115 Hours Minutes Seconds Milliseconds Ticks TotalDays TotalHours TotalMinutes TotalSeconds TotalMilliseconds : : : : : : : : : : 18 8 26 171 277997061718750 321.755858470775 7722.14060329861 463328.436197917 27799706.171875 27799706171.875

Thanks to PowerShell, I can tell my daughter how many seconds to go until Xmas! Now if I can only get her to stop asking me in the car. To take a look at the operator methods defined for System.DateTime, we can use the

Getmembers() method. Here’s a partial listing of the operator methods defined. We’re using the PowerShell Select-String cmdlet to limit what gets displayed to only those methods whose names contain the string “op_”: PS (5) > [datetime].getmembers()| foreach{"$_"}| select-string op_ System.DateTime op_Addition(System.DateTime, System.TimeSpan) System.DateTime op_Subtraction(System.DateTime, System.TimeSpan) System.TimeSpan op_Subtraction(System.DateTime, System.DateTime) As you can see, not all of the arithmetic operator methods are defined. In fact, there are no methods defined for any operations other than addition and subtraction. If you try to divide a

DateTime object by a number, you’ll get the same error we saw when we tried to divide two strings: PS (4) > [datetime] "1/1/2006" / 22 Method invocation failed because [System.DateTime] doesn't contain a method named 'op_Division'. At line:1 char:24 + [datetime] "1/1/2006" / $c=$p=1; 1; while ($c -lt 100) { $c; $c,$p = ($c+$p),$c } 1 1 2 3 5 8 13 21 34 55 89 In this example, we begin by initializing the two variables $c (current) and $p (previous) to 1. Then we loop while $c is less than 100. $c contains the current value in the sequence, so we emit that value. Next we have the double assignment, where $c becomes the next element in the sequence and $p becomes the current (now previous) value in the sequence. So far, we’ve seen that using multiple assignments can simplify basic operations such as swapping values. However, when combined with some of PowerShell’s other features, you can do much more interesting things than that. We’ll see this in the next section.

4.2.2 Multiple assignments with type qualifiers
This is all interesting, but let’s look at a more practical example. Say we are given a text file containing some data that we want to parse into a form we can work with. First let’s look at the data file: quiet 0 25 normal 26 50 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

118 loud 51 75 noisy 75 100 This file contains a set of sound level descriptions. The format is a string describing the level, followed by two numbers describing the upper and lower bounds for these levels out of a possible 100. We want to read this information into a data structure so we can use it to categorize a list of sounds later on. Here’s the fragment of PowerShell code needed to do this: PS (2) > $data = get-content data.txt | foreach { >> $e=@{} >> $e.level, [int] $e.lower, [int] $e.upper = -split $_ >> $e >> } >> We start by using the Get-Content cmdlet to write the data into a pipeline. Each line of the file is sent to the ForEach-Object cmdlet to be processed. The first thing we do in the body of the foreach cmdlet is initialize a hashtable in $e to hold the result. We take each line stored in the $_ variable and apply the -split operator to it. This splits the string into an array at each space character in the string. (The split operator is covered in detail later in this chapter.) For example, the string "quiet 0 25" becomes an array of three strings "quiet","0","25" Then we assign the split string to three elements of the hashtable: $e.level, $e.lower, and

$e.upper. But there’s one more thing we want to do. The array being assigned is all strings.
For the upper and lower bounds, we want numbers, not strings. To do this, we add a cast before the assignable element. This causes the value being assigned to first be converted to the target type. The end result is that the upper and lower fields in the hashtable are assigned numbers instead of strings. Finally, note that the result of the pipeline is being assigned to the variable $data, so we can use it later on. Let’s look at the result of this execution. Since there were four lines in the file, there should be four elements in the target array. PS (3) > $data.length 4 We see that there are. Now let’s see if the value stored in the first element of the array is what we expect. It should be the “quiet” level. PS (4) > $data[0] Key Value ------upper 25 level quiet lower 0 It is. Finally, let’s verify that the types were properly converted. PS (5) > $data[0].level quiet PS (6) > $data[0].lower 0 PS (7) > $data[0].upper 25 PS (8) > $data[0].level.gettype().fullname System.String ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

119 PS (9) > $data[0].lower.GetType().fullname System.Int32 PS (10) > $data[0].upper.GetType().fullname System.Int32 Again we use the GetType() method to look at the types, and we can see that the level description field is a string and that the two bounds fields are integers, as expected. In this last example, we’ve seen how array assignment can be used to perform sophisticated tasks in only a few lines of code. By now, you should have a good sense of the utility of assignments in processing data in PowerShell. There’s just one last point to cover about assignment expressions, which we’ll cover in the next section.

4.2.3 Assignment operations as value expressions
The last thing you need to know about assignment expressions is that they are, in fact, expressions. This means that you can use them anywhere you’d use any other kind of expression. This lets you initialize multiple variables at once. Let’s initialize $a, $b, and $c to the number 3. PS (1) > $a = $b = $c = 3 Now verify that the assignments worked: PS (2) > $a, $b, $c 3 3 3 Yes, they did. So what exactly happened? Well, it’s the equivalent of the following expression: PS (3) > $a = ( $b = ( $c = 3 ) ) That is, $c is assigned 3. The expression ($c = 3) returns the value 3, which is in turn assigned to $b, and the result of that assignment (also 3) is finally assigned to $a so once again, all three variables end up with the same value: PS (4) > $a, $b, $c 3 3 3 Now, since we can “intercept” the expressions with parentheses, we can perform additional operations on the values returned from the assignment statements before this value is bound in the outer assignment. Here’s an example that does this: PS (5) > $a = ( $b = ( $c = 3 ) + 1 ) + 1 In this expression, $c gets the value 3. The result of this assignment is returned, and 1 is added to that value, giving 4, which is then assigned to $b. The result of this second assignment also has 1 added to it, so $a is finally assigned 5, as shown in the output: PS (6) > $a, $b, $c 5 4 3 Now we have assignment and arithmetic operators covered, but a language isn’t much good if you can’t compare things, so let’s move on to the comparison operators.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

120

4.3 Comparison operators
In this section, we’ll cover what the comparison operators are in PowerShell and how they work. These operators are shown in figure 4.4.

Comparison operators (case-insensitive)

-eq -ieq

-ne -ine

-gt -igt

-ge -ige

-lt -ilt

-le -ile

Comparison operators (case-sensitive) -ceq -cne -cgt -cge -clt -cle

Figure 4.4 This diagram shows the comparison operators in PowerShell. Each operator has case-sensitive and case-insensitive versions.

We’ll cover how case sensitivity factors into comparisons and how the operators work for scalar values and for collections of values. The ability of these operators to work on collections eliminates the need to write looping code in a lot of scenarios. PowerShell has a sizeable number of comparison operators, in large part because there are case-sensitive and case-insensitive versions of all of the operators. These are listed with examples in table 4.3.

Table 4.3 PowerShell comparison operators

Operator -eq –ceq –ieq -ne –cne –ine -gt –cgt –igt -ge –cge –ige -lt –clt –ilt

Description Equals

Example 5 –eq 5

Result $true

Not equals Greater than Greater than or equal Less than

5 –ne 5 5 –gt 3 5 –ge 3 5 –lt 3

$false $true $true $false

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

121

-le –cle -ile

Less than or equals

5 –le 3

$false

In table 4.3, you can see that for each operator there is a base or unqualified operator form, like -eq and its two variants -ceq and -ieq. The “c” variant is case-sensitive and the “i” variant is case-insensitive. This raises the obvious question, what is the behavior for the base operators with respect to case? The answer is that the unqualified operators are caseinsensitive. All three variants are provided to allow script authors to make their intention clear—that they really meant a particular behavior rather than accepting the default.

NOTE
Let’s talk about the most contentious design decision in the PowerShell language. And the winner is: why the heck did we not use the conventional symbols for comparison like “>”, “>=”, “” or “->”, either for redirection or comparison. We did usability tests and held focus groups, and in the end, settled on what we had started with. The redirection operators are “>” and “ 01 -eq 001 True Because we’re doing a numeric comparison, the leading zeros don’t matter and the numbers compare as equal. Now let’s try it when the right operand is a string. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

122 PS (28) > 01 -eq "001" True Following the rule, the right operand is converted from a string into a number, then the two are compared and are found to be equal. Finally, try the comparison when the left operand is a string. PS (27) > "01" -eq 001 False In this example, the right operand is converted to a string, and consequently they no longer compare as equal. Of course you can always use casts to force a particular behavior. In the next example, let’s force the left operand to be a number: PS (29) > [int] "01" -eq 001 True And, because we forced a numeric comparison, once again they are equal. TYPE CONVERSIONS AND COMPARISONS As with any PowerShell operator that involves numbers, when comparisons are done in a numeric context, the widening rules are applied. This can produce somewhat unexpected results. Here’s an example that illustrates this. In the first part of the example, we use a cast to convert the string “123” into a number. Once we’re doing the conversion in a numeric context, the numbers get widened to double since the right operand is a double; and since 123.4 is larger than 123.0, the -lt operator returns true. PS (37) > [int] "123" -lt 123.4 True Now try it using a string as the right operand. The cast forces the left operand to be numeric; however, the right operand is not yet numeric. It is converted to the numeric type of the left operand, which is [int], not [double]. This means that the value is truncated and the comparison now returns false. PS (38) > [int] "123" -lt "123.4" False Finally, if we force the context to be [double] explicitly, the comparison again returns true. PS (39) > [double] "123" -lt "123.4" True While all these rules seem complicated (and, speaking as the guy who implemented them, they are), the results are generally what you would intuitively expect. This satisfies the principle of least astonishment. So most of the time you don’t need to worry about the specifics and can just let the system take care of the conversions. It’s only when things don’t work as expected that you really need to understand the details of the conversion process. To help you debug cases where this happens, PowerShell provides a type conversion tracing mechanism to help you track down the problems. How to use this debugging feature is described in chapter 7. Finally, you can always apply a set of casts to override the implicit behavior and force the results you want.

4.3.2 Comparisons and case-sensitivity
Next let’s look at the “i” and “c” versions of the comparison operators—the case-sensitive and case-insensitive versions. Obviously, case sensitivity only applies to strings. All of the comparison operators have both versions. For example, the -eq operator has the following variants: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

123

PS (1) > "abc" -eq "ABC" True PS (2) > "abc" -ieq "ABC" True PS (3) > "abc" -ceq "ABC" False The default case -eq is case-insensitive, as is the explicitly case-insensitive operator -ieq, so in the example, “abc” and “ABC” compare as equal. The -ceq operator is case-sensitive, so with this operator, “abc” and “ABC” compare as not equal. The final item to discuss with scalar comparisons is how things that aren’t strings and numbers are compared. In this case, the .NET comparison mechanisms are used. If the object implements the .NET IComparable interface, then that will be used. If not, and if the object on the left side has a .Equals() method that can take an object of the type of the right operand, this is used. If there is no direct mechanism for comparing the two an attempt will be made to convert the right operand into an instance of the type of the left operand, then PowerShell will try to compare the resulting objects. This lets you compare things such as

[datetime] objects as shown in the next example:
PS (4) > [datetime] "1/1/2006" -gt [datetime] "1/1/2005" True PS (5) > [datetime] "1/1/2006" -gt [datetime] "2/1/2006" False PS (6) > Of course, not all objects are directly comparable. For example, there is no direct way to compare a System.DateTime object to a System.Diagnostics.Process object. PS (6) > [datetime] "1/1/2006" -gt (get-process)[0] The '-gt' operator failed: Cannot convert "System.Diagnostics.Process (ALCXMNTR)" to "System.DateTime".. At line:1 char:26 + [datetime] "1/1/2006" -gt [datetime] "1/1/2007" -gt (get-process)[0].StartTime True In this expression, we’re looking to see whether the first element in the list of Process objects had a start time greater than the beginning of this year (no) and whether it had a start time from before the beginning of next year (obviously true). You can use this approach to find all the processes on a computer that started today, as shown: get-process | where {$_.starttime -ge [datetime]::today}

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

124 The Get-Process cmdlet returns a list of all of the processes on this computer, and the where cmdlet selects those processes where the StartTime property of the process is greater than or equal to today.

NOTE
The where used in the previous example is an alias for the Where-Object cmdlet, which is described in chapter 6.

This completes our discussion of the behavior of the comparison operators with scalar data. We paid a lot of attention to the role types play in comparisons, but so far we’ve avoided discussing collection types—lists, arrays and so on. We’ll get to that in the next section.

4.3.3 Using comparison operators with collections
In this section, we focus on the behavior of the comparison operators when they are used with collections of objects. BASIC COMPARISON OPERATIONS INVOLVING COLLECTIONS Here is the basic behavior. If the left operand is an array or collection, then the comparison operation will return the elements of that collection which match the right operand. Let’s illustrate the rule with an example: PS (1) > 1,2,3,1,2,3,4 -eq 2 2 2 This expression searches the list of numbers on the left side and returns those that match—the two “2”s. And of course this works with strings as well: PS (2) > "one","two","three","two","one" -eq "two" two two When processing the array, the scalar comparison rules are used to compare each element. In the next example, the left operand is an array containing a mix of numbers and strings, and the right operand is the string “2”. PS (3) > 1,"2",3,2,"1" -eq "2" 2 2 Again, it returns the two “2”s. Let’s look at some more examples where we have leading zeros in the operands. In the first example: PS (4) > 1,"02",3,02,"1" -eq "2" 2 we only return the number 2 because 2 and “02” compare equally example. PS (5) > 1,"02",3,02,"1" -eq 2 2 When the elements are compared as numbers, they match. When compared as strings, they don’t match because of the leading zero. Now one final example: PS (6) > 1,"02",3,02,"1" -eq "02" 02 2 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 in a numeric context; however “2” and “02” are different in a string context. The same thing happens in the next

Licensed to Andrew M. Tearle

125 Now they both match. In a numeric context, the leading zeros don’t matter and in the string context, the strings match. THE CONTAINMENT OPERATORS All of the comparison operators we’ve discussed so far return the matching elements from the collection. While this is extremely useful, there are times when you just want to find out whether an element is there or not. This is what the -contains and -notcontains operators, shown in figure 4.5, are for.
Containment operators (case-insensitive) -contains -notcontains -icontains -inotcontains

Containment operators (case-sensitive) -ccontains -cnotcontains

Figure 4.5 This diagram shows the PowerShell containment operators in case-insensitive and cas-sensitive versions.

These operators return true if the set contains the element you’re looking for instead of returning the matching elements. They're listed in table 4.4 with examples.

Table 4.4 PowerShell containment operators

Operator -contains -ccontains -icontains

Description The collection on the left side contains the value specified on the right side.

Example 1,2,3 –contains 2

Result $true

-notcontains -cnotcontains -inotcontains

The collection on the left side does not contain the value on the right side.

1,2,3 –notcontains 2

$false

Let’s redo the example at the end of the last section, but this time we'll use -contains instead of -eq. PS (1) > 1,"02",3,02,"1" -contains "02" True PS (2) > 1,"02",3,02,"1" -notcontains "02" False

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

126 Now, instead of returning 02 and 2, we just return a single Boolean value. Since all values in PowerShell can be converted into a Boolean value, this doesn’t seem as if it would particularly matter, and usually it doesn’t. The one case where it does matter is if the matching set of elements is something that is false. This even includes Booleans. This is easiest to understand with an example: PS (3) > $false,$true -eq $false False PS (4) > $false,$true -contains $false True In the first command, -eq searches the list for $false, finds it, then returns the matching value. However, since the matching value was literally $false, a successful match looks as if it failed. When we use the -contains operator in the expression, we get the result we’d expect, which is $true. The other way to work around this issue is to use the @( .. ) construction and the Count property. This looks like: PS (5) > @($false,$true -eq $false).count 1 The @( ... ) sequence forces the result to be an array and then takes the count of the results. If there are no matches the count will be zero, which is equivalent to $false. If there are matches the count will be nonzero, equivalent to true. There can also be some performance advantages to -contains, since it stops looking on the first match instead of checking every element in the list.

NOTE
The @( .. ) construction is described in detail in chapter 5.

In this section, we covered all of the basic comparison operators. We addressed the issue of case-sensitivity in comparisons, and we covered the polymorphic behavior of these operations, first for scalar data types, then for collections. Now let’s move on to look at PowerShell's operators for working with text. One of the hallmark features of dynamic languages is great support for text manipulation and pattern matching. In the next section, we’ll cover how PowerShell incorporates these features into the language.

4.4 The pattern matching and text manipulation operators
In this section, we cover the pattern matching and text manipulation operators in PowerShell which are shown in figure 4.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

127

Pattern matching and text manipulation operators (case-insensitive) -like -notlike -match -notmatch -replace -split -ilike -inotlike -imatch -inotmatch -ireplace -isplit Pattern matching and text manipulation operators (case-sensitive)

-clike -cnotlike -cmatch -cnotmatch -creplace -csplit

The -join operator -join

Figure 4.6 This diagram shows the pattern matching and text manipulation operators in PowerShell. All of the operators that use patterns (everything except -join) have case-sensitive and case-insensitive forms.

Beyond the basic comparison operators, PowerShell has a number of pattern matching operators. These operators work on strings, allowing you to search through text, extract pieces of it and edit or create new strings. The other text manipulation operators allow you to break strings apart into pieces or add individual pieces back together into single string. We'll start with the pattern matching operators. PowerShell supports two built-in types of patterns—wildcard expressions and regular expressions. Each of these pattern types are useful in distinct domains. We'll cover the operation and applications of both types of patterns along with the operators that use them in the next few sections.

4.4.1 Wildcard patterns and the -like operator
You usually find wildcard patterns in a shell for matching file names. For example, the following command dir *.txt finds all of the files ending in .txt. Similarly, cp *.txt c:\backup will copy all the text files into the directory c:\backup. In these examples, the “*” matches any sequence of characters. Wildcard patterns also allow you to specify character ranges. In the next example, the pattern dir [st]*.txt will return all of the files that start with either the letters “s” or “t” that have a “.txt” extension. Finally, you can use the question mark (?) to match any single character. The wildcard pattern matching operators are listed in table 4.4. This table lists the operators and includes some simple examples of how each one works.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

128

Table 4.4 PowerShell wildcard pattern matching operators

Operator

Description

Example

Result

-like –clike –ilike

Do a wildcard pattern match.

“one” –like “o*”

$true $false

-notlike –cnotlike -inotlike Do a wildcard pattern match; true “one” –notlike “o*” if the pattern doesn’t match.

You can see from the table that there are several variations on the basic -like operator. These variations include case-sensitive and case-insensitive versions of the operator, as well as variants that return true if the target doesn’t match the pattern. Table 4.5 summarizes the special characters that can be used in PowerShell wildcard patterns.

Table 4.5 Special characters in PowerShell wildcard patterns

Wildcard

Description

Example

Matches

Doesn’t Match

*

Matches zero or more characters anywhere in the string.

a*

a aa abc ab abc aXc abc acc adc

bc babc

?

Matches any single character Matches a sequential range of characters

a?c

a, ab aac aec afc abbc a ab Ac adc

[-]

a[b-d]c

[…]

Matches any one character from a set of characters

a[bc]c

abc acc

While wildcard patterns are very simple, their matching capabilities are limited, so PowerShell also provides a set of operators that use regular expressions. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

129

4.4.2 Regular expressions
Regular expressions are conceptually (if not syntactically) a superset of wildcard expressions. By this, we mean that you can express the same patterns in regular expressions that you could in wildcard expression, but with slightly different syntax.

NOTE
In fact, in versions 1 and 2 of PowerShell, wildcard patterns are translated internally into the corresponding regular expressions under the covers.

With regular expressions, instead of using “*” to match any sequence of characters as you would in wildcard patterns, you use “.*”. And, instead of using “?” to match any single character, you use the dot “.” instead.

NOTE
The name “regular expressions” comes from theoretical computer science, specifically the branches of automata theory (state machines) and formal languages. Ken Thompson, one of the creators of the UNIX operating system, saw an opportunity to apply this theoretical aspect of computer science to solve a real-world problem—namely finding patterns in text in an editor—and the rest is history. Most modern languages and environments that work with text now allow you to use regular expressions. This includes languages such as Perl, Python, and VBScript, and environments such as EMACS and Microsoft Visual Studio. The regular expressions in PowerShell are implemented using the .NET regular expression classes. The pattern language implemented by these classes is very powerful; however, it’s also very large, so we can’t completely cover it in this book. On the other hand, since PowerShell directly uses the .NET regular expression classes, any source of documentation for .NET regular expressions is also applicable to PowerShell. For example, the Microsoft Developer Network has extensive (if rather fragmented) online documentation on .NET regular expressions.

However, while regular expressions are similar to wildcard patterns, they are much more powerful and allow you to do very sophisticated text manipulation with very small amounts of script. We'll look at the kinds of things you can do with these patterns in the next few secions.

4.4.3 The -match operator
The PowerShell version 1 operators that work with regular expressions are -match and -

replace. These operators are shown in table 4.6 along with a description and some examples. PowerShell V2 introduced an additional -split operator which we'll cover in its own detail.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

130

Table 4.6 PowerShell regular expression match and replace operators

Operator

Description

Example

Result

-match -cmatch -imatch

Do a pattern match using regular expressions.

“Hello” –match “[jkl]”

$true

-notmatch Do a regex pattern match; return -cnotmath true if the pattern doesn’t match. -inotmatch -replace -creplace -ireplace

“Hello” –notmatch “[jkl]”

$false

Do a regular expression “Hello” –replace “ello”,”i’ substitution on the string on the left side and return the modified string. Delete the portion of the string matching the regular expression.

“Hi”

“abcde” –replace “bcd”

“ae”

The -match operator is similar to the -like operator in that it matches a pattern and returns a result. However, along with that result, it also sets the $matches variable. This variable contains the portions of the string that are matched by individual parts of the regular expressions. The only way to clearly explain this is with an example. Here we go: PS (1) > "abc" -match "(a)(b)(c)" True In this example, the string on the left side of the -match operator is matched against the pattern on the right side. In the pattern string, you can see three sets of parentheses. Figure 4.7 shows this expression in more detail. You can see on the right side of the match operator that each of the components in parentheses is a “submatch”. We’ll get to why this is important in the next section.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

131

Match operator

(0) Complete pattern

"abc" -match "(a)(b)(c)"

String to match (1) First Submatch (2) Second Submatch (3) Third Submatch

Figure 4.7 This diagram shows the anatomy of a regular expression match operation where the pattern contains submatchs. Each of the bracketed elements of the pattern corresponds to a submatch pattern.

This diagram shows the anatomy of a regular expression match operation where the pattern contains submatches. Each of the bracketed elements of the pattern corresponds to a submatch pattern. The result of this expression was true, which means that the match succeeded. It also means that $matches should be set, so let’s look at what it contains: PS (2) > $matches Key --3 2 1 0 Value ----c b a abc

$matches contains a hashtable where the keys of the hashtable are indexes that correspond to parts of the pattern that matched. The values are the substrings of the target string that matched. Note that even though we only specified three subpatterns, the hashtable contains four elements. This is because there is always a default element that represents the entire string that matched. Here’s a more complex example that shows multiple nested matches. PS (4) > "abcdef" -match "(a)(((b)(c))de)f" True PS (5) > $matches Key --5 4 3 2 1 0 Value ----c b bc bcde a abcdef

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

132 Now we have the outermost match in index 0, which matches the whole string. Next we have a top-level match at the beginning of the pattern that matches “a” at index 1. At index 2, we have the complete string matched by the next top-level part, which is “bcde”. Index 3 is the first nested match in that top-level match, which is “bc”. This match also has two nested matches: b at element 4 and c at element 5. MATCHING USING NAMED CAPTURES Of course, calculating these indexes is fine if the pattern is simple. If it’s complex as in the previous example, it’s hard to figure out what goes where; and even if you do, when you look at what you’ve written a month later, you’ll have to figure it out all over again. The .NET regular expression library provides a way to solve this problem by using named captures. You specify a named capture by placing the sequence “?” immediately inside the parentheses that indicate the match group. This allows you to reference the capture by name instead of by number, making complex expressions easier to deal with. This looks like: PS (10) > "abcdef" -match "(?a)(?((?b)(?c))de)f" True PS (11) > $matches Key --o1 e3 e4 o2 1 0 Value ----a b c bcde bc abcdef

Now let’s look at a more realistic example. PARSING COMMAND OUTPUT USING REGULAR EXPRESSIONS Existing utilities for Windows produce text output, so you have to parse the text to extract information. (As you may remember, avoiding this kind of parsing was one of the reasons PowerShell was created. However, we still need to interoperate with the rest of the world.) For example, the net.exe utility can return some information about your computer configuration. The second line of this output contains the name of the computer. Our task is to extract the name and domain for this computer from that string. One way to do this is to calculate the offsets and then extract substrings from the output. This is tedious and error prone (since the offsets might change). Here’s how to do it using the $matches variable. First let’s look at the form of this string. PS (1) > (net config workstation)[1] Full Computer name brucepay64.redmond.corp.microsoft.com It begins with a well-known pattern “Full Computer name”, so we start by matching against that to make sure there are no errors. Then we see that there is a space before the name, and the name itself is separated by a period. We’re pretty safe in ignoring the intervening characters, so here’s the pattern we’ll use: PS (2) > $p='^Full Computer.* (?[^.]+)\.(?[^.]+)' Figure 4.8 shows this pattern in more detail.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

133

^ anchors string

Sequence containing anything but ‘.’

^Full Computer.* (?[^.]+)\.(?[^.]+)' .* matches any characters matches ‘.’

Figure 4.8 This is an example of a regular expression pattern that uses the named submatch capability. When this expression is used with the -match operator, instead of using simple numeric indexes in the $matches variable for the substrings, the names will be used,

This is an example of a regular expression pattern that uses the named submatch capability. When this expression is used with the -match operator, instead of using simple numeric indexes in the $matches variable for the substrings, the names will be used. We check the string at the beginning, then allow any sequence of characters that ends with a space, followed by two fields that are terminated by a dot. Notice that we don’t say that the fields can contain any character. Instead we say that they can contain anything but a period. This is because regular expressions are greedy—that is, they match the longest possible pattern, and since the period is any character, the match will not stop at the period. Now let’s apply this pattern. PS (3) > (net config workstation)[1] -match $p True It matches, so we know that the output string was well formed. Now let’s look at what we captured from the string. PS (4) > $matches.computer brucepay64 PS (5) > $matches.domain redmond We see that we’ve extracted the computer name and domain as desired. This approach is significantly more robust than using exact indexing for the following reasons. First, we checked with a guard string instead of assuming that the string at index 1 was correct. In fact, we could have written a loop that went through all of the strings and stopped when the match succeeded. In that case, it wouldn’t matter which line contained the information; we would find it anyway. We also didn’t care about where in the line the data actually appeared, only that it followed a basic well-formed pattern. With a pattern-based approach, output format can vary significantly, and this pattern would still retrieve the correct data. By using techniques like this, you can write more change-tolerant scripts than you would otherwise do. The -match operator lets us match text, now let's look at how to go about making changes to text. This is what the -replace operator is for so we'll look at that next.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

134

4.4.4 The -replace operator
The -replace operator allows you to do regular expression-based text substitution on a string or collection of strings. The syntax for this operator is shown in figure 4.9.

Replace operator

Replacement string

"1, 2, 3,4" -replace "\s*,\s*",”+”

Target string

Pattern to replace

Figure 4.9 This diagram shows the syntax of the -replace operator..

Let's run the example from the syntax diagram: PS {1) > "1, 2, 3,4" -replace "\s*,\s*","+" 1+2+3+4 What this has done is replace every instance of a comma surrounded by zero or more spaces with a '+' sign. Now let's look at the example we saw at the beginning of this chapter: ${c:old.txt} -replace 'is (red|blue)','was $1' > new.txt We can now discuss what the -replace operator is doing in this case. First look at the pattern to replace: 'is (red|blue)'. From our earlier discussion about regular expression with -match we know that parenthesis establish a submatch. Now look at the replacement string. It contains '$1' which might be assumed to be a PowerShell variable. However, since the string is in single quotes, it won't be expanded. Instead, the regular expression engine uses this notation to allow submatches to be referenced in the replacement expression. This what allows it to intelligently replace "is" with "was": PS {2) > "The car is red" -replace 'is (red|blue)','was $1' The car was red PS {3) > "My boat is blue" -replace 'is (red|blue)','was $1' My boat was blue The pattern matches "is red" or "is blue" but we only want to replace "is". These substitutions make this possible. The complete set of substitution character sequences is shown in table 4.7. Finally, what happens if the pattern doesn't match? Let's try it PS {4) > "My bike is yellow" -replace 'is (red|blue)','was $1' My bike is yellow and we see that if the pattern isn't matched, the string is simply returned as is.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

135

Table 4.7 Character sequences for doing substitutions in the replacement pattern for the -replace operator

CharacterSequence

Description Substitutes the last submatch matched by group number.. Substitutes the last submatch matched by a named capture of the form (? ) . Substitutes a single "$" literal. Substitutes a copy of the entire match itself. Substitutes all the text from the argument string before the matching portion.

$number ${name}

$$ $& $`

$' $+_ $_

Substitutes all the text of the argument string after the matching portion. Substitutes the last submatch captured. Substitutes the entire argument string. Now sometimes we'll want to use regular expression substitutions and PowerShell variable

expansion at the same time. We can do this by escaping the '$' before the substitution with a backtick '`'. The result looks like this: PS {5) > $a = "really" PS {6) > "The car is red" -replace 'is (red|blue)',"was $a `$1" The car was really red In the output string, the word "red" was preserved using the regular expression substitution mechanism and the word "really" was added by expanding the $a variable. We've looked at lots of ways to substitute one thing for something else. But sometimes you don't want to substitute something, you want to substitute nothing. More simply, you just want to remove the matching parts. You can do this using -replace by simply omitting the replacement string. PS {7) > "The quick brown fox" -replace 'quick' The brown fox In this example, the word "quick" was removed from the sentence. And now, here's one final point we should make clear. The -replace operator doesn't actually change strings - it returns a new string with the necessary edits applied. To illustrate this, we'll put a string in a variable and then use it in a -replace expression. PS {8) > $orig = "abc" PS {9) > $orig -replace "b","B" aBc ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

136 PS {10) > $orig abc PS {11) > In the resulting output from the -replace expression, the lower case 'b' has been change to an upper case "B". However, when we look at the original string, we see that it is unchanged. The result string is actually a new string with the substitutions performed on it rather than on the original. Up to this point, all of the operations we've looked at have involved transformations on a single string. Now let's look at how to take strings apart and put them back together using two more string operators: -split and -join. This will complete the our knowledge of the set op operators PowerShell provides for manipulating strings.

4.4.5 The -join operator
PowerShell version 2 introduced two new operators for working with collections and strings: -

split and -join. These operators allow you to join the elements of a collections into a single string or split strings into a collection of substrings. We'll look at the -join operator first as it is the simpler of the two. As we mentioned, the -join operator allows us to join collections of objects into a single string. This operator can be used both as a unary operator and a binary operator. The syntax for the unary form of the -join operator is shown in figure 4.10.

Join operator -join 1,2,3

Collection to join

Figure 4.10 The unary join operator allows you to join a collection of objects in to a single string with nothing between each element.

The unary form of the -join operator allows you to concatenate a collections of strings together into a single string with no separator between each item in the resulting string. Here's is a simple example. First we'll assign an array of numbers to the variable $in. PS {1) > $in = 1,2,3 Now check the type of the variable's value: PS {2) > $in.GetType().FullName System.Object[] and we see that it's an array of objects. (Remember that PowerShell arrays are always created as polymorphic arrays, regardless of the content of the array.) Now we'll use the -join operator on this variable and assign the result to a new variable $out. PS {3) > $out = -join $in Checking the type of the result, PS {4) > $out.GetType().FullName System.String

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

137 we see that it's a string. The -join operator first converted each array element into a string and then joined the results into a single larger string. Let's look the contents of the $out to see what the result looks like: PS {5) > $out 123 and it's "123" as expected. Now let's do something a bit more sophisticated. Say we want to reverse a string. Unfortunately the .NET [string] type has no built-in reverse operator. However, the [array] type, does have a static method for reversing arrays. This method takes an array as input and sorts it in place. In order to use this, we need to do two conversions - from string to array of characters and from array of characters back to a string. From chapter 3, we know that we can use a cast to convert a string into a character array. PS {6) > $ca = [char[]] "abcd" Now that we have a character array, we can use the Reverse() method. PS {7) > [array]::reverse($ca) This method reverses the contents of the array in-place so when we look at the result, we see that it's reversed as desired however is still an array of chars and we need a string. PS {8) > $ca d c b a This is where the unary -join comes into play. We'll use it to convert the character array back into a string. PS {9) > $ra = -join $ca And now we'll verify that the string has been created properly. PS {10) > $ra dcba Yes it has. Now let's look at one potential "gotcha" using the unary form of the operator. We'll redo the join of 1,2,3 again, but without using a variable to hold the value. Here's what that looks like. PS {11) > -join 1,2,3 1 2 3 Surprise! Instead of joining the array members into a single string, it just returned the same array. This is because unary operators have higher precedence than binary operators and, in PowerShell, the comma is a binary operator. As a result, the expression is parsed like PS {12) > (-join 1),2,3 1 2 3 So remember, to use the unary -join operator in a more complex expression, make sure you put parentheses around the argument expression: PS {13) > -join (1,2,3) 123 When parentheses are used, the result of the expression is as expected. So - that's the unary form of this operator. Now - let's look at the (much more useful) binary form. The binary form for the -join operator is shown in figure 4.11. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

138

Join operator 1,2,3 -join "+" Collection to join String to join with

Figure 4.11 The binary form of the join operator allows you to join a collection of objects in to a single string using the specified join string.

The obvious difference with this operator is that you can specify the string to use as an element separator instead of always using the default of nothing between the joined strings. Let's execute the example from the figure. We'll place the array to join into a variable called

$numbers and put the joined result into a variable $exp:
PS {1) > $numbers = 1,2,3 PS {2) > $exp = $numbers -join '+' Looking at the contents of $exp PS {3) > $exp 1+2+3 It contains the numbers with a plus sign between each number. Since this is a valid PowerShell expression, we can pass the resulting string to the Invoke-Expression cmdlet for evaluation PS {4) > Invoke-Expression $exp 6 where we get the result 6. Of course this works on any operator. We'll use the range operator (see chapter 5) and the multiply operator to calculate the factorial of 10. Here's what the code looks like: PS {5) > $fact = Invoke-Expression (1..10 -join '*') This is evaluating 1*2*3 and so on up to ten, with the result PS {6) > $fact 3628800 of 366200. While this is a simple way to calculate factorials (which, of course are a day to day requirement in system administration - yeah - sure.) it's not very efficient. Later on we'll see more efficient ways of writing this type of expression. For now, let's look at a more practical example. We'll do some work with a file. We'll use a here-string to generate a test file on disk. PS {7) > @' >> line1 >> line2 >> line3 >> '@ > out.txt >> Now we'll use the Get-Content cmdlet to read that file into a variable $text. PS {8) > $text = Get-Content out.txt

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

139 Use the text property to see how long the file was. PS {9) > $text.Count 3 Clearly this is not the number of characters in the file. It's actually the number of lines in the file. The Get-Content cmdlet returns the contents of a file as an array of strings. For example, to see the second line in the file, you can do PS {10) > $text[1] line2 and to check the type of the value in $text, we'll again use the GetType() method PS {11) > $text.GetType().FullName System.Object[] and it it's an [object] array which we should be used to by now. While this is exactly what we want most of the time, sometimes we just want the entire file as a single string. The GetContent cmdlet, as of PowerShell V2, has no parameter for doing this, so we'll have to take the array of strings and turn it back into a single string. We can do this with the binary -join operator if we specify the line separator as the string to use when joining the array elements. On Windows, the line separator is actually two characters: carriage return (`r) and a line feed (`n). In a single string, this is expressed as "`r`n". Now we can use this separator string in a join expression. PS {12) > $single = $text -join "`r`n" PS {13) > $single.Length 19 Now that's more like it. And when we check index zero: PS {14) > $single[0] l we see that it's now a single character instead of a string. Now one more example. Let's look at how we can generate a string containing comma separated values - as CSV string. This is shown in the next example. PS {16) > $csv = -join ('"',($numbers -join '","'), '"') PS {17) > $csv "1","2","3" PS {18) > We use -join to insert the sequence (",") between each element and then use string concatenation to add double quotes to either end. A very simple 1-line CSV generator. Now that we know how to put things together, let's find out how to take them apart with -

split.

4.4.6 The -split operator
The -split operator performs the opposite operation to -join: it splits strings into a collection of smaller strings. Again, this operator can be used in both binary and unary forms. The unary form of split is shown in figure 4.12.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

140

Split operator

-split "a b c"

String to split

Figure 4.12 The unary -split operator allows you to split a string into a collection of smaller strings.

In its unary form, this operator will split a string on whitespace boundaries, where whitespace is any number of spaces, tabs or newlines. We saw this in the example earlier in this chapter. The binary form of the operator is much more, ahem, sophisticated. It allows you to specify the pattern to match on, the type of matching to do and the number of elements to return as well as match type-specific options. The full (and rather intimidating) syntax for this operator is shown in figure 4.13.

Split operator

The pattern to split with

Match-type specific options

"a, b ,c" -split "\w*,\w*”,n,MatchType,Options Maximum number of substrings Type of matching to use

String to split

Figure 4.13 The -split operator allows you to split a string into a collection of smaller strings. It allows you to specify a variety of arguments and options to control how the target string is split.

Ok - while this looks intimidating, most of the time you just need to specify an argument string and split pattern and let the rest of the options use their default values. Let's take a look at the basic application of this operator. First, we'll split a string on a character other than whitespace. PS {11) > 'a:b:c:d:e' -split ':' a b c d e This is pretty straight forward. The string is split into five elements at the ':' character. Of course sometimes we don't want all of the matches. The -split operator allows us to limit the number of elements to return. This is done by specifying an integer after the match pattern as shown in the following: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

141 PS {12) > 'a:b:c:d:e' -split ':',3 a b c:d:e In this case, we only asked for 3 elements to be returned. Notice that the third piece is the entire remaining string. If you specify a split count number less than or equal to 0, then all of the splits take place: PS {13) > 'a:b:c:d:e' -split ':',0 a b c d e We'll see why this is important in a minute. By default, -split uses regular expressions just line -match and -replace. However, if the string you are trying to split contains one of the many characters that have special meaning in regular expressions, things become a bit more difficult because you'll have to escape these characters in the split pattern. This can be inconvenient and error prone so -

split allows you to choose simple matching through an option "simplematch". When
"simplematch" is specified, instead of treating the split pattern as a regular expression, it's handled as a simple literal string that must be matched. the For example, say we want to split on '*'. Let's try this. PS {14) > 'a*b*c' -split "*" Bad argument to operator '-split': parsing "*" - Quantifier {x,y} following nothing.. At line:1 char:15 + 'a*b*c' -split $opts = '' Now we'll pass this in the options position for the operator as shown: PS {2) > 'axbXcxdXe' -csplit 'x',0, $opts a bXc dXe Since the option variable was empty, we get the expected behavior where the split is done in the case-sensitive manner as determined by -csplit. Let's assign 'ignorecase' to the variable and try it again. PS {3) > $opts = 'ignorecase' PS {4) > 'axbXcxdXe' -csplit 'x',0, $opts a b c d e This time the string splits on all instances of 'x' regardless of case, even though the csplit operator was used. This shows how the parse time defaults can be overridden at runtime. The next option we want to look at is the 'multiline' option. This option can only be used with regualr expression matches and changes what the pattern matcher considers to be the beginning of the string. In regular expressions, you can match the beginning of a line with the '^' metacharacter. In the default 'singleline' mode, the beginning of the line is the beginning of the string. Any newlines in the string are not treated as the beginning of a line. When you use the multiline option, embedded newlines are treated as the beginning of a line. Let's look at an example. First we need some text to split - we'll use a here-string to put this text into the

$text variable.
PS >> >> >> >> >> >> >> >> >> >> >> >> {5) > $text = @' 1 aaaaaaa aaaaaaa 2 bbbbbbb 3 ccccccc ccccccc 4 ddddddd '@

In our example text, each section of the document is numbered. We want to split these 'chapters' into elements in an array so $a[1] is chapter 1, $a[2] is chapter 2 and so on. The pattern we're using ('^\d') will match lines that begin with a number. pattern to split the document in multiline mode, assigning the result to $a. PS {6) > $a = $text -split '^\d', 0, "multiline" If all went as planned, $a should now contain 4 elements. PS {7) > $a.length 5 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 Now let's use this

Licensed to Andrew M. Tearle

144 Wait a minute - it's 5! But there were only 4 sections! There are actually 5 sections because the empty text before the '1' get's its own 'chapter'. Now let's look at chapter 1, which should be in $a[1] PS {8) > $a[1] aaaaaaa aaaaaaa and chapter in $a[2] PS {9) > $a[2] bbbbbbb PS {10) > As we can see, the multiline option with regular expressions allows for some pretty slick text processing. USING SCRIPTBLOCKS WITH THE -SPLIT OPERATOR As powerful as regular expressions are, sometimes you may need to split a string in a way that isn't convenient or easy to handle with regular expressions. To deal with these cases, PowerShell allows you to pass scriptblock to the operator which is used as predicate function that determines if there is a match or not. Here's an example of how to use this. First we'll set up a string to split. This string contains a list of colors that we want to split into pairs, two colors per pair. PS {17) > $colors = "Black,Brown,Red,Orange,Yellow," + >> "Green,Blue,Violet,Gray,White'" Next we initialize a counter variable that will be used by the scriptblock. We're using an array here because we need to be able to modify the contents of this variable. Since the scriptblock is executed in its own scope we need to pass it an array so it can modify the value. PS {18) > $count=@(0) And now let's split the string. The scriptblock, in braces in the example, splits the string on every other ','. PS {19) > $colors -split {$_ -eq "," -and ++$count[0] % 2 -eq 0 } Black,Brown Red,Orange Yellow,Green Blue,Violet Gray,White' This gives us the color pairs we were looking for. Whew! So that’s it for the pattern matching and text manipulation operators. In this section, we covered the two types of pattern matching operators—wildcard patterns and regular expressions. Wildcard patterns are pretty simple, but learning to use regular expressions effectively requires more work. On the other hand, you’ll find that the power of regular expressions is more than worth the effort invested to learn them. (We’ll come back to these patterns again in chapter 6 when we discuss the switch statement.) We also looked at how to split strings into collections and join collections into strings. All very spiffy but let’s come back down to earth now and cover the last of the basic operators in the PowerShell

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

145 language. These are the logical operators (-and, -or, -not) and their bitwise equivalents (-band, -bor, -bnot).

4.5 Logical and bitwise operators
Finally, PowerShell has logical operators -and, -or, -xor, and -not for composing simpler comparisons into more complex expressions. The logical operators convert their operands into Boolean values and then perform the logical operation. PowerShell also provides corresponding bitwise operators for doing binary operations on integer values. These operators can be used to test and mask bit fields. Both of these sets of operators are shown in figure 4.14.
Logical operators -and -or -not -xor

Bitwise operators -band -bor -bnot -bxor

Figure 4.14 This diagram shows the logical and bitwise operators available in PowerShell.

Table 4.9 lists these operators with examples showing how each of these operators can be used.

Table 4.9 Logical and bitwise operators

Operator

Description Do a logical and of the left and right values.

Example

Result

-and

0xff -and $false

$false

-or -xor

Do a logical or of the left and right values. Do a logical exclusive-or of the left and right values.

$false –or 0x55 $true $false –xor $true $true –xor $true -not $true $true

$false

-not

Do the logical complement of the argument value. Do a binary and of the bits in the values on the

$false

-band

0xff –band 0x55 85 (0x55)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

146 left and right side.

-bor

Do a binary or of the bits in the values on the left and right side. Do a binary exclusive-or of the left and right values.

0x55 -bor 0xaa

255 (0xff)

-bxor

0x55 -bxor 0xaa 255 (0xff) 0x55 -bxor 0xa5 240 (0xf0)

-bnot

Do the bitwise complement of the argument value.

-bnot 0xff

-256 (0x ffffff00)

As with most C/C++ based languages, the PowerShell logical operators are short-circuit operators—they only do as much work as they need to. With the -and operator, if the left operand evaluates to $false then the right operand expression is not executed. With the -or operator, if the left operand evaluates to $true then the right operand is not evaluated.

AUTHOR'S NOTE
In version 1 of PowerShell, the bitwise operators were limited in that they only supported 32 bit integers ([int]). In version 2, support was added for 64 bit integers ([long]). If the arguments to the operators are neither [int] nor [long], PowerShell will attempt to convert them into [long] and then perform the operation.

4.6 Summary
This concludes our tour of the basic PowerShell operators. We covered a lot of information, much of it in great detail. We covered the basic PowerShell operators and expressions with semantics and applications of those operators. The important points to remember are: 12.PowerShell operators are polymorphic with special behaviors defined by PowerShell for the basic types: numbers, strings, arrays, and hashtables. For other object types, the "op_" methods are invoked. 13.The behavior of most of the binary operators is determined by the type of the operand on the left. 14.PowerShell uses widening when working with numeric types. For any arithmetic operation, the type of the result will be the narrowest .NET numeric type that can properly represent the result. Also note that integer division will underflow into floating point if the result of the operation isn't an integer. Casts can be used to force and integer result. 15.There are two types of pattern matching operations in PowerShell—wildcard patterns (usually used for matching filenames) and regular expressions.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

147 16.Because the comparison and pattern matching operators work on collections, in many cases you don’t need a looping statement to search through collections. 17.Regular expressions are powerful and can be used to do complex text manipulations with very little code. PowerShell uses the .NET regular expression classes to implement the regular expression operators in the language. 18.PowerShell version 2 introduced two new operators for working with text: -split and join. With the addition of these two, the set of text manipulation operators is now complete. 19.PowerShell has build-in operators for working with binary values: -band, -bor, -bxor and -bnot. But we’re not done yet! Join us in the next chapter for “Operators: The Sequel” or “Son of Operators”. In that chapter, we’ll finish off operators and expressions and also go over how variables are used. Please stay tuned.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

148

5
Advanced operators and variables

The greatest challenge to any thinker is stating the problem in a way that will allow a solution. —Bertrand Russell

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

149 The previous chapter covered the basic operators in PowerShell, and in this chapter we’re going to continue the discussion of operators by covering the more advanced ones, which include things that some people don’t think of as operators at all. We'll break the operators into related groups as shown in figure 5.1.

Operators for working with types -is -isnot -as

Unary Operators -not + - -- ++ [cast] ,

Grouping, Expression and Subexpression Operators ( ) $( ) ) @(

Array Operators [ ] , ..

Property and Method Reference Operators ::() .()

The Format operator

Redirection Operators

-f

>

>>

2> 2>> 2>&1

Figure 5.1 This diagram shows the broads groups of operators we'll cover in this chapter.

In this chapter, we'll look at how to work with types, properties and methods and how we can use these operators to build complex data structures. The chapter concludes with a detailed discussion of how variables work in PowerShell, and how you can use them with operators to accomplish significant tasks.

5.1 Operators for working with types
The type of an object is fundamental to determining the sorts of operations we can perform on that object. Up until now, we’ve been allowing the type of the object to implicitly determine the operations that are performed. But sometimes we want to do this explicitly. So that we may do this, PowerShell provides a set of operators that can work with types, as shown in figure 5.2. They are also listed in table 5.1 with examples and more description.

Operators for working with types -is -isnot -as

Figure 5.2 In this section we'll cover the binary operators for working with types ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

150 These operators let us test whether an object is of a particular type or enable us to convert an object to a new type. The -is operator returns true if the object on the left side is of the type specified on the right side. By “is”, we mean that the left operator is either of the type specified on the right side or is derived from that type. (See the section “Brushing up on objects” in chapter 1 for an explanation of derivation.) The -isnot operator returns true if the left side is not of the type specified on the right side. The right side of the operator must be represented as a type or a string that names a type. This means that you can either use a type literal such as [int] or the literal string “int”. The as operator will try to convert the left operand into the type specified by the right operand. Again, either a type literal can be used or you can use a string naming a type.

AUTHOR’S NOTE
The PowerShell -is and -as operators are directly modeled on the corresponding operators in C#. However, PowerShell’s version of -as uses PowerShell’s more aggressive approach to casting. For example, the C# as will not cast the string “123” into the number 123, whereas the PowerShell operator will do so. The PowerShell -as operator will also work on any type and the C# operator is restricted to reference types.

You may be wondering why we need the -as operator when we can just use a cast. The reason is that the -as operator allows you to use a runtime expression to specify the type, whereas the cast is fixed at parse time. Here’s an example showing how you can use this runtime behavior. PS (1) > foreach ($t in [float],[int],[string]) {"0123.45" -as $t} 123.45 123 0123.45 In this example, we looped over a list of type literals and converted the string into each of the types. This isn’t possible when types are used as casts. Finally, there is one additional difference between a regular cast and using the -as operator. In a cast, if the conversion doesn’t succeed, an error is generated. With the -as operator, if the cast fails then the expression returns $null instead of generating an error. PS (2) > [int] "abc" -eq $null Cannot convert "abc" to "System.Int32". Error: "Input string was not in a correct format." At line:1 char:6 + [int] $l=1 PS {2) > foreach ($s in "one","two","three") >> { "$($l++): $s" } >> : one : two : three The foreach statement loops over the strings and emits our output. The ‘++’ in the subexpressions (which we’ll get to next) causes the variable to be incremented. However, because the expression is treated as a statement, there is no output in the string. Here’s how we can fix it. We’ll make one small change and add ‘(‘ and ‘)’ around the increment expression. Let’s try it again: PS {3) > $l=1 PS {4) > foreach ($s in "one","two","three") >> { "$(($l++)): $s" } >> 1: one 2: two 3: three PS {5) > This time it works properly – we see the numbers in the output strings.

AUTHOR’S NOTE
Only some statements are considered voidable. For other statement types, you’ll have to explicitly discard the output of a statement. In effect, you want to turn a regular statement into a voidable one. The way to do this through an explicit cast is to [void] as in [void]

(Write--Output "discard me"). The statement whose value you want to discard is enclosed in parentheses and the whole thing is cast to void. We’ll see another way to accomplish the same effect using the redirection operators later in this chapter.

Now having touched on subexpressions in our discussion of voidable statements, let’s take a more formal look at them in the next section where we cover all of the grouping constructs in the language..

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

155

5.3 Grouping, subexpressions, and array subexpressions
So far we’ve seen a variety of situations where collections of expressions or statements have been grouped together. We’ve even used these grouping constructs in string expansions as we saw in the last section. These operators are shown in figure 5.4.

Grouping, Expression and Subexpression Operators ( ) $( ) @( )

Figure 5.4 The PowerShell operators for grouping expressions and statement.

Now we’ll look at them in more detail. Table 5.3 provides more details and some examples.

Table 5.3 Expression and statement grouping operators
Operator Example Results Description

( … )

( 2 + 2 ) * 3 (get-date).dayofweek
12 Returns the current week day.

Parentheses group expression operations and may contain either a simple expression or a simple pipeline. They may not contain more than one statement or thing like while loops and so on. Subexpressions group collections of statements as opposed to being limited to a single expression. If the contained statements return a single value, it will be returned as a scalar. If the statements return more than one value, they will be accumulated in an array.

$( … )

$($p = “a*”; get-process $p )

Returns the process objects for all proc-esses starting with the letter a.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

156

@( … ) @( dir c:\; dir d:\)

Returns an array containing the

FileInfo objects in the root of the C:\ and D:\ drives.

The array subexpression operator groups collections of statements in the same manner as the regular subexpression operator, but with the additional behavior that the result will always be returned as an array.

The first grouping notation is the simple parenthetical notation�. As in most languages, the conventional use for this notation is to control the order of operations�, as shown by the following example: PS (1) > 2+3*4 14 PS (2) > (2+3)*4 20 The parentheses in the second expression cause the addition operation to be performed first. In PowerShell, parentheses also have another use. Looking at the syntax specification shown in figure 5.4 for parenthetical expressions illustrates this: ( ) From the syntax, we can see that pipelines are allowed between simple parentheses. This allows us to use a command or pipeline as a value in an expression. For example, to obtain a count of the number of files in a directory, we can use the dir command in parentheses, then use the Count property to get the number of objects returned. PS (1) > (dir).count 46 Using a pipeline in the parentheses lets us get a count of the number of files matching the wildcard pattern “*.doc”. PS (2) > (dir | where {$_.name -like '*.doc'}).count 32

AUTHOR’S NOTE
People familiar with other languages tend to assume that the expression (1,2,3,4) is an array literal in PowerShell. In fact, as was discussed at length in chapter 3, this is not the case. The comma operator, discussed in the next section, allows you to easily construct arrays in PowerShell, but there are no array literals as such in the language. All that the parentheses do is control the order of operations. Otherwise, there is nothing special about them. In fact, the precedence of the comma operator is such that you typically never need parentheses for this purpose. More on that later.

Now let’s move on to the next set of grouping constructs—the subexpressions. There are two forms of the subexpression construct, as shown in the following: $( ) @( )

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

157

5.3.1 Subexpressions $( ... )
The syntactic difference between a subexpression (either form) and a simple parenthetical expression is that you can have any list of statements in a subexpression instead of being restricted to a single pipeline. This means that you can have any PowerShell language element in these grouping constructs, including loop statements�. It also means that you can have several statements in the group. Let’s look at an example. Earlier in this chapter, we looked at a short piece of PowerShell code that calculates the numbers in the Fibonacci sequence below 100. At the time, we didn’t count the number of elements in that sequence. We can do this easily using the subexpression grouping construct. PS (1) > $($c=$p=1; while ($c -lt 100) {$c; $c,$p=($c+$p),$c}).count 10 By enclosing the statements in $( ... ), we can retrieve the result of the enclosed collection of statements as an array.

AUTHOR’S NOTE
Many languages have a special notation for generating collections of objects. For example, Python� and functional languages such as Haskell� have a feature called list comprehensions� for doing this. PowerShell (and shell languages in general) don’t really need special syntax for this kind of operation. Collections occur naturally as a consequence of the shell pipeline model. If a set of statements used as a value returns multiple� objects, they will automatically be collected into an array.

Another difference between the subexpression construct and simple parentheses is how voidable expressions are treated. We mentioned this concept earlier with the increment and decrement operators. A voidable expression is one whose result is discarded when used directly as a statement. Here’s an example that illustrates this. First we initialize $a to 0 and then use a post-increment expression in parentheses and assign it to the variable $x. PS (1) > $a=0 PS (2) > $x=($a++) And checking the value of $x, we see that it is zero, as expected, and that $a is now 1. PS (3) > $x 0 PS (4) > $a 1 Now do a second assignment, this time with the expression in $( ... ). PS (5) > $x=$($a++) Checking the value, we see that it’s actually $null. PS (6) > $x PS (7) > $x -eq $null True This is because the result of the post-increment operation was discarded, so the expression returned nothing. Now try a more complex statement in the subexpression: PS (8) > $x=$($a++;$a;$a++;$a) PS (9) > $x 3 4

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

158 Notice that even though there are four statements in the subexpression, $x only received two values. Again, the results of the post-increment statements were discarded so they don’t appear in the output. Now let’s take a look at the difference between the array subexpression� @( ... ) and the regular subexpression.

5.3.2 Array Subexpressions @( ... )
The difference is that in the case of the array subexpression, the result is always returned as an array; this is a fairly small but very useful difference. In effect, it’s shorthand for: [object[]] $( … ) This shorthand exists because in many cases you don’t know if a pipeline operation is going to return a single element or a collection. Rather than writing complex checks, you can use this construction and be assured that the result will always be a collection. If the pipeline returns an array, no new array is created and the original value is returned as is. If, however, the pipeline returns a scalar value, that value will be wrapped in a new one-element array. It’s important to understand how this is different from the behavior of the comma operator, which always wraps its argument value in a new one-element array. Doing something like @( @(1) ) does not give you a 1-element array containing a second 1 element array containing a number. The expressions @(1), @(@(1)) or @(@(@(1))) all return the same value. On the other hand, ,’ nests to 1 level, ,,1 nests to two levels, etc.

AUTHOR’S NOTE
How to figure out what the pipeline returns is the single hardest thing to explain in the PowerShell language. As one of the designers of the language, this more than anything kept me up at night. The problem is that people get confused; they see that @(12) returns a one-element array containing the number 12. Because of prior experience with other languages, they expect that @(@(12)) should therefore produce a nested array, an array of one element containing an array of one element which is the integer 12. As mentioned previously, this is not the case. @(@(12)) returns exactly the same thing as @(12). If you think of rewriting this expression as [object[]]$([object[]] $( 12 )), then it is clear why this is the case—casting an array into an array of the same type has no effect; it’s already the correct type, so you just get the original array.

Here’s an example of where this feature is useful. We’ll write a pipeline expression that sorts some strings, then returns the first element in the sorted collection. We’ll start by sorting an array of three elements: PS (1) > $("bbb","aaa","ccc" | sort )[0] aaa This returns “aaa” as we expect. Now do it with two elements: PS (2) > $("bbb","aaa" | sort )[0] aaa Still “aaa”, so everything makes sense. Now try it with one element: PS (3) > $("aaa" | sort )[0] a

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

159 Wait a minute—what happened here? In fact, what happened is that we sorted one element and in a pipeline, we can’t tell if the commands in the pipeline mean to return a single object (a scalar) or an array containing a single object. The default behavior in PowerShell is to assume that if we return one element, we intended to return a scalar. In this case, the scalar is the string “aaa” and index 0 of this array is the letter “a”, which is what the example returns. This is where we use the array subexpression notation because it ensures that we always get what we want. We know we want the pipeline to return an array, and by using this notation, we can enforce the correct behavior. Here are the same three examples again, but this time using the array subexpression: PS (4) > @("bbb","aaa","ccc" | sort )[0] aaa PS (5) > @("bbb","aaa" | sort )[0] aaa PS (6) > @("aaa" | sort )[0] aaa PS (7) > This time, all three commands return “aaa” as intended. So why have this notation? Why not just use the casts? Well, here’s what it looks like using the cast notation: PS (7) > ( [object[]] ("aaa" | sort ))[0] aaa Because of the way precedence works, you need an extra set of parentheses to get the ordering right, which makes the whole expression harder to write. In the end, the array subexpression notation is easy to use, but it is a bit difficult to learn and understand. As we discussed in chapter 1, on the whole, we’d rather be both—easy to use and easy to learn—but we’ll take easy to use over easy to learn. You only have to learn something once, but you have to use it over and over again. In any case, since we’re discussing arrays, this is a great time to move on to the other operations PowerShell provides for dealing with collections and arrays of objects. The ability to manipulate collections of objects effectively is the heart of any automation system. You can easily perform a single operation manually but the hard problem is performing operations on a large set of objects. Let's see what PowerShell has to offer here.

5.4 Array operators
Arrays or collections of objects occur naturally in many of the operations that you execute. Operations such a getting a directory listing in the file system results in a collection of objects. Getting the set of processes running on a machine or a list of services configured on a server both result in collections of objects. Not surprisingly, PowerShell has a set of operators and operations for working with arrays and collections. These operators are shown in figure 5.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

160

Array Operators [ ] , , ..

Figure 5.5 The PowerShell array operators

We'll go over these operators in the following sections.

5.4.1 The comma operator “,”
We’ve already seen many examples using the comma operator to build arrays. This was covered in some detail in chapter 3, but there are a couple of things we still need to cover. In terms of precedence, the comma operator has the highest precedence of any operator except for casts and property or array references. This means that when you’re building up an array with expressions, you need to wrap those expressions in parentheses. In the next example, we’re trying to build up an array containing the values 1, 2, and 3. We’re using addition to calculate the final value. Because “,” binds more strongly than plus, we won’t get what we wanted. PS (1) > 1,2,1+2 1 2 1 2 The result was an array of four elements 1,2,1,2 instead of 1,2,3. This is because the expression was parsed as (1,2,1)+2, building an array of three elements and then appending a fourth. You have to use parentheses to get the desired effect: PS (2) > 1,2,(1+2) 1 2 3 Now you get the result you wanted.

AUTHOR’S NOTE
The comma operator has higher precedence� than any other operator except type casts and property and array references. This is worth calling out again because it’s important to keep in mind when writing expressions. If you don’t remember this, you will produce some strange results.

The next thing to look at is nested arrays. Since a PowerShell array can hold any type of object, obviously it can also hold another array. We’ve already mentioned that using the array subexpression operation was not the way to build a nested array. Now let’s talk about how we ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

161 actually do it using assignments and the comma operator. Our task will be to build the tree structure shown in figure 5.6.

1

2

3

4

5

6

7

8

Figure 5.1 A binary tree (arrays of arrays of arrays)

This data structure starts with an array of two elements. These two elements are also both arrays of two elements, and they, in turn contain arrays of two numbers. Let's see how we go about constructing something like this. There are a couple of ways we can approach this. First, we can build nested arrays one piece at a time using assignments. Alternatively, we can just nest the comma operator within parentheses. Starting with last things first, here’s how to build up a nested array structure using commas and parentheses. The result is concise: PS (1) > $a = (((1,2),(3,4)),((5,6),(7,8)))

AUTHOR’S NOTE
LISP� users should feel fairly comfortable with this expression if they ignore the commas. Everybody else is probably shuddering.

And here’s the same construction using intermediate variables and assignments. It’s rather less concise but hopefully easier to understood. $t1 = 1,2 $t2 = 3,4 $t3 = 5,6 $t4 = 7,8 $t1_1 = $t1,$t2 $t1_2 = $t3,$t4 $a = $t1_1, $t2_2 In either case, what we’ve done is built up a data structure that looks like the tree shown in figure 5.6:

. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

162

AUTHOR’S NOTE
For Perl and PHP users: in those languages, you have to do something special to get reference semantics with arrays. In PowerShell, arrays are always reference types, so there is no special notation needed.

Let’s verify the shape of this data structure. First, use the length property to verify that $a does hold an array of two elements. PS (2) > $a.length 2 Next, check the length of the array stored in the first element of that array: PS (3) > $a[0].length 2 It’s also two elements long, as is the array stored in the second element. PS (4) > $a[1].length 2 Now let’s look two levels down. This is done by indexing� the result of an index as shown: PS (5) > $a[1][0].length 2 Note that $a[0][0] is not the same as $a[0,0], which is either a subset of the elements in the array called a slice if $a is one-dimensional, or a single index if the array is two dimensional (see the section on Array slices for more information on slices). You can compose index operations as deeply as you need to. Here we’re retrieving the second element of the first element of the second element stored in $a. PS (6) > $a[1][0][1] 6 To see exactly what’s going on here, take a look at figure 5.7. In this figure, the dotted lines show the path we followed in this last example that led us to get to the value 6.

$a[1][0][1] 6

[1]

[0]

[1]
1 2 3 4 5 6 7 8

Figure 5.7 Indexing through a binary tree with the expression $a[1][0][1]

And here's another example. We'll index with $a[0][0][0] which follows the left most edge of the tree producing 1 as shown in figure 5.8. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

163

$a[0][0][0] [0] 1 [0]

[0]
1 2 3 4 5 6 7 8

Figure 5.8 Indexing the left most edge of the same tree with $a[0][0][0]

These examples show how you can construct arbitrarily complex data structures in PowerShell. While this is not something you’ll need to use frequently, the capability is there if you need it. In section 5.4.3.1, when we discuss array slices, we’ll see an example where we use nested arrays� to index multi-dimensional arrays.

5.4.2 The range operator
The next operator to discuss is the range operator “..”. This operator is effectively a shortcut for generating a sequential array of numbers. For example, the expression: 1..5 is equivalent to 1,2,3,4,5 although it’s somewhat more efficient than using the commas. The syntax for the range operator is: .. It has higher precedence than all the binary operators except for the comma operator. This means that expressions like: PS (1) > 1..3+4..6 1 2 3 4 5 6 work, but the following gives you a syntax error: PS (2) > 1+3..4+6 Cannot convert "System.Object[]" to "System.Int32". At line:1 char:3 + 1+3 "1.1" .. 2.6 1 2 3 The range operator is most commonly used with the foreach loop because it allows us to easily loop a specific number of times or over a specific range of number. In fact this is done so often that the PowerShell engine treats this in a special way. A range like 1..10mb doesn’t really generate a 10MB array – it just treats the range endpoints as the lower and upper bounds of the loop making it very efficient. (The foreach loop is described detail in the next chapter.)

AUTHOR’S NOTE
In version 1 of PowerShell, the range operator was limited to an upper bound of 40KB to avoid accidentally creating arrays that were too large. In practice this was never really a problem so this limit was removed in version 2 with one exception. In restricted language mode, this limit is still enforced. Restricted language mode is covered in chapter XX.

The other place where the range operator gets used frequently is with array slices which are covered in the next section when we go over the details of array indexing.

5.4.3 Array indexing and slicing
Most people don’t think of indexing into an array as involving operators or that “[ ]” is an operator, but in fact, that’s exactly what it is. It has a left operand and a right operand (the “right” operand is inside the square brackets). The syntax for an array indexing expression is [ ] There are a couple of things to note here. First, this is one of the few areas where you can’t directly use a pipeline. That’s because square brackets don’t (and can’t) delimit a pipeline. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

165 Square brackets are used in pipeline arguments as wildcard patterns, as shown in the following command: dir [abc]*.txt | sort length This pipeline returns all the text files in the current directory that start with a, b, or c, sorted by length. Now, if the square bracket ended the pipeline, you’d have to type this instead: dir "[abc]*.txt" | sort length So, if you do want to use a pipeline as an index expression�, you have to use parentheses or the subexpression notation. The second thing to note is that spaces are not allowed between the last character of the expression being indexed and the opening square bracket. This is necessary to distinguish array expressions on the command line from wildcard patterns. Here’s an example to illustrate why this is a problem. First assign an array of three elements to $a: PS (14) > $a=1,2,3 Now write out the entire array along with the string “[0]” (remember, on the command line, strings don’t need to be quoted). PS (15) > write-host $a [0] 1 2 3 [0] Next, let’s just write out the first element of the array: PS (16) > write-host $a[0] 1 You can see that the only difference between the first and second command lines is the presence of a space between the array variable and the opening square bracket. This is why spaces are not permitted in array indexing operations. The square bracket is used for wildcard expressions, and we don’t want those confused with array indexing on the command line. From the syntax (and from previous examples), you can see that array indexing works on more than just variables. In fact, it can be applied to any expression that returns a value. Of course, because the precedence of the square brackets is high (i.e. they get evaluated before most other operators), you usually have to put the expression in parentheses. If you don’t, you’ll get an error, as in the following example. PS (1) > 1,2,3[0] Unable to index into an object of type System.Int32. At line:1 char:7 + 1,2,3[0 (1,2,3)[-1] 3 PS (4) > (1,2,3)[-2] 2 PS (5) > (1,2,3)[-3] 1 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

166 Specifying -1 retrieves the last element in the array, -2 retrieves the second-to-last element, and so on. In fact, negative indexes are exactly equivalent to taking the length of the array and subtracting the index from the array: PS (7) > $a[$a.length - 1] 3 PS (8) > $a[$a.length - 2] 2 PS (9) > $a[$a.length - 3] 1 In the example, $a.Length - 1 retrieves the last element of the array just like -1 did. In effect, negative indexing is just a shorthand for $array.Length - $index. 5.4.3.1 ARRAY SLICES We’ve seen how to get individual elements out of an array. We can get sequences of elements out of arrays as well. Extracting these sequences is called array slicing and the results are array slices ais illustrated in figure 5.8.

$a =

1

2

3

4

5

6

7

Original array

$a[2,3,4,5] =

3

4

5

6

Array slice

Figure 5.8 This figure shows how an array slice is generated from the original array

Slicing is done by specifying an array of indexes instead of just a single index. The corresponding element for each index is extracted from the original array and returned as a new array that is a slice of the original. From the command line, this operation looks as follows: PS (1) > $a = 1,2,3,4,5,6,7 PS (2) > $a[2,3,4,5] 3 4 5 6 PS (3) >

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

167 In this example, we used the array 2,3,4,5 to get the corresponding elements out of the array in $a. Here’s a variation on this example: PS (3) > $indexes = 2,3,4,5 PS (4) > $a[$indexes] 3 4 5 6 This time we stored the list of indexes in a variable, then used the variable to do the indexing. The effect was the same. Now let’s process the values that are stored in the $indexes variable. We’ll use the Foreach-Object cmdlet to process each element of the array and assign the results back to the array. PS (5) > $indexes = 2,3,4,5 | foreach {$_-1} We want to adjust for the fact that arrays start at index 0, so we subtract one from each index element. Now when we do the indexing : PS (6) > $a[$indexes] 2 3 4 5 we get the elements that correspond to the original index value—2 returns 2, and so on. But do we need to use the intermediate variable? Let’s try it: PS (7) > $a[2,3,4,5 | foreach {$_-1}] Missing ']' after array index expression. At line:1 char:12 + $a[2,3,4,5 | $a[0..3] 0 1 2 3

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

168 By taking advantage of the way negative indexing works, you can get the last four elements of the array by doing: PS (3) > $a[-4..-1] 6 7 8 9 You can even use ranges to reverse an array. To do this, you need to know the length of the array, which you can get through the length property. The following example shows this. (We’re casting the result of the expression to string so it will be displayed on one line.) PS (6) > [string] $a[ ($a.length-1) .. 0] 9 8 7 6 5 4 3 2 1 0

AUTHOR’S NOTE
This isn’t an efficient way of reversing the array. Using the Reverse static member on the

[array] class is more efficient. See section 5.4.4 for more information on how to use .NET methods in PowerShell.

In PowerShell, slicing works for retrieving elements of an array, but you can’t use it for assignments. You get an error if you try. For example, let’s try to replace the slice [2,3,4] with a single value 12. PS (1) > $a = 1,2,3,4,5,6,7,8 PS (2) > $a[2,3,4] = 12 Array assignment to [2,3,4] failed because assignment to slices is not supported. At line:1 char:4 + $a[2 $2d = new-object 'object[,]' 2,2

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

170 This statement created a 2 by 2 array of objects. If we look at the dimensions of the array by retrieveing the Rank property from the object: PS {2) > $2d.Rank 2 Now let’s set PS (3) > PS (4) > PS (5) > PS (6) > PS (7) > d the value in the array to particular values. We do this by indexing into the array. $2d[0,0] = "a" $2d[1,0] = 'b' $2d[0,1] = 'c' $2d[1,1] = 'd' $2d[1,1]

This appears to imply that slices don’t work in multi-dimensional arrays, but in fact they do when you use nested arrays of indexes and wrap the expression by using the comma operator in parentheses. PS (8) > $2d[ (0,0) , (1,0) ] a b Here we retrieved the elements of the array at indexes (0,0) and (1,0). And, as in the case of one-dimensional arrays, we can use variables for indexing: PS (9) > $one=0,0 ; $two=1,0 PS (10) > $2d [ $one, $two ] Unexpected token ' $one, $two ' in expression or statement. At line:1 char:18 + $2d [ $one, $two ] $2d[ $pair ] a b This covers pretty much everything we need to say about arrays. Now let’s move on to properties and methods. As you willl remember from chapter 1, properties and methods are the attributes of an object that let us inspect and manipulate that object. Because PowerShell is an object-based shell, a good understanding how properties and methods work is necessary if you really want to master PowerShell. We're going to be spending a fair bit of time on these features so let's get started.

5.5 Property and method operators
As we’ve seen in many examples so far, the property dereference operator in PowerShell is the dot “.”. As was the case with array indexing, this is properly considered an operator in PowerShell with left and right operand expressions. This operator, along with the static member operator ‘::’, is shown in figure 5.11.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

171

Property and Method Reference Operators :: . ::() .()

Figure 5.11 This figure shows the property and method operators in PowerShell.

We’ll get to what that means in a second.

AUTHOR’S NOTE
When we say property here, we’re talking about any kind of data member on an object, regardless of the underlying Common Language Runtime representation (or implementation) of the member. If you don’t know what this means – good - because it doesn’t matter. But some people do like to know all of the details of what’s going on.

First let’s look back at the basics. Everything in PowerShell is an object (even scripts and functions as we’ll see later on). As discussed in chapter 1, objects have properties (data) and methods (code). To get at both, you use the dot “.” operator. To get the length of a string, you use the length property: PS (1) > "Hello world!".length 12 In a similar fashion, we can get the length of an array: PS (3) > (1,2,3,4,5).length 5 As was the case with the left square bracket in array indexing, spaces are not permitted between the left operand and the dot. PS (4) > (1,2,3,4,5) .count Unexpected token '.count' in expression or statement. At line:1 char:18 + (1,2,3,4,5) .count "Hello world".$prop 11 This mechanism gives you that magic “one more level of indirection” computer science people are so very fond of. Let’s expand on this. To get a list of all of the properties on an object, we can use the Get-Member (or gm) cmdlet. Let’s use this on an object. We’ll use dir to get a

FileInfo object to work with.
PS (1) > @(dir c:\windows\*.dll)[0] | gm -type property TypeName: System.IO.FileInfo Name ---Attributes CreationTime CreationTimeUtc Directory DirectoryName Exists Extension FullName IsReadOnly LastAccessTime LastAccessTimeUtc LastWriteTime LastWriteTimeUtc MemberType ---------Property Property Property Property Property Property Property Property Property Property Property Property Property Definition ---------System.IO.FileAttributes Attributes … System.DateTime CreationTime {get;s … System.DateTime CreationTimeUtc {ge … System.IO.DirectoryInfo Directory … System.String DirectoryName {get;} System.Boolean Exists {get;} System.String Extension {get;} System.String FullName {get;} System.Boolean IsReadOnly {get;set;} System.DateTime LastAccessTime {get;s System.DateTime LastAccessTimeUtc {ge System.DateTime LastWriteTime {get;se System.DateTime LastWriteTimeUtc {get

… … … …

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

173 Length Name Property Property System.Int64 Length {get;} System.String Name {get;}

This gives us a list of all of the properties. Of course, we only need the name, so we can use the Name property on these objects. PS (2) > @(dir c:\windows\*.dll)[0] | gm -type property | >>> foreach {$_.name} Attributes CreationTime CreationTimeUtc Directory DirectoryName Exists Extension FullName IsReadOnly LastAccessTime LastAccessTimeUtc LastWriteTime LastWriteTimeUtc Length Name Now we’ll use this list of names to get the corresponding values from the original object. First get the object into a variable: PS (1) > $obj = @(dir c:\windows\*.dll)[0] And get list of names; for brevity’s sake, we’ll just get the properties that start with “l”. PS (2) > $names = $obj | gm -type property l* | foreach {$_.name} Finally use the list of names to print out the value: PS (3) > $names | foreach { "$_ = $($obj.$_)" } LastAccessTime = 3/25/2006 2:18:50 AM LastAccessTimeUtc = 3/25/2006 10:18:50 AM LastWriteTime = 8/10/2004 12:00:00 PM LastWriteTimeUtc = 8/10/2004 7:00:00 PM Length = 94784 PS (4) > Next let’s look at using methods. The method call syntax is: . ( , , … ) As always, spaces are not allowed before or after the dot or before the opening parenthesis for the reasons discussed previously. Here’s a basic example: PS (1) > "Hello world!".substring(0,5) Hello In this example, we used the Substring method to extract the first five characters from the left operand string. As you can see, the syntax for method invocations in PowerShell matches what you see in pretty much every other language that has methods. Contrast this with how commands are called. In method calls, arguments in the argument list are separated by commas and the whole list is enclosed in parentheses. With commands, the arguments are separated with spaces and the command ends at the end of line or at a command terminator, such as the semicolon or the pipe symbol. This is another area where we experimented with alternate syntaxes. One of the experiments we conducted resulted in a command-like method invocation syntax that looked something like: "Hello world!".(substring 0 5) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

174 We chose not to use this syntax for two reasons (which, by the way, means that you’ll get an error if you try using it). First, it collided with the ability to perform indirect property name retrievals. The second (and more important) reason was that people also found it uncomfortably strange. Empirically, a programmer-style syntax for programmer-style activities like method invocations and a shell-style syntax for shell-style activities like command invocation seems to work best. Of course, this is also not without some small issues. First, if you want to pass an expression to a method, you have to wrap that array in parentheses so the array comma operator is not confused with the argument separator commas. Next, if you want to use the output of a command as an argument, you have to wrap the command in parentheses. Here’s an example: PS (1) > [string]::join('+',(1,2,3)) 1+2+3 We’re using the [string]::join method to create a string out of the array 1,2,3 with a plus sign between each one. Now let’s do the same thing with the output of a command. Here’s a command that returns the handle count for the rundll processes. PS (1) > get-process rundll* | foreach{$_.handles} 58 109 Now let’s join that output into a string, again separated with the plus sign (with spaces on either side this time). PS (2) > [string]::join(" + ", (get-process rundll* | >>> foreach{$_.handles})) 58 + 109 Of course, the observant reader will have noticed the use of the operator '::' in these examples. We briefly discussed this operator in chapter 3 as part of our discussion of types in PowerShell. In the next section, we'll look at it in more detail.

5.5.2 Static methods and the “::” operator
The '::' operator is the static member accessor. Where the “.” operator retrieved instance members, the double-colon operator accesses static members on a class, as is the case with the join method in the example at the end of the last section. The left operand to the static member accessor is required to be a type—either a type literal or an expression returning a type as we PS (1) PS (2) 1+2+3 PS (3) see here: > $t = [string] > $t::join('+',(1,2,3)) >

We chose to use a separate operator for accessing static methods because of the way static methods are accessed. Here’s the problem. If we had a type MyModule with a static property called Module, then the expression [MyModule].Module is ambiguous. This is because there is also an instance member Module on the System.Type instance representing the type MyModule. Now we can’t tell if the “Module” instance member on System.Type or the “Module” static member on MyModule should be retrieved. By using the double-colon operator, this ambiguity is removed.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

175

AUTHOR’S NOTE
Other languages get around this ambiguity by using the typeof() operator. Using

typeof() in this example, typeof(MyModule).Module retrieves the instance property on the Type object and MyModule.Module retrieves the static property implemented by the MyModule class.

5.5.3 Indirect Method Invocation
Earlier we talked about how we could do indirect property references by using a variable on the right side of the ‘.’ operator. We can do the same thing with methods but it’s a bit more complicated. The obvious approach $x.$y(2) doesn’t work. In fact what happens is the that $x.$y returns an object that describes the method you want to invoke: PS {1) > "abc".substring MemberType : Method OverloadDefinitions : {string Substring(int startIndex), st ring Substring(int startIndex, int le ngth)} TypeNameOfValue : System.Management.Automation.PSMethod Value : string Substring(int startIndex), str ing Substring(int startIndex, int len gth) Name : Substring IsInstance : True This turns out to be a pretty handy way to get information about a method. Let’s pick out the overloads for Substring – that is, the different forms of this method that we can use: PS {2) > "abc".substring | foreach { >> $_.OverloadDefinitions -split '\),' } >> string Substring(int startIndex) string Substring(int startIndex, int length) PS {3) > Ok – now we have this information object – what else can we do with it? The thing we most probably want to do is to invoke it. The way to do this is to use the Invoke method on the method information object: PS {3) > "abc".substring.Invoke(1) bc In version 2 of PowerShell, this also works for static methods. First we’ll assign the name of the operation to invoke to a variable: PS {4) > $method = "sin" Now let’s look at the information about that method: PS {5) > [math]::$method MemberType : Method OverloadDefinitions : {static double Sin(double a)} ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

176 TypeNameOfValue Value Name IsInstance : : : : System.Management.Automation.PSMethod static double Sin(double a) Sin True

And finally, let’s invoke it: PS {6) > [math]::$method.invoke(3.14) 0.00159265291648683 While it’s an advanced technique, the ability to invoke properties and methods indirectly turns out to be very powerful since it means that the behavior of your script can be configured at runtime. We’ll see how this can be used when we talk about scriptblocks in chapter 9. This finishes our discussion of properties and methods. Now you may have noticed that in some of the examples so far, we’ve had to do some fairly complicated things to display the results in the way we want. Clearly, on occasion you’ll need a better way to present output, and that’s what the format operator, covered in the next section, is for.

5.6 The PowerShell format operator -F
Most of the time, PowerShell’s built-in formatting and output system will take care of presenting your results, but sometimes you need more explicit control over the formatting of your output. You may also want to format text strings in a specific way like displaying numbers in hexadecimal format. PowerShell allows you to do these things with the format operator. This operator is shown in figure 5.13.

The Format operator -f

Figure 5.13 This figure shows the format operator

The format operator -f is a binary operator that takes a format string as its left operand and an array of values to format as its right operand. Here’s an example: PS (1) > '{2} {1} {0}' -f 1,2,3 3 2 1 In the format string, the values enclosed in braces correspond to the index of the element in the right operand array. The element is converted into a string and then displayed. Along with reordering, when the elements are displayed, you can also control how they are laid out.

AUTHOR’S NOTE
For people familiar with the Python language, the PowerShell format operator is modeled on the Python % operator. However, since PowerShell doesn’t use the % character as part of its formatting directives, it didn’t make mnemonic sense for the format operator in PowerShell to be %. Instead we chose -f.

Here are some more examples: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

177 PS (3) > '|{0,10}| 0x{1:x}|{2,-10}|' -f 10,20,30 | 10| 0x14|30 | Here, the first format specifier element “,10” tells the system to pad the text out to 10 characters. The next element is printed with the specifier “:x” telling the system to display the number as a hexadecimal value. The final display specification has a field width specifier, but this time it’s a negative value, indicating that the field should be padded to the right instead of to the left. The -f operator is, in fact, shorthand for calling the .NET Format method on the System.String class. The previous example can be rewritten as PS (4) > [string]::Format('|{0,10}| 0x{1:x}|{2,-10}|',10,20,30) | 10| 0x14|30 | and you’ll get exactly the same results. The key benefit of the -f operator is that it’s a lot shorter to type. This is useful when you’re typing on the command line. The underlying

Format() method has a rich set of specifiers. The basic syntax of these specifiers is
{[,][:]} Some examples using format specifiers are shown in table 5.4.

Table 5.4 Expression and statement grouping operators
Formant Specifier

Description

Example

Output

{0} {0:x} {0:X}

Display a particular element. Display a number in Hexadecimal. Display a number in Hexadecimal with the letters in uppercase. Display a decimal number leftjustified, padded with zeros. Display a number as a percentage. Display a number as currency. Display with field width n, left aligned. Display with field width n, right aligned.

“{0} {1}” –f “a”,”b” “0x{0:x}” -f 181342 “0x{0:X}” -f 181342

ab 0x2c45e 0x2C45E

{0:dn}

“{0:d8}” -f 3

00000003

{0:p}

“{0:p}” -f .123

12.30 %

{0:C} {0,n}

“{0:c}” -f 12.34 “|{0,5}|” –f “hi”

$12.34 | hi|

{0,-n)

“|{0,-5}|” –f “hi”

|hi |

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

178

{0:hh} {0:mm}

Displays the hours and minutes from a DateTime value. Display using the currency symbol for the current culture.

“{0:hh}:{0:mm}” –f (getdate) “|{0,10:C}|” -f 12.4

01:34

{0:C}

|

$12.40|

Of course, there are many more things you can do with formatting than we can cover here. Refer to the Microsoft MSDN documentation for the full details of all of the various options. Now that we know how to format strings, let's look at how we can direct our output to files with the redirection operators.

5.7 Redirection and the redirection operators
All modern shell languages have input and output redirection operators, and PowerShell is no different. The redirection operators supported in PowerShell are shown in figure 5.14.

Redirection Operators > 2> >> 2>> &1

Figure 5.14 This figure shows the redirection operators that are available in PowerShell.

Table 5.5 presents the operators with examples and more details about their semantics.

Table 5.5 PowerShell redirection operators

Operator

Example

Results

Description

>

dir > out.txt

Contents of out.txt are replaced. Contents of out.txt are appended to.

Redirect pipeline output to a file, overwriting the current contents. Redirect pipeline output to a file, appending to the existing content. Redirect error output to a file, overwriting the current contents.

>>

dir >> out.txt

2>

dir nosuchfile.txt 2> err.txt

Contents of err.txt are replaced by the error messages.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

179

2>>

dir nosuchfile.txt 2>> err.txt

Contents of err.txt are appended with the error messages.

Redirect error output to a file, appending to the current contents.

continued on next page 2>&1 dir nosuchfile.txt 2>&1 The error message is written to the output. The error messages are written to the output pipe instead of the error pipe. This operator is reserved for input redirection which is not implemented in version 1.0 of PowerShell. Using this operator in an expression will result in a syntax error.

<

Not implemented in PowerShell V1.0

The redirection operators allow you to control where output and error objects are written (including discarding them if that’s what you want to do). In the following example, we’re saving the output of the Get-Date cmdlet to a file called “out.txt”. PS (1) > get-date > out.txt Now let’s display the contents of this file: PS (2) > type out.txt Tuesday, January 31, 2006 9:56:25 PM You can see that the object has been rendered to text using the same mechanism as would be used when displaying on the console. Now let’s see what happens when we redirect the error output from a cmdlet. We’ll let the output be displayed normally. PS (3) > dir out.txt,nosuchfile 2> err.txt Directory: Microsoft.Management.Automation.Core\FileSystem::C:\ working Mode ----a--LastWriteTime ------------1/31/2006 9:56 PM Length Name ------ ---40 out.txt

Obviously no error was displayed on the console. Let’s see what was written to the error file. PS (4) > type err.txt get-childitem : Cannot find path 'C:\working\nosuchfile' because it does not exist. At line:1 char:4 + dir > out.txt PS (6) > type out.txt ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

180 Tuesday, January 31, 2006 9:56:25 PM Tuesday, January 31, 2006 9:57:33 PM We see that there are now two records containing the current date. You can also append error records to a file using the 2>> operator. The next operator to discuss is the stream combiner 2>&1. This operator causes error objects to be routed into the output stream instead of going to the error stream. This allows you to capture error records along with your output. For example, if you want to get all of the output and error records from a script to go to the same file, you would just do myScript > output.txt 2>&1 or myScript 2>&1 > output.txt The order doesn’t matter. Now all of the error records will appear inline with the output records in the file. This also works with assignment. $a = myScript 2>&1 This causes all the output and error objects from myScript to be placed in $a. You could then separate the errors by checking for their type with the -is operator, but it would be easier to separate them up front. This is another place where you can use the grouping constructs. The following construction allows you to capture the output objects in $output and the error objects in $error. $error = $( $output = myScript ) 2>&1 You would use this idiom when you wanted to take some additional action on the error objects. For example, you might be deleting a set of files in a directory. Some of the deletions might fail. These will be recorded in $error, allowing you to take additional actions after the deletion operation has completed. Sometimes you want to discard output or errors. In PowerShell, you do this by redirecting to $null. For example, if you don’t care about the output from myScript then you would write: myScript > $null and to discard the errors, you would write: myScript 2> $null The last thing to mention for I/O redirection is that, under the covers, redirection is done using the Out-File cmdlet. If fact, myScript > file.txt is just “syntactic sugar” for myScript | out-file –path file.txt In some cases, you’ll want to use Out-File directly because it gives you more control over the way the output is written. The synopsis for Out-File is Out-File [-FilePath] [[-Encoding] ] [-Append] [-Force] [-NoClobber] [-Width ] [-InputObject ] [-Verbose] [-Debug] [-ErrorAction ] [-ErrorVariable ] [-OutVariable ] [-OutBuffer ] [-WhatIf] [-Confirm]] The interesting parameters are -encoding, which lets you specify the encoding (such as ASCII, Unicode, UTF8, and so on); -append, which appends to an existing file instead of ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

181 overwriting it; -noclobber, which prevents you from overwriting (clobbering) an existing file; and -width, which tells the cmdlet how wide you want the output formatted. The full details for this cmdlet are available by running the command: get-help out-file at the PowerShell command line. Earlier in this section, we talked about assignment as being a kind of output redirection. In fact, this analogy is even more significant than we alluded to there. We’ll go into details in the next section, when we finally cover variables themselves.

5.8 Working with PowerShell Variables
In many of the examples we’ve seen so far, we’ve used variables. Now let’s look at the actual details of PowerShell variables. First off, PowerShell variables aren’t declared; they’re just created as needed on first assignment. There also isn’t really any such thing as an uninitialized variable. If you reference a variable that does not exist yet, the system will return the value $null (although it won’t actually create a variable). PS (1) > $NoSuchVariable PS (2) > $NoSuchVariable -eq $null True In the example, we looked at a variable that doesn’t exist and see that it returns $null.

AUTHOR’S NOTE

$null, like $true and $false, is a special constant variable that is defined by the system. You can’t change the value of these variables.

You can tell whether a variable exists or not by using the Test-Path cmdlet as shown: PS (3) > test-path variable:NoSuchVariable False This works because variables are part of the PowerShell unified namespaces. Just as files and the registry are available through virtual drives, so are PowerShell variables. You can get a list of all of the variables that currently exist by doing dir variable:/ So how do we create a variable? Let’s find out.

5.8.1 Creating Variables
First off, there are a number of variables that are defined by the system: $true, $false, and $null are the ones we’ve seen so far (we’ll mention the others as we come to them). User variables are PS (3) > PS (4) > 1 PS (5) > PS (6) > Hi there PS (7) > PS (8) > created on first assignment, as we see in the next example. $var = 1 $var $var = "Hi there" $var $var = get-date $var

Sunday, January 29, 2006 7:23:29 PM ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

182 In this example, first we assigned a number, then a string, then a DateTime object. This illustrates that PowerShell variables can hold any type of object. If you do want to add a type attribute to a variable, you use the cast notation on the left of the variable. Let’s add a type attribute to the variable $val. PS (1) > [int] $var = 2 Looking at the result, we see the number 2. PS (2) > $var 2 That’s fine. What happens if we try to assign a string to the variable? Let’s try it. PS (3) > $var = "0123" PS (4) > $var 123 First, there was no error. Second, by looking at the output of the variable, you can see that the string “0123” was converted into the number 123. This is why we say that the variable has a type attribute. Unlike strongly typed languages where a variable can only be assigned an object of the correct type, PowerShell will allow you to assign any object as long as it is convertible to the target type using the rules described in chapter 3. If the type is not convertible then you’ll get a runtime type conversion error (as opposed to a “compile-time” error.) PS (5) > $var = "abc" Cannot convert "abc" to "System.Int32". Error: "Input string was no t in a correct format." At line:1 char:5 + $var ${this is a variable name with a `} in it} 13 Earlier, we said that the colon character was special in a variable name. This is used to delimit the namespace that the system uses to locate the variable. For example, to access PowerShell global variables, you use the global namespace: PS (1) > $global:var = 13 PS (2) > $global:var 13

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

183 This example set the variable “var” in the global context to the value 13. You can also use the namespace notation to access variables at other scopes. This is called a scope modifier. Scopes will be covered in chapter 7, so we won’t say anything more about that here. Along with the scope modifiers, the namespace notation lets you get at any of the resources surfaced in PowerShell as drives. For example, to get at the environment variables, you use the

env namespace as shown:
PS (1) > $env:SystemRoot C:\WINDOWS In this example, we retrieved the contents of the SystemRoot environment variable. You can use these variables directly in paths. For example: PS (3) > dir $env:systemroot\explorer.exe Directory: Microsoft.Management.Automation.Core\FileSystem::C:\ WINDOWS Mode ----a--LastWriteTime ------------8/10/2004 12:00 PM Length Name ------ ---1032192 explorer.exe

This retrieved the file system information for explorer.exe.

AUTHOR’S NOTE
For

cmd.exe or command.com users, the equivalent syntax would be %systemroot%\explorer.exe. There, the percent signs delimit the variable. In
PowerShell, this is done with braces.

Many of the namespace providers are also available through the variable notation (but you usually have to wrap the path in braces). Let’s look back at an example we saw at the beginning of chapter 4: ${c:old.txt} -replace 'is (red|blue)','was $1' > new.txt The initial construct should now start to make sense. The sequence ${c:old.txt} is a variable that references the file system provider through the C: drive and retrieves the contexts of the file named “old.txt”. With this simple notation, we read the contents of a file. No open/read/close—we treat the file itself as an atomic value.

AUTHOR’S NOTE
Using variable notation to access a file can be startling at first, but it’s a logical consequence of the unified data model in PowerShell. Since things like variables and functions are available as drives, things such as drives are also available using the variable notation. In effect, this is an application of the Model-View Controller (MVC) pattern. Each type of data store (filesystem, variables, registry, and so forth) is a “model”. The PowerShell provider infrastructure acts as the controller and there are (by default) two views: the “filesystem” navigation view and the variable view. The user is free to choose and use the view most suitable to the task at hand.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

184 You can also write to a file using the namespace variable notation. Here’s that example rewritten to use variable assignment instead of a redirection operator (remember, earlier we said that assignment can be considered a form of redirection in PowerShell.) ${c:new.txt} = ${c:old.txt} -replace 'is (red|blue)','was $1' In fact, you can even do an in-place update of a file by using the same variable on both sides of the assignment operator. To update the file “old.txt” instead of making a copy, do ${c:old.txt} = ${c:old.txt} -replace 'is (red|blue)','was $1' All we did was change the name in the variable reference from “new.txt” to “old.txt”. This won’t work if you use the redirection operator, because the output file is opened before the input file is read. This would have the unfortunate effect of truncating the previous contents of the output file. In the assignment case, the file is read atomically; that is, all at once, processed, then written atomically. This allows for “in-place” edits because the file is actually buffered entirely in memory instead of in a temporary file. To do this with redirection, you’d have to save the output to a temporary file and then rename the temporary file so it replaces the original. Now let’s leverage this feature along with multiple assignments to swap two files “f1.txt” and “f2.txt”. Earlier in this chapter we showed how to swap two variables. We can use the same technique to swap two files. Here we go: ${c:f1.txt},${c:f2.txt} = ${c:f2.txt},${c:f1.txt}

AUTHOR’S NOTE
All of these examples using variables to read and write files cause the entire contents of files to be loaded into memory as a collection of strings. On modern computers it’s possible to handle moderately large files this way, but doing it with very large files is memoryintensive, inefficient, and might even fail under some conditions. Keep this in mind when using these techniques.

When the filesystem provider reads the file, it returns the file as an array of strings.

AUTHOR’S NOTE
When accessing a file using the variable namespace notation, PowerShell assumes that it’s working with a text file. Since the notation doesn’t provide a mechanism for specifying the encoding, you can’t use this technique on binary files. You’ll have to use the Get-Content and Set-Content cmdlets instead.

This provides a simple way to get the length of a file: ${c:file.txt}.length The downside of this simple construct is that it requires reading the entire file into memory and then counting the result. It works fine for small files (a few megabytes) but it won’t work on files that are gigabytes in size.

5.8.3 Working with the variable cmdlets
So far we've been using the PowerShell language features to access variables but we can also work with variables using the variable cmdlets. These cmdlets let us do a couple of things we can't do directly from the language. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

185 INDIRECTLY SETTING A VARIABLE Sometime it's useful to be able to get or set a variable when we won't know the name of that variable until runtime. For example, we might want to initialize a set of variables from a .csv file. We can't do this using the variable syntax in the language because the name of the variable to set is resolved at parse time. Let's work through this example. First we need a .csv file: PS (1) > cat variables.csv "Name", "Value" "srcHost", "machine1" "srcPath", "c:\data\source\mailbox.pst" "destHost", "machine2" "destPath", "d:\backup" As we can see, .csv file is simply a text file with rows of values separated by commas, hence CSV or "Comma-Separated Values". Now we'll use the Import-CSV cmdlet to import this file as structured objects. PS (2) > import-csv variables.csv Name ---srcHost srcPath destHost destPath Value ----machine1 c:\data\source\mailbox.pst machine2 d:\backup

We can see the cmdlet has treated the first row in the table as the names of the properties to use on the objects and then added the name and value property to each object. The choice of "name" and "value" was deliberate because these are the names of the parameters on the

Set-Variable cmdlet. This cmdlet takes input from the pipeline by property name so we can simply pipe the output of Import-CSV directly into Set-Variable PS (3) > import-csv variables.csv | set-variable and it's as simple as that. If we wanted to see the full details, we could specify the -verbose parameter to the cmdlet and it would display each variable as it was set. Now use the normal variable syntax to verify that we've set things up the way we planned. PS (4) > $srcHost Name ---srcHost Value ----machine1

Ok - good. Of course we can use the parameters on the cmdlet to directly set this variable: PS (5) > set-variable -name srcHost -value machine3 PS (6) > $srcHost machine3 or use the (much) shorter alias sv to do it. PS (7) > sv srcHost machine4 PS (8) > $srcHost machine4 Now let's see what else we can do with the cmdlets.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

186 GETTING AND SETTING VARIABLE OPTIONS If there is a cmdlet to set a variable, obviously there should also be a variable to get variables - the Get-Variable cmdlet: PS (9) > get-variable -value srcHost machine4 Notice that we specified the -Value parameter in this example. What happens if we don't do that? Let's find out PS (10) > get-variable srcHost | gm TypeName: System.Management.Automation.PSVariable Name ---Equals GetHashCode GetType IsValidValue ToString Attributes Description Module ModuleName Name Options Value Visibility MemberType ---------Method Method Method Method Method Property Property Property Property Property Property Property Property Definition ---------bool Equals(System.Object obj) int GetHashCode() type GetType() bool IsValidValue(System.Object ... string ToString() System.Collections.ObjectModel.C... System.String Description {get;s... System.Management.Automation.PSM... System.String ModuleName {get;} System.String Name {get;} System.Management.Automation.Sco... System.Object Value {get;set;} System.Management.Automation.Ses...

What Get-Variable returns if -Variable is not specified is the PSVariable object that PowerShell uses to represent this object. We can see the Name and Value properties on this object but there are a lot of other properties as well. Let's explore the Options property. This property allows us to set options on the variable including things like ReadOnly and Constant. The variables we've read from the .csv file are still changeable as we can see PS (11) > $srcHost = "machine9" PS (12) > $srcHost machine9 but, if we're using them to configure the environment, we may not want them to be. To address this, we can set the ReadOnly option using Set-Variable and the -Option parameter. PS (13) > set-variable -option readonly srcHost machine1 PS (14) > $srcHost = "machine4" Cannot overwrite variable srcHost because it is read-only o r constant. At line:1 char:9 + $srcHost if ($x -gt 100) "It's greater than one hundred" Missing statement block after if ( condition ). At line:1 char:17 + if ($x -gt 100) " > {$_ -gt 7} {"greater >> greater than three PS (12) > switch (8) { >> {$_ -gt 3} {"greater >> {$_ -gt 7} {"greater >> greater than three greater than 7 PS (13) > the switch value is made available through the variable $_. than three"} than 7"}}

than three"} than 7"}}

In the first statement, only the first clause was triggered because 5 is greater than 3 but less than 7. In the second statement, both clauses fired. You can use these matching clauses with any of the other three matching modes, as we see in the following: PS (13) > switch (8) { >> {$_ -gt 3} {"greater than three"} >> 8 {"Was $_"}} >> greater than three Was 8 The first expression {$_ -gt 3} evaluated to true so “greater than three” was printed, and the switch value matched 8 so “Was 8” also printed (where $_ was replaced by the matching value). Now we have exact matches, pattern matches, conditional matches, and the default clause. But what about the switch value itself? So far, all of the examples have been simple scalar values. What happens if you specify a collection of values? This is where the switch statement acts like a form of loop.

AUTHOR’S NOTE

switch works like the other looping statements in that the expression in the parentheses is fully evaluated before it starts iterating over the individual values.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

213

Let’s look at another example where we specify an array of values. PS (2) > switch(1,2,3,4,5,6) { >> {$_ % 2} {"Odd $_"; continue} >> 4 {"FOUR"} >> default {"Even $_"} >> } >> Odd 1 Even 2 Odd 3 FOUR Odd 5 Even 6 In this example, the switch value was 1,2,3,4,5,6. The switch statement loops over the collection, testing each element against all of the clauses. The first clause returns “Odd $_” if the current switch element is not evenly divisible by two. The next clause prints out “FOUR” if the value is 4. The default clause prints out “Even $_” if the number is even. Note the use of continue in the first clause. This tells the switch statement to stop matching any further clauses and move on to the next element in the collection. In this instance, the switch statement is working in the same way that the continue statement works in the other loops. It skips the remainder of the body of the loop and continues on with the next loop iteration. What happens if we’d used break instead of continue? Let’s try it: PS (3) > switch(1,2,3,4,5,6) { >> {$_ % 2} {"Odd $_"; break} >> 4 {"FOUR"} >> default {"Even $_"} >> } >> Odd 1 As with the other loops, break doesn’t just skip the remainder of the current iteration; it terminates the overall loop processing. (If you want to continue iterating, use continue instead. More on that later.) Of course, iterating over a fixed collection is not very interesting. In fact, you can use a pipeline in the switch value, as the next example shows. In this example, we want to count the number of DLLs, text files, and log files in the directory c:\windows. First we initialize the counter variables: PS (1) > $dll=$txt=$log=0 Now we run the actual switch statement. This switch statement uses wildcard patterns to match the extensions on the filenames. The associated actions increment a variable for each extension type: PS (2) > switch -wildcard (dir c:\windows) >> {*.dll {$dll++} *.txt {$txt++} *.log {$log++}} Once we have the totals, let’s display them: PS (3) > "dlls: $dll text files: $txt log files: $log" dlls: 6 text files: 9 log files: 120 Note that in this example the pipeline element is being matched against every clause. Since a file can’t have more than one extension, this doesn’t affect the output, but it does affect ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

214 performance somewhat. It’s faster to include a continue statement after each clause so the matching process stops as soon as the first match succeeds. Here’s something else we glossed over earlier in our discussion of $_—it always contains the object that was matched against. This is important to understand when you’re using the pattern matching modes of the switch statement. The pattern matches create a string representation of the object to match against, but $_ is still bound to the original object. Here’s an example that illustrates this point. This is basically the same as the last example, but this time, instead of counting the number of files, we want to calculate the total size of all of the files having a particular extension. Here are the revised commands: PS (1) > $dll=$txt=$log=0 PS (2) > switch -wildcard (dir) { >> *.dll {$dll+= $_.length; continue} >> *.txt {$txt+=$_.length; continue} >> *.log {$log+=$_.length; continue} >> } >> PS (3) > "dlls: $dll text files: $txt log files: $log" dlls: 166913 text files: 1866711 log files: 6669437 PS (4) > Notice how we’re using $_.length to get the length of the matching file object. If $_ were bound to the matching string, we would be counting the length of the file names instead of the lengths of the actual files.

6.4.4 Processing files with the switch statement
There is one last mode of operation for the switch statement to discuss: the -file option. Instead of specifying an expression to iterate over as the switch value, the -file option allows you to name a file to process. Here’s an example where we’re processing the Windows update log file. Again we start by initializing the counter variables: PS (1) > $au=$du=$su=0 Next we use the -regex and -file options to access and scan the file WindowsUpdate.log, looking update requests from automatic updater, Windows Defender, and SMS triggered updates. PS (2) > switch -regex -file c:\windows\windowsupdate.log { >> 'START.*Finding updates.*AutomaticUpdates' {$au++} >> 'START.*Finding updates.*Defender' {$du++} >> 'START.*Finding updates.*SMS' {$su++} >> } >> Finally we print out the results. PS (3) > "Automatic:$au Defender:$du SMS:$su" Automatic:195 Defender:10 SMS:34 Now it’s possible to do basically the same thing by using Get-Content or even the filesystem name trick we looked at in chapter 4: PS (4) > $au=$du=$su=0 PS (5) > switch -regex (${c:windowsupdate.log}) { >> 'START.*Finding updates.*AutomaticUpdates' {$au++} >> 'START.*Finding updates.*Defender' {$du++} >> 'START.*Finding updates.*SMS' {$su++} >> } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

215 >> PS (6) > "Automatic:$au Defender:$du SMS:$su" Automatic:195 Defender:10 SMS:34 Here we used ${c:windowsupdate.log} to access the file content instead of -file. So why have the -file option? There are two reasons. The -file operation reads one line at a time, so it uses less memory than the GetContent cmdlet, which has to read the entire file into memory before processing. Also, because -file is part of the PowerShell language, the interpreter can do some optimizations, which gives -file some performance advantages. So overall, the -file option can potentially give you both speed and space advantages in some cases (the space advantage typically being the more significant, and therefore more important of the two). When your task involves processing a lot of text files, the -file switch can be a very useful tool.

6.4.5 Using the $switch loop enumerator in the switch statement
One more point: just as the foreach loop used $foreach to hold the loop enumerator, the switch statement uses $switch to hold the switch loop enumerator. This is useful in a common pattern—processing a list of options. Say we have a list of options where the option b takes an argument and -a, -c, and -d don’t. Let’s write a switch statement to process a list of these arguments. First let’s set up a list of test options. For convenience, we’ll start with a string and then use the -split operator to break it into an array of elements: PS (1) > $options= -split "-a -b Hello -c" Next let’s initialize the set of variables that will correspond to the flags: PS (2) > $a=$c=$d=$false PS (3) > $b=$null Now we can write our switch statement. The interesting clause is the one that handles -b. This clause uses the enumerator stored in $switch to advance the item being processed to the next element in the list. We use a cast to [void] to discard the return value from the call to $switch.movenext() (more on that later). Then we use $switch.current to retrieve the next value and store it in $b. Then the loop continues processing the remaining arguments in the PS >> >> >> >> >> >> >> list. (4) > switch ($options) { '-a' { $a=$true } '-b' { [void] $switch.movenext(); $b= $switch.current } '-c' { $c=$true } '-d' { $d=$true } }

The last step in this example is to print out the arguments in the list to make sure they were all set properly. PS (5) > "a=$a b=$b c=$c d=$d" a=True b=Hello c=True d=False PS (6) > We see that $a and $c are true, $b contains the argument “Hello”, and $d is still false since it wasn’t in our list of test options. The option list has been processed correctly.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

216

AUTHOR’S NOTE
This isn’t a robust example because it’s missing all error handing. In a complete example, we would have a default clause that generated errors for unexpected options. Also, in the clause that processes the argument for -b, rather than discarding the result of

MoveNext() it should check the result and generate an error if it returns false. This would indicate that there are no more elements in the collection, so -b would be missing its mandatory argument.

This finishes the last of the flow-control statements in the PowerShell language, but as we mentioned at the beginning of this chapter, there is another way to do selection and iteration in PowerShell by using cmdlets. In the next section, we’ll go over a couple of the cmdlets that are a standard part of the PowerShell distribution. These cmdlets let you control the flow of your script in a manner similar to the flow control statements. (In later sections, we’ll describe how you can create your own specialized flow control elements in PowerShell.)

AUTHOR’S NOTE
For a more pragmatic example of using the switch statement, take a look at listing 10.5 in chapter 10. This is a function that illustrates the use of nested switch statements in processing XML documents using the .NET XmlReader class.

6.5 Flow control using cmdlets
While PowerShell’s control statements are part of the language proper, there are also some cmdlets, as shown in figure 6.12, that can be used to accomplish similar kinds of things.
Flow-control cmdlets … | ForEach-Object … | ForEach-Object -Begin -Process -End … | Where-Object

Figure 6.12 This diagram shows flow control cmdlets.

These cmdlets use blocks of PowerShell script enclosed in braces to provide the “body” of the control statement. These pieces of script are called ScriptBlocks and are described in detail in chapter 8. The two most frequent flow-control cmdlets that you’ll encounter are Foreach-

Object and Where-Object.

6.5.1 The ForEach-Object cmdlet
The Foreach-Object cmdlet operates on each object in a pipeline in much the same way that the foreach statement operates on the set of values that are provided to it. For example, here’s a foreach statement that prints out the size of each text file in the current directory: PS (1) > foreach ($f in dir *.txt) { $f.length } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

217 48 889 23723 328 279164 Using the Foreach-Object cmdlet, the same task can be accomplished this way: PS (2) > dir *.txt | foreach-object {$_.length} 48 889 23723 328 279164 The results are the same, so what’s the difference? One obvious difference is that you didn’t have to create a new variable name to hold the loop value. The automatic variable $_ is used as the loop variable.

AUTHOR’S NOTE
Automatic variables are common in scripting languages. These variables aren’t directly assigned to in scripts. Instead, they are set as the side-effect of an operation. One of the earlier examples of this is in AWK. When a line is read in AWK, the text of the line is automatically assigned to $0. The line is also split into fields. The first field is placed in $1, the second is in $2, and so on. The Perl language is probably the most significant user of automatic variables. In fact, as mentioned previously, Perl inspired the use of $_ in PowerShell. Things are, however, not all skittles and beer. Automatic variables can help reduce the size of a script, but they can also make a script hard to read and difficult to reuse because your use of automatics may collide with mine. From a design perspective, our approach with automatic variables follows the salt curve. A little salt makes everything taste better. Too much salt makes food inedible. We’ve tried to keep the use of automatics in PowerShell at the “just right” level. Of course, this is always a subjective judgment. Some people really like salt.

A more subtle difference, as discussed previously, is that the loop is processed one object at a time. In a normal foreach loop, the entire list of values is generated before a single value is processed. In the Foreach-Object pipeline, each object is generated and then passed to the cmdlet for processing. The Foreach-Object cmdlet has an advantage over the foreach loop in the amount of space being used at a particular time. For example, if you are processing a large file, the

foreach loop would have to load the entire file into memory before processing. When using the Foreach-Object cmdlet, the file will be processed one line at a time. This significantly reduces the amount of memory needed to accomplish a task. You’ll end up using the Foreach-Object cmdlet a lot in command lines to perform simple transformations on objects (in fact we’ve already used it in many examples so far). Given the frequency of use, there are two standard aliases for this cmdlet. The first one is (obviously)

foreach. But wait a second—didn’t we say earlier in this chapter that foreach is a keyword, and keywords can’t be aliased? This is true but remember, keywords are only special when they are the first unquoted word in a statement (in other words, not a string). If they appear ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

218 anywhere else (for example as an argument or in the middle of a pipeline), they’re just another command with no special meaning to the language. Here’s another way to think about this: the first word in a statement is the key that the PowerShell interpreter uses to decide what kind of statement it is processing, hence the term “keyword”. This positional constraint is how the interpreter can distinguish between the keyword “foreach”: foreach ($i in 1..10) { $i } and the aliased cmdlet “foreach”: 1..10 | foreach {$_} When foreach is the first word in a statement, it’s a keyword; otherwise it’s the name of a command. Now let’s look at the second alias. Even though foreach is significantly shorter than Foreach-Object, there have still been times when users wanted it to be even shorter.

AUTHOR’S NOTE
Actually users wanted to get rid of this notation entirely and have “foreach” be implied by an open brace following the pipe symbol. This would have made about half of our users very happy. Unfortunately, the other half were adamant that the implied operation be Where-

Object instead of Foreach-Object.
Where extreme brevity is required, there is a second built-in alias that is simply the percent sign (%). Oh ho—now people are really upset! You told us the percent sign is an operator! Well that’s true but only when it’s used as a binary operator. If it appears as the first symbol in a statement, it has no special meaning, so we can use it as an alias for Foreach-Object. As with keywords, operators are also context-sensitive. The '%' alias you write very concise (but occasionally hard to read) statements such as the following, which prints out the numbers from 1 to 5, times two: PS (1) > 1..5|%{$_*2} 2 4 6 8 10 PS (2) > Clearly this construction is great for interactive use where brevity is very important, but it probably shouldn’t be used when writing scripts. The issue is that Foreach-Object is so useful that a single-character symbol for it, one that is easy to distinguish, is invaluable for experienced PowerShell users. However, unlike the word “foreach”, “%” is not immediately meaningful to new users. So this notation is great for “conversational” PowerShell, but generally terrible for scripts that you want other people to be able to read and maintain. The last thing to know about the Foreach-Object cmdlet is that it can take multiple scriptblocks. If three scriptblocks are specified, the first one is run before any objects are processed, the second is run once for each object, and the last is run after all objects have been processed. This is good for conducting accumulation-type operations. Here’s another ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

219 variation, where we sum up the number of handles used by the service host “svchost” processes: PS (3) > gps svchost |%{$t=0}{$t+=$_.handles}{$t} 3238 The standard alias for Get-Process is gps. This is used to get a list of processes where the process name matches “svchost”. These process objects are then piped into ForeachObject, where the handle counts are summed up in $t and then emitted in the last scriptblock. We used the % alias here to show how concise these expressions can be. In an interactive environment, brevity is important. And now here’s something to keep in mind when using Foreach-Object. The ForeachObject cmdlet works like all cmdlets: if the output object is a collection, it gets unraveled. One way to suppress this behavior is to use the unary comma operator. For example, in the following, we assign $a an array of two elements, the second of which is a nested array. PS (1) > $a = 1,(2,3) Now when we check the length, we see that it is two as expected: PS (2) > $a.length 2 and the second element is still an array. PS (3) > $a[1] 2 3 However, if we simply run it through Foreach-Object, we’ll find that the length of the result is now three, PS (4) > PS (5) > 3 PS (6) > 2 and the second element in the result is simply the number “2”. $b = $a | foreach { $_ } $b.length $b[2]

In effect, the result has been “flattened”. However, if we use the unary comma operator before the $_ variable, the result has the same structure as the original array. PS (7) > $b = $a | foreach { , $_ } PS (8) > $b.length 2 PS (9) > $b[1] 2 3 When PS PS 2 PS 2 3 chaining foreach cmdlets, we need to repeat the pattern at each stage: (7) > $b = $a | foreach { , $_ } | foreach { , $_ } (8) > $b.length (9) > $b[1]

Why did we do this? Why didn’t we just preserve the structure as we pass the elements through instead of unraveling by default? Well, both behaviors are, in fact, useful. Consider the follow example, which returns a list of loaded module names: get-process | %{$_.modules} | sort -u modulename Here the unraveling is exactly what we want. When we were designing PowerShell, we considered both cases; and in applications, on average, unraveling by default was usually what ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

220 we needed. Unfortunately, it does present something of a cognitive bump that surprises users learning to use PowerShell. USING THE RETURN STATEMENT WITH FOREACH-OBJECT Here's another tidbit of information on something that sometimes causes problems. While the ForEach-Object cmdlet looks like a PowerShell statement, remember that it is in fact a command and the body of code it executes is a scriptblock, also known as an anonymous function. (By anonymous, we just mean that we haven't given it a name. Again, we cover this in detail in chapter 9.) The important thing to know is that return statement (see chapter 7), when used in the scriptblock argument to ForEach-Object only exits from the ForEachObject scriptblock, not from the function or script that is calling ForEach-Object. So - if you do want to return out of a function or script in a foreach loop, either use the foreach statement where the return will work as desired, or use the non-local labeled break statement discussed earlier in this chapter. HOW FOREACH-OBJECT PROCESSES ITS ARGUMENTS Let's talk for a moment, about how the ForEach-Object cmdlet processes its argument scriptblocks. A reader of the first edition of this book observed what he thought was an inconsistency between how the cmdlet is documented and how this example: $words | % {$h=@{}} {$h[$_] += 1} behaves. The help text for the cmdlet (do help foreach-object to see this text) says that the -Process parameter is the only positional parameter, and that it's in position 1. Therefore, according to the help file, since the -Begin parameter isn't positional, the example shouldn't work. This led the reader to assume that either there was an error in the help file, or that they were misunderstanding the idea of positional parameters. In fact the help file is correct (since the cmdlet information is extracted from the code) however the way it works is the tricky bit. If you look at the signature of the -Process parameter, you'll see that, yes, it is positional but it also takes a collection of scriptblocks and also receives all remaining unbound arguments. So, in the case of dir | foreach {$sum=0} {$sum++} {$sum} The -Process parameter is getting an array of three scriptblocks whereas -Begin and -End are empty. Now here's the trick. If -Begin is empty and -Process has more than two scriptblocks in the collection, then the first one is treated as the -Begin scriptblock and the second one is treated as the -Process scriptblock. If -Begin is specified, but -End is not and there are two scriptblocks, then the first one is treated as the Process clause and the second one is the End clause. Finally, if both Begin and End are specified then the remaining arguments will be treated as multiple Process clauses. This allows dir | foreach {$sum=0} {$sum++} {$sum} dir | foreach -begin {$sum=0} {$sum++} {$sum} dir | foreach {$sum=0} {$sum++} -end {$sum} dir | foreach -begin {$sum=0} {$sum++} -end {$sum} and dir | foreach -begin {$sum=0} -process {$sum++} -end {$sum} to all work as expected.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

221 On that note, we're finished with our discussion of ForEach-Object. We'll touch on it again in chapter 8 when we discuss scriptblocks but for now, let's move on to the other flow control cmdlet commonly used in PowerShell (which, by the way, also uses scriptblocks - you may detect a theme here).

6.5.2 The Where-Object cmdlet
The other common flow control cmdlet is the Where-Object cmdlet. This cmdlet is used to select objects from a stream, kind of like a simple switch cmdlet. It takes each pipeline element it receives as input, executes its scriptblock (see!) argument, passing in the current pipeline element as $_ and then, if the scriptblock evaluates to true, the element is written to the pipeline. We’ll show this with yet another way to select even numbers from a sequence of integers: PS (4) > 1..10 | where {! ($_ -band 1)} 2 4 6 8 10 The scriptblock enclosed in the braces receives each pipeline element, one after another. If the least significant bit in the element is 1 then the scriptblock returns the logical complement of that value ($false) and that element is discarded. If the least significant bit is zero then the logical complement of that is $true and the element is written to the output pipeline. Notice that the common alias for Where-Object is simply where. And, as with ForEach-Object, because this construction is so commonly used interactively, there is an additional alias, which is simply the question mark (?). This allows the previous example to be written as: PS (5) > 1..10|?{!($_-band 1)} 2 4 6 8 10 Again—this is brief, but it looks like the cat walked across the keyboard (trust me on this one). So as before, while this is fine for interactive use, it is not recommended in scripts because it’s hard to understand and maintain. As another, more compelling example of “Software by Cats”, here’s a pathological example that combines elements from the last few chapters—type casts, operators, and the flow control cmdlets to generate a list of strings of even-numbered letters in the alphabet, where the length of the string matches the ordinal number in the alphabet (“A” is 1, “B” is 2, and so on). PS (1) > 1..26|?{!($_-band 1)}|%{[string][char]([int][char]'A'+$_-1)*$_} >> BB DDDD FFFFFF HHHHHHHH JJJJJJJJJJ LLLLLLLLLLLL NNNNNNNNNNNNNN PPPPPPPPPPPPPPPP ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

222 RRRRRRRRRRRRRRRRRR TTTTTTTTTTTTTTTTTTTT VVVVVVVVVVVVVVVVVVVVVV XXXXXXXXXXXXXXXXXXXXXXXX ZZZZZZZZZZZZZZZZZZZZZZZZZZ PS (2) > The output is fairly self-explanatory, but the code is not. Figuring out how this works is left as an exercise to the reader and as a cautionary tale not to foist this sort of rubbish on unsuspecting coworkers. They know where you live. WHERE-OBJECT AND GET-CONTENT'S –READCOUNT PARAMETER On occasion, a question comes up about the Get-Content cmdlet and how its ReadCount parameter works. This can be a particular issue when using this cmdlet and parameer with Where-Object to filter the output of Get-Content. The issue comes up when the read count is greater than one. This causes PowerShell to act as if some of the objects returned from Get-Content are being skipped and affects both ForEach-Object and Where-

Object. After all, these cmdlets are supposed to process or filter the input one object at a time and this is not what appeared to be happening. Here’s what’s going on. Unfortunately the –ReadCount parameter has a very confusing name. From the PowerShell user’s perspective, it has nothing to do with reading. What it actually does is control the number for records written to the next pipeline element, in this case

Where-Object or ForEach-Object. The following examples illustrate how this works. We'll use a simple text file test.txt which contains 10 lines of text and the ForEach-Object cmdlet (through its alias ‘%’) to count the length of each object being passed down the pipeline. We’ll use the @( … ) construct to guarantee that we’re always treating $_ as an array. Here are the examples with –readcount varying from 1 to 4.
PS 1 PS 2 PS 3 PS 4 (119) > gc test.txt -ReadCount 1 | % { @($_).count } | select -fir 1 (120) > gc test.txt -ReadCount 2 | % { @($_).count } | select -fir 1 (121) > gc test.txt -ReadCount 3 | % { @($_).count } | select -fir 1 (122) > gc test.txt -ReadCount 4 | % { @($_).count } | select -fir 1

In each case where -ReadCount is greater than 1, the variable $_ is set to a collection of objects where the object count of that collection is equivalent to the value specified by -

ReadCount. Taking another example, we’ll use ForEach-Object to filter the pipeline:
PS (127) > gc test.txt -read 5 | ? {$_ -like '*'} | % { $_.count } 5 5 We can see that the filter result contained two collections of 5 objects each written to the pipeline for a total of 10 objects. Now let’s use ForEach-Object and the if statement to filter the list: PS (128) > (gc test.txt -read 10 | % {if ($_ -match '.') {$_}} | >>> Measure-Object).count >>> 10

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

223 This time we see a count of ten because the value of $_ in the ForEach-Object cmdlet is unraveled when written to the output pipe. And now let’s look at one final example using

Where-Object:
PS (130) > (gc test.txt -read 4 | %{$_} | where {$_ -like '*a*'} | >>> Measure-Object).count >>> 10 Here we’ve inserted one more ForEach-Object command between the gc and the WhereObject which simply unravels the collections in $_ and so we again see a count of 10.

AUTHOR’S NOTE:
Here’s the annoying thing: from the Get-Content developer’s perspective, it actually *is* doing a read of -ReadCount objects from the provider. Get-Content reads ReadCount objects then writes them as a single object to the pipeline instead of unraveling them. (I suspect that this is actually a bug that’s turned into a feature…) Anyway, the name makes perfect sense to the developer and absolutely no sense to the user. This is why developers always have to be aware of the user’s perspective even if it doesn’t precisely match the implementation details.

In summary, whenever -ReadCount

is set to a value greater than 1, usually for

performance reasons, object collections are sent through the pipeline to Where-Object instead of individual objects. As a result, we have to take extra steps to deal with unraveling the batched collections of objects. At this point we covered the two main "flow-control" cmdlets in detail. We discussed how they work, how they can be used and some of the benefits (and pitfalls) when using them. An important point to note is that there is nothing "special" about these cmdlets - they can be implemented by anyone and require no special access to the inner workings of the PowerShell engine. This is a characteristic we'll explore in later chapters where we'll see how we can take advantage of it. In the meantime, let's look at one final feature of the PowerShell language the ability to use all of these statements we've been talking about as expressions that return values. While not unique to PowerShell, this feature may seem a bit unusual to people who are used to working with languages like VBScript or C#. Let's take a look.

6.6 Statements as values
Let’s return to something we discussed a bit earlier when we introduced subexpressions in chapter 5—namely the difference between statements and expressions. In general, statements don’t return values, but if they’re used as part of a subexpression (or a function or script as we’ll see later on), they do return a result. This is best illustrated with an example. Assume that we didn’t have the range operator and wanted to generate an array of numbers from 1 to 10. Here’s the traditional approach we might use in a language such as C#. PS (1) > $result = new-object System.Collections.ArrayList PS (2) > for ($i=1; $i -le 10; $i++) { $result.Append($i) } PS (3) > "$($result.ToArray())" 1 2 3 4 5 6 7 8 9 10 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

224 First we create a instance of System.Collections.ArrayList to hold the result. Then we use a

for loop to step through the numbers, adding each number to the result ArrayList. Finally we convert the ArrayList to an array and display the result. This is a straightforward approach to creating the array, but requires several steps. Using loops in subexpressions, we can simplify it quite a bit. Here’s the rewritten example: PS (4) > $result = $(for ($i=1; $i -le 10; $i++) {$i}) PS (5) > "$result" 1 2 3 4 5 6 7 8 9 10 Here we don’t have to initialize the result or do explicit adds to the result collection. The output of the loop is captured and automatically saved as a collection by the interpreter. In fact, this is more efficient than the previous example, because the interpreter can optimize the management of the collection internally. This approach applies to any kind of statement. Let’s look at an example where we want to conditionally assign a value to a variable if it doesn’t currently have a value. First verify that the variable has no value: PS (1) > $var Now do the conditional assignment. This uses an if statement in a subexpression: PS (2) > $var = $(if (! $var) { 12 } else {$var}) PS (3) > $var 12 From the output, we can see that the variable has been set. Now change the variable and rerun the conditional assignment: PS (4) > $var="Hello there" PS (5) > $var = $(if (! $var) { 12 } else {$var}) PS (6) > $var Hello there This time the variable is not changed.

6.6.1 Direct assignment of flow control statements
For PowerShell Version 2, the ability to assign the output of a flow control statement has been simplified so we can directly assign the output to a variable. While this doesn't actually add any new capabilities, it does make things simpler and cleaner. For instance, the previous example can be simplified to PS (7) > $var = if (! $var) { 12 } else {$var} using this feature. And the for example we saw earlier can be simplified to PS (4) > $result = for ($i=1; $i -le 10; $i++) {$i} making it (somewhat) easier to read. Used judiciously, the fact that statements can be used as value expressions can be used to simplify your code in many circumstances. By eliminating temporary variables and extra initializations, creating collections is greatly simplified, as we saw with the for loop. On the other hand, it’s entirely possible to use this statement-as-expression capability to produce scripts that are hard to read. (Remember the nested if statement example we looked at earlier in this chapter?) You should always keep that in mind when using these features in scripts. The other thing that the way we use statements is the performance of our scripts. Let's dig into this in a bit more detail.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

225

6.7 A word about performance
Now that we've covered loops in PowerShell, this is a good time to talk about performance. PowerShell is an interpreted language which has performance implications. Tasks with a lot of small repetitive actions can take a long time to execute. Anything with a loop statement can be a performance hotspot for this reason. Identifying these hotspots and rewriting them can have a huge impact on script performance. Let's take a look at a real example. The author was writing a script to process a collection of events extracting events having a specific provide name and id and placing them into a new collection. The script looked something like the script shown in figure 6.X $results = @() for ($i=0; $i -lt $EventList.length ; $i++) { $name = [string] $Events[$i].ProviderName $id = [long] $Events[$i].Id if ($name -ne "My-Provider-Name") { continue } if ($id -ne 3005) { continue } $results += $Events[$i] } This script indexed through the collection of events using the for statement then used the continue statement to skip to the next event if the current event didn't match the desired criteria. If the event did match the criteria, it was appended to the result collection. While this worked correctly, for large collections of events it was talking several minutes to execute. Let's look at some ways to speed it up and make it smaller. First consider how we're indexing through the collection. This requires a lot of index operations, variable retrievals and increments which are not the most efficient operations in an interpreted language like PowerShell. Instead, PowerShell has a number of constructs that let you iterate through a collection automatically. Given that the task is to select events where some condition is true, the Where-Object cmdlet is an obvious choice. The second optimization is how the result list is built. The original code manually adds each element to the result array. If you remember our discussion on how array catenation works, this means that the array has to be copied each time an element is added. The alternative approach, as we discussed is to simply let the pipeline do the collection for you. With these to design changes, the new script looks like: $BranchCache3005Events = $events | where { $_.Id –eq 3005 –and $_.ProviderName -eq "My-Provider-Name”} The revised script is both hundreds of times faster and significantly shorter and clearer. So - the rule for writing efficient PowerShell scripts is to let the system do the work for you. Use “foreach” instead of explicit indexing with “for” if you can. If you ever find yourself doing catenation in a loop to build up a string or collection, look at using the pipeline instead. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

226 You can also take advantage of the fact that all powershell statements return values so an even faster (but less obvious/simple) way to do this is to use the foreach statement: $BranchCache3005Events = @( foreach ($e in $events) { if ($e.Id –eq 3005 –or $e.ProviderName -eq "Microsoft-Windows-BranchCacheSMB”) {$e}} ) The key here is still letting the system implicitly build the result array instead of constructing it manually with +=. Likewise for string catenation, this $s = -join $( foreach ($i in 1..40kb) { "a" } ) is faster than $s = ""; foreach ($i in 1..40kb) { $s += "a" }

By following these guidelines, not only will your scripts be faster, they will end up being shorter and frequently simpler/clearer (though not always.)

6.8 Summary
In chapter 6, we formally covered the branching and looping statements in the Power-Shell language as summarized in the following list. 20.PowerShell allows you to use pipelines where other languages only allow expressions. This means that, while the PowerShell flow control statements appear to be similar to the corresponding statements in other languages, there are enough differences to make it useful for you to spend time experimenting with them. 21.There are two ways of handling flow control in PowerShell. The first way is to use the language flow control statements such as while and foreach. However, when performing pipelined operations, the alternative mechanism—the flow control cmdlets Foreach-Object and Where-Object—can be more natural and efficient. 22.When iterating over collections, you should keep in mind the tradeoffs between the foreach statement and the Foreach-Object cmdlet. 23.Any statement can be used as a value expression when nested in a subexpression. For example, you could use a while loop in a subexpression to generate a collection of values. In PowerShell V2, for simple assignments, the subexpression notation is no longer needed and the output of a statement can be assigned directly to a variable. This mechanism can be a concise way of generating a collection, but keep in mind the potential complexity that this kind of nested statement can introduce. 24.The PowerShell switch statement is a powerful tool. On the surface it looks like the switch statement in C# or the select statement in Visual Basic, but with powerful pattern matching capabilities, it goes well beyond what the statements in the other languages can do. And, along with the pattern matching, it can also be used as a looping construct for selecting and processing objects from a collection or lines read from a file. In fact, much of its behavior was adapted from the AWK programming language. 25.The choice of statements and how you use them can have a significant effect on the performance of your scripts. This is something to keep in mind but remember, only worry about performance if it becomes a problem. Otherwise try to focus on making things as clear as possible.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

227

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

228

7
PowerShell Functions

Porcupine quills. We’ve always done it with porcupine quills —Dilbert

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

229 In this chapter we'll begin looking at how to combine the features from the previous chapters into reusable commands. As you will remember from chapter 2, there are four types of PowerShell commands: functions, cmdlets, scripts and external commands. Functions and scripts are the two command types that can be written in the PowerShell language. We're going to start with functions as they are the simpler of the two and are also easy to enter interactively in a session. In the next chapter we'll expand our discussion to include scripts as well as introducing some of the more advanced programming features available to both functions and scripts. Before we dive in, there's one thing you need to be aware of if you have prior programming experience. This prior experience can be both a blessing and a curse when learning to program in PowerShell. Most of the time, what you already know makes it easier to program in PowerShell. The syntax and most of the concepts will probably be familiar. Unfortunately, similar is not identical, and this is where prior experience can trip you up. You’ll expect PowerShell to work like your favorite language, and it won’t work quite the same way. We'll call out these issues as we encounter them. So, put away your porcupine quills and let’s get started.

7.1 Fundamentals of PowerShell functions
In this section we'll cover the basic concepts and features of PowerShell functions. Functions are the most lightweight form of PowerShell command. They only exist in memory for the duration of a session. When you exit the shell session, the functions are gone. They are also simple enough that you can create useful functions in a single line of code. We'll start by working through a number of examples showing how to create simple functions. Let's take a look at our first example: PS (1) > function hello { "Hello world" } In this example, 'hello' is pretty obviously a function since it is preceded by the function keyword. And, equally obvious, this function should emit the string "hello world". We'll execute it to verify this. PS (2) > hello; hello; hello Hello world Hello world Hello world and yes - it works exactly as expected. We've created our first command. Ok - that was easy. Now we know how to write a simple PowerShell function. The syntax is shown in figure 7.1
List of statements that make up the function body.

The name of the function.

function { } Braces mark beginning and end of the function body.

The function Keyword

7.1 This diagram shows the simplest form of a function definition in PowerShell. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

230 But a function that writes only “hello world” isn’t very useful. Let's see how to personalize this function by allowing an argument to be passed in.

7.1.1 Passing arguments using $args
The ability to pass values into a function is called parameterizing the function. In most languages, this means that modifying the function to declare the parameters to process. For simple PowerShell functions, we don’t actually have to do this because there's a default argument array that contains all of the values passed to the function. This default array is available in the variable $args. Here’s the previous hello example modified to use $args to receive arguments: PS (3) > function hello { "Hello there $args, how are you?" } PS (4) > hello Bob Hello there Bob, how are you? This example uses string expansion to insert the value stored in $args into the string that is emitted from the hello function. Now let’s see what happens with multiple arguments: PS (5) > hello Bob Alice Ted Carol Hello there Bob Alice Ted Carol, how are you? Following the string expansion rules described in chapter 3, the values stored in $args get interpolated into the output string with each value separated by a space. Or, more specifically, separated by whatever is stored in the $OFS variable. So let’s take one last variation on this example. We’ll set $OFS in the function body with the aim of producing a more palatable output. We’ll take advantage of the interactive nature of the PowerShell environment to enter this function over several lines: PS (6) > function hello >> { >> $ofs="," >> "Hello there $args and how are you?" >> } >> PS (7) > hello Bob Carol Ted Alice Hello there Bob,Carol,Ted,Alice and how are you? That’s better. Now at least we have commas between the names. Let’s try it again, with commas between the arguments: PS (8) > hello Bob,Carol,Ted,Alice Hello there System.Object[] and how are you? This is not the result we were looking for! So what happened? Let’s define a new function to clear up what happened. PS (1) > function count-args { >> "`$args.count=" + $args.count >> "`$args[0].count=" + $args[0].count >> } >> This function will display the number of arguments passed to it as well as the number of elements in the first argument. First we use it with three scalar arguments: PS (2) > count-args 1 2 3 $args.count=3 $args[0].count=

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

231 As expected, it shows that we passed three arguments. It doesn’t show anything for the Count property on $args[0] because $args[0] is a scalar (the number 1) and consequently doesn’t have a Count property. Now let’s try it with a comma between each of the arguments: PS (3) > Count-Args 1,2,3 $args.count=1 $args[0].count=3 Now we see that the function received one argument, which is an array of three elements. And finally, let’s try it with two sets of comma-separated numbers: PS (4) > count-args 1,2,3 4,5,6,7 $args.count=2 $args[0].count=3 The results show that the function received two arguments, both of which are arrays. The first argument is an array of three elements and the second is an array with four elements. Hmm, you should be saying to yourself—this sounds familiar. And it is—the comma here works like the binary comma operator in expressions, as discussed in chapter 5. Two values on the command line with a comma between them will be passed to the command as a single argument. The value of that argument is an array of those elements. This applies to any command, not just functions. If you want to copy three files f1.txt, f2.txt, and f3.txt to a directory, the command would be copy-item f1.txt,f2.txt,f3.txt target The Copy-Item cmdlet receives two arguments: the first is an array of three file names, and the second is a scalar element naming the target directory. Now let's look at a couple of examples where $args enables simple but powerful scenarios.

7.1.2 Example functions: ql and qs
The way $args works is simple and straightforward, but it actually allows you to write some pretty slick commands. Here are two functions that aren’t in the PowerShell base installation (although they may be in the future but not in either V1 or V2. Sigh). function ql { $args } function qs { "$args" } They may not look like much, but they can significantly streamline a number of tasks. The first function is ql which stands for “quote list”. This is a Perl-ism. Here’s what you can do with it. Say you want to build up a list of the color names. To do this with the normal comma operator, you’d do the following: $col = "black","brown","red","orange","yellow","green", "blue","violet","gray","white" which requires lots of quotes and commas. With the ql function, you could write it this way: $col = ql black brown red orange yellow green blue violet gray white This is much shorter and requires less typing. Does it let you do anything you couldn’t do before? No, but it lets you do something more efficiently when you have to. Remember that elastic syntax concept? When you’re trying to fit a complex expression onto one line, things like

ql can help. What about the other function qs? It does approximately the same thing, but uses string concatenation to return its arguments as a single string instead of an array. PS (1) > $string = qs This is a string PS (2) > $string This is a string PS (3) > ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

232 Note that the arguments are concatenated with a single space between them. The original spacing on the command line has been lost, but that usually doesn’t matter.

7.1.3 Simplifying $args processing with multiple assignment
As handy as $args is, it can become awkward when trying to deal with parameters in a more complex way. Let's look at an example that illustrates this. We'll write a function that takes two arguments and adds them together. With what we've seen so far, we could use array indexing to get each element then add them together. The result might look like this: PS (1) > function Add-Two { $args[0] + $args[1] } PS (2) > add-two 2 3 5 Notice that most of the work in this function is getting the arguments out of the array. This is where multiple assignment comes in. It allows us to extract the elements of the array in $args into names variables in a very convenient way. Using this feature, the updated function would look like PS (3) > function Add-Two { >> $x,$y=$args >> $x+$y >> } >> PS (4) > add-two 1 2 3 In this example, the first statement in the function assigns the values passed in $args to the local variables $x and $y. Perl users will be familiar with this approach for dealing with function arguments, and, while it’s a reasonable way to deal with parameters, it isn’t the way most languages do it.

AUTHOR'S NOTE
The $args approach will be familiar to Perl 5 or earlier users. Perl 6 has a solution to the problem that is similar to what PowerShell does. I’d claim great minds think alike, but it’s really just the most obvious way to solve the problem.

For this reason, PowerShell provides other ways to declare the formal parameters. We’ll cover those approaches in the next couple of sections.

7.2 Declaring formal parameters for a function
With the fundamentals out of the way, we’ll start to look at some of the more sophisticated features PowerShell functions. We'll start with a better way for declaring function parameters. While the $args variable is a simple and automatic way of getting at the arguments to functions, it takes a fair amount of work to do anything with a level of sophistication. as we saw in the last section. PowerShell provides a much more convenient (and probably more familiar to many people) way to declare parameters, which is shown in Figure 7.2.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

233
The name of the function. List of statements that make up the function body.

function Keyword

function ( ) { }

The list of parameters for the function.

Braces mark beginning and end of the function body.

7.2 This diagram shows the syntax for defining a function with explicit parameters in PowerShell. The parameter list is optional in which case you can either have empty parentheses or simply omit them as we saw in figure 7.1.

Here’s a simple example of what this looks like in a real functon: function subtract ($from, $count) { $from - $count } In this function definition, there are two formal parameters: $from and $count. When the function is called, each actual argument will be bound to the corresponding formal parameter, either by position or by name. What does that mean? Well, binding by position is obvious, as we can see: PS (1) > subtract 5 3 2 In this case, the first argument 5 is bound to the first formal parameter $x, and the second argument is bound to the second parameter $y. Now let’s look at using the parameter names as keywords: PS (2) > subtract -from 5 -count 2 3 PS (3) > subtract -from 4 -count 7 -3 What happens if you try and use the same parameter twice? You’ll receive an error message that looks like this: PS (4) > subtract -count 4 -count 7 subtract : Cannot bind parameter because parameter 'count' is spec ified more than once. To provide multiple values to parameters tha t can accept multiple values, use the array syntax. For example, "-parameter value1,value2,value3". At line:1 char:25 + subtract -count 4 -count function a ($x, $y) { >> "x is $x" >> "y is $y" >> "args is $args" >> } >> Now let’s use it with a single argument. PS (12) > a 1 x is 1 y is args is The single argument is bound to $x. $y is initialized to $null and $args has zero elements in it. Now try it with two arguments. PS (13) > a 1 2 x is 1 y is 2 args is This time $x and $y are bound, but $args is still empty. Next try it with three arguments, and then with five. PS (14) > a 1 2 3 x is 1 y is 2 args is 3 PS (15) > a 1 2 3 4 5 x is 1 y is 2 args is 3 4 5 Now you can see that the extra arguments end up in $args. This automatic handling of excess arguments is useful behavior but in a lot of cases, we prefer that extra arguments to be treated as an error. One way to make sure that no extra arguments are passed to your function is to check whether the length of the $args.length is zero in the function body. If it’s not zero then some arguments were passed. This is, however, a bit awkward. In chapter 8, we'll look at a much better way to handle this. Earlier we mentioned that formal arguments that didn’t have corresponding actual arguments were initialized to $null. While this is a handy default, it would be more useful to have a way of initializing the parameters to specific values instead of having to write a lot extra code in the body of the function to handle this. We’ll look at that next.

7.2.4 Initializing function parameters with default values
In this section, we’ll show you how to initialize the values of function parameters. The syntax for this is shown in figure 7.3

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

238 function Keyword The name of the function. List of parameter specifications

function ( $p1 = , $p2 = ... ) { }

Parameter name followed by = symbol followed by an expression

Additional parameter specifications are separated by commas

Figure 7.3 This figure shows the more complex function definition syntax where initializer expressions are provided for each variable. Note that the initializers are constrained to be expressions, but using the subexpression notation, you can actually put anything here.

Let’s move right into an example: PS (14) > function add ($x=1, $y=2) { $x + $y } This function initializes the formal parameters $x to 1 and $y to 2 if no actual parameters are specified. So when we use it with no arguments: PS (15) > add 3 it returns 3. With one argument: PS (16) > add 5 7 it returns the argument plus 2, which in this case is 7. And finally with two actual arguments PS (17) > add 5 5 10 it returns the result of adding them. From this example, it’s obvious that you can initialize the variable to a constant value. What about something more complex? The initialization sequence as shown in figure 7.2 says that an initializer can be an expression. If you remember from chapter 5, an expression can be a subexpression and a subexpression can contain any PowerShell construct. In other words, an initializer can do anything: calculate a value, execute a pipeline, reformat your hard-drive (not recommended), or send out for snacks from Tahiti by carrier pigeon (Personally, I’ve not had much luck with this one). Let’s try this feature out. We’ll define a function that returns the day of the week for a particular date. PS (28) > function dow ([datetime] $d = $(get-date)) >> { >> $d.dayofweek >> } >> This function takes one argument $d that is constrained to be something that matches a date or time. If no argument is specified then it is initialized to the result of executing the Get-

Date cmdlet (which returns today’s date). Now let’s try it out. First run it with no arguments:
PS (29) > dow Tuesday And it prints out what today is. Then run it with a specific date: PS (30) > dow "jun 2, 2001" Saturday ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

239 and we see that June 2, 2001 was a Saturday. This is a simple example of using a subexpression to initialize a variable.

7.2.5 Handling mandatory parameters V1-style
There is one interesting scenario that we should still talk about. What happens if we don’t want a default value? In other words, how can we require the user to specify this value? This is another thing you can use initializer expressions for though it's a bit of a hack.

AUTHOR'S NOTE
This hack was the best way to handle mandatory parameters in PowerShell V1. It's not recommended for V2. A much better approach is to use parameter metadata and the Mandatory property as described in chapter 8.

Here's how it works. Since the variable initializer expression can, by using a subexpression notation, be any piece of PowerShell code, we can use it to generate an error rather than initialize the variable. We'll do this using the throw statement (we’ll cover the throw statement in detail in chapter 13). Here’s how you can use the throw statement to generate the error. First we define the function. PS (31) > function zed ($x=$(throw "need x")) { "x is $x" } Notice how the throw statement is used in the initializer subexpression for $x. Now run the function—first with a value to see whether it works properly PS (32) > zed 123 x is 123 and then without an argument PS (33) > zed need x At line:1 char:25 + function zed ($x=$(throw

Remove-Item function:/clippy And make sure that it’s gone. PS (14) > (dir function:/).count 78 PS (15) > dir function:clippy Get-ChildItem : Cannot find path 'clippy' because it does not ex ist. At line:1 char:4 + dir function two { $x = 22; one } Function one prints out a string displaying the value of $x. Function two sets the variable $x to a particular value, and then calls function one. Now let’s try them out. Before we work with the functions, we will set $x to 7 interactively, to help illustrate how scoping works. PS (3) > $x=7 Now we’ll call function one. PS (4) > one x is 7 As expected, it prints out “x is 7”. Now let’s call function two. PS (5) > two x is 22 Not surprisingly, since two sets $x to 22 before calling one, we see “x is 22” being returned. So what happened to $x? Let’s check: PS (6) > $x 7 It’s still 7! Let’s call one again. PS (7) > one x is 7 It prints out “x is 7”. So what exactly happened here? When we first assigned 7 to $x, we created a new global variable $x. When we called function one the first time, it looked for a variable $x, found the global definition, and used that to print out the message. When we called function two, it defined a new local variable called $x before calling one. This variable is local; i.e., it didn’t change the value of the global $x, but it did put a new $x on the scope stack. When it called one, this function searched up the scope stack looking for $x, found the new variable created by function two, and used that to print out “x is 22”. On return from function two, the scope containing its definition of $x was discarded. The next time we called function one, it found the top-level definition of $x. Now let’s compare this to a language that is ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

260 lexically scoped. I happen to have Python installed on my computer, so from Power-Shell, I’ll start the Python interpreter. PS (1) > python Python 2.2.3 (#42, May 30 2003, 18:12:08) [MSC 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more informa tion. Now let’s set the global variable x to 7. (Note—even if you aren’t familiar with Python, these examples are very simple, so you shouldn’t have a problem following them.) >>> x=7 Now print out x to make sure it was properly set. >>> print x 7 We see that it is, in fact, 7. Now let’s define a Python function one. >>> def one(): ... print "x is " + str(x) ... And now define another function two that sets x to 22 and then calls one. >>> def two(): ... x=22 ... one() ... As with the PowerShell example, we call one and it prints out “x is 7”. >>> one() x is 7 Now call two. >>> two() x is 7 Even though two defines x to be 22, when it calls one, one still prints out 7. This is because the local variable x is not lexically visible to one—it will always use the value of the global x, which we can see hasn’t changed. >>> print x 7 >>> So now, hopefully, you have a basic understanding of how variables are looked up in PowerShell. Sometimes, though, you want to be able to override the default lookup behavior. We’ll discuss this in the next section.

AUTHOR'S NOTE
UNIX shells used dynamic scoping because they didn’t really have a choice. Each script is executed in its own process and receives a copy of the parent’s environment. Any environment variables that a script defines will then be inherited by any child scripts it, in turn, calls. The process-based nature of the UNIX shells predetermines how scoping can work. The interesting thing is that these semantics are pretty much what PowerShell uses, even though we weren’t limited by the process boundary. We tried a number of different schemes and the only one that was really satisfactory was the one that most closely mimicked traditional shell semantics. I suppose this shouldn’t be a surprise, since it’s worked pretty well for several decades now.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

261

7.6.2 Using variable scope modifiers
We’ve now arrived at the subject of variable scope modifiers. In the previous section we discussed scope and the default PowerShell lookup algorithm. Now you’ll see that you can override the default lookup by using a scope modifier. These modifiers look like the namespace qualifiers mentioned in chapter 6. To access a global variable $var, you would write: $global:var Let’s revisit the functions from the previous section. PS (1) > function one { "x is $global:x" } This time, in the function one, we’ll use the scope modifier to explicitly reference the global $x. PS (2) > function two { $x = 22; one } The definition of function two is unchanged. Now set the global $x to 7 (commands at the top level always set global variables, so you don’t need to use the global modifier). PS (3) > $x=7 Now run the functions. PS (4) > one x is 7 PS (5) > two x is 7 This time, because we told one to bypass searching the scope change for $x and go directly to the global variable, calls to both one and two return the same result, “x is 7”. When we look at scripts in chapter 8, we'll see that there are additional scoping rules and qualifiers but for now, we have all we need to work with functions. In the next chapter, we'll extend our PowerShell programming knowledge to include writing scripts. We'll also look at some of the advanced features in PowerShell, especially new features introduced with PowerShell V2 that we can use for our work.

7.7 Summary
This chapter finally introduced the idea of programming in PowerShell. While there was a lot of material, the following are the key points:     PowerShell programming can be done either with functions or scripts though in this chapter we focused only on functions. Functions are created using the function keyword The simplest form of function uses $args to receive parameters automatically. More sophisticated parameter handling for functions requires the use of parameter declarations. This can be done by placing the parameter names in parentheses after the name of the function or in the body of the function using the param keyword. PowerShell uses dynamic scoping for variables. You can modify how a variable name is resolved by using the scope modifiers in the variable names. Functions stream their output. In other words, they return the results of every statement executed as though it were written to the output stream. This feature means that you almost never have to write your own code to accumulate results. Because of the differences between how functions work in PowerShell and how they work in more conventional languages, you may receive some unexpected results when

 



©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

262 creating your functions, so you picked up some tips on debugging these problems.   Functions can be used as filters using the filter keyword or by specifying begin, process and end blocks in the function body. The function drive is used to manage the functions defined in your session. This means that you use the same commands you use for managing files to manage functions.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

263

8
Advanced functions and scripts

And now for something completely different… —Monty Python

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

264 In chapter 7, we introduced the basic elements needed for programming in PowerShell when we looked at PowerShell functions. In this chapter we're going to expand our repertoire by introducing PowerShell scripts.

AUTHOR'S NOTE lf you skipped chapter 7, you should probably go back and read it before proceeding. Why? Because all of the material we covered on functions also applies to scripts.

Once we're done with the basics of scripts (which won't take long) we're going to move on to the advanced "production scripting" features introduced in PowerShell V2. With these new features, it's now possible to use the PowerShell language to write full featured applications complete with proper documentation. By the end of this chapter, you should be well on your way to becoming an expert PowerShell programmer.

8.1 PowerShell scripts
In this section we're going to dig into scripts to see what behaviors they have in common with functions and what additional features we need to be aware of. We'll begin by looking at the "Execution Policy" that controls what scripts can be run. Then we'll look at how parameters and the exit statement work in scripts. We'll also spend some time on the additional scoping rules that scripts introduce. Finally we'll look at some of the ways we can apply and manage the scripts we write. Let's begin with defining what a script actually is. A PowerShell script is simply a file with a

.ps1 extension that contains some PowerShell commands. It's as simple as that. Back in chapter 1, we talked about how PowerShell has the world’s shortest “Hello world” program. The full text of that script was "Hello world" That's it - one line. Let's create this script now. We can do it from the command line using redirection to write the script text to a file called "hello.ps1". PS (2) > '"Hello world"' > hello.ps1 Note the double quotes the example. We want the script to contain "Hello world" with the quotes intact, not Hello world Now let’s execute the script: PS (3) > ./hello.ps1 Hello world We see that the file executed and returned the expected phrase.

AUTHOR'S NOTE
In this example, even though hello.ps1 is in the current directory, we had to put “./” in front of it to run it. This is because PowerShell doesn’t execute commands out of the current directory by default. This prevents accidental execution of the wrong command. See chapter 13 on security for more information.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

265

8.1.1 Script Execution Policy
Now there’s a possibility that instead of getting the expected output, you received a nasty looking error message that looked something like this: PS (5) > ./hello.ps1 The file C:\Documents and Settings\brucepay\hello.ps1 cannot be loaded. The file C:\Documents and Settings\brucepay\hello.ps1 is not digitally signed. The script will not execute on the system . Please see "get-help about_signing" for more details.. At line:1 char:11 + ./hello.ps1 > if ($args) { $name = "$args" } else { $name = "world" } >> "Hello $name!" >> '@ > hello.ps1 >> This script has two lines. The first sets a local variable $name to the value of $args if it’s defined. If it’s not defined then it sets $name to “world”. If we run the script with no arguments, we get: PS (12) > ./hello Hello world! the generic greeting. If we run it with an argument, we get a specific greeting: PS (13) > ./hello Bruce Hello Bruce! PS (14) > These are the same basic things we did with functions, and, as was the case with functions, they have limitations. It would be much more useful to have named, typed parameters as we did with functions. There’s a slight wrinkle however. As you'll remember from chapter 7, the formal arguments to a function are defined outside the body of the function. or inside the body with the param statement. Obviously the external definition going to work isn't with scripts since there’s no “external”. Consequently, there's only one way to define formal parameters for a script - through the param statement. USING THE PARAM STATEMENT IN SCRIPTS As mentioned in the previous section, if we want to specify formal parameters for a script, we need to use the param statement to do this. The param statement must be the first executable line in the script just like it must be the first executable line in a function. Only ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

267 comments and empty lines may precede it. Let’s visit our hello example one more time. Again we’ll use a here-string and redirection to create the script. The here-string makes it easy to define a multiline script. PS (14) > @' >> param($name="world") >> "Hello $name!" >> '@ > hello.ps1 >> We've added a second line to the script to declare the script parameter. When we run the script, we find the expected results, first with no arguments PS (15) > ./hello Hello world! and then with a name argument: PS (16) > ./hello Bruce Hello Bruce! PS (17) > In fact the script could be written as a single line but splitting it across two lines makes it easier to read.. PS (17) > 'param($name="world") "Hello $name"' > hello.ps1 PS (18) > ./hello Hello world PS (19) > ./hello Bruce Hello Bruce The interesting thing that this example illustrates that there is no need for any kind of separator after the param statement for the script to be valid. Since PowerShell lends itself to one-liner type solutions this can be handy. Obviously scripts must have some additional characteristics that we don't find with functions. Let's explore those now.

8.1.3 Exiting scripts and the exit statement
We've seen that we can "exit" scripts (or functions) simply by getting to the end of the script. We’ve also looked at the return statement in our discussion of functions (section 7.3.2). The

return statement lets us exit early from a function. It will also let you return early from a script but only if called from the "top" level of a script (i.e. not from a function called in the script.) The interesting question is what happens when you return from a function defined inside a script. As discussed in chapter 7, what the return statement does is let you exit from the current scope . This remains true whether that scope was created by a function or script. But what happens when you want to cause a script to exit from within a function defined in that script? PowerShell has the exit statement to do exactly this. So far, we've been using this statement to exit a PowerShell session. However, when exit is used inside a script, it exits that script. This is true even when called from a function in that script. Here’s what that looks like: PS (1) > @' >> function callExit { "calling exit from callExit"; exit} >> CallExit >> "Done my-script" >> '@ > my-script.ps1 >> ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

268 The function CallExit defined in this script calls exit. Since the function is called before the line that emits "Done my-script" we shouldn’t see this line emitted. Let’s run it: PS (2) > ./my-script.ps1 calling exit from CallExit We see that the script was correctly terminated by the call to exit in the function CallExit. The exit statement is also how we set the exit code for the PowerShell process when calling PowerShell.exe from another program. Here’s an example that shows how this works. From within cmd.exe, we’ll run PowerShell.exe, passing it a string to execute. This “script” will emit the message “Hi there” and then call exit with an exit code of 17. C:\>powershell "'Hi there'; exit 17" Hi there And now we’re back at the cmd.exe prompt. Cmd.exe makes the exit code of a program it’s run available in the variable ERRORLEVEL, so we’ll check that variable: C:\>echo %ERRORLEVEL% 17 We see that it is 17 as expected. This shows how a script executed by PowerShell can return an exit code to the calling process. Let's recap - so far in our discussion of scripts behaviors, we've covered execution policy, parameterization and how to exit scripts. In the next section we'll look at another feature of scripts that we need to understand: variable scoping.

8.1.4 Scopes and scripts
In chapter 7, we covered the scoping rules for functions. These same general rules also apply to scripts:    Variables are created when they are first assigned. They are always created in the current scope, so a variable with the same name in an outer (or global) scope is not affected. In both scripts and functions, we can use the $global:name scope modifier to explicitly modify a global variable.

Now let's look at what's added for scripts. Scripts introduce a new named scope called the script scope, indicated using the $script: scope modifier. This scope modifier is intended to allow functions defined in a script to affect the “global” state of the script without affecting the overall global state of the interpreter. This is shown in figure 8.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

269

Global Scope: $a = 1, $b = 2, $c = 3; $d=4

Script s1 Scope: $b = 20

User calls script ‘s1’ which creates a new script scope. ‘s1’ calls function ‘one’ which causes a new function scope to be created. ‘one’ calls function ‘two’ creating a second function scope resulting in a total of 4 scopes in the scope chain.

Script calls function one

Function ‘one’ scope function one { $b=200; c = 300; two } In function two, $a resolves in the global scope, $script:b resolves in the script scope (skipping the function one scope because of the $script: modifier), $c resolves in the function one scope and $d resolves in the function two scope (i.e. $d is local to function two.) Function one calls function two

Function ‘two’ scope function two {$d = 4000; “$a $script:b $c $d” } function returns “1 20 300 4000”

Figure 8.1 How variables are resolve across different scopes when scripts are involved. Variables prefixed with the $script: modified resolve in the script scope. Variable references with no scope modifier resolve using the normal look-up rules.

Let’s look at an example. First, set a global variable $x to be 1. PS (1) > $x = 1 Then create a script called my-script. In this script, we’ll create a function called lfunc. This

lfunc function will define a function-scoped variable $x to be 100 and a script-scoped variable $x to be 10. The script itself will run this function and then print out the script-scoped variable
x. We’ll use a here-string and redirection to create the script interactively. PS (2) > @' >> function lfunc { $x = 100; $script:x = 10 ; "lfunc: x = $x"} >> lfunc >> "my-script:x = $x" >> '@ > my-script.ps1 >> Now let’s run the script. PS (3) > ./my-script.ps1 lfunc: x = 100 my-script:x = 10 We see that the function scoped variable $x was 100; the script scoped $x was 10 PS (4) > "global: x = $x" global: x = 1 while the global $x is still 1. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

270 SIMPLE LIBRARIES: INCLUDING ONE SCRIPT FROM ANOTHER As we build up libraries of useful functions, we need to have a mechanism to "include" one script inside another (or to run in the global environment) to make these library functions available. PowerShell allows us to do this through a feature called “dot-sourcing” a script or function.

AUTHOR'S NOTE
The dot-sourcing mechanism (sometimes called "dotting") is the only way to build libraries in PowerShell v1. In PowerShell V2, dot-sourcing is still used for configuration but the modules feature (chapters 9 and 10) is the recommended way to script libraries.

So far in our discussions, we've usually only cared about the results of a function and wanted all of the local variables when the script or function exits. This is why scripts and functions get their own scope. But sometimes you do care about all of the intermediate by-products. This is typically the case when you want to create a library of functions or variable definitions. In this situation, you want the script to run in the current scope.

AUTHOR'S NOTE
This is how cmd.exe works by default, as this example shows. We have a cmd file foo.cmd

C:\files>type foo.cmd set a=4 now set a variable a to 1 and display it:

C:\files>set a=1 C:\files>echo %a% 1
Next run the cmd file

C:\files>foo C:\files>set a=4 and we see that the variable has been changed.

C:\files>echo %a% 4
As a consequence of this behavior, it’s common to have cmd files that do nothing but set a bunch of variables. To do this in PowerShell, you would dot the script.

DOTTING-SOURCING SCRIPTS AND FUNCTIONS So how do you “dot-source” a script? By putting a dot or period in front of the name when you execute it. Note that there has to be a space between the dot and the name, otherwise it will be considered part of the name. Let’s look at an example. First we create a script that sets $x to 22. PS (5) > @' >> "Setting x to 22" ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

271 >> $x = 22 >> '@ > my-script.ps1 >> and we’ll test it. First set $x to a known value PS (6) > $x=3 PS (7) > $x 3 then run the script as we would normally: PS (8) > ./my-script Setting x to 22 Checking $x, we see that it is (correctly) unchanged. PS (9) > $x 3 Now we’ll dot the script. PS (10) > . ./my-script Setting x to 22 PS (11) > $x 22 This time $x is changed. What follows the . isn’t limited to a simple file name. It could be a variable or expression, as was the case with “&”: PS (12) > $name = "./my-script" PS (13) > . $name Setting x to 22 The last thing to note is that dot-sourcing works for both scripts and functions. Let’s define a function to show this: PS (17) > function set-x ($x) {$x = $x} PS (18) > . set-x 3 PS (19) > $x 3 In this example, we define the function set-x and dot it, passing in the value 3. The result is that the global variable $x is set to 3. This covers how scoping works scripts and functions. When we look at modules in chapter 9, we'll look at one more variation on scoping. Now that we know how to build simple script libraries, let’s look at how to manage all of these scripts we’re writing.

8.1.5 Managing your scripts
Earlier we looked at managing function using the function: drive. Since scripts live in the file system, there's no need to have a special drive for them - the file system drives are sufficient. It is important to understand how scripts are found when you type their names into the interpreter. Like most shells, PowerShell uses the path environment to find scripts. You can look at the contents of this variable using the environment variable provider $ENV:PATH. The other thing to know (and we've mentioned it previously but people still forget it) is that PowerShell does not run scripts out of the current directory (at least not by default.) If you want to run a script out of the current directory, you can either add that director to the path or prefix your command with "./" as in "./mycmd.ps1" or simply "./mycmd". The script search algorithm will look for a command with ".ps1" extension if there isn't one on the command. A common approach is to have a common scripts directory where all of your personal scripts are placed and a share for sharing scripts between multiple users. Since scripts are just text, using a version control system like RCS or Subversion will work well for managing your scripts. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

272 Now let's look at one more variation on scripting. So far, we've only been running PowerShell scripts from within PowerShell. There are times when you need to run a PowerShell script from a non-PowerShell application like cmd.exe. For example, we may have an existing batch file that needs to call PowerShell to one specific task. To do this we need to launch a PowerShell process to run the script or command. We also have to do this when creating shortcuts that launch PowerShell scripts since PowerShell.exe is not the default file association for a .ps1 file (security strikes again - this prevents accidental execution of scripts.)

8.1.6 Running PowerShell scripts from other applications
Let's look at what's involved in using powershell.exe to run a script and go over a few issues that exist. Here's something that can trip people up when using powershell.exe to execute a script. The V2 PowerShell interpreter has two different parameters that let you run PowerShell code when PowerShell is started. These parameters are -Command and -File as shown in figure 8.2.

How the command line is processed when -Command is specified:
Command “c:\my” Argument 1 “scripts\script1.ps1” Argument 2 “myfile.ex1”

powershell.exe -command “c:\my scripts\script1.ps1” data.csv

How the command line is processed when -File is specified:
Command “c:\my scripts\script1.ps1” Argument 2 “myfile.ex1”

powershell.exe -file “c:\my scripts\script1.ps1” data.csv
Figure 8.2 This figure shows the difference in how command line is processed when using the -command parameter vs. the -file parameter. With -command, the first argument is parsed into 2 tokens. With -file, the entire first argument is treated as the name of a script to run.

If you use the -Command parameter, the arguments to powershell.exe are accumulated and then treated as a script to execute. This is important to remember when we try to run a script using PowerShell from cmd.exe using this parameter. Here’s the problem people run into: because the arguments to the powershell.exe are a script to execute, not the name of a file to run, if the path to that script has a space in it, then, since

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

273 PowerShell treats the spaces as delimiters, we’ll get an error. Consider a script called “my script.ps1”. When we try to run this by doing: powershell "./my script.ps1" PowerShell will complain about “my” being an unrecognized command. It treated “my” as a command name and “script.ps1” as an argument to that command. To execute a script with a space in the name, we need to do the same thing we’d do at the PowerShell command prompt: put the name in quotes and use the call (&) operator: powershell.exe "& './my script.ps1'" Now the script will be run properly. This is one of the areas where having two types of quotes comes in handy. Also note that we still have to use the relative path to find the script if it’s in the current directory. To address this problem, in version 2, powershell.exe now has a second parameter that makes this easier; the -File parameter. This parameter takes the first argument after the parameter as the name of the file to run and the remaining arguments are passed to the script. The example now simplifies to powershell -file "my script.ps1" This is clearly much simpler than the version 1 example. There is one more advantage to using -File. When you run a script using -Command, the

exit keyword will exit the script but not the PowerShell session (though usually it looks like it did). This is because the arguments to -Command are treated the same way commands typed interactively into PowerShell work. We wouldn't want a script we're running to cause our session to exit accidently. If you use -File instead of -Command calling exit in the script will cause the powershell.exe process to exit. This is because -File treats the entire contents of the script as the command to execute instead of executing a command that names the script file. Now let's see why this is important. It matters if you're depending on the exit code of the PowerShell process to decide some condition in the calling script. If you use -Command, the exit code of the script is set but the process will still exit with 0. If you use -File, powershell.exe will exit with the correct exit code. Let's try this. We'll create a script called exit33.ps1. Here's what it looks like: PS (1) > gc exit33.ps1 exit 33 This is a very simple script - all it does is exit with the exit code '33'. Now let's run it using Command then check the exit code: PS (2) > powershell.exe -command ./exit33.ps1 PS (3) > $LASTEXITCODE 0 We see that the exit code of the powershell.exe process is 0 not 33 which is what we wanted. Let's take a closer look. We'll run the command again, but follow it with '$LASTEXITCODE' to emit the code returned from the script. PS (6) > powershell.exe -command ` >> './exit33.ps1 ; $LASTEXITCODE' >> 33 and we see that 33 was output because that was the last exit code of the script. But when we check the exit code of the process ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

274 PS (7) > $LASTEXITCODE 0 we see that it's still 0. (In fact, the piece of script that emits the exit code shouldn't even have run if the call to exit in the script had caused the process to exit.) In contrast, when we use -

File instead or -Command,
PS (4) > powershell.exe -file ./exit33.ps1 PS (5) > $LASTEXITCODE 33 we get the correct exit code because the -File option runs the specified command directly. This means that if the caller of powershell.exe depends on the exit code of the process, it will get the correct value. This concludes our coverage of the basic information needed to run PowerShell scripts. If you've used other scripting languages, little of what we've seen so far should seem all that unfamiliar. In the next few sections we're going to look at things that are rather more advanced.

8.2 Writing Advanced Functions and Scripts
For the most part, all of the features we've discussed so far were available in PowerShell version 1. While V1 scripts and functions were very powerful, they didn't have all of the features that compiled cmdlets did. In particular, there wasn't a good way to write productionquality scripts complete with integrated help, and so on. Version 2 of PowerShell introduced features that addressed these problems. With version 2, commands written in the PowerShell language have all of the capabilities available to cmdlets. In the rest of this section we'll introduce these new features and see how to use them to create fully cmdlet-enabled functions and scripts in PowerShell. We'll be using functions for all of the examples just for simplicity's sake. Everything that applies to functions applies equally to scripts. All of these new features are enabled by adding metadata to the function or script parameters. Metadata is "information about information" which we use in PowerShell to declaratively control the behavior of functions and scripts. What this means is that we're telling what we want PowerShell to do, but not how to do it. It's like telling a taxi driver where you want to go without having to tell them how to get there (although there as here, it's always a good idea to make sure you're ending up where you want to be.) We're all ready to dive in now but first - a warning. There is a lot of material here and some of it is a bit complex so taking your time and experimenting with the features is recommended.

AUTHOR'S NOTE
This stuff is really much more complex than we wanted. Could it have been simpler? Maybe but we haven't figured out a way to do it yet. The upside of the way these features are implemented is that they match how things are done in compiled cmdlets. This way, time invested in learning this material will be of benefit if you want to learn to write cmdlets in at some point. And conversely, if you know how to write cmdlets, then all of this stuff will be pretty familiar.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

275

8.2.1 Specifying script and function attributes
In this section, we'll look at the features we can control through metadata attributes on the function or script definition. (as opposed to on parameters which we'll get to in a minute.) Figure 8.3 shows how the metadata attributes are used when defining a function, including attributes that affect the function as well as individual parameters on that function.
The name of the function.

function Keyword

Attribute specifying function metadata

Attribute declaring the function output type of the { function. [CmdletBinding()] [OutputType()] param ( [Parameter(ParameterSet=”set1",Position=0,Mandatory=$true)] [int] $p1 = , [Parameter(ParameterSet=”set1",Position=0,Mandatory=$true)] [string] $p2 = ) List of parameter : specifications : } Attribute specifying parameter Function body metadata

Figure 8.3 This figure shows where the V2 metadata attributes are added in function definitions. Attributes that apply to the entire function appear before the param statement and attributes for an individual parameter appear before the parameter declaration.

Notice that there are two places where the attributes can be added to functions - to the function itself and to the individual parameters. With scripts, the metadata attribute has to go appear before the param statement. (Earlier we said param has to be the first non-comment line. This is still true since the metadata attributes are considered to be part of the param statement. The [CmdletBinding()] attribute is used to added metadata to the function, specifying things like behaviors that apply to all parameters as well as things like the return type of the function. You should notice that the attribute syntax where the attribute names is enclosed in square brackets is similar to the way we specify types. This is because attributes are implemented using .NET types. The important distinction is that an attribute must have parentheses after the name. As we can see in the figure, we can place properties on the attribute in the parentheses. However, even if we're specifying no attributes, the parentheses must still be there so the interpreter can distinguish between a type literal and an attribute. Now let's look at the most important attribute: [CmdletBinding()].

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

276

8.2.2 The [CmdletBinding()] attribute
The [CmdletBinding()] attribute is used to specify properties that apply to the whole function or script. In fact, simply having the attribute in the definition changes how excess parameters are handled. If the function is defined without this attribute, the arguments for which there aren't formal parameters are simply added to the $args variable as shown in the next example. First we define a function that takes two formal parameters. PS (1) > function x {param($a, $b) "a=$a b=$b args=$args"} Now call that function with 4 arguments PS (2) > x 1 2 3 4 a=1 b=2 args=3 4 and we see that the excess arguments end up in $args. As discussed earlier, while this can be useful, much of the time we'll want an error message in too many arguments are provided. We can check for this case ourselves and see if $args.Count is greater than zero however it's easier to handle this declaratively by adding the metadata attribute as shown in this example. PS (3) > function x {[CmdletBinding()] param($a, $b) >> "a=$a b=$b args=$args"} >> Now when we run the command with extra arguments PS (4) > x 1 2 3 4 x : A positional parameter cannot be found that accepts argument '3'. At line:1 char:2 + x > { >> [CmdletBinding(SupportsShouldProcess=$True)] param( >> [parameter(mandatory=$true)] [regex] $pattern >> ) >> foreach ($process in Get-WmiObject Win32_Process | >> where { $_.Name -match $pattern }) >> { >> if ($PSCmdlet.ShouldProcess( >> "process $($process.Name) " + >> " (id: $($process.ProcessId))" , >> "Stop Process")) >> { >> $process.Terminate() >> } >> } >> } >> ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

279 Next we'll start a notepad process to terminate. PS (2) > notepad Now call Stop-ProcessUsingWMI, specifying the -WhatIf parameter PS (3) > Stop-ProcessUsingWMI notepad -Whatif What if: Performing operation "Stop Process" on Target "proc ess notepad.exe (id: 6748)". and we see a description of the operation that would be performed. The -WhatIf option was only supposed to show what it would have done, but not actually do it so we'll use Get-

Process to verify that the command is still running.
PS (4) > get-process notepad | ft name,id -auto Name Id ----notepad 6748 and it is. Let's perform the operation again but this time we'll use the -Confirm flag. This requests that we be prompted for confirmation before executing the operation. When we get the prompt, we'll respond "y" for yes - continue with the operation. PS (5) > Stop-ProcessUsingWMI notepad -Confirm Confirm Are you sure you want to perform this action? Performing operation "Stop Process" on Target "process notepad.exe (id: 6748)". [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend[?] Help (default is "Y"): y __GENUS __CLASS __SUPERCLASS __DYNASTY __RELPATH __PROPERTY_COUNT __DERIVATION __SERVER __NAMESPACE __PATH ReturnValue : : : : : : : : : : : 2 __PARAMETERS __PARAMETERS 1 {}

0

And the operation was performed. Use Get-Process to confirm that the notepad process no longer exists. PS (6) > get-process notepad | ft name,id -auto Get-Process : Cannot find a process with the name "notepad" . Verify the process name and call the cmdlet again. At line:1 char:12 + get-process function Test-OutputType >> { >> [CmdletBinding(DefaultParameterSetName = "1nt")] >> [OutputType("asInt", [int])] >> [OutputType("asString", [string])] >> [OutputType("asDouble", ([double], [single]))] >> [OutputType("lie", [int])] >> param ( >> [parameter(ParameterSetName="asInt")] [switch] $asInt, >> [parameter(ParameterSetName="asString")] [switch] $asString, >> [parameter(ParameterSetName="asDouble")] [switch] $asDouble, >> [parameter(ParameterSetName="lie")] [switch] $lie >> ) >> Write-Host "Parameter set: $($PSCmdlet.ParameterSetName)" >> switch ($PSCmdlet.ParameterSetName) { >> "asInt" { 1 ; break } >> "asString" { "1" ; break } >> "asDouble" { 1.0 ; break } >> "lie" { "Hello there"; break } } >> } >> Now let's try out each of the different switches.
PS (2) > (Test-OutputType -asString).GetType().FullName Parameter set: asString System.String PS (3) > (Test-OutputType -asInt).GetType().FullName Parameter set: asInt System.Int32 PS (4) > (Test-OutputType -asDouble).GetType().FullName Parameter set: asDouble System.Double Ok - everything is as expected - in each case the correct type was returned. Now let's use the

-lie parameter.
PS (5) > (Test-OutputType -lie).GetType().FullName Parameter set: lie System.String Even though we specified the OutputType to be [int] the function returned a string. As we said - the attribute is only documentation - it doesn't actually enforce the type. So - if it's just documentation, then at least there must be an easy way to get at it correct? Well, unfortunately that's not the case either. This is another feature that was added right at the end of the ship cycle. As a consequence, there is no real interface to get this information. Instead, we have to look at the compiled attributes directly to see the values. For functions and scripts, we can retrieve this information from the scriptblock the defines the function body. This looks like: PS (6) > (Get-Command Test-OutputType).ScriptBlock.Attributes | >> Select-Object typeid, type | ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

282 >> >> Format-List

TypeId : System.Management.Automation.CmdletBindingAttribute type : TypeId : System.Management.Automation.OutputTypeAttribute Type : {asInt, int} TypeId : System.Management.Automation.OutputTypeAttribute Type : {asString, string} TypeId : System.Management.Automation.OutputTypeAttribute Type : {asDouble, System.Double System.Single} TypeId : System.Management.Automation.OutputTypeAttribute Type : {lie, int} and for cmdlets, it's even less friendly because we have to go through the .NET type that was used to define the cmdlet. Here's what that looks like: PS (9) > $ct = (Get-Command Get-Command).ImplementingType PS (10) > $ct.GetCustomAttributes($true) | >> Select-Object typeid, type | >> Format-List >> TypeId : System.Management.Automation.CmdletAttribute type : TypeId : System.Management.Automation.OutputTypeAttribute Type : {System.Management.Automation.AliasInfo, System.Management.A utomation.ApplicationInfo, System.Management.Automation.Func tionInfo, System.Management.Automation.CmdletInfo...} At this point, you might be saying - why bother to specify this? The answer is that good scripts will last beyond any individual release of PowerShell. This information is somewhat useful now and will probably be much more useful in the future. As a best practice, it is strongly recommended that this information be included in scripts that you want to share with others. Something we skipped over in the [OutputType()] example was the [Parameter()] attribute. We used it but didn't actually talk about what it does. We'll remedy this in the next section.

8.2.3 Specifying parameter attributes
We specify additional information on parameters using the [Parameter()] attribute. This information is used to control how the parameter is processed. The attribute is placed before the parameter definition as shown in figure 8.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

283
Attribute specifying parameter metadata Property indicating this parameter is required Parameter set this parameter belongs to [Parameter(Mandatory=$true, Can take argument Position=0, from a property on ParameterSetName=”set1", the object coming ValueFromPipeline=$false, from from the ValueFromPipelineByPropertyName=$true, pipeline ValueFromRemainingArguments=$false, HelpMessage=”some help this is”)] [int] $p1 = 0

Parameter is positional, ocupying position 1 in parameter set “set1” Can’t take argument from the pipeline as is Won’t consume remaining unbound arguments

Parameter is constrained to be an integer.

The parameter’s name is “p1” and it is initialized to 0

FIgure 8.6 This figure shows how the [Parameter()] attribute is used with a parameter. It also shows all of the properties that can be specified to this attribute.

As was the case with the [CmdletBinding()] attribute, specific behaviors are controlled through a set of properties provides as "arguments" to the attribute. While the figure shows all of the properties that can be specified, you only have to provide the ones you want to set to something other than the default value. Let's look at an example first, then go through each of the properties. The following example shows a parameter declaration that defines a -Path parameter. We want the parameter to have the following characteristics:    It is mandatory, that is, the user must specify it or there is an error. It takes input from the pipeline. It requires it's argument to be convertible to an array of strings.

The parameter declaration needed to do all of this looks like: param ( [parameter(Mandatory=$true, ValueFromPipeline=$true)] [string[]] $Parameter ) The result is fairly simple as most of the properties can just use their default values. In the next few sections, we'll look at each of the properties, what it does and how it can be used. THE "MANDATORY" PROPERTY By default, all function and script parameters are optional which means that the caller of the command doesn't need to specify them. If we want to require that the parameter be specified, then we set the Mandatory property in the [Parameter()] attribute to $true otherwise, If the property is absent or set to $false, the parameter is optional. The following example shows the declaration of a parameter that is required when the function is run. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

284 PS (1) > function Test-Mandatory >> { >> param ( [Parameter(Mandatory=$true)] $myParam) >> $myParam >> } >> Now let's run this function without a parameter PS (2) > Test-Mandatory cmdlet Test-Mandatory at command pipeline position 1 Supply values for the following parameters: myParam: HELLO THERE HELLO THERE PS (3) > The PowerShell runtime notices that a mandatory parameter wasn't specified on the command line so it prompts the user specify it, which we do. Now the function can run to completion. THE "POSITION" PROPERTY We saw earlier in this chapter that all parameters are both positional and named by default. When using advanced parameter specification metadata, either adding the

[CmdletBinding()] attribute to the whole function or specifying an individual [Parameter()] attribute, parameters remain positional by default, until we specify a position for at least one of them. Once we start formally specifying positions, all parameters default to non-positional unless the Position property for that parameter is set. The following example shows PS >> >> >> >> >> >> >> >> a function with two parameters, neither one having Position set. (1) > function Test-Position { param ( [parameter()] $p1 = 'p1 unset', $p2 = 'p2 unset' ) "p1 = '$p1' p2='$p2'" }

Now when we run it with positional parameters, PS (2) > Test-Position one two p1 = 'one' p2='two' the arguments are bound by position and there is no error. Let's add a position specification to one of PS >> >> >> >> >> >> >> >> the parameters. Now the function looks like: (3) > function Test-Position { param ( [parameter(Position=0)] $p1 = 'p1 unset', $p2 = 'p2 unset' ) "p1 = '$p1' p2='$p2'" }

We'll run it again with two positional parameters PS (4) > Test-Position one two Test-Position : A positional parameter cannot be found that accepts argument 'two'. At line:1 char:14 + Test-Position function Test-ParameterSets >> { >> param ( >> [parameter(ParameterSetName="s1")] $p1='p1 unset', >> [parameter(ParameterSetName="s2")] $p2='p2 unset', >> [parameter(ParameterSetName="s1")] >> [parameter(ParameterSetName="s2",Mandatory=$true)] >> $p3='p3 unset', >> $p4='p4 unset' >> ) >> "Parameter set = " + $PSCmdlet.ParameterSetName >> "p1=$p1 p2=$p2 p3=$p3 p4=$p4" >> } >> Let's try it out. First we'll call our function specifying -p1 and -p4. PS (2) > Test-ParameterSets -p1 one -p4 four Parameter set = s1 p1=one p2= p3=p3 unset p4=four and the parameter binder resolves to parameter set "s1" where the -p3 parameter is not mandatory. Next we'll specify -p1, -p3 and -p4.

PS (3) > Test-ParameterSets -p1 one -p3 three -p4 four Parameter set = s1 p1=one p2= p3=three p4=four We still resolve to parameter set "s1" but this time -p3 is bound. Now we'll look at the other parameter set. Because we're specifying -p2 instead of -p1, the second parameter set "s2" is used as we can see in the output. PS (4) > Test-ParameterSets -p2 two -p3 three Parameter set = s2 p1= p2=two p3=three p4=p4 unset Now in parameter set "s2", the parameter -p3 is mandatory. Let's try running the function without specifying it

PS (5) > Test-ParameterSets -p2 two cmdlet Test-ParameterSets at command pipeline position 1
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

286

Supply values for the following parameters: p3: THREE Parameter set = s2 p1= p2=two p3=THREE p4=p4 unset and the runtime will prompt for the missing parameter. We provide the missing value at the prompt and the function completes successfully. Now let's verify that the parameter -p4 is allowed in both parameter sets. We run the following command specifying -p4 PS (6) > Test-ParameterSets -p2 two -p3 three -p4 four Parameter set = s2 p1= p2=two p3=three p4=four which works properly. Now try specifying all four of the parameters in the same cmd. Of course this shouldn't work because -p1 and -p2 are in different parameter sets so the parameter binder can't resolve to a single parameter set. PS (7) > Test-ParameterSets -p1 one -p2 two -p3 three -p4 fo ur Test-ParameterSets : Parameter set cannot be resolved using the specified named parameters. At line:1 char:19 + Test-ParameterSets >> >> >> >> >> it difficult to handle both pipeline and command line bindings The

ValueFromPipeline property greatly simplifies this. In the following example we'll define a parameter $x that can take values from the command line and the pipeline. (1) > function Test-ValueFromPipeline { param([Parameter(ValueFromPipeline = $true)] $x) process { $x } }

Now let's try it with the command line. PS (2) > Test-ValueFromPipeline 123 123 and it works properly. Now try a pipelined value PS (3) > 123 | Test-ValueFromPipeline 123 and this also works properly. And, since we're using the process block, we can handle a collection of values. PS (4) > 1,2,3 | Test-ValueFromPipeline 1 2 3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

287 PS (5) > The ValueFromPipeline property allows us to tell the runtime to bind the entire object to the parameter but sometimes we only want a property on the object. This is what the

ValueFromPipelineByPropertyName attribute is for as we'll see next..
THE "VALUEFROMPIPELINEBYPROPERTYNAME" PROPERTY Where ValueFromPipeline caused the entire pipeline object to be bound to the parameter, the ValueFromPipelineByPropertyName property tells the runtime to just use a property on the object instead of the whole object when binding the parameter. The name of the property to use comes from the parameter name. Let's modify the previous example to illustrate this. PS (19) > function Test-ValueFromPipelineByPropertyName >> { >> param( >> [Parameter(ValueFromPipelineByPropertyName=$true)] >> $DayOfWeek >> ) >> process { $DayOfWeek } >> } >> This function has one parameter named "DayOfWeek" that is bound from the pipeline by property name. Notice that we haven't added a type constraint to this property so any type of value will work. We'll use the Get-Date cmdlet to emit an object with a "DayOfWeek" property: PS (20) > Get-Date | Test-ValueFromPipelineByPropertyName Tuesday which returns "Tuesday". So binding from the pipeline works fine. What happens when we bind from the command line. PS (21) > Test-ValueFromPipelineByPropertyName (Get-Date) Tuesday, June 30, 2009 12:27:56 AM This time we got the entire DateTime object. Normal command line binding isn't affected by the attribute. To get the same result, we have to extract the property ourselves. PS (22) > Test-ValueFromPipelineByPropertyName ` >> ((Get-Date).DayOfWeek) Tuesday That takes care of the single value case. Now let's look at the case where we have multiple coming in. PS (23) > $d = Get-Date PS (24) > $d, $d.AddDays(1), $d.AddDays(2) | >> Test-ValueFromPipelineByPropertyName >> Tuesday Wednesday Thursday PS (25) > Each inbound pipeline object is bound to the parameter by property name one at a time. Next we'll look at how to handle variable numbers of arguments when using command metadata. THE "VALUEFROMREMAININGARGUMENTS: PROPERTY We saw earlier that, when we didn't use any of the metadata annotations, excess arguments ended up in the $args variable. However, once we add the metadata, the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

288 presence of excess arguments results in an error. Since it is useful to allow of a variable number of parameters sometimes, PowerShell provides the

ValueFromRemainingArguments property which tells the runtime to bind all excess arguments to this parameter. The following example shows a function with two parameters. The first argument goes into the -first parameter and the remaining arguments are bound to -rest. PS (1) > function vfraExample >> { >> param ( >> $First, >> [parameter(ValueFromRemainingArguments=$true)] $Rest >> ) >> "First is $first rest is $rest" >> } >> We'll run the fuction with 4 arguments PS (2) > vfraExample 1 2 3 4 First is 1 rest is 2 3 4 and the first ends up in $first with the remaining being placed in $rest. Now let's try using

-rest as a named parameter:
PS (3) > vfraExample 1 -rest 2 3 4 vfraExample : A positional parameter cannot be found that a ccepts argument '3'. At line:1 char:12 + vfraExample helpMessageExample cmdlet helpMessageExample at command pipeline position 1 Supply values for the following parameters: (Type !? for Help.) Path[0]: !? An array of path names. Path[0]: fo Path[1]: bar Path[2]: Path: fo bar PS (3) >) when prompted, we can enter '!?' to see the help message giving us a more information about the type of thing we're supposed to enter. And with that, we're done with the [Parameter()] attribute and its properties however we're not done with parameter attributes quite yet. The next thing to look at is the [Alias()] attribute. This is a pretty simple feature but it has a couple of very important uses. Let's take a look.

8.2.4 Creating Parameter Aliases with the [Alias()] Attribute
The [Alias()] attribute allows us to specify alternate names for a parameter. It's typically used to add a well defined shortcut for that parameter. Here's why this is important. If you'll remember our parameter discussion in chapter 2, we said that you only have to specify enough of a parameter name to uniquely identify it. Unfortunately there's a problem with that approach and that's versioning the command over time. By versioning we mean being able to add new capabilities to future versions of a command in such a way that older scripts using this command aren't broken. If we add a new parameter to a command that has the same prefix as an existing parameter, we now need a longer prefix to distinguish the name. Any scripts that used the old short prefix would fail with because they would be unable to distinguish which parameter to use. This is where the [Alias()] attribute comes in. It can be used to add distinctive and mnemonic short forms for parameters. Let's look at an example. The following function defines a single parameter -ComputerName. We'll give this parameter an alias: -cn. Here's PS >> >> >> >> >> >> >> >> the function definition. (1) > function Test-ParameterAlias { param ( [alias("CN")] $ComputerName ) "The computer name is $ComputerName" }

When we run it using the full parameter name, it works as expected PS (2) > Test-ParameterAlias -computername foo ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

290 The computer name is foo and, of course, it alse works with the alias. PS (3) > Test-ParameterAlias -cn foo The computer name is foo Now we'll try a prefix of the -ComputerName parameter. PS (4) > Test-ParameterAlias -com foo The computer name is foo And it too works fine. Now let's create a new version of the command. We'll add a new parameter -Compare. Here's the new function definition. PS (5) > function Test-ParameterAlias >> { >> param ( >> [alias("CN")] >> $ComputerName, >> [switch] $Compare >> ) >> "The computer name is $ComputerName, compare=$compare " >> } >> Let's try running the command with the parameter prefix -com again. PS (6) > Test-ParameterAlias -com foo Test-ParameterAlias : Parameter cannot be processed because the parameter name 'com' is ambiguous. Possible matches in clude: -ComputerName -Compare. At line:1 char:20 + Test-ParameterAlias function validateCountExample >> { >> param ( >> [int[]] [ValidateCount(2,2)] $pair >> ) >> "pair: $pair" >> } >>

We'll try the function with 1 argument
PS (2) > validateCountExample 1 validateCountExample : Cannot validate argument on paramete r 'pair'. The number of supplied arguments (1) is less than the minimum number of allowed arguments (2). Specify more than 2 arguments and then try the command again. At line:1 char:21 + validateCountExample >> >> >> >> >> >> letter from a-z followed by 1 to 7 digits. (1) > function validatePatternExample { param ( [ValidatePattern('^[a-z][0-9]{1,7}$')] [string] $hostName ) $hostName }

Let's try it with a valid string PS (2) > validatePatternExample b123 b123 and it returns the argument with no error. Now let's try an invalid argument that has too many numbers: PS (3) > validatePatternExample c123456789 validatePatternExample : Cannot validate argument on parame ter 'hostName'. The argument "c123456789" does not match th e "^[a-z][0-9]{1,7}$" pattern. Supply an argument that matc hes "^[a-z][0-9]{1,7}$" and try the command again. At line:1 char:23 + validatePatternExample { >> param ( >> [int[]][ValidateRange(1,10)] $count >> ) >> $count >> } >> As we saw with the [ValidateLength()] attribute for strings, this attribute can be applied to a collection in which case it will validate each member of the collection. PS (2) > validateRangeExample 1 1 PS (3) > validateRangeExample 1,2,3 1 2 3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

295 These two examples return no error because all of the members are within range. Let's try one with a member outside of the range. PS (4) > validateRangeExample 1,2,3,22,4 validateRangeExample : Cannot validate argument on paramete r 'count'. The 22 argument is greater than the maximum allo wed range of 10. Supply an argument that is less than 10 an d then try the command again. At line:1 char:21 + validateRangeExample > { >> param ( >> [ValidateSet("red", "blue", "green")] >> [ConsoleColor] $color >> ) >> $color >> } >> Let's try it with a valid argument PS (6) > validateSetExample red Red and an invalid argument. PS (7) > validateSetExample cyan validateSetExample : Cannot validate argument on parameter 'color'. The argument "Cyan" does not belong to the set "re d,blue,green" specified by the ValidateSet attribute. Suppl y an argument that is in the set and then try the command a gain. At line:1 char:19 + validateSetExample > { >> param ( >> [int] [ValidateScript({$_ % 2 -eq 0})] $number >> ) >> $number >> } >> This succeeds for '2' PS (9) > validateScriptExample 2 2 and fails for '3'. PS (10) > validateScriptExample 3 validateScriptExample : Cannot validate argument on paramet er 'number'. The "$_ % 2 -eq 0" validation script for the a rgument with value "3" did not return true. Determine why t he validation script failed and then try the command again. At line:1 char:22 + validateScriptExample function abc ([int] $x, $y) >> { >> #.SYNOPSIS >> #This is my abc function >> } >> Now run the Get-Help command again. The last time, we got a very basic synopsis. Let's see what we get this time: PS (2) > get-help abc NAME abc SYNOPSIS This is my abc function SYNTAX abc [[-x] ] [[-y] ] [] DESCRIPTION RELATED LINKS REMARKS To see the examples, type: "get-help abc -examples". For more information, type: "get-help abc -detailed". For technical information, type: "get-help abc -full". Now we get much more output. Of course much of its empty - we haven't specified all of the sections yet. We can guess pretty easily what some of the sections should be. to work with. Let's add a description for the function. This time we'll use the block comments notation - it's much easier

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

303

AUTHOR'S NOTE
In fact, block comments were specifically added to make it easier to write doc comments. The sequences were chosen, in part because they look which is used for external help files. somewhat like XML

Here's PS >> >> >> >> >> >> >> >> >> >>

what the new function definition looks like: (3) > function abc ([int] $x, $y) { }

and when we run Get-Help we see PS (4) > get-help abc NAME abc SYNOPSIS This is my abc function SYNTAX abc [[-x] ] [[-y] ] [] DESCRIPTION This function is used to demonstrate writing doc comments for a function. RELATED LINKS REMARKS To see the examples, type: "get-help abc -examples". For more information, type: "get-help abc -detailed". For technical information, type: "get-help abc -full". with the description text in its proper place. The basic pattern should be pretty obvious by now. Each help section begins with a special tag of the form '.TAGNAME' followed by the content for that section. The tag must appear on a line by itself to be recognized as a tag but can be preceeded or followed by whitespace. The order in which tags appear doesn't matter. Tags are not case-sensitive however by convention, they're always written in uppercase. (This makes the structure of the comment easier to follow.) For a comment block to be processed as a doc comment, it must contain at least one section tag. Most tags can only be specified once per function definition however there are some exceptions. For example, .EXAMPLE, can appear many times in the same comment block. The Help content for each tag begins on the line after the tag and can span multiple lines. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

304

8.4.4 Tags used in documentation comments
There are a fairly large number of tags that can be used when creating documentations comments. These tags are shown in table 8.2. They're listed in the order in which they typically appear in output of Get-Help.

Table 8.2 This table lists all of the tags that can be used in documentation comments with a brief description.
Tag Name .SYNOPSIS Description of Tag Content A brief description of the function or script. This tag can be used only once in each help topic. A detailed description of the function or script. The description of a parameter. An example showing how to use a command. The type of object that can be piped into a command. The types of objects that the command returns. Additional information about the function or script. The name of a related topic. The technology or feature that the command is associated with. The user role for this command. The intended use of the function. Redirects to the help topic for the specified command. Specifies the help category of the item in FORWARDHELPTARGETNAME tag. .REMOTEHELPRUNSPACE Specifies the path to an external help file for the command.

.DESCRIPTION .PARAMETER .EXAMPLE .INPUTS .OUTPUTS .NOTES .LINK .COMPONENT .ROLE .FUNCTIONALITY .FORWARDHELPTARGETNAME .FORWARDHELPCATEGORY

.REMOTEHELPRUNSPACE .EXTERNALHELP

Some of these tags require a bit more explanation. This is addressed in the following sections. .PARAMETER HELP TAG This is where we add the description for a parameter. The parameter must be named in the argument to the tag. We can include a .Parameter tag for each parameter in the function or script and the .Parameter tags can appear in any order in the comment block. The order in which things are presented is controlled by the parameter definition order, not the help tag order. If we want to change the display order, we have to change the order in which the parameters are defined.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

305 Alternatively, we can specify a parameter description simply by placing a comment before the parameter definition on the body of the function or script. the syntax comment is ignored. .LINK HELP TAG The link tag lets you specify the names of one or more related topics. Repeat this tag for each related topic. The resulting content appears in the Related Links section of the help topic. The .Link tag argument can also include a Uniform Resource Identifier (URI) to an online version of the same Help topic. The online version opens when you use the -Online parameter of Get-Help. The URI must begin with "http" or "https". .COMPONENT HELP TAG This tag describes the technology or feature area that the function or script that the function is associated with. For example, the component for Get-Mailbox would be If you use both a syntax comment and a Parameter tag, the description associated with the Parameter tag is used, and

Exchange.
.FORWARDHELPTARGETNAME HELP TAG Redirects to the help topic for the specified command. You can redirect users to any Help topic, including help topics for a function, script, cmdlet, or provider. .FORWARDHELPCATEGORY HELP TAG This tag specifies the Help category of the item in ForwardHelpTargetName. Valid values are

Alias, Cmdlet, HelpFile, Function, Provider, General, FAQ, Glossary, ScriptCommand, ExternalScript, Filter, or All. You should use this tag to avoid

conflicts when there are commands with the same name. .REMOTEHELPRUNSPACE HELP TAG This tag won't really make sense until we cover remoting in chapter 11. It's used to specifie a session that contains the help topic. The argument to the tag is the name of a variable that contains the PSSession to use. This tag is used by the Export-PSSession cmdlet to find the Help topics for the exported commands. .EXTERNALHELP The .EXTERNALHELP tag specifies the path to an XML-based help file for the script or function. In versions of Windows from Vista onwards, if the specified path to the XML file contains UI-culture-specific subdirectories, Get-Help searches the subdirectories recursively for an XML file with the name of the script or function in accordance with the language fallback standards for Windows Vista, just as it does for all other XML-based help topics. See Appendix D for additional information about UI culture, message catalogs and world-ready scripting. And - at long last - we're done our journey through the advanced function and script features. We now know how create, declare, constrain and document our functions. this chapter. At this point, we're well on our way to becoming scripting experts. Let's review what we covered in

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

306

8.5 Summary
This chapter finally introduced scripting and programming in general in PowerShell. It also covered the advanced scripting features introduced in PowerShell version 2. While there was a lot of material, the following are the key points:  PowerShell programming can be done either with functions or scripts. Scripts are simply pieces of PowerShell script text stored in a file with a .ps1 extension. In PowerShell, scripts and functions are very closely related and most of the same principles and techniques apply to both. There are a few exceptions of course. In scripts, only the param keyword can be used to declare formal parameters.   Scripts introduced a new kind of variable scope - the script come and an new

$script: scope modifier is used to reference variables at the script scope.
In Version 2, PowerShell introduced a sophisticated attribution system for annotating parameters. Using attributes, a wide variety of argument binding behaviors can be controlled. The script author can also specify alternate names for parameters using parameter aliases and additional constraints on the values that can be bound using validation attributes. Version 2 of PowerShell also introduced comprehensive new mechanisms for documenting your scripts and functions. We get simple documentation for free just by declaring a function. We can add documentation in-line with our code using doc comments and finally we can provide external help files containing the documentation.



Even though we covered a lot of material in this chapter, we’ve really only covered part of the story around programming with PowerShell. In chapter 9, we’ll introduce modules, which are new in PowerShell version 2 and then, in chapter 10, we'll dive into the plumbing underlying all of this when we cover scriptblocks, which are the objects underlying the infrastructure for scripts and functions.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

307

9
Using and Authoring Modules

"The value of a telecommunications network is proportional to the square of the number of connected users of the system." Robert Metcalfe (Metcalfe’s Law)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

308 A popular meme in the software industry is that the best programmers are lazy. The idea is that, rather than writing new code to solve a problem, a good programmer will try and reuse existing code thereby leveraging the work that others have already done to debug and document that code. Unfortunately this kind of reuse happens less often than it should. The typical excuses for not doing this are overconfidence (I can do a better job also known as the "Not-Invented-Here" syndrome), underestimating (it will only take me 10 minutes to do that) and ignorance (I didn’t know somebody had already implemented that.) There is really no excuse for this last point any more. With modern search engines, it's usually pretty easy to find things - that is - if you bother to look. However finding the code is only part of the solution because the code has to be in a form that can be reused. The most common way to facilitate code reuse is to package the code in a module. In this chapter, we're going to examine how PowerShell V2 facilitates code reuse with its module system. We'll look at how to find existing modules on your system and how to install new modules on the system. Then we'll look at how you can create modules and package your code so that others can use it.

AUTHOR'S NOTE
From user studies, we've verified that the most common reuse pattern in the IT Professional community is copy and paste. This is not surprising given that, for languages like

VBScript and cmd.exe, it's pretty much the only reuse pattern. A user gets a script from
"somewhere", copies and then modifies it, repeating this processes for each new application. While this works to a degree and has a low barrier to entry, it doesn’t scale very well.

9.1 The role of a module system
In this section, we'll look at the intent behind PowerShell modules and the roles that they play in our ecosystem. Before we get into the details of the way modules work in PowerShell, let's look at an example of just how successful a module system can be. The preeminent example of a network of community-created modules is Perl's CPAN. CPAN stands for the "Comprehensive Perl Archive Network" and is an enormous collection of reusable scripts and modules, created by the community that can be easily and effectively searched for exiting solutions. This repository can explored at http:/www.cpan.org and currently claims the following statistics: 2009-06-21 online since 1995-10-26 5600 MB 230 mirrors 7451 authors 15999 modules This is an incredible (and enviable) resource for Perl developers. Clearly such a thing would be of tremendous value to the PowerShell. With the introduction of modules in PowerShell version 2, we're taking the first important steps to get this going.

9.1.1 An overview of PowerShell Modules
In the previous chapter we organized our code into functions and scripts and used dotsourcing to load "libraries" of reusable script code. This is the traditional shell-language approach to code reuse. PowerShell modules provide a more manageable, production-oriented ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

309 way to package code. and, as is the usual case with PowerShell, modules build on features we've already learned. For example, a PowerShell script module is simply a PowerShell script with a special extension (.psm1) loaded in a special way. We'll cover all of these details in later sections but first, we need to understand the problem domains the PowerShell module system was designed to address. MODULE ROLES IN POWERSHELL Modules serve three roles in PowerShell. These roles are listed in table 9.1

Table 9.1 The roles modules play in PowerShell
Role Configuring the environment Description The first role is configuration – packaging a set of functions to configure the environment. This is what you usually use dotsourcing for but modules allow you to do this in a more controlled way. The second major role for modules is to facilitate the creation of reusable libraries. This is the traditional role of modules in a programming language. The final role is the most unusual one. Modules can be used to create solutions – essentially a domain-specific application. PowerShell modules have the unique characteristic of being nested. In most programming languages, when one module loads another, all of the loaded modules are globally visible. In PowerShell, modules nest. If the user loads module A and module A loads module B, then all the user sees is Module A (at least by default.) In fact sometimes all you’ll do in a module is import some other modules and republish a subset of those modules members.

Code reuse

Composing solutions

The concepts involved in the first role - configuration - were covered when we talked about dot-sourcing files. The second role - facilitating reuse - is, as we said, the traditional role for modules. The third role however, is unique to PowerShell. Let's look at this third role in a bit more detail. MODULE MASH-UPS: COMPOSING AN APPLICATION One of the features that PowerShell modules offer that is unique is the idea of a composite management application. This is conceptually similar to the idea of "Web Mash-Ups" that are (or were) so popular. A mash-up takes an existing service and "tweaks" or layers on top of it to achieve some other more specific purpose. The notion of management mashups is important as we move into the era of "Software+Services" (or "Clients+Clouds" if you prefer). Hosted services allow of economies of scale around operating costs that make them very attractive. The problem is how do you manage all these services, especially when you need to delegate administration responsibility to some slice of the organization. For example, you might have ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

310 each department manage its own user resources - maiboxes, customer lists, web portals, etc. To do this, we need to slice the management interfaces and republish them as a single coherent management experience. Sounds like magic doesn't it? Well - much of it still is but PowerShell modules can help because they allow you to merge the interfaces of several modules and republish only those parts of the interfaces that need to be exposed. The individual modules that are being composed are hidden from the user so components can be swapped out as needed without necessarily impacting the end-user. This magic is accomplished through module manifests and nested modules. We'll cover nested modules in this chapter but manifests are a large enough topic that they get their own chapter (chapter 10). Now that we understand why we want modules, let's look at how to use them in PowerShell.

9.2 Module Basics
In this section, we'll cover the basic information needed to use PowerShell modules. The first thing to know is that the module features in PowerShell are exposed through cmdlets, not language keywords. For example, we can get a list of the module commands using the Get-

Command command as follows:
PS {1) > get-command -type cmdlet *-*module* | select name Name ---Export-ModuleMember Get-Module Import-Module New-Module New-ModuleManifest Remove-Module Test-ModuleManifest Note that in the command name pattern, we used wildcards because there are a couple of different types of module cmdlets. These cmdlets and their descriptions are shown in table 9.2.

Table 9.2 The cmdlets used for working with modules.
Module CmdLet Get-Module pattern Import-Module Description Gets a list of the modules currently loaded in memory. Loads a module into memory and imports the public commands from that module. Removes a module from memory and removes the imported members. Used to specify the members of a module to export to the user of the module. Used to create a new metadata file for a module directory. Run a series of tests on a module manifest validating its contents.

Remove-Module

Export-ModuleMember

New-ModuleManifest Test-ModuleManifest

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

311 New-Module Create a new dynamic module.

We can also use the Get-Help command to search for a list of all of the help topics for modules available in the on-line help. PS (2) > get-help *module* | select name Name ---New-Module Import-Module Export-ModuleMember Get-Module Remove-Module New-ModuleManifest Test-ModuleManifest about_modules As expected, we see all of the cmdlets listed but there is also an about_modules help topic that describes modules and how they work in PowerShell. You can use this on-line help as a quick reference when working in a PowerShell session.

9.2.1 Module concepts and terminology
Before we get too far into modules, there are a number of concepts and definitions we should cover. Along with the names of the cmdlets, table 9.2 introduced some new terms - "module member" and "module manifest", as well as reintroducing a couple of familiar terms import and export used in the context of modules. These terms and their definitions are shown in table 9.3.

Table 9.3 A glossary of module terminology
Term Module Member Description A module member is any function, variable or alias defined inside a script. Modules can control which members are visible outside the module by using the Export-ModuleMember cmdlet. A module manifest is a PowerShell data file that contains information about the module and controls how the module gets loaded. The type of the module. Just as PowerShell commands can be implemented by different mechanisms like functions and cmdlets, so modules also have a variety of implementation types. PowerShell has three module types - script, binary and manifest modules. One module can load another, either procedurally by calling Import-Module or by adding the desired module to the NestedModules element in the module manifest for that module. The root module is the main module file loaded when a module is imported. It's

Module Manifest

Module Type

Nested Module

Root Module

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

312 called the root module because it may have associated nested modules. Imported Member An imported module member is a function, variable or alias imported from another module. An exported member is a module member that has been marked for export. In other words, it's marked to be visible to the caller when the module is imported. Of course if module foo imports module bar as a nested member, the exported members of bar become the imported members in foo.

Exported Member

We'll talk more about these concepts in the rest of this chapter. Now let's introduce another core module concept. MODULES ARE SINGLE INSTANCE OBJECTS An important characteristic of modules is that there is only ever one instance of the module in memory. If a second request is make to load the module, the fact that the module is already loaded will be caught and the module will not be reprocessed (at least , as long as the module versions match. Module versions are covered in chapter 10). There are a couple of reasons for this behavior. Modules can depend on other modules so an application may end up referencing a module multiple times and we don't want to be reloading all the time because it slows things down. The other reason is that we want to allow for private static resources - bits of data that are reused by the functions exported from a module and aren't discarded when those functions as is normally the case. For example, say we have a module that establishes a connection to a remote computer when the module is loaded. This connection will be used by all of the functions exported from that module. If the functions had to reestablish the connection every time they were called would be extremely inefficient. By storing the connection in the module, it will persist across the function calls. Let's recap: in this section, we presented a glossary of module-specific terms then discussed how modules are single instance. In the next section we'll start using modules and the module cmdlets. We'll look at how we go about finding, loading and using modules.

9.3 Working with Modules
Let's start working with PowerShell modules. We'll begin by seeing which modules are loaded in our session, which modules are available for loading, learning how to load additional modules and finally how to unload them. (We'll leave actually creating a module until section 9.4). Let's get started by finding out we have loaded in out session.

9.3.1 Finding Modules on the system
The Get-Module cmdlet is used to find modules - either the modules that are currently loaded or the modules that are available to load. Running Get-Module with no parameters will show you all of the top-level modules that have been loaded. Let's try it out: PS {1) > get-module PS {2) > Ok - that wasn't very exciting. By default, PowerShell doesn't load any modules into a session. (Even in PowerShell V2, the system cmdlets are still loaded using the version 1 snapin commands for compatibility reasons - more on this later.) Since we have nothing in memory ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

313 to look at, let's see what's available for loading on the system. The -List parameter on GetModule lets us do this. Let's use this to find the system modules that are available. (We're filtering the output with Where-Object so we don't pick up non-system modules that some of us might be installed.) PS {3) > get-module -list | where { $_.path -match "system32" } ModuleType ---------Manifest Manifest PS {4) > And we see 2 modules listed. Of course this may vary depending on what features are installed on the computer. By default, the output only shows the module name, the module type and the exported commands. Since PowerShell is a dynamic language, the set of exported commands can't be known until the module has been loaded. This is why the list is empty - we haven't loaded them yet. Name ---FileTransfer PSDiagnostics ExportedCommands ---------------{} {}

AUTHOR'S NOTE
Yeah - ok - sure. We could have implemented modules so that we could statically determine the set of exported members. We thought about it but we didn't have time. Next release. I hope. Maybe. But no promises - ok?

In fact the set of properties for a module is much larger than what we saw in the default. Let's look at the full set of properties for the PSDiagnostics module. PS {1) > get-module -list psdiag* | fl : PSDiagnostics : C:\Windows\system32\WindowsPowerShell\v1.0\ Modules\PSDiagnostics\PSDiagnostics.psd1 Description : Windows PowerShell Diagnostic Utilities Mod ule ModuleType : Manifest Version : 1.0.0.0 NestedModules : {} ExportedFunctions : {} ExportedCmdlets : {} ExportedVariables : {} ExportedAliases : {} In this output we see the various types of module members that can be exported: functions, cmdlets, variables and aliases. We also see the module type (Manifest), the module version and a description. An important property to note is the Path property. This is path to where the module file lives on disk. Name Path

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

314

Get-Module lists all of the modules loaded in the current session. If -All is specified, both explicitly loaded and nested modules will be shown.
Get-Module [[-Name] ] [-All]

If -ListAvailable is specified, Get-Module lists all of the modules available to be loaded based on the current $ENV:ModulePath setting. If -All is specified, the contents of the module directories will be shown, including subdirectories. Get-Module [[-Name] ] [-All] [-ListAvailable]

Figure 9.1 This figure shows the syntax for the Get-Module cmdlet. This cmdlet is used to find modules, either in your session or available to be loaded.

In the next section, we'll see how PowerShell goes about finding modules on the system. THE $ENV:PSMODULEPATH VARIABLE As we saw in the output from a Get-Module, loadable modules are identified by their path just like executables. In fact, they are loaded in much the same way as executables - a list of directories is searched until a matching module is found. There are a however. Instead of using $ENV:PATH, modules are loaded using searched for subdirectories containing module files. THE MODULE SEARCH ALGORITHM The following algorithm, expressed in pseudocode (part code, part English description), is used for locating a module: if (the module is an absolute path) { if (the specified module name has a known extension) { join the path element and module name if (the composite path exists) { load the module and stop looking } else { continue with the next path element } } else ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 couple of differences a new environment

$ENV:PSModulePath. And, where the execution path is searched for files, the module path is
This arrangement allows a module to include more than one file. In the next section, we'll cover how the search algorithm in detail.

Licensed to Andrew M. Tearle

315 { foreach ($extension in ".psd1",".psm1", ".dll" { join the path element, module name and extension if (the composite path exists) { load the module and stop looking } else { continue with the next path element } } } } foreach ($pathElement in $ENV:PSModulePath) { if (the specified module name has a known extension) { Join the path element and module base name and name to create a new path if (the composite path exists) { load the module and stop looking } else { continue with the next path element } } else { foreach ($extension in ".psd1",".psm1", ".dll" { Join the path element and module base name, module name and extension. if (the composite path exists) { load the module and stop looking } else { continue with the next path element } } } } if (no module was found) { generate an error } As we can see from the number of steps in the algorithm, the search mechanism used in loading a module is pretty sophisticated but also a bit complicated. Later on we'll look at ways to get more information about what's being loaded. Now that we know what modules are available, we need to be able to load them. We'll look how to do this in the next section. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

316

9.3.2 Loading a module
Modules are loaded using the Import-Module cmdlet. The syntax for this cmdlet is shown in figure 9.3. As we see, this cmdlet has a lot of parameters allowing it to address a wide variety of scenarios. We'll look at the basic features of this cmdlet in this section and cover some of the more obscure features in later sections of this chapter.

The Import-Module syntax which specifies the name (or path) of the module to load and which members to import from the loaded module. Other options allow a prefix for the command noun to be specified, to display verbose information about the loading process and so on. Import-Module [-Name] [-Global] [-Prefix ] [-Function ] [-Cmdlet ] [-Variable ] [-Alias ] [-Force] [-PassThru] [-AsCustomObject] [-Version ] [-ArgumentList ] [-DisableNameChecking] [-Verbose]

As well as from a path, Modules can be imported from a loaded assembly or an existing module info object. All of the other options can also be used in this case. Import-Module [-Assembly] ...

Import-Module [-ModuleInfo] ...

Figure 9.2 This figure shows the syntax for the Import-Module cmdlet. This cmdlet is used to import modules into the current session context or the global context if -Global is specified.

Let's see how this cmdlet works. LOADING A MODULE BY NAME The most common way to load a module is to specify its name. We saw how to find modules using the Get-Module cmdlet in the last section. One of the modules we discovered was the PSDiagnostics module. Let's use Import-Module to load this cmdlet now. PS (1) > import-module psdiagnostics By default, nothing is output when you load a module. This is as expected and desirable because when we're loading library modules in scripts or in our profile, we don't want chattiness. Unless there is an error, loading a module should be silent . When we do what to see what was loaded, we'll use Get-Module: PS (2) > get-module ModuleType Name ---------- ---Script psdiagnostics ExportedCommands ---------------{Enable-PSTrace, Enable-...

This shows that we have one module loaded named PSDiagnostics. This output is substantially abbreviated when displayed as a table so let's use Format-List to see the details of the loaded modules just like we did when we were exploring the on-disk modules. PS (3) > get-module | fl ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

317

: psdiagnostics : C:\Windows\system32\WindowsPowerShell\v1.0\M odules\psdiagnostics\PSDiagnostics.psm1 Description : ModuleType : Script Version : 1.0.0.0 NestedModules : {} ExportedFunctions : {Disable-PSTrace, Disable-PSWSManCombinedTra ce, Disable-WSManTrace, Enable-PSTrace...} ExportedCmdlets : {} ExportedVariables : {} ExportedAliases : {} Let's examine this output for a minute. The most obvious thing to notice is that the

Name Path

ExportedFunctions member in the output is no longer empty. When we load a module into memory, we can finally see all of the available exported members. The other thing to notice is that the module type has been changed from "Manifest" to "Script". Again, the details of the implementation of the module aren't known until after the module has been loaded. We'll cover module manifests and the details on module types at length in chapter 10. To see what commands were actually imported, we can use Get-Command with the -

Module option.
PS (5) > get-command -module psdiagnostics CommandType ----------Function Function Function Function Function Function Function Function Function Function Name ---Disable-PSTrace Disable-PSWSManCombin... Disable-WSManTrace Enable-PSTrace Enable-PSWSManCombine... Enable-WSManTrace Get-LogProperties Set-LogProperties Start-Trace Stop-Trace Definition ---------... ... ... ... ... ... ... ... ... ...

This list matches the list of exports from the module as we can see with Get-Module. PS (6) > (get-module psdiag*).exportedfunctions Key --Disable-PSTrace Disable-PSWSManCombinedTrace Disable-WSManTrace Enable-PSTrace Enable-PSWSManCombinedTrace Enable-WSManTrace Get-LogProperties Set-LogProperties Start-Trace Stop-Trace Value ----Disable-PSTrace Disable-PSWSManCombinedTrace Disable-WSManTrace Enable-PSTrace Enable-PSWSManCombinedTrace Enable-WSManTrace Get-LogProperties Set-LogProperties Start-Trace Stop-Trace

Let's remove this module using the Remove-Module cmdlet and look at other ways we can specify which module to load. PS (7) > Remove-Module PSDiagnostics ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

318 Again the command completes with no output. As well as loading a module by name, we can also load it by path, again paralleling the way executables work. Let's do this with the PSDiagnostics module. We saw the path in the output the earlier example. Let's use this path to load the module. Since this is a system module, its loaded from the PowerShell install directory. This means that we can use the builtin $PSHOME variable in the path. Let's do this now. PS (8) > import-module $PSHOME/modules/psdiagnostics/psdiagnostics Call Get-Module verify that it has been loaded PS (9) > get-module ModuleType Name ---------- ---Script psdiagnostics and there it is. By loading a module using a full path, we know exactly which file will be processed. This can be useful when developing modules as we will see in section 9.4. Let's remove this module again as we move on to the next example. PS (7) > Remove-Module PSDiagnostics TRACING MODULE LOADS WITH -VERBOSE So far we've allowed the modules to be loaded without actually caring about the details of what's happening. This is fine as long as everything works but remember how complex the module search algorithm was. When we get into more complex scenarios where we are loading multiple modules, it's useful to see what's happening. We can do this by specifying the ExportedCommands ---------------{Enable-PSTrace, Enable-...

Verbose flag. Let's what this looks like:
PS (15) > import-module psdiagnostics -verbose VERBOSE: Loading module from path 'C:\Windows\system32\WindowsPowerShell\v1.0\Modules\psdiagnostic s\psdiagnostics.psd1'. VERBOSE: Loading module from path 'C:\Windows\system32\WindowsPowerShell\v1.0\Modules\psdiagnostic s\PSDiagnostics.psm1'. VERBOSE: Importing function 'Disable-PSTrace'. VERBOSE: Importing function 'Disable-PSWSManCombinedTrace'. VERBOSE: Importing function 'Disable-WSManTrace'. VERBOSE: Importing function 'Enable-PSTrace'. VERBOSE: Importing function 'Enable-PSWSManCombinedTrace'. VERBOSE: Importing function 'Enable-WSManTrace'. VERBOSE: Importing function 'Get-LogProperties'. VERBOSE: Importing function 'Set-LogProperties'. VERBOSE: Importing function 'Start-Trace'. VERBOSE: Importing function 'Stop-Trace'. All of the output that begins with "VERBOSE:" is generated when the verbose flag is specified. It shows two things - the path to the module file and a list all of the members (in this case functions) being imported into our session. This is pretty straightforward with a simple scenario. We'll see that it can become much more complicated when we get to nested modules in section 9.4.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

319 IMPORTS AND EXPORTS So far, we've defaulted to loading everything that a module exports into our session. We don't have to do that and there are cases where we don't want to do it. Importing too many commands clutters up our session and makes it hard to find want we're looking for. To avoid this, we can control what gets imported by using the -Function, -Cmdlet, -Alias and -

Variable parameters on Import-Module. As one would expect, each of these parameters controls a particular type of import from the module. We've seen all of the command types previously as well as PowerShell variables. The PSDiagnostics module only exports functions so we can use that feature to restrict what gets loaded. Say we only wanted to load Enable-

PSTrace. This would look like:
PS (12) > import-module psdiagnostics -verbose -function EnablePSTrace VERBOSE: Loading module from path 'C:\Windows\system32\WindowsPowerShell\v1.0\Modules\psdiagnostic s\psdiagnostics.psd1'. VERBOSE: Loading module from path 'C:\Windows\system32\WindowsPowerShell\v1.0\Modules\psdiagnostic s\PSDiagnostics.psm1'. VERBOSE: Importing function 'Enable-PSTrace'. PS (13) > get-command -module psdiagnostics CommandType ----------Function Name ---Enable-PSTrace Definition ---------...

In the verbose output, we see that only Enable-PSTrace was imported into our session. Now we know how to avoid creating clutter in our session. But what if it's too late and we already have too much stuff loaded? We'll look at how to fix this in the next section.

9.3.3 Removing a loaded module
Because your PowerShell session can be long running there may be times when you want to remove a module. As we saw earlier, this is done with the Remove-Module cmdlet.

AUTHOR'S NOTE
Though typically, the only people who remove modules are either people who are developing the module in question or an application environment that is encapsulating various stages in the process as modules. A typical user rarely needs to remove a module. In fact we almost cut this feature because it turns out to be quite hard to do in a sensible way.

The syntax for Remove-Module is shown in figure 9.3.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

320

The Remove-Module cmdlet will remove the module specifed by the -Name parameter. Wildcards are not supported. If -Force is specified, the module will be removed even if it is marked constant.
Remove-Module [-Name] [-Force] [-Verbose]

If a PSModuleInfo object is passed in, the module reference by that object will be removed from the session. If -Force is specified, the module will be removed even if it is marked constant.
Remove-Module [-ModuleInfo] [-Force] [-Verbose]

Figure 9.3 This figure shows the syntax for the remove module command. Note that this command does not take wildcards.

When a module is removed, all of the modules it loaded as nested modules are also removed from the global module table. This happens even if the module was explicitly loaded at the global level. To try and illustrate how this works, let's take a look at how the module tables are organized in the environment. This organization is shown in figure 9.4.

Global Module Table

Global Environment

Module1

Module2

Module3

Figure 9.4 This figure shows how the module tables are organized. The global module table holds a reference to all loaded modules. Each module in turn has a reference to the modules it has loaded.

First let's talk about the global module table. This is the master table that has references to all of the modules that have been loaded either explicitly or implicitly by another module.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

321 Any time a module is loaded this table is updated. An entry is also made in the environment of the caller. In the figure, modules 1 and 3 are loaded from the global module environment so there are references from the top level module table. Module 1 loads module 2 causing a reference to be added the global module table and the private module table for module 1. Module 2 loads module 3 as a nested module. Since module 1 has already been loaded from the global environment, no new entry is added to the global module table but a private reference is added to the module table for module 2. Now we'll remove module 3 from the global environment. The updated arrangement of modules is shown in figure 9.5.

Global Module Table

Global Environment

Module1

Module2

Module3

Figure 9.5 This figure shows how the module tables are organized after module 3 is removed at the top level. The global module table no longer has a reference to module 3 but the local module table for module 2 still has a link to that object.

Next, we'll update module 3 and reload it at the top level. The final arrangement of modules is shown in figure 9.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

322

Global Module Table Global Environment Module3 (new)

Module1

Module2

Module3 (original)

Figure 9.6 This figure shows how the module tables are organized when the revised module 3 is loaded at the top level. The global module table now has a reference to the new module 3 but the local module table for module 2 still has a link to the original module 3.

In this final arrangements of modules, there are two versions of the Module3 loaded into the same session. While this is extremely complicated, it allows multiple versions of a module to be loaded at the same time in the same session allowing different modules that depend on different versions of a module to work at the same time. This is a pretty pathological scenario but the real world is not always tidy. Eventually we do have to deal things we'd rather ignore so it's good to know how. HOW EXPORTED ELEMENTS ARE REMOVED With an understanding of how modules are removed, we also need to know how the imported members are removed since that's really what's important. There are two different flavors of member removal behavior depending on the type of member we are removing. Functions, aliases and variables have one behavior. Cmdlets imported from binary modules have a slightly different behavior. This is an artifact of the way the members are implemented. Functions, aliases and variables are simply data structures that are dynamically allocated and so can be simply replaced. Cmdlets are backed by .NET classes which can't be unloaded from a session because .NET doesn't allow the assemblies containing these classes to be unloaded. Because of this, the implementation of the cmdlet table depends on hiding or shadowing a command when there is a name collision when importing a name from a module. For the other member types, the current definition of the member is simply replaced. So why does this matter? It doesn't really matter at all until we try and remove a module. If we remove a module that has imported cmdlets causing existing cmdlets to be shadowed, when the module is removed, the previously shadowed cmdlets becomes visible again. However, when we remove removed. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 a module importing colliding functions, aliases or variables, because the old definitions were overridden instead of shadowed, the definitions are simply

Licensed to Andrew M. Tearle

323 Ok - this has gotten bit heavy. Let's move on to something more creative and exciting. In the section 9.4, we'll finally start writing our own modules.

9.4 Writing script modules
In this section we start writing modules instead of just using them. For now, we'll limit our coverage to script modules. This is because script modules are written in the PowerShell language - something we all familiar with by now. In section 9.5, we'll expand our coverage to include binary modules which requires dabbling a bit with C#. In looking at how to write script modules, we'll also be covering how script modules work in more detail. Let's start by explaining what a script module is. A script module is just a file that contains PowerShell script text with a .psm1 extension instead of a .ps1 extension. In other words, a PowerShell script module is just a script with a different extension.

EXECUTION POLICY AND MODULES
Since a script module is a form of script, it obeys execution policy just like a script. So before you can load a script module, you'll need to change the execution policy to be remote signed as a minimum as described in section 8.1.1

Is it really as simple as that? Well - almost. Let's walk through an example where we convert a script into a module and see what changes during the process.

9.4.1 A quick review of scripts
We're going to write a short script to work with in this conversion exercise. This script is indented to implement a simple counter. We get the next number from the counter by calling

Get-Count and we reset the sequence using the Reset-Count command. Here's what the script looks like: PS (STA) (27) > Get-Content counter.ps1 $script:count = 0 $script:increment = 1 function Get-Count { return $script:count += $increment } function Reset-Count { $script:count=0 setIncrement 1 } function setIncrement ($x) { $script:increment = $x } As we can see, this script defines the two functions we mentioned - Get-Count and ResetCount but it also defines a number of other things that aren't part of the specification - a helper function setIncrement and two script-level variables: $count and $increment. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

324 These variables hold the "state" of the counter. Obviously just running the script won’t be very useful as the commands are defined at the script scope and are removed when the script exits. Instead, we'll dot-source the script to load the script members into our environment. PS {2) > . .\counter.ps1 PS {3) > which creates the elements without showing anything (which is what you want a library to do in most cases.) Let's manually verify that we got what we intended: PS {3) > Get-Command *-count CommandType ----------Function Function Name ---Get-Count Reset-Count Definition ---------... ...

The functions are there so let's try them out. We'll start with Get-Count. PS (4) > Get-Count 1 PS (5) > Get-Count 2 Each call to Get-Count returns the next number in the sequence. Now let's use the Reset-

Count command.
PS (6) > Reset-Count and call Get-Count to verify that the count has been reset. PS (7) > Get-Count 1 Ok - great. But what about the other "private" members in the script? Using Get-Command we see that the setIncrement function is also visible. PS (8) > Get-Command setIncrement CommandType ----------Function Name ---setIncrement Definition ---------param($x)...

And we can even call it directly. PS (9) > setIncrement 7 PS (10) > Get-Count 8 PS (11) > Get-Count 15 Since this function was supposed to be a private implementation detail, the fact that it's publically visible this isn't desirable. Likewise, we can also get at the state variables we created. PS (12) > Get-Variable count, increment Name ---count increment Value ----15 7

The problem with this is pretty clear - $count is not a very unique name so the chance of it colliding with a similarly named variable from another script is pretty high. This lack of isolation between scripts makes using dot-sourcing a very fragile way to build libraries.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

325 Finally, let's try and "remove" this script, emulating what we've been doing with RemoveModule. This turns out to be quite complicated. We end up having to write a command that looks like: PS (13) > rm -verbose variable:count, >>> variable:increment,function:Reset-Count, >>> function:Get-Count,function:setIncrement VERBOSE: Performing operation "Remove Item" on "Item: count". VERBOSE: Performing operation "Remove Item" on "Item: increment". VERBOSE: Performing operation "Remove Item" on "Item: Reset-Count". VERBOSE: Performing operation "Remove Item" on "Item: Get-Count". VERBOSE: Performing operation "Remove Item" on "Item: setIncrement". PS (14) > script.

Target Target Target Target Target

This is necessary because there is no implicit grouping of all of the members created by a

AUTHOR'S NOTE
It's not actually true that there is no way to find out which functions came from a particular file. Another change in PowerShell V2 was to attach the path to the file where a function was defined to the scriptblock of the function. For the counter example we've been discussing, the path might look like:

PS (23) > ${function:Get-Count}.File C:\wpia_v2\text\chapter09\code\counter.ps1 PS (24) >
This File property makes it easier to figure out where things came from in your environment when you have to debug it. For example, all of the files that were defined in your profile will have the path to your profile in it, functions that were defined in the system profile will have the system profile path, etc. (We discussed the set of profiles that PowerShell uses in chapter 2.) Of course this only fixes part of the problem - managing functions. It doesn’t deal with variables and aliases.

At this point it's clear that, while it's possible to build libraries using dot-sourcing, there are a number of problems with this approach. Private implementation details "leak" into the public namespace and the members of a dot-sourced script lose any sort of grouping that allows you to manage them as a whole. Let's turn this script into a module and see how that fixes the problem.

9.4.3 Turning a script into a module
Now let's turn the counter script into a module. We do this simply by changing the extension on the module from .ps1 to .psm1 (where the 'm' rather obviously stands for module.) Let's do this: PS (1) > copy .\counter.ps1 .\counter.psm1 -force -verbose VERBOSE: Performing operation "Copy File" on Target "Item: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

326 C:\wpia_v2\text\chapter09\code\counter.ps1 Destination: C:\wpia_v2\text\chapter09\code\counter.psm1". PS (2) > (We're using the force parameter here simply to make the example work all the time.) Now we'll try loading the renamed file. Figure 9.7 shows what you'll probably see when you do this.

Figure 9.7 This figure shows what happens when we try to directly run a module file. The module file is opened up in the editor associated with the .psm1 extension. Modules aren't commands and can't simply be run. They need to be imported using the Import-Module cmdlet.

The module was not run. The default action is open the file in the editor associated with the extension. This is because module files aren't commands and can't just be run. Instead, we us the Import-Module command to load this module: PS {3) > import-module .\counter.psm1 PS {4) > Now that we've finally loaded a module, we can try the Get-Module command and see something useful: PS {5) > get-module ModuleType Name ---------- ---Script counter ExportedCommands ---------------{setIncrement, Get-Coun...

Again we'll use the Format-List cmdlet to see the object in more detail. PS {6) > get-module | fl ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

327 Name Path Description ModuleType Version NestedModules ExportedFunctions ExportedCmdlets ExportedVariables ExportedAliases : : : : : : : : : : counter C:\wpia_v2\text\chapter09\code\counter.psm1 Script 0.0 {} {Get-Count, Reset-Count, setIncrement} {} {} {}

Important things to notice is that the Path property stores the full path to where the module was loaded from. The module type is "script" and the version is 0.0 - the default for a script module. (When we look at manifests in chapter 10, we'll see how to change this.) The most important thing to notice are the export lists. We see all of the functions defined in the script module are being exported but no variables are. To verify this, let's use Get-Command to look for all of the functions defined by the script: PS {7) > Get-Command -module counter CommandType ----------Function Function Function Name ---Get-Count Reset-Count setIncrement Definition ---------... ... param($x)...

We can immediately see one of the benefits of using modules - we can work with sets of related elements as a unit. More on this in a bit. Now that we've loaded the functions, we have to run PS 1 PS 2 PS 3 them to make sure they work: {8) > Get-Count {9) > Get-Count {10) > Get-Count

and, as before, we see that Get-Count returns the next element in the sequence. Now let's check on the variables used by Get-Count - these were a big problem when we dotted the script. PS (14) > $count PS (16) > $increment and neither of them exist. Let's try assigning a value to $count and see if it makes a difference. PS (17) > $count = 100 PS (18) > get-count 4 As desired, it has no effect on Get-Count. Let's try Reset-Count and verify that it works. PS (19) > reset-count PS (20) > get-count 1 and it does. Now let's look at another issue we had to deal with when using script libraries how to remove the imported elements. With modules, you can simply remove the module PS (21) > Remove-Module Get-Count This will remove the module from the session and remove all imported members so if we try to run Get-Count now, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

328 PS (22) > Get-Count The term 'Get-Count' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling o f the name, or if a path was included, verify that the path is c orrect and try again. At line:1 char:10 + Get-Count Import-Module .\counter1.psm1 We verify that the count and reset commands are there PS (3) > Get-Command *-Count CommandType ----------Function Function Name ---Get-Count Reset-Count Definition ---------... ...

but the setIncrement command is not because it was not explicitly exported. PS (4) > Get-Command setIncrement Get-Command : The term 'setIncrement' is not recognized as the n ame of a cmdlet, function, script file, or operable program. Che ck the spelling of the name, or if a path was included, verify t hat the path is correct and try again. At line:1 char:12 + Get-Command Import-Module .\counter2.psm1 and verify the contents. Are the "*-Count" commands loaded? PS (8) > Get-Command *-Count CommandType ----------Function Function Name ---Get-Count Reset-Count Definition ---------... ...

Yes - they are all there. What about setIncrement - we were supposed to export it so there should be an error when we try calling. PS (9) > setIncrement 10 The term 'setIncrement' is not recognized as the name of a cmdle t, function, script file, or operable program. Check the spellin g of the name, or if a path was included, verify that the path i s correct and try again. At line:1 char:13 + setIncrement Get-Count 1 PS (13) > Get-Count 2 PS (14) > Get-Count 3 and call the reset command PS (15) > reset We didn't get a "command not found error" so the command exists so check that the value was actually reset PS (16) > Get-Count 1 PS (17) > Get-Count 2 and, once again, we can see from the output that it was. WHEN MODULE EXPORTS ARE CALCULATED Now let's return to something we mentioned earlier - that the set of module members to export is not known until runtime. In fact the Export-ModuleMember cmdlet doesn't really export the function; it simply adds it to a list of members to export. Once execution of the module body is completed, the PowerShell runtime looks at the accumulated lists of exports and exports those functions. The export algorithm is as shown in figure 9.8: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

331
Make sure that the module is syntactically correct and only contains valid members. It must contain a version number.

Load the module script

Execute the module script.

Execute the module script body, defining functions as we go along, optionally calling Export-ModuleMember

Has Export-ModuleMember been called?

If yes, get the accumulated exports

If no, get a list of all the functions that have been defined

Build the export table from the list

Export the selected members.

Figure 9.8 This figure show ordering of the steps when processing a module manifest. At any point prior to the second last step, if an error occurs, module processing will stop and an error will be thrown.

If there were no calls to Export-ModuleMember, then find all of the functions currently defined in the module and export them. If there was at least one call to Export-

ModuleMember, then export the accumulated export list.
So far we've been loading the module using the path to the module file. This is a good approach for development but eventually we need to put it into production. In the next section we'll see how to do this.

9.4.4 Installing a module
Once we have our module debugged and ready to put into production, we need to know how to install it. Fortunately, unlike snapins, installation is simple. All we have to do is create a subdirectory of one of the directories in the module path - the proverbial "xcopy install" that people like to talk about. Let's look at the first element of our module path PS (1) > ($ENV:PSModulePath -split ';')[0] C:\Users\brucepay\Documents\WindowsPowerShell\Modules

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

332 This directory is our personal module repository. We're going to install our counter module in to it so we don't have to load it using the full path any more. Let's get the repository path into a variable so it's easier to use: PS (2) > $mm = ($ENV:PSModulePath -split ';')[0] and create the module directory: PS (4) > mkdir $mm/Counter Directory: C:\Users\brucepay\Documents\WindowsPowerShell\Modules Mode LastWriteTime Length Name --------------------- ---d---1/31/2010 1:06 AM Counter We "install" the module by copying it into the directory we just created. PS (7) > cp .\counter.psm1 $mm/Counter Let's try it out. Use -the -List option on Get-Module to see if the module look up algorithm will find it. PS (10) > get-module -list Counter | fl name, path Name : Counter Path : C:\Users\brucepay\Documents\WindowsPowerShell\Modules\Counter\ Counter.psm1 and it does. This means we should be able to loaded it by name: PS (12) > import-module -verbose counter VERBOSE: Loading module from path 'C:\Users\brucepay\Documents\WindowsPowerShell\Modules\counter\counte r.psm1'. VERBOSE: Exporting function 'Get-Count'. VERBOSE: Exporting function 'Reset-Count'. VERBOSE: Exporting function 'setIncrement'. VERBOSE: Importing function 'Get-Count'. VERBOSE: Importing function 'Reset-Count'. VERBOSE: Importing function 'setIncrement'. and it works. Installing a module is as simple as copying a file. OK - why did we have to put in into a directory since it's just a single file. In chapter 10, we'll see that a "production" module is more than just a single .psm1 file. This is why modules are stored in a directory - it allows all of the module resources to be gathered in one place making it easy to distribute a multi-file module. Just "zip" it up and send it out. In all of the exercises so far, we've implicitly depended on the module scoping semantics to make things work. Now is a good time to develop our understanding of exactly how these new scoping rules operate. In the next section, we'll walk through the changes to function and variable look-up algorithm.

9.4.4 How scopes work in script modules
In section 8.4, we covered how scripts introduced script-specific scoping rules. As we've seen, modules also introduce some new scoping rules. The primary goal of these module-specific rules is to "insulate" modules from accidental contamination picked up from the caller's environment. This insulating property makes module behavior more predictable and that, in turn, makes modules more reusable.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

333 To accomplish this isolation, each module gets its own scope chain. As with the default scope chain, the module scope chain eventually ends at the global scope (which means that module and default scope chains both share the same global variables.) Walking up the module scope chain, right before we reach the global scope, we'll encounter a new distinguished scope - the module scope. This scope is somewhat similar to the script scope except it's only created once per loaded module and is used to share and preserve the state of that module. A diagram of all of these pieces is shown in figure 9.9.

Global scope: $x = 1, $y = 2

Function ‘one’ calls ‘two’ and ‘two’ calls the module function ‘foo’ Function scope function one { $y = 20; foo }

Functions ‘one’ and ‘two’ look up variables in the default scope. The module function ‘foo’ uses the module scope chain.

Module Scope: $x = 10

Default scope chain

Module scope chain

Function scope function two { $x + $y; foo } returns 21, 12

Function scope function foo { $x + $y } returns 12

Figure 9.9 This diagram traces how variables are resolved in a module context.

Let's spend some time walking through this figure. In the diagram, we see boxes indicating three functions. The two on the left ('one' and 'two') are defined in the default scope and will use the default scope chain to look up variables. The function shown on the right ('foo') is defined inside a module and so uses the module scope chain. Now let's call function 'one'. This function sets a local variable $y to 20 then calls function 'two'. In the body of 'two', we're adding $x and $y together. This means that we have to look up the variables to get their values. The smaller gray arrows in the figure show the order in which the scopes will be checked. Following the default scope path, the first instance of a variable named $y is found in the local scope of function 'one' and has a value of 20. Next we follow the scope path to find $x, and don't find it until we hit the global scope where it resolves to 1. Now we can add them, emitting the value 21. Function 'two' then calls the module function 'foo'. This function also adds $x and $y but this time we'll use the module scope chain to look up the variables. We travel up the module chain and don't find a defined variable $y until we hit the global scope where its value is 2. When we look up $x, we find that it was set to 10 in the module scope. We add 2+10 and get 12. This shows how local variables defined in the caller's scope can't have an impact on the module's behavior. The module's operations are insulated from the caller's environment. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

334 At this point, we've covered most of the important details of what happens when a module is loaded into the global environment. But modules can be loaded into other modules too. This is where reuse can really kick in - modules building on modules delivering more and more functionality. Let's look at how this works in the next section when we introduce nested modules.

9.4.6 Nested Modules
In this section we're going to cover what happens when modules import other modules. Since the Import-Module cmdlet is just a regular cmdlet, it can be called from anywhere. When it's called from inside another module the result is a nested module. A nested module is only directly visible to the calling module. This is much easier to show than to explain. We'll look at a module called usesCount.psm1. Here are the contents of the module file: PS (1) > cat usesCount.psm1 import-module .\counter2 function CountUp ($x) { while ($x-- -gt 0) { Get-Count } } This module imports the "counter" module we created in section 9.4.3 single function countUp. We'll import this module: PS (2) > ipmo .\usesCount.psm1 Now call Get-Module to see what's loaded. PS (3) > get-module ModuleType Name ---------- ---Script usesCount ExportedCommands ---------------{CountUp, Get-Count, Res... and then defines a

The first thing to notice in this output is the list of loaded modules doesn't show the nested module. This is by design - we don't what to expose module implementation details by default. The other thing to notice is that there are more commands in the ExportedCommands list than just CountUp. Let's use the Format-List (alias: fl) to see all of the information about the module: PS (4) > get-module usesCount | fl

: usesCount : C:\wpia_v2\text\chapter09\code\usesCount.psm 1 Description : ModuleType : Script Version : 0.0 NestedModules : {counter2} ExportedFunctions : {CountUp, Get-Count, Reset-Count} ExportedCmdlets : {} ExportedVariables : {} ExportedAliases : {} This shows us that there were 3 functions exported from this module even though the module itself only defined one function. This is because the functions that are being imported from the nested module are exported from the root module usesCount. Remember - by default all ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Name Path

Licensed to Andrew M. Tearle

335 defined function in a module are exported by default. This includes function definitions that where imported from a nested module as well as those defined in the module body. While nested modules are hidden by default, there is a way to see all of the modules that are loaded, including nested modules and this is to use the -All flag on Get-Module. PS (5) > get-module -all ModuleType ---------Script Script Name ---counter2 usesCount ExportedCommands ---------------{Get-Count, Reset-Count} {CountUp, Get-Count, Res...

Using this flag we see both of the modules that are loaded. Now let's look at some of the commands that were imported. First look at the function that came from the root module: PS (6) > Get-Command CountUp | fl -force * HelpUri ScriptBlock : : param($x) while ($x-- -gt 0) { Get-Count }

CmdletBinding : False DefaultParameterSet : Definition : param($x) while ($x-- -gt 0) { Get-Count } Options Description OutputType Name CommandType Visibility ModuleName Module Parameters ParameterSets : : : : : : : : : None

{} CountUp Function Public usesCount usesCount {[x, System.Management.Automation.Paramete rMetadata]} : {[[-x] ]} names the module that this function

There is a lot of information here but the properties that are most interesting for this discussion are the ModuleName and Module. The ModuleName was exported from whereas the Module property points to the module that defined this function. For top level modules, the defining and exporting modules are the same. For nested modules, they aren't. From the ModuleName property, we see that this function was exported from module usesCount. Now let's look at one of the functions that was imported from the nested module and then re-exported. PS (7) > Get-Command Get-Count | fl * HelpUri ScriptBlock : : return $script:count += $increment CmdletBinding : False DefaultParameterSet : Definition : return $script:count += $increment

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

336 Options Description OutputType Name CommandType Visibility ModuleName Module Parameters ParameterSets : : : : : : : : : : None {} Get-Count Function Public usesCount usesCount {} {}

From the output, we see that the module name for the function shows the top-level module name, not the name of the module where the function was defined. This makes sense because they are both exported from the same module. However, they were defined in separate files. Knowing where a function is defined is critical to debugging as we'll see in chapter 15. The way to see where a function was defined is to look at the File property on the scriptblock that makes up the body of the function. Here's how to do this: PS (8) > ${function:CountUp}.File C:\wpia_v2\text\chapter09\code\usesCount.psm1 PS (9) > ${function:Get-Count}.File C:\wpia_v2\text\chapter09\code\counter2.psm1 PS (10) > This is a fairly easy way to see where the module came from, once you know how. IMPORT INTO THE GLOBAL ENVIRONMENT WITH -GLOBAL When one module loads another, by default it becomes a nested module. This is usually what we want but consider the case were we want to write a module that manipulates modules. In this scenario, we need to be able to import the module into a context other than our own. While there isn't a way to import directly into an arbitrary context, the -Global flag on

Import-Module allows us to import into the global context. Let's work on a variation of the usersCount module to see how this works. The modified script module looks like:
PS (1) > cat .\usesCount2.psm1 import-module -global .\counter2 function CountUp ($x) { while ($x-- -gt 0) { Get-Count } } The significant difference in this version is the use of the -Global parameter on Import-

Module. Let's import the module
PS (2) > Import-Module .\usesCount2 then look at the modules that are loaded. PS (3) > get-module ModuleType ---------Script Script Name ---counter2 usesCount2 ExportedCommands ---------------{Get-Count, Reset-Count} CountUp

This time we see the both modules are loaded at the top level instead of one being nested inside another. Also, the ExportedCommand property for usesCount2 does not report the functions defined in counter2 as being exported from usesCount2. When we use Get-

Command to look at functions from each of the modules,
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

337 PS (4) > get-command Get-Count | ft name,module Name ---Get-Count Module -----counter2

the functions defined in counter2 are shown as being in the correct module as is the case for the CountUp functions: PS (5) > get-command CountUp | ft name,module Name ---CountUp Module -----usesCount2

In effect, we've written a module that manipulates modules. This completes our coverage of script modules which are the type of module most people are likely to write. The next type of module we'll look at are binary modules which everyone uses but are mostly created by programmers since they are written in languages that must be compiled in an assembly or .dll file.

9.5 Binary modules
In this section, we're going to look at how binary modules operate in PowerShell. Binary modules contain the classes that define cmdlets and providers. Unlike script modules, binary module are written in compiled languages like C# or Visual Basic. They're used to deliver most of the packaged functionality in the PowerShell distribution. From a technical perspective, a binary module is simply a .Net assembly (a .dll) compiled against the PowerShell libraries. While programming topics are not the focus of the book, we will spend a bit of time looking at how binary modules are written and compiled. This implies that we will have to do some C# programming to produce a module to work with. In the following sections, we'll go over how to create and load binary modules, how they interact with script modules and any issues you need to be aware of when working with them.

9.5.1 Binary modules vs SnapIns
Binary modules are the preferred way of packaging compiled cmdlets and providers in PowerShell version 2. They essentially replace the SnapIn concept used in PowerShell V1. However, though the emphasis is now on modules, the snapin mechanism is still present in V2 so need to spend some time on it. (In fact, all of the core assemblies in PowerShell V2 are still delivered a snapins to avoid accidently breaking things.) Like binary modules, snapins are just assemblies with a .dll extension. However, unlike modules, before we can load a snapin into PowerShell, we have to register the snapin. This registration model is based on a similar mechanism used by the Microsoft Management Console (MMC) and has the singular advantage that all modules on the system can be found by looking in the registry. Unfortunately it also has a great many disadvantages. First, registration is done using the installutil.exe program. This utility is installed in the .NET frameworks installation directory, not in the normal command path so we can't just call it. Instead, the first thing we have to do to register a snapin is find installutil.exe.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

338

Fortunately we can create an alias to take care of this by doing Set-Alias installutil ` (Join-Path ` (Split-Path ([object].Assembly.Location) -Parent) ` installutil.exe) This expression works by using the Location property on the assembly containing the

[object] type to find the .NET framework directory. It joins that path with the name of the command. While this is a bit annoying there is a way to mitigate the annoyance. Next, in order to be able to actually use this utility, we need to running with local administrator capabilities. This is because need to write to the system registry as part of the registration process. Finally, all registered snapins are visible system-wide as soon as the registration completes. This means that there is no way to load and test changes to a snapin without making those changes visible to all users of the system. All of these things combined makes developing and testing snapins quite tedious. Modules solve all of these problems - we don't need a separate tool to load modules, we don't need to be admin on the machine, the module path allows public and private modules, and finally, to test a module, we can just use Import-Module and pass it the path to the file. There's nothing novel here - this works the same way for all types of modules. This consistency of experience is another benefits that the modules provide. Another difference between snapins and binary modules is the fact that the snapin, besides containing the cmdlets and providers, also contains the snapin metadata - its name, author, version and so on as part of the snapin assembly. The module mechanism handles this in a different way by specifying the metadata using a separate manifest file. This separation allows for a common representation of metadata independent of the type of module. (We'll spend a lot of time on module manifests in chapter 10). Now that we know the advantages provided by binary modules over snapins, let's start working with them.

9.5.2 Creating a binary module
The first thing we'll need for our experiments is a module to work with so in this section, we'll create that module. Remember that binary modules are written in a language like C# so we'll need to do a little bit of non-PowerShell programming. Fortunately we have some C# code handy from the example cmdlet we saw in chapter 2. Let's take this code and make a binary module out of it. Here's that code: PS (1) > Get-Content ExampleModule.cs using System.Management.Automation; [Cmdlet("Write", "InputObject")] public class MyWriteInputObjectCmdlet : Cmdlet { [Parameter] public string Parameter1; [Parameter(Mandatory = true, ValueFromPipeline=true)] public string InputObject; ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

339 protected override void ProcessRecord() { if (Parameter1 != null) WriteObject(Parameter1 + ":" + else WriteObject(InputObject); } } If you were paying attention in the last chapter, this should be pretty comprehensible. You should certainly recognize the [Parameter()] attributes from advanced functions. Before we can use this C# code as a module, we need to compile it. PowerShell V2 adds a handy, very powerful new cmdlet called Add-Type. This cmdlet is designed making this kind of thing easy. In this case, we'll use it to compile the source code from the path

InputObject);

.\ExampleModule.cs into the output assembly .\ExampleModule.dll.
PS (2) > Add-Type -Path .\ExampleModule.cs ` >> -OutputAssembly .\ExampleModule.dll >> Use the dir command to sure that we got what we wanted: PS (3) > dir examplemodule.dll

Directory: C:\wpia_v2\text\chapter09\code

Mode ----a---

LastWriteTime ------------8/31/2009 7:25 PM

Length Name ------ ---3584 examplemodule.dll

and there's our module: "examplemodule.dll". Once the module .dll has been created, we can load it the same way we loaded a script module, using Import-Module. Let's try this. PS (4) > Import-Module ./examplemodule and, as before, we'll use Get-Module to look at the module info object for ExampleModule. PS (5) > Get-Module | Format-List

: examplemodule : C:\wpia_v2\text\chapter09\code\examplemodule .dll Description : ModuleType : Binary Version : 0.0.0.0 NestedModules : {} ExportedFunctions : {} ExportedCmdlets : Write-InputObject ExportedVariables : {} ExportedAliases : {} We see the name and path as expected. The module type is binary and it is exporting a single cmdlet "Write-InputObject". Let's try this new cmdlet: PS (7) > 1,2,3 | write-inputobject -parameter1 "Number" Number:1 Number:2 Number:3 and it's all working fine. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Name Path

Licensed to Andrew M. Tearle

340 So, other than the implementation of a binary module, there is not much different in behavior when using it. Well - ok - there is one major difference. Binary modules are implemented as .NET assemblies and .NET assemblies can't be unloaded from a session (it's a .NET thing, not a PowerShell thing) therefore binary modules can't be unloaded from a session. This means that you can't update a binary module once it's been loaded. We can't even update the assembly on disk because the file is locked when the assembly is loaded. If we re-run the

Add-Type cmdlet we used to build the assembly previously we get a rather an intimidating error message: PS (8) > Add-Type -Path .\ExampleModule.cs ` >> -OutputAssembly .\ExampleModule.dll >> Add-Type : (0) : Could not write to output file 'c:\wpia_v2\text\chap ter09\Code\ExampleModule.dll' -- 'The process cannot access the file because it is being used by another process. ' At line:1 char:9 + Add-Type wof 123 123 and it works so everything is as intended. This is an important pattern to be aware of. Using this pattern of, we can use a script modules to wrap a cmdlet but leave the cmdlet itself hidden. This allows us to customize the command experience even though we may not be able to change the cmdlet itself. Now let's reverse the scenario. We'll reload the script module (using the -Force flag to make sure the script actually gets processed again) but this time, we'll pass in an argument to the script. PS (5) > Import-Module .\WrapBinaryModule.psm1 -Force ` >> -ArgumentList $true >> VERBOSE: Importing cmdlet 'Write-InputObject'. Because the binary module is already loaded, we just see the "importing" message. Remember - we can't update a binary module in our session once it has been loaded. The point here is to use script modules to give us at least a partial workaround for this scenario, in this case controlling the visibility of the cmdlet. Once again we'll call Get-Module to see what was imported. PS (6) > Get-Module WrapBinaryModule | >> Format-List Name, ExportedFunctions, ExportedCmdlets >> Name : WrapBinaryModule ExportedFunctions : {} ExportedCmdlets : {[Write-InputObject, Write-InputObject]} This time we see the cmdlet but not the function as intended. Even though we couldn't change the binary module we could still control what it exported.

AUTHOR'S NOTE
There are limits to this - we can't export more cmdlets, only filter the existing imports. We also can't rename the cmdlet itself though we could proxy it through a function if we wanted to "change" it's name.

So far all of our work with modules has been pretty much ad hoc - we're just making stuff up as we go along. The modules we're writing have none of the metadata (description, author information, copyright, etc.) needed in a production environment for figuring out things like which modules need to be patched. In the next section we'll address this and see how module manifests are used to fill in the missing pieces.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

343

9.8 Summary
In this chapter, we introduced PowerShell modules - a feature new in PowerShell version 2. Modules allow us to package collections of PowerShell resources into shareable, reusable units. Using this feature, we can build start to build our library of reusable modules in a manageable way. The important points to remember are that: 31.Modules are manipulated, managed and imported using cmdlets in PowerShell. Unlike many languages, no special syntax needed. Modules are discovered , both in memory and on disk, using the Get-Module cmdlet. They are loaded with Import-Module and removed from memory with Remove-Module. These three cmdlets are all you need to know if you just want to use modules on your system. 32.PowerShell uses the $ENV:PSModulePath environment variable to search the file system for modules to load when an unqualified module name is specified. Alternatively, a fully-qualified path name can be used to load a module directly without going through the search process. 33.There are two basic types of modules: script modules which are written using the PowerShell language and binary modules written in a compiled language. Both types of modules are simply files on disk. No registration process is needed to make a module available for use - we just need to be able to read the file somehow. 34.Since script modules are another form of script with a .psm1 extension. Because they are a type of script, they obey the execution policy setting just like regular scripts. 35.Script modules execute in their own isolated environment called the module context. A script module also has access to the global environment which is shared across all modules. In the next chapter we'll continue our exploration of modules. The focus in this chapter was on how to construct simple ad hoc modules. In the next chapter, we introduce module manifests a mechanism to add production metadata to our modules as well as providing a way to deal with multi-file modules.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

344

10
Module Manifests and Metadata

The world is my world: this is manifest in the fact that the limits of language (of that language which alone I understand) mean the limits of my world. Ludwig Wittgenstein, Tractatus Logico-Philosophicus

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

345 In chapter 9 we introduced PowerShell modules and covered the basics needed for using and writing modules. That chapter focused on simple ad hoc modules and ignored details like module descriptions, versioning and so on. It's these details that make the difference between ad hoc and production scripting. Since one of the goals for PowerShell V2 was to enable production-oriented scripting PowerShell, we needed a way to attach production-oriented metadata to our modules. This is where module manifests come in. They allow us to annotate and organize the pieces in more complex, multi-file modules like the example from section 9.5.3. Manifests are subject of this chapter. We'll start the a discussion of the layout of a module's directory structure. Then we'll introduce the manifest and look at its contents. We'll look at the tools provided for authoring and testing manifests and then we'll walk through each manifest element, describing its purpose and how to it. In the latter part of the chapter, move on and look at some advanced module techniques including how to manipulate metadata from within a module, control the module access mode and set up actions to take when a module is removed.

10.1 Module Folder Structure
Before we jump into manifests, let's spend a little time reviewing the way modules are organized on disk. A module in the module path ($ENV:PSModulePath) is simply a directory containing a collection of files. One of the files in the module directory is the module manifest. This file usually has the same name as the directory and has a .psd1 extension. We can see an example of this structure by looking at the contents of the system modules directory. This directory contains modules that are shipped with PowerShell and are visible in the directory

$PSHome/modules. The structure of some of the modules in this directory are shown in figure
10.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

346

Figure 10.1 This figure shows the layout of the modules that ship with PowerShell. Each module is stored in its own folder with a .psd1 file containing the module manifest.

Taking a look at this figure, we see that there are two modules stored in the system module directory. These modules are just directories containing the files that make up the module contents. Each folder contains a .psd1 file that this is the manifest for the module. The first module is PSDiagnostics which we looked at briefly in section 9.3.1 when we talked about finding modules on the system. The module directory contains two files - the manifest file and a script module that defines the commands for this module. Notice that the directory, manifest and script file all have the same name. The second module is the BitsTransfer module. The structure of this module folder is a little more complicated. As well as the manifest, it includes a format file, an interop .dll and a subdirectory "en-US". This subdirectory is used to hold the message catalogs that allow for localized messages. We'll go over how all of these elements are used when we describe the contents of module manifests in the next section.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

347

10.2 Module Manifest Structure
As we saw in the last section, a module manifest is stored in a file with .psd1 extension. This indicates that it's PowerShell data file which is a type of script that is limited in the things it can contain. We'll talk about these restrictions in section 10.6 but for now, we just need to know that it's a text file containing some PowerShell script. This script code must return a hashtable containing a predetermined set of keys when executed by the system. These keys define the manifest elements for the module. Since these manifest files are fairly long and somewhat complex, PowerShell provides a cmdlet, New-ModuleManifest to help create a manifest. Let's run this command so we'll have an example manifest to work with: PS (1) > New-ModuleManifest testManifest.psd1 cmdlet New-ModuleManifest at command pipeline position 1 Supply values for the following parameters: NestedModules[0]: Author: CompanyName: Copyright: ModuleToProcess: Description: TypesToProcess[0]: FormatsToProcess[0]: RequiredAssemblies[0]: FileList[0]: PS (2) > The cmdlet takes advantage of the way PowerShell prompts for missing mandatory parameters. It will prompt for each of the values to assign to elements in the generated hashtable. In this example, we're just hitting the enter key and taking the default value. The generated file will also contain comments for each element, describing what the element is used for. Let's take a look at the file that was generated: PS (3) > Get-Content testManifest.psd1 # # Module manifest for module 'testManifest' # # Generated by: brucepay # # Generated on: 8/28/2009 # @{ # Script module or binary module file associated with this manifest ModuleToProcess = '' # Version number of this module. ModuleVersion = '1.0' # ID used to uniquely identify this module GUID = '586ce129-7e3b-4383-9c2c-b6e2e6920e21' # Author of this module Author = 'brucepay' # Company or vendor of this module ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

348 CompanyName = 'Unknown' # Copyright statement for this module Copyright = '(c) 2009 brucepay. All rights reserved.' # Description of the functionality provided by this module Description = '' # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '' # Name of the Windows PowerShell host required by this module PowerShellHostName = '' # Minimum version of the Windows PowerShell host required by this module PowerShellHostVersion = '' # Minimum version of the .NET Framework required by this module DotNetFrameworkVersion = '' # Minimum version of the common language runtime (CLR) required by this module CLRVersion = '' # Processor architecture (None, X86, Amd64, IA64) required by this module ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module RequiredModules = @() # Assemblies that must be loaded prior to importing this module RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment p rior to importing this module ScriptsToProcess = @() # Type files (.ps1xml) to be loaded when importing this module TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module FormatsToProcess = @() # Modules to import as nested modules of the module specified in ModuleToProcess NestedModules = @() # Functions to export from this module FunctionsToExport = '*' # Cmdlets to export from this module CmdletsToExport = '*' # Variables to export from this module VariablesToExport = '*' # Aliases to export from this module AliasesToExport = '*' ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

349 # List of all modules packaged with this module ModuleList = @() # List of all files packaged with this module FileList = @() # Private data to pass to the module specified in ModuleToProces s PrivateData = '' } Remember that I said it was long and complex? In fact it's complex enough that PowerShell also includes a cmdlet to test a manifest. This cmdlet is called (surprise) Test-

ModuleManifest. Let's use it to test the manifest we've generated to make sure it's valid
(though it would be surprising if it wasn't since we just created it.) PS (4) > Test-ModuleManifest testManifest.psd1 ModuleType Name ---------- ---Manifest testManifest ExportedCommands ---------------{}

If the test is successful, the module info object is returned. Now that we know it's valid, let's import it. Normally a module doesn't emit anything but in this case we want to see it immediately so we'll specify -Pass which will cause the module information object to be written to the output pipe. PS (5) > Import-Module .\testManifest.psd1 -Pass | Format-List

: testManifest : C:\wpia_v2\text\chapter09\code\testManifest. psd1 Description : ModuleType : Manifest Version : 1.0 NestedModules : {} ExportedFunctions : {} ExportedCmdlets : {} ExportedVariables : {} ExportedAliases : {} and we see our essentially empty module. The New-ModuleManifest cmdlet creates a manifest that contains all of the allowed fields but in fact most of the fields are not required. The only field that is required is the module version. We can manually generate the minimal manifest simply by using redirection. PS (4) > '@{ModuleVersion="1.0"}' > testManifest2.psd1 Now load the manifest and look at what was generated: PS (5) > ipmo .\testManifest2.psd1 PS (6) > get-module testManifest2 | fl

Name Path

Name Path Description ModuleType

: testManifest2 : C:\wpia_v2\text\chapter09\code\testManifest2 .psd1 : : Manifest http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

350 Version NestedModules ExportedFunctions ExportedCmdlets ExportedVariables ExportedAliases : : : : : : 1.0 {} {} {} {} {}

This is identical to what we got from the more complicated default manifest. In practice, it's always best to use New-ModuleManifest to generate a complete manifest for your modules even if you aren't going to use all of the fields immediately. Once you've generated the manifest, you can easily add additional data to it over time using your favorite text editor. In the next few sections, we'll go over the contents of the manifest. To make our exploration a bit more manageable, we've divided the manifest elements into three broad categories - production, construction and content elements. We'll cover each of these areas and the elements they contain starting with the "production" elements.

10.3 Production manifest elements
In this section we'll cover the elements that make up the production metadata. These elements are used to add things like copyright information, version numbers and so on. The fields in the module for this are shown in table 10.1. The use of some of the elements is pretty obvious -

Author, CompanyName, Copyright and so forth. We won't cover them beyond the comments in the table. The remaining elements will be covered in the subsections that follow.

Table 10.1 The manifest elements in a module manifest file that contain production-oriented metadata.
Manifest Element ModuleVersion Type string Default Value 1.0 Description The version number of this module. This string must be in a form that can be converted into an instance of [System.Version]. ID used to uniquely identify this module

GUID

string

autogenerated

Author

string

none

The name of the module creator.

CompanyName

string

Unknown

The company, if any, that produced this module.

Copyright

string

(c) Year Author. All rights reserved.

The copyright declaration for this module with the copyright year and name of the copyright holder.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

351 Description string '' The description of this module. Since this description may be used when searching for a module, it should be a reasonable description, mentioning the purpose of the module and technology area it relates to. Minimum version of the Windows PowerShell engine required by this module

PowerShellVersion

string

''

PowerShellHostName

string

''

Name of the Windows PowerShell host required by this module

PowerShellHostVersion

String

''

Minimum version of the Windows PowerShell host required by this module

DotNetFrameworkVersion

String

''

Minimum version of the .NET Framework required by this module

CLRVersion

String

''

Minimum version of the common language runtime (CLR) required

ProcessorArchitecture

string

''

The processor architecture this module requires. It may be '', None, X86, Amd64, IA64

RequiredModules

[object[]]

@()

Modules that must be imported into the global environment prior to importing this module

In the next few sections, we'll look at how the elements in this table are used to make modules more production worthy. We'll begin with a very important topic - module identity.

10.3.1 Module Identity
For modules to be shared and serviced (i.e. patched) effectively, there needs to be a strong notion of identity which allows us to uniquely identify a module. It can't just be the module name. The name of the module comes from the manifest file name and there is no guarantee somebody else won't give their module the same name as ours. In order to guarantee that we can always identify a module regardless of path changes, renames and so on, the manifest contains a GUID or Globally Unique Identifier. The algorithm used to generate GUIDs is ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

352 guaranteed to produce a globally unique number, hence the name. Once we know the GUID for a module, we can always identify it, even if the file gets renamed. Another important aspect of module identity is the version number. Versioning is what allows us to determine if the module has been patched properly. The ModuleVersion element in the manifest is used to hold the module's version. This element uses the type

System.Verison to represent the version of a module internally. In the manifest file, the element should be assigned a string that can be converted into an instance of

System.Version. This string must have the form of "#.#.#.#", for example "1.0.0.0. When we use the -Version parameter on Import-Module, it will search the path in $ENV:PSModulePath, looking for the first module whose name matches the requested name and whose module number is at least as large as the required version.

AUTHOR'S NOTE
Unfortunately no mechanism has been provided for loading a module by specifying its GUID. This is a deficiency in the current implementation of Import-Module. The only place we can actually use the GUID to identify a module is when we specify module dependencies which we'll see in the next section. As a workaround for this issue, a proxy function can be written that wraps Import-Module and adds this functionality.

10.3.2 Runtime dependencies
The remainder of the production elements in the manifest have to do with identifying environmental dependencies - what needs to be in the environment for the module to work properly. For many script modules, most of these elements can be left in their default state. Let's go through these elements and what they are used for. The CLRVersion and DotNetFrameworkVersion identify dependencies on what version of the CLR (or .NET) is installed on the system. So why do we need two elements? Because the CLR runtime and the framework (all of the libraries) can and do vary independently. This is exactly what happened with "CLR 3.0". In this version, the runtime version remained at 2.0 but there was a new framework version - 3.0 - where the LINQ technologies were introduced (we'll talk about link in chapter 14.). The framework version changed again with CLR 3.5. As before, the runtime remained at 2.0 but the framework moved to 3.5 where things like WPF (Windows Presentation Foundation) were added. The next version of the CLR, version 4.0 will updated both the runtime and the framework. As a consequence of this pattern, it's necessary to be able to independently express the version requirements for each part. When adding the dependencies to the manifest you should specify the minimum highest version required. This depends on the higher revisions are backwards compatible with earlier versions and is a fairly safe assumption for the CLR. Expressing a dependency on the processor architecture isn't likely to be common however it is possible to have a module that uses .NET interoperation (chapter 1X) or COM (chapter 1X) and, as a consequence, has some processor architecture specific dependency.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

353 The next set of dependencies is on PowerShell itself. The PowerShell version is pretty straight forward. It specifies the minimum version of PowerShell needed by this module. The PowerShell host name and version are only slightly more complex. They allow you to place a dependency on the application that is hosting the PowerShell runtime rather than on the runtime itself. For example, we can have a module that adds custom elements to the PowerShell ISE. This module clearly has a dependency on the name of the host. To find out the name of the string to place here, in the host you are using, look at the Name property on the object in $host. Figure 10.2 shows how this looks in the PowerShell console host.

Figure 10.2 This figure shows how to get the PowerShell host name in the console host.

Figure 10.3 shows the same information in the PowerShell ISE.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

354

Figure 10.3 This figure shows the host name and version properties for the PowerShell ISE.

Once you know which host you are depending on, you also need the version number which is available through the Version property on $host. This information for both the console host and ISE are also shown in figures 10.10 and 10.11. The final type of dependency is on the modules that are already loaded into the system. This is done through the RequiredModules manifest element. Now this element probably doesn't do what you'd expect. It doesn't load dependencies, it just checks to see if certain modules are loaded. This seems a bit useless but, if we go back to thinking about versioning, it may be that the version of the required module currently loaded is too low but a newer module can't be loaded because there are other modules already using the loaded module. Where the other elements we've seen so far are either simple strings or strings that can be converted into a version number, this element can either take a module name string or a hash table contain two or three elements. These hashtable elements allow us to precisely specify the module we are dependent on as they include the module name, the version number and the GUID of the module that must be loaded (although the GUID is optional.) This covers all of the production elements in the manifest. Now that we know we have the right module (Identity) and that it will work in our environment (Dependencies), let's look at the manifest elements that control what happens when the module is loaded. Load-time behavior is controlled by a set of manifest elements that contain entries that are used to construct the in-memory representation of the module.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

355

10.4 Construction manifest elements
The construction metadata in this module are the fields that tell the engine what to load as part of this module. These fields are listed in table 10.2.

Table 10.2 The module manifest elements that contain data used in constructing the module
Manifest Element ModuleToProcess Type Default Value Description Script module or binary module file associated with this manifest Assemblies that must be loaded prior to importing this module Script files (.ps1) that are run in the caller's environment prior to importing this module Type files (.ps1xml) to be loaded when importing this module Format files (.ps1xml) to be loaded when importing this module Modules to import as nested modules of the module specified in ModuleToProcess Functions to export from this module Cmdlets to export from this module Variables to export from this module Aliases to export from this module

string

''

RequiredAssemblies

[string []] [string []] [string []] [string [] [string []] String String String String

@()

ScriptsToProcess

@()

TypesToProcess

@()

FormatsToProcess

@()

NestedModules

@()

FunctionsToExport CmdletsToExport VariablesToExport AliasesToExport

"*" "*" "*" "*"

There are two subcategories in the construction elements - "things to load" and "things to export". We'll start with loading since you can't export anything until something has been loaded. As mentioned previously, none of the fields are actually required. If they aren't there, then PowerShell assumes the default value for each field as shown in the table.

10.4.1 The loader manifest elements
In the next few sections, we'll cover each of these manifest elements in the order that you are most likely to use them in when creating a manifest. This is not, however, the order that they are processed in when the module is loaded. We'll cover the load order as a separate topic in section 10.4.2. MODULETOPROCESS MANIFEST ELEMENT The first loader element that we're going to look at is ModuleToProcess. This is the most commonly used manifest element and identifies the main or root active module to load.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

356

AUTHOR'S NOTE
So why isn't it called "RootModule"? Because we named (and renamed and renamed again) this field throughout the development process but it wasn't until everything was done and we started explain it to other people that the concept of a "root module" started spontaneously popping up conversations. Unfortunately, by then we were too far into the release process to be able to change it. Thus "RootModule" became victim of the tyranny of the clock.

By active, we mean that the file defines executable elements, instead of just providing metadata definitions. The type of the module file specified in this member will determine the final module type. If no file is specified as the ModuleToProcess, then the type shown in the module info object will be "Manifest". If it's a script or binary module, then it will be the respective module type. Other types will raise errors. The various combinations are shown in table 10.3.

Table 10.3 Module types as determined by the ModuleToProcess member
Contents of ModuleToProcess empty Script module (.psm1) Binary module (.dll, .exe) Module manifest (.psd1) Script file Final Module Type Manifest ScriptModule BinaryModule Error - not permitted Error - not permitted

If a script or binary module is specified in the ModuleToProcess element, then the type of the loaded module will be the same as the type of the module specified in the

ModuleToProcess element even through we're loading through a manifest. In other words, if the root module was binary, then the Type property on the module info object will show "Binary". If the root module was script then the Type property will return "Script". What it can't be, however, is another manifest module. It must be either a script or binary module (or be left empty). The reason for this constraint is that the job of a manifest is to add metadata to a script or binary module. If the main module is another manifest, then we would have to deal with colliding metadata. For example, one manifest may declare that the module version 1.0.0.0 but the second module says it's version 1.2.0.0 and there is no way to reconcile this type of collision so it's simply not allowed. As a result PowerShell just won't look for a .psd1 file when searching for the module to process. As we mentioned at the beginning of this subsection, it is expected that production modules will use ModuleToProcess to identify a single main module.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

357 NESTEDMODULES MANIFEST ELEMENT We'll jump to

NestedModules

next.

NestedModules

are

loaded

before

the

ModuleToProcess is loaded. While the net effect is equivalent to the having the main module call Import-Module, it there are two advantages to doing this way. First, it's easy to see what the module is going to loaded before loading the module. Second, if there is a problem with loading the nested modules, the main module will not have been loaded and won't have to deal with the load failures. REQUIREDASSEMBLIES M ANIFEST ELEMENT The RequiredAssembies field sounds like it should have the same behavior as RequiredModules from the previous section. It doesn't. What this field does is to actually load the assemblies listed in the element if they are not already loaded. Figure 10.4 shows the set of steps taken when trying to find the module to load.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

358

Load using assembly name

Try to load the assembly using the assemblyqualified name using the Assembly.Load() method.

Success?

No Load using a path name Yes Try to load the assembly using a path with the Assembly.LoadFrom() method.

Success?

No Load using a partial name. No Try to load the assembly using the partial assembly name with the Assembly.LoadWithPartialName() method.

Load failed, generate error and halt processing

Load succeeded, continue processing

Figure 10.4 This figure shows the steps taken when trying to load an assembly from the RequiredAssembies module manifest field.

If one of the steps results in a successful load, then PowerShell will proceed on to the next step in loading a module. If it fails, however, the entire module loading process is considered a failure. SCRIPTSTOPROCESS M ANIFEST ELEMENT Now let's talk about ScriptsToProcess and scripts in general. Something we didn't mention earlier is that NestedModules can also refer to script files. These script files are run in the root module's module context - essentially equivalent to dot sourcing them into the root ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

359 module script. The scripts listed in ScriptToProcess do something quite different. These scripts are run in the caller's environment not the module environment and are run before any of the modules are loaded. This allows for custom setup and environment validation logic. When we talked about how version checks work - that the first module with a version number equal to or greater than the requested version number will be loaded, assuming that things are backward compatible. In fact, something this might not be true but there is no explicit support for this level of dependency checking currently. If you are in a situation where you do need to do this, this can be done through a script referenced in the ScriptsToProcess. TYPESTOPROCESS AND FORMATSTOPROCESS MANIFEST ELEMENTS The last of the "loaded" manifest elements are the TypesToProcess and FormatsToProcess. These are files with a .ps1xml extension that contain formatting instructions and additional type metadata. (We'll more into the content of these files in chapter 16.)

10.4.2 Module component load order
Module components are loaded into the PowerShell environment using a fixed sequence of steps called the module load order. This load order is shown in figure 10.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

360
Make sure that the module manifest is syntactically correct and only contains valid members. Also verify that it contains a version number.

Validate Module Manifest

Check RequiredModules

Raise an error if any of the required modules are not currently loaded. Missing modules will not be loaded.

Process RequiredAssemblies

Check for required assemblies and load any that are missing.

Load Types and Formats files

Process all for the Types .ps1xml files then load all of the Format .ps1xml files.

Load Nested Modules

Load all of the nested modules in the order they appear in the manifest element.

Load Module To Process

Finally, load the main module if one has been specified.

Add to Module Table

If no errors have occurred up to this point, the module has loaded successfully and gets added to the module table

Process Exports

Import all members exported from the main module context, subject to the filters specified for the Import-Module cmdlet.

Figure 10.5 This figure show ordering of the steps when processing a module manifest. At any point prior to the second last step, if an error occurs, module processing will stop and an error will be thrown.

The order that these steps are taken in can be of significance when trying to construct a module with a complex structure. In particular, there is an issue load order that causes problems when using binary modules with types and format files. Because types and format files are loaded before the ModuleToProcess is, if the types and format files contain references to any of the .Net types in the binary module, an error will occur saying that the referenced types can't be found because the module's .dll hasn't been loaded yet. To work around this, we need to make sure the .dll for the binary module is loaded first. This is done by adding the .dll to the list of RequiredAssemblies. Since

RequiredAssemblies is processed before the types and format file entries there won't be an problem resolving the types. Then, when it's time load the binary module, the .dll will ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

361 already loaded and just needs to be scanned to find the cmdlets and providers. This resolves the problem with effectively no performance impact and only a small increase of complexity for the module owner. At this point, we've covered all of the major module manifest topics. There are only a couple of things left to look at and then we'll be done.

10.5 Content manifest elements
The content manifest elements are where we can list all of the component files that make up a module. There are two lists provided - a list of all loadable module files and a separate list for any other files (data files, icons, audio clips, etc.) that are part of the module. These elements are shown in table 10.4.

Table 10.4 Module manifest elements used to list the module's contents
Manifest Element ModuleList Type [string[]] Default Value @() Description Non-normative list of all modules packaged with this module

FileList

[string[]]

@()

Non-normative list of all files packaged with this module Private data to pass to the module specified in ModuleToProcess

PrivateData

[object]

''

Note that these "packing lists" are not normative. In other words, they aren't actually processed or enforced by PowerShell and filing them in is entirely optional. As a best practice however, it is recommended that they contain accurate data since external tools may be created to do the actual validation . This last manifest element - PrivateData - provides a way for module writers to include custom data in manifests and make it available to modules when loaded. This element can contain any type of object permitted by the "restricted language" subset of PowerShell: strings, numbers, arrays or hashtables. The system makes the data available to both script and binary modules, including to providers defined in binary modules. We'll look at how modules can access the data specified by this element section 10.7.3. At long last, we've finally covered all of the manifest elements. We have one last thing to look at before we're done with manifest content. Back in section 10.2 we said that, while manifests are written in PowerShell, they use a restricted subset of the language. The restricted language is used to reduce the potential security risk associated with loading a script. This allows us to load a manifest file to access the metadata without being concerned that we'll execute malicious code. This also means that we need to know how to write these scripts properly. In the next section, we'll look at what we can do in a manifest file.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

362

10.6 Language restrictions in a manifest
Because the manifest is a PowerShell data file, its contents are restricted to a small subset of PowerShell language features. This subset includes the basic PowerShell data types (numbers, strings, hashtables and so on), the if statement and the arithmetic and comparison operators. Things like assignment statement, function definitions and loop statements are not allowed. (See Appendix D for full details on the language limitations.) With only these elements, we'd be limited to using static values for element definitions. This means we wouldn't be able to accommodate for variations in system configuration - things like paths to system directories, software installation directories and drive letters. To allow us to handle these situations, manifests are permitted to read (but not write) the $ENV: environment provider and can use the Join-Path cmdlet to construct paths at runtime. This allows manifest elements to be written in such a way that system differences can be handled. Let's look at an example illustrating how these features can be used. In this example, imagine we have with an application that uses PowerShell for management. This application installs its PowerShell modules in a directory in the module path then puts the rest of the application files in "Program Files". Since the modules need access to some of the resources in the application directory, the application installer will set an environment variable

$ENV:MYAPPDIR at install time that can be used by the manifest to find the necessary resources. A module entry using this environment variable would look like this: RequiredAssemblies = (Join-Path $ENV:MYAPPDIR requiredAssembly.dll) In the fragment, the Join-Path cmdlet is used to generate the absolute path to the required assembly using $ENV:MYAPPDIR. Now, to complicate things, we'll say that this library is processor dependent, and we'll need to load a different .dll based on the setting of the system variable $ENV:PROCESSOR_ARCHITECTURE. This entry would look like: RequiredAssemblies = if ($ENV:PROCESSOR_ARCHITECTURE -eq "X86") { Join-Path $ENV:MYAPPDIR requiredAssembly.dll } else { Join-Path $ENV:MYAPPDIR64 requiredAssembly.dll } In this second example, we're using the if statement to select a different branch based on the processor architecture and then generating the system-specific path using Join-Path. These techniques allow modules to be very flexible when dealing with system variations. READING A MODULE MANIFEST One thing missing from the module manifest story is the ability for scripts to read the manifest files without actually loading the module. This is unfortunate because it limits our ability to write scripts to explore the modules we have. The Test-ModuleManifest cmdlet does process the manifest but it doesn’t return all of the data in the manifest file. Now – since the language in the manifests is a subset of regular PowerShell, it is possible to load the module file contents into a string and then use Invoke-Expression to evaluate it. This will give us the data we want but it means that the module is no longer running in restricted mode. As a workaround, we can use a hybrid approach. First we’ll validate the manifest with TestModuleManifest. This will verify that the manifest only contains permitted elements that are permitted in the restricted language. Then we’ll read and evaluate the module file to get the data. Listing 10.1 shows a function that can be used to do this. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

363

Listing 10.1 The Read-ModuleManifest function function Read-ModuleManifest ($manifestPath) { trap { break } $fullpath = Resolve-Path $manifestPath -ea Stop if (test-modulemanifest $fullPath) { $PSScriptRoot = Split-Path -parent $fullPath $content = (get-content $fullPath) -join "`n" Invoke-Expression $content } } #1 #2 #3 #4 #6 #7 Pass in the manifest path Stop if anything goes wrong Get full path to manifest file Verify that manifest is valid Read the manifest file Evaluated it and return result #1 #2 #3 #4 #5 #6 #7

Let’s use this function to load the BitsTransfer module manifest. PS {1) > cd $pshome\modules\BitsTransfer PS {2) > Read-ModuleManifest .\BitsTransfer.psd1 Name ---ModuleVersion CLRVersion FormatsToProcess PowerShellVersion GUID NestedModules Copyright CompanyName Author RequiredAssemblies Value ----1.0.0.0 2.0 BitsTransfer.Format.ps1xml 2.0 {8FA5064B-8479-4c5c-86EA-... Microsoft.BackgroundIntel... c Microsoft Corporation. ... Microsoft Corporation Microsoft Corporation C:\Windows\System32\Windo...

The output of the function is the hashtable defined in the manifest file. And now, at long last, we're done with manifests! Like bookkeeping and inventory management, manifests are complicated and a bit boring but absolutely necessary when doing production scripting. In the next section, we'll at some features that are less tedious, but (hopefully) more exciting.

10.7 Advanced Module Operations
In this section we'll look at some of the more sophisticated things we can do with modules. These features are not intended for typical day-to-day use but they do allow for some very sophisticated scripting. As always, if you aren't just scripting for yourself, have pity on the person who will have to maintain your code and avoid "stunt-scripting".

10.7.1 The PSModuleInfo Object
PowerShell modules, like everything in PowerShell, are objects we can work with directly. The type of the object used to reference modules is

System.Management.Automation.PSModuleInfo.
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

364 We've actually been looking at these objects all along - this is what Get-Module returns - but we've only been using them to get basic information about a module. In practise, there are a lot of other things that can be done once we have a PSModuleObject. In this section we'll look at what can be done (and try to explain why you'd do these things). INVOCATION IN THE MODULE CONTEXT In our discussion about module scopes, we introduced the concept of a module-level scope which is used to isolate the private variables and functions. When we execute code where function and variable lookup is done in a module scope, we call this "executing in the module context". This is, of course, what happens anytine we execute a function that has been exported from a module. However, we can also cause arbitrary code to be executed in the module context even though it wasn't defined in that context. In effect, we're pushing code into the module context. This is done with a PSModuleInfo object using the call operator '&'.

AUTHOR'S NOTE
Yes - this ability to inject code into a module context violates all the principles of isolation and information hiding. And from a language perspective this is a bit terrifying however people do it all the time when debugging. One of the nice things about dynamic languages is that you are effectively running the debugger attached all the time.

To try this out, we'll need a module object to play with. Let's load our counter module again. First - let's quickly review the contents of the module - we'll use the Select-Object cmdlet to limit what gets output to the first 8 lines as this is all that we're concerned with here. PS (1) > get-content counter.psm1 | select -first 8 $script:count = 0 $script:increment = 1 function Get-Count { return $script:count += $increment } This module has private state in the form of the two variables $count and $increment and one public function Get-Count. Now import it PS (2) > Import-Module .\counter.psm1 and use Get-Module to get the module reference: PS (3) > $m = get-module counter We could have done this in one step with the -PassThru parameter as we saw earlier but we're using two steps here to illustrate that these techniques can be done with any in-memory module. Now run the Get-Count function and it returns one as it should right after the module is first loaded. PS (4) > Get-Count 1 Now set a global variable $count using the Set-Variable command (again we're using the command instead of assignment to set the variable for illustration purposes.) PS (5) > Set-Variable count 33 When we run Get-Count again, of course it returns two since the $count it uses exists in the module context. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

365 PS (6) > Get-Count 2 So far nothing much to see - now let's do something a bit fancier. Let's see what the current value of $count in the module context is. We can do this by invoking Get-Variable in the module context with the call operator: PS (7) > & $m Get-Variable count Name ---count Value ----2

and we see the value is two. Great - now we can inspect the "private" inner state of a module to see what's going on. Next, let's alter that state. We'll execute the same Set-Variable command as before but inside the module this time. PS (8) > & $m Set-Variable count 33 and call Get-Count to verify that we have made a change. PS (9) > Get-Count 34 The call to Get-Count returned 34 so we have successfully changed the value of the variable it uses in its operation. Ok - we know how to get and set state in the module, let's try altering the code. First we'll look at the body of the Get-Count function PS (10) > & $m get-item function:Get-Count CommandType ----------Function Name ---Get-Count Definition ---------...

Now we'll redefine the function in the module. Instead of simply adding the increment, we'll add the increment times 2. PS (11) > & $m { >> function script:Get-Count >> { >> return $script:count += $increment * 2 >> } >> } >> Although we've redefined the function in the module, we have to re-import the module for in order to get the new definition into our function table. PS (12) > Import-Module .\counter.psm1 Now that we've done that, we can call the function again to make sure we're getting what we expected. PS (13) > Get-Count 36 PS (14) > Get-Count 38 and yes, Get-Count is now incrementing by 2 instead of 1. All of these tweaks that we've been doing on the module only affect the module in memory. The module file on disk hasn't change: PS (15) > get-content counter.psm1 | select -first 8 $script:count = 0 $script:increment = 1 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

366 function Get-Count { return $script:count += $increment } If we use the -Force parameter on Import-Module, we'll force the system to reload the file from disk, reinitializing everything to the way it was: PS (16) > Import-Module .\counter.psm1 -Force Verify PS 1 PS 2 PS 3 this by running Get-Count: (17) > Get-Count (18) > Get-Count (19) > Get-Count

Again this is one of the characteristics of dynamic languages - the ability of programs to modify themselves in a profound way are runtime and then restore the original state. In the next section we'll look at how to we can use properties on the PSModuleInfo to access the members of a module without importing them. ACCESSING MODULES EXPORTS USING THE PSMODULEINFO OBJECT The exported members of a module are discoverable through properties on the PSModuleInfo object that represents the module. This gives us a way to look at the exported members without having to import them into our environment. For example, the list of exported functions is available in the ExportedFunctions member. These properties are hashtables, indexed by the name of the exported member. Let's look at some examples of what we can do using these properties. As always, we need a module to work with. In this case, we'll use a dynamic module, which we'll cover in more detail chapter 10. Dynamic modules don't require a file on disk which makes them easy to use for experiments. We'll create a dynamic module and save the

PSModuleInfo object in a variable $m
PS >> >> >> >> (1) > $m = new-module { function foo {"In foo x is $x"} $x=2 export-modulemember -func foo -var x }

and now we can use the export lists on the PSModuleInfo see what was exported. PS (2) > $m | Format-List exported* ExportedCommands ExportedFunctions ExportedCmdlets ExportedVariables ExportedAliases ExportedFormatFiles ExportedTypeFiles : : : : : : : {foo} {[foo, foo]} {} {[x, System.Management.Automation.PSVariable]} {} {} {}

In the output, we see that there is one function and one variable exported. We also see the function turn up in the ExportedCommands member. Modules can export more than one type of command - functions, aliases or cmdlets - and this property exists to provide a convenient way to see all commands regardless of type.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

367

AUTHOR'S NOTE
By implementing the exported member properties as hashtables, we allow you to access and manipulate the state of the module in a fairly convenient way. The downside is that the default output for the exported members is a bit strange, especially for functions where we see things like [foo, foo]. These tables map the name of a command to the CommandInfo object for that command. When the contents of the table are displayed, both the key and the value are displayed as strings and, since the presentation of a CommandInfo object as a string is the name of the object, we see the name twice.

Let's use the ExportedFunctions property to see how the function foo is defined. We'll use the property syntax instead of explicit indexing to access the member because it's easier to write: PS (3) > $m.ExportedFunctions.foo CommandType ----------Function Name ---foo Definition ---------"In foo x is $x"

The value returned from the expression is a CommandInfo object. This means that we can use the call operator '&' to invoke this function: PS (4) > & $m.ExportedFunctions.foo In foo x is 2 We can also use the PSModuleInfo object to change the value of the exported variable $x: PS (5) > $m.ExportedVariables.x.value = 3 Call the function again to validate this change. PS (6) > & $m.ExportedFunctions.foo In foo x is 3 and the return value from the call is the updated value as expected. Next, we'll look at some of the methods on PSModuleInfo objects.

10.7.2 Using the PSModuleInfo methods
The call operator is not the only way to use the module info object. The object itself has a number of methods that can be useful. Let's take a look at some of these methods: PS (20) > [psmoduleinfo].GetMethods() | >> select-string -notmatch '(get_|set_)' >> System.Management.Automation.ScriptBlock NewBoundScriptBlock( System.Management.Automation.ScriptBlock) System.Object Invoke(System.Management.Automation.ScriptBlock, System.Object[]) System.Management.Automation.PSObject AsCustomObject() We'll cover the first two listed Invoke() and NewBoundScriptBlock() and save

AsCustomObject() for chapter 10.
THE INVOKE() METHOD This method is essentially a .NET programmer way of doing what we did earlier with the call operator. Assuming we still have our counter module loaded, let's use this method to reset the count and change the increment to be 5. First get the module info object: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

368 PS (21) > $m = get-module counter Now invoke a scriptblock in the module context using the method: PS (22) > $m.Invoke({$script:count = 0; $script:increment = 5}) The corresponding invocation using the call operator would be & $m {$script:count = 0; $script:increment = 5} which PS 5 PS 10 PS 15 PS is more scripter friendly. Either way, let's try verify the result. (23) > Get-Count (24) > Get-Count (25) > Get-Count (26) >

And the count was reset and Get-Count now increments by 5 instead of 1. Next we'll look at a way to attach modules to a scriptblock. THE NEWBOUNDSCRIPTBLOCK() METHOD In this topic, we're jumping ahead a bit as we won't really covered scriptblocks in depth until chapter 10. Regardless, we push forward and explore module-bound scriptblocks. A module-bound scriptblock is a piece of code - a scriptblock - that has the module context to use attached to it. Normally an unbound scriptblock is executed in the caller's context but once a scriptblock is bound to a module, it always executes in the module context. In fact that's how exported functions work - they are implicitly bound to the module that defined them. Let's use this mechanism to define a scriptblock that will execute in the context of the counter module. First we need to get the module (again). We could use Get-Module as before but now that we know that exported functions are bound to a module, we can just use the

Module property on an exported command to get the module info object. Let's do this with Get-Count.
PS (26) > $gcc = Get-Command Get-Count Now we can get the module for this command PS (27) > $gcc.module ModuleType Name ---------- ---Script counter ExportedCommands ---------------{setIncrement, Get-Count...

Next we need to define the scriptblock we're going to bind. We'll do this and place the scriptblock into a variable. PS (28) > $sb = {param($incr) $script:increment = $incr} This scriptblock takes a single parameter which it uses to set the module level $increment variable. Now let's bind it to the target module. Note that this doesn't bind the module to the original scriptblock. Instead is creates a new scriptblock with the module attached. PS (29) > $setIncrement = $gcc.Module.NewBoundScriptblock( $sb ) Now test using the scriptblock to set the increment. Invoke the scriptblock with the call operator passing in an increment of 10 PS (30) > & $setIncrement 10 And verify that the increment has been changed. PS (31) > Get-Count 110 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

369 PS (32) > Get-Count 120 Ok - good. However, if we want to use this mechanism frequently, it would be useful to actually have a named function. We can do this by assigning to the function drive. PS (33) > ${function:Set-CountIncrement} = $setIncrement Let's test the function. PS (34) > Set-CountIncrement 100 PS (35) > Get-Count 220 PS (36) > Get-Count 320 And now the increment is 100 per the argument to the Set-CountIncrement. Now let's use Get-Command to look at the function we've defined: PS (37) > Get-Command Set-CountIncrement | Format-Table name, module Name ---Set-CountIncrement Module -----counter

and, like Get-Count, it's listed as being associated with the counter module. Now that we've introduced the idea of a function being dynamically attached to a module, we really should have a more in-depth discussion about the context where a function gets evaluated. This is covered in the next section.

10.7.4 The Defining Module vs. the Calling Module
In this section we'll go into more details about how the execution context for a module is established. We covered module scoping in section 9.4.4. By getting a deeper undestanding of the details of how this works, we're setting the stage for some of the more advanced topics we'll cover in chapter 11. Commands always have two module contexts – the context where they were define and the context where they were called from. This is a somewhat subtle concept. Before PowerShell had modules, this wasn’t terribly interesting except for getting filename and line number information for where the function was called and where it was defined. With modules, this distinction becomes more significant. Among other things, the module where the command was defined contains the module specific resources like the manifest PrivateData element mentioned in section 10.6.4. For functions, the ability to access the two contexts allows the function to access the callers variables instead of the module variables. ACCESSING THE DEFINING MODULE The module that a function was defined in can be retrieved by using the expression

$MyInvocation.MyCommand.Module.

Similarly, the module a cmdlet was defined in is

available through the instance property this.MyInvocation.MyCommand.Module. If the function is defined in the global scope (or 'top level'), the module field will be $null. Let's try this. First define a function at the top level. PS (1) > function Test-ModuleContext { >> $MyInvocation.MyCommand.Module >> } >> then run it, formatting the result as a list showing the module name and PrivateData fields. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

370 PS (2) > Test-ModuleContext | fl name,privatedata PS (3) > Nothing was output because the defining module at the top level is always null. Now lets define the function inside a module. We'll use a here-document to create a .psm1 file. PS (4) > @' >> function Test-ModuleContext { >> $MyInvocation.MyCommand.Module >> } >> '@ > TestModuleContext.psm1 >> Now load the file and run the same test command as we did previously. PS (5) > Import-Module ./TestModuleContext.psm1 PS (6) > Test-ModuleContext | fl name,privatedata Name : TestModuleContext PrivateData : This time the result of the function was not null - we see the module name and, of course, the PrivateData filed is empty because there was no module manifest to provide this data. We can remedy this by creating a module manifest to go along with the .psm1 file. This abbreviated manifest defines the minimum - the module version, then module to process and specifies a hash table for PrivateData. PS (7) > @' >> @{ >> ModuleVersion = '1.0.0.0' >> ModuleToProcess = 'TestModuleContext.psm1' >> PrivateData = @{a = 1; b = 2 } >> } >> '@ > TestModuleContext.psd1 >> Now load the module using the manifest and -force to make sure everything gets updated. PS (8) > Import-Module -force ./TestModuleContext.psd1 Run the test command: PS (9) > Test-ModuleContext | fl name,privatedata Name : TestModuleContext PrivateData : {a, b} and we see that the PrivateData field is now also filled in. ACCESSING THE CALLING MODULE The module that a function was called from can be retrieved by using the expression

$PSCmdlet.SessionState.Module. Similarly, the module a cmdlet called from in is available through this.SessionState.Module. In either case, if the command is being invoked from the top level, this value will be null because there is no "global module".

AUTHOR'S NOTE
It’s unfortunate that we didn’t get a chance to wrap the global session state in a module before we shipped. This means that this kind of code has to be special cased for the module being $null some of the time.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

371 WORKING WITH BOTH CONTEXTS Now let's look at a very tricky scenario where we access both contexts at once. This is something that is rarely necessary but, when needed, is absolutely required. In functions and script modules, accessing the module session is trivial since unqualified variables are resolved in the module context by default. To access the callers context we need to use the caller's session state which is available as a property on $PSCmdlet. Let's update the Test-ModuleContext module to access a variable "testv" both in the callers context and the module context. Here's the module definition. PS (1) > @' >> $testv = 123 >> function Test-ModuleContext { >> [CmdletBinding()] param() >> "module testv is $testv" >> $ctestv = $PSCmdlet.SessionState.PSVariable.Get("testv").Value; >> "caller's testv is $ctestv" >> } >> '@ > TestModuleContext.psm1 >> This defines our test function, specifying the cmdlet binding be used so we can access

$PSCmdlet. The module body also defines a module-scoped variable $testv. The test function will emit the value of this variable and then use the expression $PSCmdlet.SessionStaet.PSVariable.Get("testv").Value to get the value of the caller's $testv variable. Let's load the module PS (2) > Import-Module -force ./TestModuleContext.psd1 Now define a global $testv PS (3) > $testv = "456" and run the command. PS (4) > Test-ModuleContext module testv is 123 caller's testv is 456 And we see the module $testv was correctly displayed as "123" and the caller's variable is the global value "456". Now wait a minute you say! We could have done this much more easily by simply specifying $global:testv. This is true if we were only interested in accessing variables at the global level. But sometimes we want to get the local variable in the caller's dynamic scope. Let's try this. We'll define a new function "nested" that will set a local $testv. PS (5) > function nested { >> $testv = "789" >> Test-ModuleContext >> } >> This function-scoped $testv variable is the "caller's variable" we want to access so we should get "789" instead of the global value "456". Let's try this: PS (6) > nested module testv is 123 caller's testv is 789 and it works. The module $testv was returned as "123" and the caller's testv returned the value of the function-scoped variable instead of the global variable. So when would we need this functionality? If we want to write a function that manipulates the callers scope - say something like the Set-Variable cmdlet implemented as a function, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

372 then we'd need this capability. The other time we might need to do this is when we want to access the value of locally scoped configuration variables like $OFS.

10.7.5 Setting module properties from inside a script module
We've talked at length about how manifests are required to set metadata on a module but turns out that there is a way for the script module to do some of this itself during the module load operation. In order to do this it needs to have access to its own PSModuleInfo object during the load. This can be retrieve using the rather awkward expression

$MyInvocation.MyCommand.ScriptBlock.Module however once we have the PSModuleInfo object, the rest is easy. Let's try it out by setting the Description property on our own module.
SETTING THE MODULE DESCRIPTION In this example, we're going to set the Description property for a module from within the module itself. We'll create a module file in the current directory called

setdescription.psm1. Let's look at the contents of this file:
PS (1) > Get-Content .\setdescription.psm1 $mInfo = $MyInvocation.MyCommand.ScriptBlock.Module $mInfo.Description = "My Module's Description on $(get-date)" On the first line of the module, we copy the reference to the PSModuleInfo object into a variable $mInfo. On the second line, we assign a value to the Description property on that object. Let's try it out. We'll import the module PS (2) > Import-Module .\setdescription.psm1 and then call Get-Module, piping into Format-List so we can just see the module name and its description. PS (3) > Get-Module setdescription | >> Format-List name, description >> Name : setdescription Description : My Module's Description on 01/16/2010 21:33:13 and there we go. We've dynamically set the Description property on our module. As well as being able to set this type of metadata entry on the PSModuleInfo object, there are a couple of behaviors we can control as well. We’ll look at how this works in the next two sections.

10.7.6 Controlling when modules can be unloaded
The module AccessMode feature allows us to restrict when a module can be unloaded. There are two flavors of restriction: static and constant. A static module is a module that can't be removed unless the -Force option is used on the Remove-Module cmdlet. A constant module can never be unloaded and will remain in memory until the session that loaded it ends. This model parallels the pattern for making variables and functions constant. To make a module either static or constant, we need to set the AccessMode property on the module’s PSModuleInfo object to the appropriate setting. Set it to ReadOnly for static modules and Constant for constant modules. Let’s look at how this is done. Here’s an example script module called readonly.psm1 that makes itself ReadOnly. PS (1) > Get-Content .\readonly.psm1 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

373 $mInfo = $MyInvocation.MyCommand.ScriptBlock.Module $mInfo.AccessMode = "readonly" The first line of the module is the same as the example in the previous section and retrieves the PSModuleInfo object. The next line sets the AccessMode to "readonly". We’ll load this module and verify the behavior. PS (2) > Import-Module .\readonly.psm1 PS (3) > Get-Module ModuleType Name ---------- ---Script readonly ExportedCommands ---------------{}

We’ve verified that it’s been loaded so let's try and remove it: PS (5) > remove-module readonly Remove-Module : Unable to remove module 'readonly' because it is read -only. Use the -force flag to remove read-only modules. At line:1 char:14 + remove-module Import-Module .\onremove.psm1 then remove it PS (3) > Remove-Module onremove I was removed on 01/16/2010 22:05:00 and the message from the scriptblock was printed confirming that the OnRemove action was executed. And with that, we are done with modules - well - mostly done, there are a few even more advanced techniques that will be covered in chapter 11. For now, let's just review what we've covered.

10.8 Summary
In this chapter, we expanded our knowledge of PowerShell modules to look at features provided to support production-oriented coding in PowerShell. We looked a module manifests and how they are used to add metadata to a module. Next we looked at each of the manifest elements and their role in the process. Finally we looked at some advanced techniques using the module info object. Some important points using modules with manifests are: 36.Production modules are stored in a directory containing the module manifest and content. The metadata or information about a module is contained in a .psd1 file usually with the same name as the module directory. 37.The easiest way to create a module manifest is to use the New-ModuleManifest cmdlet. A second cmdlet Test-ModuleManifest is provided to test an existing module for issues. 38.A manifest let's you define 3 types of information for your module - production, construction and content. Production metadata defines thinks like version number and dependencies. Construction elements control how a module is constructed including specifying any nested modules. Content manifest elements deal with other types of content in the module. In the latter part of the chapter, we looked in more detail at how modules are represented in memory and the kinds of operations we can perform once we have the module information object. Here are some of the key topics we covered: 39.Modules in memory are represented by a PSModuleInfo object. This object allows us to perform a number of advanced scenarios with modules. 40.The PSModuleInfo object for a module can be retrieved using Get-Module. Alternatively, the module object for a function and be retrieved using the Module property on scriptblock for that function. 41.If we have access to the PSModuleInfo object for a module, we can inject code into the module where it will be executed in the module context. This allows us to manipulate the state of a module without having to reload it. This feature is primarily intended for diagnostic and debugging purposes.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

375 42.From within a script module, we can use the PSModuleInfo object to directly set some metadata elements like the module description. 43.PSModuleInfo objects have an AccessMode field that controls the ability to update or remove a module from the session. This field is set to ReadWrite by default but can be set to Static, requiring the use of the -Force parameter to update it or Constant where it cannot be removed from the session. A Constant module remains in the session until the session ends. 44.To set up an action to be taken when a module is removed, a scriptblock can be assigned to the OnRemove property on the PSModuleInfo object for that module. Let's stop for a minute and check where we are. With this chapter, we've now covered all of the core topics necessary for understanding how to script in PowerShell. We covered syntax, operators and data types in chapters 2 through 6. In chapters 7 and 8 we covered functions, scripts and finally, in chapters 9 and 10 we've covered modules. In the next chapter, we'll look at some more advanced programming topics that build on what we've learned. will also engender a deep understand of how PowerShell works. These advanced topics will not only introduce some very powerful new ways of using PowerShell, they

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

376

11
Metaprogramming with ScriptBlocks and Dynamic Code

Philosophy have I digested, The whole of Law and Medicine, From each its secrets I have wrested, Theology, alas, thrown in. sweated lore, I stand no wiser than I was before. —Johann Wolfgang Goethe, Faust Greek letters are cool… —Not actually a quote from Beavis and Butthead Poor fool, with all this

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

377 Chapters 7 and 8 covered the basic elements of programming in PowerShell: functions and scripts. Chapters 9 and 10 introduced modules as a way of aggregating our code into reusable pieces through modules. In this chapter, we’ll take things to the next level and talk about metaprogramming. Metaprogramming is the term used to describe the activity of writing programs that create or manipulate other programs. If you’re not already familiar with this concept, you may be asking why you should care. In chapter 1, we talked about designing classes and how hard it is to get those designs right. In most environments, if the designer makes a mistake then the user is stuck with the result. This is not true in PowerShell. Metaprogramming lets us poke into the heart of the system and make things work the way we need them to. Here’s an analogy that should give you the full picture. Imagine buying a computer that was welded shut. There is still a lot you can do with it—run all the existing programs and even install new programs. But there are some things you can’t do. If it doesn’t have any USB ports then you can’t add them. If it doesn’t have any way to capture video, you can’t add that either without opening the case. And even though most people buy a computer with the basic features they need and never add new hardware, a case that’s welded shut allows for no hardware tinkering. Traditional programming languages are much like that welded computer. They have a basic set of features, and while you can extend what they do by adding libraries, you can’t really extend the core capabilities of the language. For example, you can’t add a new type of looping statement. On the other hand, in a language that supports metaprogramming, you can undertake such activities as adding new control structures. This is how the Where-Object and

Foreach-Object cmdlets are implemented. They use the metaprogramming features in
PowerShell to add what appear to be new language elements. We can even create our own variations of these commands. We’ll begin our investigations with a detailed discussion of PowerShell scriptblocks which are at the center of most of the metaprogramming techniques. This discussion takes up the first part of this chapter and lays the groundwork for the rest of what we’ll discuss. With that material as context, we’ll look at how and where scriptblocks are used in PowerShell. We’ll look at the role scriptblocks play in the creation of custom objects and types, and how they can be used to extend the PowerShell language. We'll cover techniques like proxy functions, dynamic modules and custom objects - all of which are examples of applied metaprogramming. Then we'll move on and look at how we can use similar techniques with static languages like C# from within our scripts. Finally we'll look at events - a related technique that also involves scriptblocks. But first we need to understand scriptblocks themselves.

11.1 Scriptblock basics
In this section we’ll talk about how to create and use scriptblocks. We’ll begin by looking at how commands are invoked so we can understand all the ways to invoke scriptblocks. Next we’ll cover the syntax for scriptblock literals and the various types of scriptblocks you can create. This includes using scriptblocks as functions, as filters, and as cmdlets. Finally we’ll look at how we can use scriptblocks to define new functions at runtime. Let’s dive into the topic by starting with definitions. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

378 In PowerShell, the key to metaprogramming (writing programs that write or manipulate other programs) is the scriptblock. This is a block of script code that exists as an object reference, but does not require a name. The Where-Object and ForEach-Object cmdlets rely on scriptblocks for their implementation. In the example 1..10 | foreach { $_ * 2 } the expression in braces “{ $_ * 2 }” is actually a scriptblock. It’s a piece of code that is passed to the ForEach-Object cmdlet and is called by the cmdlet as needed. So that’s all a scriptblock is—a piece of script in braces—but it’s the key to all of the advanced programming features in PowerShell.

AUTHORS NOTE
What we call scriptblocks in PowerShell are called anonymous functions or sometimes lambda expressions in other languages. The term lambda comes from the lambda calculus developed by Alonzo Church and Stephen Cole Kleene in the 1930s. A number of languages, including Python and dialects of LISP, still use lambda as a language keyword. In designing the PowerShell language, we felt that calling a spade and spade (and a scriptblock a scriptblock) was more straightforward (the coolness of using Greek letters aside).

We’ve said that scriptblocks are anonymous functions, and of course functions are one of the four types of commands. But wait! You invoke a command by specifying its name. If scriptblocks are anonymous, they have no name—so how can you invoke them? This necessitates one more diversion before we really dig into scriptblocks. Let’s talk about how commands can be executed.

11.1.1 Invoking commands
The way to execute a command is just to type its name followed by a set of arguments, but sometimes you can’t type the command name as is. For example, you might have a command with a space in the name. You can’t simply type the command because the space would cause part of the command name to be treated as an argument. And you can’t put it in quotes, as this turns it into a string value. So you have to use the call operator “&”. If, for instance, you have a command called “my command”, you would invoke this command by typing the following: & "my command" The interpreter sees the call operator and uses the value of the next argument to look up the command to run. This process of looking up the command is called command discovery. The result of this command discovery operation which is tells an the object of type what

System.Management.Automation.CommandInfo,

interpreter

command to execute. There are different subtypes of CommandInfo for each of the types of PowerShell commands. In the next section, we’ll look at how to obtain these objects and how to use them.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

379 GETTING COMMANDINFO OBJECTS We’ve used the Get-Command cmdlet before as a way to attain information about a command. For example, to get information about the Get-ChildItem cmdlet, you’d do the following: PS (1) > get-command get-childitem CommandType ----------Cmdlet command, and so on. Name ---Get-ChildItem Definition ---------Get-ChildItem [[-Pat...

This shows you the information about a command: the name of the command, the type of

AUTHOR'S NOTE
In the previous Get-Command example, the command’s definition was truncated to fit the book-formatting requirements. You can control how this information is described by using the Format-List and Format-Table commands.

This is useful as a kind of lightweight help, but in addition to displaying information, the object returned by Get-Command can be used with the call operator to invoke that command. This is pretty significant. This extra degree of flexibility, invoking a command indirectly, is the first step on the road to metaprogramming. Let’s try this out—we’ll get the CommandInfo object for the Get-Date command. PS (1) > $d = get-command get-date PS (2) > $d.CommandType Cmdlet PS (3) > $d.Name Get-Date As we can see from this example, the name “get-date” resolves to a cmdlet with the name “get-date”. Now let’s run this command using the CommandInfo object with the call operator: PS (4) > & $d Sunday, May 21, 2006 7:29:47 PM It’s as simple as that. So why do we care about this? Because it’s a way of getting a handle to a specific command in the environment. Say we defined a function “get-date”. PS (1) > function get-date {"Hi there"} PS (2) > get-date Hi there Our new get-date command outputs a string. Because PowerShell looks for functions before it looks for cmdlets, this new function definition hides the Get-Date cmdlet. Even using “&” with the string “get-date” still runs the function: PS (3) > & "get-date" Hi there Since we created a second definition for get-date (the function), now if you use Get-

Command you will see two definitions. So how do we unambiguously select the cmdlet GetDate?
PS (4) > get-command get-date

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

380 CommandType ----------Cmdlet Function Name ---Get-Date get-date Definition ---------Get-Date [[-Date] get-command -commandtype cmdlet get-date CommandType ----------Cmdlet Name ---Get-Date Definition ---------Get-Date [[-Date] $ci = get-command -commandtype cmdlet get-date and then run it using the call operator. PS (7) > &$ci Sunday, May 21, 2006 7:34:33 PM The Get-Date cmdlet was run as expected. Another way to select which command to run, since Get-Command returns a collection of objects, is to index into the collection to get the right object: PS (8) > &(get-command get-date)[0] Sunday, May 21, 2006 7:41:28 PM Here we used the result of the index operation directly with the call operator to run the desired command. This is all interesting, but what does it have to do with scriptblocks? We’ve demonstrated that you can invoke a command through an object reference instead of by name. This was the problem we set out to work around. Scriptblocks are functions that don’t have names; so, as you might expect, the way to call a scriptblock is to use the call operator. Here’s what that looks like: PS (1) > & {param($x,$y) $x+$y} 2 5 7 In this example, the scriptblock is {param($x,$y) $x+$y} We used the call operator to invoke it with two arguments, 2 and 5, so the call returns 7. This is how we can execute a function if doesn't have a name. As long as we have access to the scriptblock, we can call it.

11.1.2 The ScriptBlock literal
Scriptblocks are the center of pretty much everything we do in this chapter. Since the most common form of scriptblock is the scriptblock literal, it's worth investing some time looking at them in detail. We’ll look at how to specify a scriptblock literal that acts as a function, how to specify one that acts like a filter, and finally how to define a scriptblock cmdlet. What we’ve been writing to create scriptblocks is called a scriptblock literal—in other words, a chunk of legitimate PowerShell script surrounded by braces. The syntax for this literal is shown in figure 11.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

381 param Keyword The list of parameters for the function. List of statements that make up the scriptblock body.

{ param ( ) }

Braces mark beginning and end of the scriptblock body.

Figure 11.1 This shows how to define a simple scriptblock. Note that the param statement is optional, so a minimal scriptblock only has the braces.

The definition of a scriptblock looks more or less like the definition of a function, except the

function keyword and function name are missing. If the param statement is not present then the scriptblock will get its arguments through $args, exactly as a function would.

AUTHOR'S NOTE
The param statement in PowerShell corresponds to the lambda keyword in other languages. For example, the PowerShell expression

& {param($x,$y) $x+$y} 2 5 is equivalent to the LISP expression

(lambda (x y) (+ x y)) 2 5) or the Python expression

(lambda x,y: x+y)(2,5)
Also note that, unlike Python lambdas, PowerShell scriptblocks can contain any collection of legal PowerShell statements.

Scriptblocks, like regular functions or scripts, can also behave like cmdlets. In other words, they can have one or all of the begin, process, or end clauses that you can have in a function or script. Figure 11.2 shows the most general form of the scriptblock syntax, showing all three clauses.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

382 param Keyword { param ( ) begin { } process { } end { } } List of statements to process in the begin phase List of statements to process for each pipeline object List of statements to process during the end phase List of formal parameters to the function

Figure 11.2 How to define a scriptblock that works like a cmdlet.

As was the case with a regular function , you don’t have to define all the clauses. Here’s an example that uses only the process clause. PS (1) > 1..5 |&{process{$_ * 2}} 2 4 6 8 10 A scriptblock written this way works like the filters we saw in chapter 7. It also works like the

ForEach-Object cmdlet, as shown in the next example:
PS (2) > 1..5 |foreach {$_ * 2} 2 4 6 8 10 The ForEach-Object cmdlet is effectively a shortcut for the more complex scriptblock construction. As we've been going along, we keep talking about how scriptblocks are really anonymous functions. This is a good time to see how scriptblocks and named functions are related.

11.1.3 Defining functions at runtime
In earlier sections, we said that scriptblocks were functions without names. The converse is also true—functions are scriptblocks with names. So what exactly is the relationship between the two? In chapter 7, we showed you how to manage the functions in your PowerShell session using the function: drive. To get a list of functions, you could do a dir of that drive: dir function:/ You could also delete or rename functions. But we didn’t cover the whole story. In fact, the function: drive is, in effect, a set of variables containing scriptblocks. Let’s explore this further. We’ll define our favorite function foo: PS (1) > function foo {2+2} PS (2) > foo 4 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

383 We can use the dir cmdlet to get the command information from the function provider: PS (3) > dir function:foo CommandType ----------Function Name ---foo Definition ---------2+2

Let’s use Get-Member to get more information about the object that was returned: PS (4) > dir function:foo | gm sc* TypeName: System.Management.Automation.FunctionInfo Name MemberType Definition ------------- ---------ScriptBlock Property System.Management.Automation.ScriptBlo... The object that came back to us was a FunctionInfo object. This is the subclass of

CommandInfo that is used to represent a function. As we see, one of the properties on the object is the scriptblock that makes up the body of the function. Let’s retrieve that member: PS (5) > (dir function:foo).ScriptBlock 2+2 The scriptblock, when displayed as a string, shows the source code for the scriptblock. Another, simpler way to get back the scriptblock that defines a function is to use the variable syntax: PS (6) > $function:foo 2+2 PS (7) > $function:foo.gettype().Fullname System.Management.Automation.ScriptBlock Now here’s the interesting part. Let’s change the definition of this function. We can do this simply by assigning a new scriptblock to the function: PS (8) > $function:foo = {"Bye!"} When we run the function again, PS (9) > foo Bye! we see that it’s changed. The function keyword is, in effect, shorthand for assigning to a name in the function provider. Now that we know how to manipulate scriptblocks and functions, let’s take this one step further. As discussed in chapter 1, objects encapsulate data and code. We've spent a lot of time on data in the earlier chapters and now we have a way of manipulating code too. This means that we're ready to take the next step and see how we can use data and scriptblocks to build our own objects.

11.2 Building and manipulating objects
Let's kick our scripting up a notch and look at ways to build custom objects. Up to this point in the chapter we’ve been talking about scriptblocks as standalone functions. Now it’s time to talk about how to use scriptblocks to build objects. At their core, as discussed in chapter 1, objects are a binding of data and behaviors. These behaviors are implemented by blocks of script. We needed to know how to build the blocks of code, scriptblocks, before we could talk about building objects. With a good understanding of scriptblocks, we may now discuss manipulating and building objects in PowerShell.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

384 In chapter 2, we talked extensively about types. Now we’re concerned with objects; that is, instances of types. A type is the pattern or template that describes an object, and an object is an instance of that pattern. In statically typed languages such as C#, once an object is instantiated, its interfaces can’t be changed. With dynamic languages such as PowerShell (or Ruby or Python), this isn’t true. Dynamic languages allow us to alter the set of members available at runtime.

AUTHOR'S NOTE
As of C# 4.0, the language is no longer strictly statically typed. C# 4.0 introduced a new "dynamic" keyword allowing us to write programs that have with dynamic types.

In the rest of this section, we’ll cover how to manipulate objects and types in PowerShell. We start with a discussion of how to examine existing members, followed by a look at the types of members available on an object. Then we’ll cover the various ways to add members to an object, and finally we’ll take a look at the plumbing of the PowerShell type system to give you a sense of the flexibility of the overall system and how it facillitates our goal of writing programs to manipulate programs - .

11.2.1 Looking at members
An object’s interface is defined by the set of public members it exposes. Public members are the public fields, properties, and methods of the class. As always, the easiest way to look at those members is with the Get-Member cmdlet. For example, here are the members defined on an integer: PS (1) > 12 | get-member TypeName: System.Int32 Name ---CompareTo Equals GetHashCode GetType GetTypeCode ToString MemberType ---------Method Method Method Method Method Method Definition ---------System.Int32 CompareTo(Int32 value), S... System.Boolean Equals(Object obj), Sys... System.Int32 GetHashCode() System.Type GetType() System.TypeCode GetTypeCode() System.String ToString(), System.Strin...

Note that this doesn’t show you all of the members on an [int]. It only shows you the instance members. You can also use Get-Member to look at the static members: PS (2) > 12 | get-member -static TypeName: System.Int32 Name ---Equals Parse ReferenceEquals TryParse MaxValue MemberType ---------Method Method Method Method Property Definition ---------static System.Boolean Equals(Objec... static System.Int32 Parse(String s... static System.Boolean ReferenceEqu... static System.Boolean TryParse(Str... static System.Int32 MaxValue {get;}

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

385 MinValue of sections. DEFINING SYNTHETIC MEMBERS One of the most powerful features in the PowerShell environment is the ability to extend existing object types and instances. This allows PowerShell to perform adaptation across a wide variety of different types of data. By adaptation, we mean overlaying a common set of interfaces onto existing data sources. This may be as simple as unifying the name of the property that counts a collection to be the string “count” across all countable objects, or as complex as taking a string containing some XML data and being able to treat that string as an object with a set of properties and attributes. This isn’t the same as subclassing or creating derived types as you would in traditional object-oriented programming languages. In those languages, if you want to extend a new type, you can only do it by creating an entirely new type. In dynamic languages such as PowerShell, you can add members to existing types and objects. This sounds odd from the point of view of a conventional object-oriented language, since types and member definitions are so tightly tied together. In languages such as PowerShell, it’s possible to have objects that don’t really have any type at all. Property static System.Int32 MinValue {get;} We’ll use this mechanism to look at the members we’ll be adding to objects in the next couple

AUTHOR'S NOTE
If you’re a JavaScript user, this won’t be surprising. The object-oriented mechanisms in JavaScript use a mechanism called “Prototypes”. Prototype-based systems don’t have types as discrete objects. Instead you use an object that has the set of members you want to use and use it as the prototype for your new object. While PowerShell is not strictly a prototypebased language, its type extension mechanisms can be used in much the same way.

Since the members we’ll be adding to objects aren’t natively part of the object’s definition, we call them synthetic members. Synthetic members are used extensively throughout PowerShell for adaptation and extension. Let’s take a look at an example. First we’ll examine the synthetic properties on an object returned by dir from the filesystem: PS (6) > dir $profile | get-member ps* TypeName: System.IO.FileInfo Name ---PSChildName PSDrive PSIsContainer PSParentPath PSPath PSProvider MemberType ---------NoteProperty NoteProperty NoteProperty NoteProperty NoteProperty NoteProperty Definition ---------System.String PSChildName=Microsof... System.Management.Automation.PSDri... System.Boolean PSIsContainer=False System.String PSParentPath=Microso... System.String PSPath=Microsoft.Pow... System.Management.Automation.Provi...

Now let’s get the same information from the registry: PS (8) > dir hklm:\software | get-member ps* TypeName: Microsoft.Win32.RegistryKey ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

386 Name ---PSChildName PSDrive PSIsContainer PSParentPath PSPath PSProvider MemberType ---------NoteProperty NoteProperty NoteProperty NoteProperty NoteProperty NoteProperty Definition ---------System.String PSChildName=Adobe System.Management.Automation.PSDri... System.Boolean PSIsContainer=True System.String PSParentPath=Microso... System.String PSPath=Microsoft.Pow... System.Management.Automation.Provi...

You can see the same set of PS* properties with the PowerShell (PS) prefix on the object, even though they are completely different types. Take a look at these properties. They allow you to work with these two different objects in the same way. This means that you can always tell if an object might have children by looking at the PSIsContainer property, regardless of the type of the underlying object. And you can always get the path to the object through the

PSPath property. We call this type of adaptation object normalization. By adding this set of synthetic properties to all objects returned from the provider infrastructure, it becomes possible to write scripts that are independent of the type of object that the provider surfaces. This makes the scripts both simpler and more reusable. In the next section we'll start looking at ways to create synthetic members.

11.2.3 Using Add-Member to extend objects
The Add-Member cmdlet is the easiest way to add a new member to an object instance, either a static .NET object type or a custom synthetic object. It can be used to add any type of member supported by the PowerShell type system. The list of possible member types that can be added with Add-Member is shown in table 11.1.

11.1 Member types that can be added with Add-Member
Member Type AliasProperty Version V1,V2 Description An alias property provides an alternate name for an existing property. For example, if there is an existing Length property then you might alias this to Count. A property that maps to a static method on a .NET class. A native property on the object. In other words, a property that exists on the underlying object that is surfaced directly to the user. For example, there might be a native property Length that we choose to also make available through an extended alias member. A data-only member on the object (equivalent to a .NET field). A property whose value is determined by a piece of PowerShell script. The collection of properties exposed by this object. A named group of properties. A native method on the underlying object. For example, the

CodeProperty Property

V1,V2 V1,V2

NoteProperty ScriptProperty

V1,V2 V1,V2

Properties PropertySet Method

V1,V2 V1,V2 V1,V2

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

387 SubString() method on the class System.String shows up as a method. CodeMethod ScriptMethod ParameterizedProperty V1,V2 V1,V2 V1,V2 A method that is mapped to a static method on a .NET class. A method implemented in PowerShell script. A property that takes both arguments and a value to assign. This is typically used for things line indexers and might look like: $collection.item(2.3) = "hello" This sets the element at 2,3 in the collection to the value “hello”. A property that is backed by a variable. This type of member was introduced in version 2 along with modules. It has an advantage over note properties because it can be type-constrained.

PSVariableProperty

V2

We’ll work through some examples showing how to use these members. We’ll use an instance of the string “Hi there” to do this. For convenience, we’ll store it in a variable $s as shown: PS (1) > $s = "Hi there" Now let’s go over how we add these member types to object instance. ADDING ALIASPROPERTY MEMBERS The first type of synthetic member we’ll add is called an alias property. This property allows you to provide a new name for an existing property. Let’s work with the length property on a string. PS (2) > $s.length 8 As we can see, this string has a length of 8. Let’s say that we want to add an alias “size” for length because we’ll be working with a set of objects that all have a size property. PS (3) > $s = add-member -passthru -in $s aliasproperty size length There are a couple things to note in this example. First (and most important) is that when you first add a synthetic member to an object, you’re really creating a new object (but not a new type). This new object wraps the original object in an instance of

System.Management.Automation.PSObject. Just as System.Object is the root of the type system in .NET, PSObject is the root of the synthetic type system in PowerShell. For this reason, we assign the result of the Add-Member call back to the original variable. To do this, we have to add the -passthru parameter to the command since, by default, the AddMember cmdlet doesn’t emit anything. Now let’s look at the new member we’ve added using gm (the alias for Get-Member).
PS (4) > $s | gm size TypeName: System.String Name MemberType Definition ---- ------------------size AliasProperty size = length Again, there are a couple things to note. We can see that the size member is there and is an alias property that maps size to length. Also we need to note that the object’s type is still

System.String. The fact that it’s wrapped in a PSObject is pretty much invisible from the
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

388 script user’s view, though you can test for it as shown in the next example. Using the -is operator, you can test to see whether the object you’re dealing with is wrapped in a PSObject or not. PS (5) > $s -is [PSObject] True PS (6) > "abc" -is [PSObject] False PS (7) > $s -is [string] True The result of the first command in the example shows that $s does contain a PSObject. The second command shows that the raw string doesn’t, and the last line shows that the object in $s is still considered a string, even though it’s also a PSObject. The question now is, after all that explanation, did we actually create this aliased member? The answer is yes: PS (8) > $s.size 8 PS (9) > $s.length 8 Both the size and length members return the value 8. ADDING NOTEPROPERTY MEMBERS Now let’s add a note property. A note property is simply a way of attaching a new piece of data (a note) to an existing object, rather like putting a sticky note on your monitor. Again we’ll use the same string in $s. Let’s add a note property called description. In this example, since we know that $s is already wrapped in a PSObject, we don’t need to use -passthru and do the assignment—we simply add the property to the existing object. PS (10) > add-member -in $s noteproperty description "A string" PS (11) > $s.description A string We see that we’ve added a “description” property to the object with the value “A string”. And, to prove that this property isn’t present on all strings, we do PS (12) > "Hi there".description PS (13) > and see that the property returned nothing. Of course, the note property is a settable property, so we can change it with an assignment like any other settable property. PS (14) > $s.description = "A greeting" PS (15) > $s.description A greeting In this example, we changed the value in the note property to “A greeting”. Note properties allow you to attach arbitrary data to an object. They aren’t type constrained, so they can hold any type. Let’s set the description property to a [datetime] object: PS (16) > $s.description = get-date PS (17) > $s.description Sunday, May 28, 2006 4:24:50 PM But the value stored in the object is still a [datetime] object, not a string. As such, we can get the DayOfWeek property out of the description property. PS (18) > $s.description.dayofweek Sunday ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

389 PS (19) > $s.description.gettype().fullname System.DateTime ADDING SCRIPTMETHOD MEMBERS Both of the synthetic members we’ve added so far have been pure data properties; no code was involved. Now we’ll look at adding members that execute code. We’ll start with

ScriptMethods, since they’re easiest. Let’s add a method that returns the string that it’s associated with, reversed. First let’s find an easy way to reverse a string. If we examine

[string], we’ll see that there is (unfortunately) no reverse method on the string class. There is, however, a static reverse method on [array] that we can use.
PS (1) > [array] | gm -static reverse TypeName: System.Array Name MemberType Definition ------------- ---------Reverse Method static System.Void Reverse(Array array), s... This method takes an array and, since it’s void, it must (obviously) reverse the array in place. This tells us two things: we need to turn the string into an array (of characters) and then save it in a variable so it can be reversed in place. Converting the string to an array of characters is simple—we can just use a cast. PS (19) > $s Hi there PS (20) > $a = [char[]] $s Casting a string into the type [char[]] (array of characters) produces a new object that is the array of individual characters in the original string; and just to verify this: PS (21) > $a.gettype().fullname System.Char[] PS (22) > "$a" H i t h e r e We see that the type of the new object is [char[]] and it does contain the expected characters. Now let’s reverse it using the [array]::reverse() static method. PS (23) > [array]::reverse($a) PS (24) > "$a" e r e h t i H When we look at the contents of the array, we see that it has been reversed. But it’s still an array of characters. The final step is to turn this back into a string. We could simply cast it or use string interpolation (expansion), but that means that we have to set $OFS to get rid of the extra spaces this would introduce (see chapter 3 for an explanation of this). Instead, we’re going to use the static join() method available on the string class. PS (25) > $ns = [string]::join("", $a) PS (26) > $ns ereht iH PS (27) > $ns.gettype().fullname System.String At this point we have the reversed string in $ns. But the goal of this effort was to attach this as a method to the string object itself. To do so, we need to construct a scriptblock to use as the body of the ScriptMethod. This definition looks like: PS (28) > $sb = { >> $a = [char[]] $this >> [array]::reverse($a) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

390 >> >> } >> [string]::join('',$a)

This example introduces a new “magic” variable which is only defined for scriptblocks that are used as methods or properties: the $this variable. $this holds the reference to the object that the ScriptMethod member was called from. Now let’s bind this scriptblock to the object as a

ScriptMethod using Add-Member:
PS (29) > add-member -in $s scriptmethod Reverse $sb Finally let’s try it out: PS (30) > $s.reverse() ereht iH We get the reversed string as desired. ADDING SCRIPTPROPERTY MEMBERS The next type of member we’ll look at is the ScriptProperty. A ScriptProperty has up to two methods associated with it—a getter and (optionally) a setter, just like a .NET property. These methods are expressed using two scriptblocks. As was the case with the

ScriptMethod, the referenced object is available in the $this member. And, in the case of the setter, the value being assigned is available in $args[0]. Here’s an example. We’re going to add a ScriptProperty member “desc” to $s that will provide an alternate way to get at the description NoteProperty we added earlier, with one difference. We’re only going to allow values to be assigned that are already strings. An attempt to assign something that isn’t a string will result in an error. Here is the definition of this property: PS (31) > Add-Member -in $s scriptproperty desc ` >> {$this.description} ` >> { >> $t = $args[0] >> if ($t -isnot [string]) { >> throw "this property only takes strings" >> } >> $this.description = $t >> } >> The first scriptblock: {$this.description} is the code that will be executed when getting the property’s value. All it does is return the value stored in the description NoteProperty. Since the setter needs to do some additional work, its scriptblock is more complex: { $t = $args[0] if ($t -isnot [string]) { throw "this property only takes strings" } $this.description = $t } First it saves the value to be assigned into a local variable $t. Next it checks whether this variable is of the correct type. If not, it throws an exception, failing the assignment.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

391 Let’s try out this property. First let’s directly set the note property to the string “Old description”. PS (32) > $s.description = "Old description" Now we’ll use the ScriptProperty getter to retrieve this value. PS (33) > $s.desc We see that it’s changed as expected. Next we’ll use the ScriptProperty to change the description. PS (34) > $s.desc = "New description" Verify the change by checking both the NoteProperty directly and the ScriptProperty. PS (35) > $s.description New description PS (36) > $s.desc New description PS (37) > Yes, it’s been changed. Now let’s try assigning a [datetime] object to the property as we did with the description NoteProperty previously. PS (37) > $s.desc = get-date Exception setting "desc": "this property only takes strings" At line:1 char:4 + $s.d Get-NextCount 1 PS (3) > Get-NextCount 2 which works properly. And, since it wasn't exported, there is no variable $c: PS (4) > $c (or at least not one related to this dynamic module.) Now let's use try and use Get-Module to look at the module information: PS (5) > get-module and we don't see anything. So what happened? Well - dynamic modules are objects like everything else in PowerShell. The New-Module cmdlet has simply created a new module object but hasn't added it to the module table. This is why we assigned the output of the cmdlet to a variable - so we'd have a reference to the module object. Let's look at that object: PS (6) > $dm | fl : __DynamicModule_55b674b0-9c3c-4db0-94a3-a095 a4ac984e Path : 55b674b0-9c3c-4db0-94a3-a095a4ac984e Description : ModuleType : Script Version : 0.0 NestedModules : {} ExportedFunctions : Get-NextCount ExportedCmdlets : {} ExportedVariables : {} ExportedAliases : {} The interesting fields are the name and path fields. Since there is (obviously) no file associated with the module, we had to make up a unique "path" for that object. Likewise, we didn't specify a name so the runtime made one up for us. So why did it do these things? It did this because, while a dynamic module is not registered by default, it can be added to the module table simply by piping it to Import-Module. Let's try this: PS (6) > $dm | import-module Now check the module table: PS (7) > get-module Name

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

396 ModuleType Name ExportedCommands ---------- ------------------Script __DynamicModule_b6dea7... Get-NextCount and there it is, ugly name and all. Now you can give a dynamic module a specific name using the -Name parameter on the New-Module cmdlet. Let's try this. But first, let's clean up from the last example: PS (1) > get-module | remove-module and define PS (2) >> >> >> >> >> a new dynamic module, with the same body as last time: > new-module Dynamic1 { $c=0 function Get-NextCount { $script:c++; $script:c }} | import-module

Rather than saving the result to a variable, we're piping it directly to Import-Module. Now let's look at the result: PS (3) > get-module ModuleType Name ---------- ---Script Dynamic1 ExportedCommands ---------------Get-NextCount

This time the module is registered in the table with a much more reasonable name. So when would we use dynamic modules? When we need to create a function or functions that have persistent resources that we don't want to expose at the global level. This is basically the same scenario as in the on-disk case, but this way we can package the module or modules to load into a single script file. There is also another way the dynamic module feature is used - to implement the idea of closures in PowerShell. Let's move on and look at how that works.

11.4.2 Closures in PowerShell
PowerShell uses dynamic modules to create dynamic closures. A "closure" in computer science terms (at least as defined in Wikipedia) is “a function that is evaluated in an environment containing one or more bound variables”. A “bound variable” is, for our purposes, simply a variable that exists and has a value. The “environment” in our case is the dynamic module. Finally the function is just a scriptblock. In effect, a closure is the inverse of an object as discussed in chapter 1. An object is data with methods (functions) attached to that data. A closure is a function with data attached to that method. The best way to understand what all of this means is to look at an example. In this example we’re going to use closures to create a set of counter functions, similar to what we did in chapter 9. We’ll allow you to specify the increment that the counter will use. Each time we call the function, it will return the next number in the sequence. Here’s the basic function: function New-Counter ($increment=1) { $count=0; { $script:count += $increment $count }.GetNewClosure() ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

397 } There’s nothing we haven’t seem so far - we create a variable and then a scriptblock that increments that variable - except for returning the result of the call to the GetNewClosure() method. Let’s try this function out to see what it does. First we create a counter. PS (1) > $c1 = New-Counter PS (2) > $c1.GetType().FullName System.Management.Automation.ScriptBlock Looking at the type of the object returned, we see that it’s a scriptblock so we use the & operator to invoke it. PS (3) > & $c1 1 PS (4) > & $c1 2 The scriptblock works as one would expect a counter to work. Each invocation returns the next number in the sequence. Now let’s create a second counter but this time we’ll set the increment to 2. PS (5) > $c2 = New-Counter 2 And now we’ll invoke the second counter scriptblock. PS (6) > & $c2 2 PS (7) > & $c2 4 PS (8) > & $c2 6 and it counts up by two. Now what about the first counter? Let’s see: PS (9) > & $c1 3 PS (10) > & $c1 4 The first counter continues to increment by one, unaffected by the second counter. So the key thing to notice is that each counter instance has its own copies of the $count and

$increment variables. When a new closure is created, a new dynamic module is created and then all of the variables in the caller’s scope are copied into this new module. Here are some more examples of working with closures to give you an idea of how flexible the mechanism is. First we'll create a new closure using a param block to set the bound variable $x. This is essentially the same as the previous example except that we're using a scriptblock to establish the environment for the closure instead of a named function. PS (11) > $c = & {param ($x) {$x+$x}.GetNewClosure()} 3.1415 Now we'll evaluate the newly created closed scriptblock: PS (12) > & $c 6.283 This evaluation returns the value of the parameter added to itself. Since closures are implemented using dynamic modules, we can use the same mechanisms we saw in chapter 9 for manipulating a modules state to manipulate the state of a closure. We can do this by accessing the module object attached to the scriptblock. We'll use this object to reset the module variable $x by evaluating sv (Set-Variable) in the closure's module context: PS (13) > & $c.Module sv x "Abc" Now evaluate the scriptblock to verify that it's been changed: PS (14) > & $c ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

398 AbcAbc Next create another scriptblock closed over the same module as the first one. We can do this by using the NewBoundScriptBlock() method on the module to create a new scriptblock attached to the module associated with the original scriptblock: PS (15) > $c2 = $c.Module.NewBoundScriptBlock({"x ia $x"}) And execute the new scriptblock to verify that it's using the same $x: PS (16) > & $c2 x ia Abc Now use $c2.module to update the shared variable: PS (17) > & $c2.module sv x 123 PS (18) > & $c2 x ia 123 And verify that it's also changed for the original closed scriptblock PS (19) > & $c 246 Finally create a named function from the scriptblock using the function provider: PS (20) > $function:myfunc = $c and verify that calling the function by name works PS (21) > myfunc 246 Set the closed variable yet again, but using $c2 to access the module this time: PS (22) > & $c2.Module sv x 3 And verify that it's changed when we call the named function: PS (23) > myfunc 6 These examples should give you an idea how all of these pieces - scriptblocks, modules, closures and functions are all related. In fact, this is how modules work. When a module is loaded, the exported functions are really closures bound to the module object that was created. These closures are assigned to the names for the functions to import. A fairly small set of types and concepts allow us to achieve some very advanced programming scenarios. In the next section, we'll go back to looking at objects and see how dynamic modules make it easier to create custom object instances.

11.4.3 Creating custom objects from modules
There's one more thing we can do with dynamic modules - provide a simpler way to build custom objects. This is a logical step since modules have private data and public members just like objects. While, as modules, they are intended to address a different type of problem than objects, given the similarity between objects and modules, it would make sense to be able to construct an object from a dynamic module. This is done using the -AsCustomObject parameter on New-Module. We'll use this mechanism to create a "point" object from a module. Here's what this looks like: PS (1) > function New-Point >> { >> New-Module -ArgumentList $args -AsCustomObject { >> param ( >> [int] $x = 0, >> [int] $y = 0 >> ) >> function ToString() >> { ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

399 >> >> >> >> >> } >> "($x, $y)" } Export-ModuleMember -Function ToString -Variable x,y }

Now let's try it - we'll define two points in $p1 and $p2. PS (2) > $p1 = New-Point 1 1 PS (3) > $p2 = New-Point 2 3 We'll use string expansion to display these objects which will call the ToString() method we exported from the module. PS (4) > "p1 is $p1" p1 is (1, 1) PS (5) > "p2 is $p2" p2 is (2, 3) Now let's try and assign a string to the X member on one of the points. PS (6) > $p1.X = "Hi" Cannot convert value "Hi" to type "System.Int32". Error: "Input string was not in a correct format." At line:1 char:5 + $p1. $dlls = gci $pshome -filter *.dll Now loop through this list passing each element to the steppable pipeline. PS (6) > foreach ($dll in $dlls) { $sp.Process($dll) } Name ---CompiledComposition.Micros... PSEvents.dll pspluginwkr.dll pwrshmsg.dll pwrshsip.dll and the Dispose() method to clean up the pipeline. PS (7) > $sp.End() PS (8) > $sp.Dispose() PS (9) > Now that we have an idea of how steppable pipelines work, let's see what we can really use them for. Length -----126976 20480 173056 2048 28672

And we see that each element is process through the pipeline. Finally we call the End() method

11.5.2 Creating a proxy command with steppable pipelines
In chapter 2, we discussed how the result of all of the things we type at the command line are streamed through Out-Default to display them on the screen. In fact Out-Default uses steppable pipelines to run the formatter cmdlets to do it's rendering and the calls Out-Host to display the formatted output. Let's see how we can add a frequently requested feature to the interactive experience using a proxy for Out-Default. A commonly requested feature for interactive use is to capture the result of the last output object so it can be made available to the next pipeline. It should work like this: Firsts we enter a command that displays a result. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

402 PS (1) > 2+2 4 We want to use that result in the next command we type so I should be available in a variable called $last. This would let us do subsequent calculations like: PS (2) > $last+3 7 PS (3) > $last*7 49 This would be a nice feature but it didn't make it into the product. Fortunately, with steppable pipelines and proxy functions, we can add this feature ourselves. The trick is to wrap the Out-

Default cmdlet in a proxy function. As mentioned in section 11.1.3, since functions are resolved before cmdlets, when the PowerShell host calls Out-Default to display output, it will call our function first. Now we could simply collect all of the output from the command the user typed and display it all at once but that doesn't provide a very good experience. Instead we'll create a steppable pipeline that runs the Out-Default cmdlet inside the Out-Default function. Every time the function receives an object, this object will be passed to the steppable pipeline for be rendered immediately. In the process of passing this object along, we can also assign it to the global $LAST variable. The function to do all of this is shown in listing 11.1.

Listing 11.1 Wrapper for the Out-Default cmdlet function Out-Default { [CmdletBinding(ConfirmImpact="Medium")] param( [Parameter(ValueFromPipeline=$true)] ` [System.Management.Automation.PSObject] $InputObject ) begin { $wrappedCmdlet = $ExecutionContext.InvokeCommand.GetCmdlet( "Out-Default") $sb = { & $wrappedCmdlet @PSBoundParameters } $__sp = $sb.GetSteppablePipeline() $__sp.Begin($pscmdlet) } process { $do_process = $true if ($_ -is [System.Management.Automation.ErrorRecord]) { if ($_.Exception -is #4 [System.Management.Automation.CommandNotFoundException]) { $__command = $_.Exception.CommandName if (test-path -path $__command -pathtype container) { set-location $__command #5 $do_process = $false } elseif ($__command -match ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

#1 #2 #3

Licensed to Andrew M. Tearle

403 '^http://|\.(com|org|net|edu)$') { if ($matches[0] -ne "http://") { $__command = "HTTP://" + $__command } [diagnostics.process]::Start($__command) $do_process = $false } } } if ($do_process) { $global:LAST = $_; $__sp.Process($_) } } end { $__sp.End() } } #1 #2 #3 #4 #5 #6 #7 Get the cmdlet info object for Out-Default Create a steppable pipelin Start executing the pipeline Check for command not found exceptions If it's a directory, cd there If it's a URL open the browser Capture the last output object #7 #6

There are a couple of things to notice in this listing. First, when we start the steppable pipeline, rather than passing in a boolean, we pass in the $PSCmdlet object (see chapter 8) for the function. This allows the steppable pipeline to write directly into the functions output and error streams so the function doesn't have to deal with any output from the pipeline. The next thing to notice is that this function does a couple of other useful things besides capturing the last output object. If the last command typed resulted in a "command not found" exception, then the we check to see if the command was actually a path to a directory. If so, then we set the current location to that directory. This allows us to type c:\mydir\mysubdir instead of cd c:\mydir\mysubdir The other thing we check is to see if the command looks like a URL. If it does, then we try to open it in the browser. This lets us open a webpage simply by typing the URL. Both of this are minor conveniences, but along with the $LAST variable, make interactive use of PowerShell a more pleasant experience. This example should give you a sense of the flexibility that steppable pipelines provide. We began this chapter with scriptblocks, moved from there to synthetic objects and then on to dynamic modules and closures and finally to steppable pipelines. Now we're going to circle back to the type system and look at it in a bit more detail. We've covered the “nice” ways to add members to objects and build synthetic objects so let’s dig into the actual plumbing of the PowerShell type system. In the next section, we’ll look at what’s happening under the covers.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

404

11.6 A closer look at the type-system plumbing
Earlier in this chapter, we said that the core of the PowerShell type system was the

PSObject type. This type is used to wrap other objects, providing adaptation and inspection capabilities, as well as a place to attach synthetic members. We’ve used Get-Member to explore objects and used the Add-Member, New-Object and Select-Object cmdlets to extend and create objects. In fact, you can do all of this directly by using the PSObject class itself. And there’s one thing you can’t do without understanding PSObject: wrapping or shadowing an existing property. In this technique, the synthetic property calls the base property that it’s hiding. (Don’t worry; this is less esoteric than it sounds. A simple example will clarify what we’re talking about here.)

AUTHOR'S NOTE
If you’ve done much object-oriented programming, this concept is similar to creating an override to a virtual method that calls the overridden method on the base class. The difference here is that it’s all instance-based; there is no new type involved.

Let’s look at PSObject in more detail. First, let’s look at the properties on this object: PS (1) > [psobject].getproperties() | %{$_.name} Members Properties Methods ImmediateBaseObject BaseObject TypeNames From the list, we see some obvious candidates of interest. But how does one get at these members, given that the whole point of PSObject is to be invisible? The answer is that there’s a special property attached to all objects in PowerShell called (surprise) PSObject. Let’s look at this. First we need a test object to work on. We’ll use get-item to retrieve the

DirectoryInfo object for the C: drive.
PS (2) > $f = get-item c:\ PS (3) > $f Directory: Mode LastWriteTime Length Name --------------------- ---d--hs 5/29/2006 3:11 PM C:\ Now let’s look at the PSObject member attached to this object. PS (4) > $f.psobject : {PSPath, PSParentPath, PSChildName, PSDriv e...} Properties : {PSPath, PSParentPath, PSChildName, PSDriv e...} Methods : {get_Name, get_Parent, CreateSubdirectory, Create...} ImmediateBaseObject : C:\ BaseObject : C:\ TypeNames : {System.IO.DirectoryInfo, System.IO.FileSy stemInfo, System.MarshalByRefObject, Syste ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 Members

Licensed to Andrew M. Tearle

405 m.Object} Right away you see a wealth of information: all of the properties we saw on the PSObject type, populated with all kinds of interesting data. First let’s look at the TypeNames member: PS (6) > $f.psobject.typenames System.IO.DirectoryInfo System.IO.FileSystemInfo System.MarshalByRefObject System.Object This member contains the names of all of the types in the inheritance hierarchy for a

DirectoryInfo object. (These types are all documented in the .NET class library documentation that is part of the Microsoft Developers Network [MSDN] collection. See http://msdn.microsoft.com for more information.) We'll look at the Properties member next. This is a collection that contains all of the properties defined by this type. Let’s get information about all of the properties that contain the pattern “name”: PS (7) > $f.psobject.properties | ?{$_.name -match "name"} MemberType IsSettable IsGettable Value TypeNameOfValue Name IsInstance MemberType Value IsSettable IsGettable TypeNameOfValue Name IsInstance MemberType Value IsSettable IsGettable TypeNameOfValue Name IsInstance : : : : : : : : : : : : : : : : : : : : : NoteProperty True True C:\ System.String PSChildName True Property C:\ False True System.String Name True Property C:\ False True System.String FullName True

This returned information on three properties, one NoteProperty PSPath and two base object properties, Name and FullName. Of course, we’ve seen these properties before; this is the same information that would be returned from Get-Member. In fact, this is exactly what

Get-Member does—it uses the PSObject properties to get this information.

11.6.1 Adding a property
Now let’s add a new member to this object. We could use Add-Member (and typically we would), but we’re talking about the plumbing here so we’ll do it the hard way. First we need to create the NoteProperty object that we want to add. We’ll do this with the New-Object cmdlet. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

406 PS (8) > $np = new-object ` >> system.management.automation.PSnoteProperty ` >> hi,"Hello there" >> Next we’ll add it to the member collection PS (9) > $f.psobject.members.add($np) and we’re done (so it wasn’t really that hard after all). The hi member has been added to this object, so let’s try it out: PS (10) > $f.hi Hello there Of course, all of the normal members are still there. PS (11) > $f.name C:\ Let’s look at the member in the member collection: PS (12) > $f.psobject.members | ?{$_.name -match "^hi"} MemberType IsSettable IsGettable Value TypeNameOfValue Name IsInstance : : : : : : : NoteProperty True True Hello there System.String hi True

Notice the Value member on the object. Since we can get at the member, we can also set the member: PS (13) > ($f.psobject.members | ?{ >> $_.name -match "^hi"}).value = "Goodbye!" >> PS (14) > $f.hi Goodbye! which is equivalent to setting the property directly on $f: PS (15) > $f.hi = "Hello again!" PS (16) > $f.psobject.members | ?{$_.name -match "^hi"} MemberType IsSettable IsGettable Value TypeNameOfValue Name IsInstance : : : : : : : NoteProperty True True Hello again! System.String hi True

Now the Value member on the note property is “Hello again!”. In section 11.4.3 we saw a different type of note property used when constructing objects out of modules. This type of note property is backed by a variable. We can also create an instance of this type of property. But first we need a variable to use to back the property value. PS (15) > [int] $VariableProperty = 0 Now create the PSVariableProperty object, passing in the variable to bind. PS (16) > $vp = new-object ` >> System.Management.Automation.PSVariableProperty ` >> (get-variable VariableProperty) >> Note that the name of the property and the name of the variable will be the same. Now add the property. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

407 PS (17) > $f.psobject.members.add($vp) and verify that it can be read and written. PS (18) > $f.VariableProperty 0 PS (19) > $f.VariableProperty = 7 PS (20) > $f.VariableProperty 7 So we can read and write integers ok but the backing variable was constrained to be an integer so let's verify that the constrain was preserved by trying to assign a string to it. PS (21) > $f.VariableProperty = "Hi" Cannot convert value "Hi" to type "System.Int32". Error: "Input string was not in a correct format." At line:1 char:4 + $f. $n=new-object Management.Automation.PSScriptProperty ` >> name,{$this.psbase.name.ToUpper()} >> In the body of the scriptblock for this PSProperty, we’ll use $this.psbase to get at the name property on the base object (if we just accessed the name property directly, we’d be calling ourselves). We apply the ToUpper() method on the string returned by name to acquire the desired result. Now add the member to the object’s Members collection PS (21) > $f.psobject.members.add($n) and try it out. PS (22) > $f.name WINDOWS When we access the name property on this object, the synthetic member we created gets called instead of the base member, so the name is returned in uppercase. The base object’s name property is, of course, unchanged and can be retrieved through psbase.name: PS (23) > $f.psbase.name windows ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

408 PS (24) > While this isn’t a technique that you’ll typically use on a regular basis, it allows you to do some pretty sophisticated work. You could use it to add validation logic, for example, and prevent a property from being set to an undesired value. You could also use it to log accesses to a property to gather information about how your script or application is being used. With a solid understanding of the plumbing, we're finally ready to use everything we've learned and do some applied metaprogramming. In the next section, we'll look at how to write a domain-specific extension to PowerShell.

11.7 Extending the PowerShell language
In the previous section, we learned how to add members to existing objects one at a time, but sometimes you’ll want to construct new types rather than extend the existing types. In this section, we’ll cover how to do that and also how to use scripting techniques to “add” the ability to create objects to the PowerShell language.

11.7.1 Little languages
The idea of “little languages”, i.e., small domain-specific languages, has been around for a long time. This was one of the powerful ideas that made the UNIX environment so attractive. Many of the tools that were the roots for today’s dynamic languages came from this environment. Of course, in effect, all programs are essentially an exercise in building their own languages. You create the nouns (objects) and verbs (methods or functions) in this language. These patterns are true for all languages that support data abstraction. Dynamic languages go further because they allow you to extend how the nouns, verbs, and modifiers are composed in the language. For example, in a language such as C#, it would be difficult to add a new looping construct. In PowerShell, this is minor. To illustrate how easy it is, let’s define a new looping keyword called loop. This construct will repeat the body of the loop for the number of times the first argument specifies. We can add this keyword by defining a function that takes a number and scriptblock. Here’s the definition: PS (1) > function loop ([int] $i, [scriptblock] $b) { >> while ($i-- -gt 0) { . $b } >> } >> Here we try it out: PS (2) > loop 3 { "Hello world" } Hello world Hello world Hello world PS (3) > In a few lines of code, we’ve added a new flow control statement to the PowerShell language that looks pretty much like any of the existing flow control statements. We can apply this technique to creating language elements that allow you to define your own custom types. Let’s add some “class” to PowerShell!

11.7.2 Adding a CustomClass keyword to PowerShell
We shall use the technique from the previous section to extend the PowerShell language to allow us to define our own custom classes. First we’ll gather our requirements. We want the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

409 syntax for defining a class to look fairly natural (at least for PowerShell). Here’s what we want a class definition to look like: CustomClass point { note x 0 note y 0 method ToString { "($($this.x), $($this.y))"} method scale { $this.x *= $args[0] $this.y *= $args[0] } } Once we’ve defined this custom class, we want to be able to use it as follows. First we can create a new instance of the point class: $p = new point then set the x and y members on this class to particular values: $p.x=2 $p.y=3 and finally call the ToString() method to display the class. $p.tostring() This would give us a natural way to define a class in PowerShell. Now let’s look at how to implement these requirements.

AUTHOR'S NOTE
In section 11.4.3 we saw how we could do this with dynamic modules. The focus here is to see how we can directly implement this type of facility. In practical circumstances, this dynamic module approach is certainly easier.

We’ll put the code for this script in a file called "class.ps1". Let’s go over the contents of that script a piece at a time. First we need a place to store the types we’re defining. We need to use a global variable for this, since we want it to persist for the duration of the session. We’ll give it a name that is unlikely to collide with other variables (we’ll put two underscores at each end to help ensure this) and initialize it to an empty hashtable. $global:__ClassTable__ = @{} Next, we define the function needed to create an instance of one of the classes we’ll create. This function will take only one argument: the scriptblock that creates an instance of this class. This function will invoke the scriptblock provided to it. This scriptblock is expected to return a collection of synthetic member objects. The function will then take these members and attach them to the object being created. This is a helper function that also has to be global, so again we’ll give it a name that is unlikely to collide with other global functions. function global:__new_instance ([scriptblock] $definition) {
At this point we define some local functions to use in the body of the __new_instance function. First we’ll define a helper method for generating error messages.

function elementSyntax ($msg) { throw "class element syntax: $msg" ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

410 }
In the example, we had “keywords” for each of the member types we could add. We’ll implement this by defining functions that implement these keywords. Because of the way dynamic scoping works (see chapter 7), these functions will be visible to the scriptblock when it’s invoked, because they’re defined in the enclosing dynamic scope. First, let’s define the function for creating a note element in the class. This implements the note keyword in the class definition. It takes the name of the note and the value to assign to it and returns a PSNoteProperty object to the caller.

function note ([string]$name, $value) { if (! $name) { elementSyntax "note name " } new-object management.automation.PSNoteProperty ` $name,$value }
Next, define the function that implements the method keyword. This function takes the method name and scriptblock that will be the body of the method and returns a

PSScriptMethod object. function method ([string]$name, [scriptblock] $script) { if (! $name) { elementSyntax "method name " } new-object management.automation.PSScriptMethod ` $name,$script }
We could continue to define keyword functions for all of the other member types, but to keep it simple, we’ll stick with just these two. Having defined our keyword functions, we can look at the code that actually builds the object. First we need to create an empty PSObject with no methods or properties.

$object = new-object Management.Automation.PSObject
Next, execute the scriptblock that defines the body of this class. As mentioned previously, the result of that execution will be the set of members to attach to the new object we’re creating.

$members = &$definition
Finally, attach the members to the object:

foreach ($member in $members) { if (! $member) { write-error "bad member $member" } else { $object.psobject.members.Add($member) } } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

411
The last thing to do is return the constructed object.

$object } As mentioned, the _new_instance function was a worker function; the user never calls it directly. Now we’ll define the function that the user employs to define a new class. Again, this has to be a global function; but this time, since the user calls it, we’ll give it a conventional name. function global:CustomClass {
This function takes the name of the class and the scriptblock to execute to produce the members that will be attached to that class.

param ([string] $type, [scriptblock] $definition)
If there is already a class defined by the name that the user passed, throw an error.

if ($global:__ClassTable__[$type]) { throw "type $type is already defined" }
At this point, we’ll execute the scriptblock to build an instance of the type that will be discarded. We do this to catch any errors in the definition at the time the class is defined, instead of the first time the class is used. It’s not strictly necessary to do this, but it will help you catch any errors sooner rather than later.

__new_instance $definition > $null
Finally, add the class to the hashtable of class definitions:

$global:__ClassTable__[$type] = $definition } and we’re finished implementing the class keyword. Next we have to define the new keyword. This turns out to be a simple function. The new keyword takes the name of the class you want to create an instance of, looks up the scriptblock to execute, and calls

__new_instance to build the object. function global:new ([string] $type) { $definition = $__ClassTable__[$type] if (! $definition) { throw "$type is undefined" } __new_instance $definition }
Finally, we’ll add one last helper function that will allow us to remove a class definition from the hashtable.

function remove-class ([string] $type) { $__ClassTable__.remove($type) }

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

412 This then is the end of the class.ps1 script. We should try it out with the point example we looked at at the beginning of this section. First we have to run the script containing the code to set up all of the definitions. (Since we explicitly defined things to be global in the script, there’s no need to “dot” this script.) PS (1) > ./class Now define the point class PS (2) > CustomClass point { >> note x 0 >> note y 0 >> method ToString { "($($this.x), $($this.y))"} >> method scale { >> $this.x *= $args[0] >> $this.y *= $args[0] >> } >> } >> Next create an instance of this class: PS (3) > $p = new point Use Get-Member to look at the members on the object that was created: PS (4) > $p | gm TypeName: System.Management.Automation.PSCustomObject Name ---Equals GetHashCode GetType x y scale ToString MemberType ---------Method Method Method NoteProperty NoteProperty ScriptMethod ScriptMethod Definition ---------System.Boolean Equals(Object obj) System.Int32 GetHashCode() System.Type GetType() System.Int32 x=0 System.Int32 y=0 System.Object scale(); System.Object ToString();

We see the actual type of the object is PSCustomType—the type that PowerShell uses for pure synthetic objects. You can also see the members we defined in the class definition: the two

NoteProperties x and y and the two methods scale() and ToString(). To try them out, we’ll first call ToString(): PS (5) > $p.tostring() (0, 0) We see the default values for the note members, formatted as intended. Next, set the note members to new values: PS (6) > $p.x=2 PS (7) > $p.y=3 Verify that they’ve been set: PS (8) > $p.tostring() (2, 3) Now call the scale() method to multiply each note member by a scale value. PS (9) > $p.scale(3) And again, verify the values of the note members with ToString(). PS (10) > $p.tostring() (6, 9) The values have been scaled. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

413 Finally, to see how well this all works, let’s use this object with the format operator and we see that our ToString() method is properly called. PS (11) > "The point p is {0}" -f $p The point p is (6, 9) So, in less than a hundred lines of PowerShell script, we’ve added a new “keyword” that lets you define you own classes in PowerShell. Obviously, this isn’t a full-featured type definition system; it doesn’t have any form of inheritance, for example. But it does illustrate how you can use scriptblocks along with dynamic scoping to build new language features in PowerShell in a sophisticated way. Now let’s change gears a bit to talk about types.

11.7.3 Type extension
You might have noticed that all of the examples we’ve shown so far involve adding members to instances. But what about adding members to types? Having to explicitly add members to every object we encounter would be pretty tedious, no matter how clever we were. We really need some way to extend types. Of course, PowerShell also let’s you do this. In this section, we’ll introduce the mechanisms that PowerShell provides which let you extend types. Type extension is performed in PowerShell through a set of XML configuration files. These files are usually loaded at startup time; however, they can be extended after the shell has started. In this section, we’ll show you how you can take advantage of these features. Let’s look at an example. Consider an array of numbers. It’s fairly common to sum up a collection of numbers; unfortunately, there’s no Sum() method on the Array class. PS (1) > (1,2,3,4).sum() Method invocation failed because [System.Object[]] doesn't conta in a method named 'sum'. At line:1 char:14 + (1,2,3,4).sum( $r=0 >> foreach ($e in $this) {$r += $e} >> $r >> } >> and finally use it: PS (4) > $a.sum() 10 But this would be painful to do for every instance of an array. What we really need is a way to attach new members to a type, rather than through an instance. PowerShell does this through type configuration files. These configuration files are stored in the installation directory for PowerShell and loaded at startup. The installation directory path for PowerShell is stored in the

$PSHome variable, so it’s easy to find these files. They have the word “type” in their names and have an extension .ps1xml:
PS (5) > dir $pshome/*type*.ps1xml Directory: Microsoft.PowerShell.Core\FileSystem::C:\Program Files\Windows PowerShell\v1.0

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

414 Mode ----a---a--LastWriteTime ------------4/19/2006 4:12 PM 4/19/2006 4:12 PM Length Name ------ ---50998 DotNetTypes.Format. ps1xml 117064 types.ps1xml

We don’t want to update the default installed types files because when we install updates for PowerShell, they will likely be overwritten and our changes will be lost. What we want to do here is create our own custom types file containing the specification of the new member for

System.Array. Once we’ve created the file, we can use the Update-TypeData cmdlet to load it. Here’s the definition for the Sum() method extension we want to add to

System.Array: System.Array Sum $r=$null foreach ($e in $this) {$r += $e} $r This definition is saved to a file called SumMethod.ps1xml. Now let’s load the file and update the type system definitions: PS (9) > update-typedata SumMethod.ps1xml If the file loaded successfully, you won’t see any output. We can now try out the sum() function: PS (10) > (1,2,3,4,5).sum() 15 It worked. And, because of the way the script was written, it will work on any type that can be added. So let’s add some strings: PS (11) > ("abc","def","ghi").sum() abcdefghi You can even use it to add hashtables: PS (12) > (@{a=1},@{b=2},@{c=3}).sum() Name ---a b c Value ----1 2 3

We can see that the result is the composition of all three of the original hashtables. We can even use it to put a string back together. Here’s the “hal” to “ibm” example from chapter 3, this time using the Sum() method: PS (13) > ([char[]] "hal" | %{[char]([int]$_+1)}).sum() ibm Here we break the original string into an array of characters, add 1 to each character, and then use the Sum() method to add them all back into a string. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

415 You should take some time to examine the set of type configuration files that are part of the default PowerShell installation. Examining these files is a good way to see what that can be accomplished using these tools. We've covered an enormous amount of material so far in this chapter, introducing ideas that are pretty new to a lot of users. If you've hung on to this point - congratulations! There are only a few more topics we need to cover to complete our knowledge of metaprogramming with PowerShell. Scriptblocks, dynamic modules and closures can be passed around, invoked, and assigned at runtime, but the body of these blocks is still defined at compile time. In the next section we'll expand our repertoire of techniques by looking at ways to dynamically create code.

11.8 Building script code at runtime
This final section presents the mechanisms that PowerShell provides for compiling script code and creating new scriptblocks at runtime. To saying that we're "compiling" when PowerShell is an interpreted language may sound a odd, but that’s essentially what creating a scriptblock is: a piece of script text is compiled into an executable object. In addition, PowerShell provides mechanisms for directly executing a string bypassing the need to first build a scriptblock. In the next few sections we'll look at how each of these features work.

11.8.1 The Invoke-Expression cmdlet
The Invoke-Expression cmdet is a way to execute an arbitrary string as a piece of code. It takes the string, compiles it, and then immediately executes it in the current scope. Here’s an example: PS (1) > invoke-expression '$a=2+2; $a' 4 In this example, the script passed to the cmdlet assigned the result of 2+2 to $a, and wrote $a to the output stream. Since this expression was evaluated in the current context, it should also have affected the value of $a in the global scope. PS (2) > $a 4 We see that it did. Let’s invoke another expression. PS (3) > invoke-expression '$a++' PS (4) > $a 5 Evaluating this expression changes the value of $a to 5. There are no real limits on what you can evaluate with Invoke-Expression. It can take any arbitrary piece of script code. Here’s an example where we build up a string with several statements in it and execute it: PS (5) > $expr = '$a=10;' PS (6) > $expr += 'while ($a--) { $a }' PS (7) > $expr += '"A is now $a"' PS (8) > [string](invoke-expression $expr) 9 8 7 6 5 4 3 2 1 0 A is now -1 The first three commands in this example build up a string to execute. The first line initializes the variable $a, the second adds a while loop that decrements and outputs $a, and the final line outputs a string telling us the final value of $a. Note the double quoting in the last script ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

416 fragment. Without the nested double quotes, it would try to execute the first word in the string instead of emitting the whole string.

11.8.2 The ExecutionContext variable
One of the predefined variables (also called automatic variables) provided by the PowerShell engine is $ExecutionContext. This variable is another way to get at various facilities provided by the PowerShell engine. It’s intended to mimic the interfaces available to the cmdlet author. The services that matter most to us in this chapter are those provided through the

InvokeCommand member. Let’s look at the methods this member surfaces:
PS (1) > $ExecutionContext.InvokeCommand | gm TypeName: System.Management.Automation.CommandInvocationIntri nsics Name ---Equals ExpandString GetHashCode GetType InvokeScript NewScriptBlock ToString The MemberType ---------Method Method Method Method Method Method Method Definition ---------System.Boolean Equals(Object obj) System.String ExpandString(String s... System.Int32 GetHashCode() System.Type GetType() System.Collections.ObjectModel.Coll... System.Management.Automation.Script... System.String ToString()

interesting methods in this list are ExpandString(), InvokeScript(), and NewScriptBlock(). These methods are covered in the next few sections.

11.8.3 The ExpandString() method
The ExpandString() method lets you perform the same kind of variable interpolation that the PowerShell runtime does in scripts. Here’s an example. First we set $a to a known quantity: PS (2) > $a = 13 Next we create a variable $str that will display the value of $a. PS (3) > $str='a is $a' Since the variable was assigned using single-quotes, no string expansion took place. We verify this by displaying the string: PS (4) > $str a is $a Now call the ExpandString() method, passing in $str: PS (5) > $ExecutionContext.InvokeCommand.ExpandString($str) a is 13 and it returns the string with the variable expanded into its value.

11.8.4 The InvokeScript() method
The next method to look at is InvokeScript(). This method does the same thing that the

Invoke-Expression cmdlet does. It takes its argument and evaluates it like a script. Call this method passing in the string “2+2” PS (7) > $ExecutionContext.InvokeCommand.InvokeScript("2+2") 4 and it will return 4. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

417

11.8.5 Mechanisms for creating scriptblocks
The final method to look at is the NewScriptBlock() method. Like InvokeScript(), this method takes a string, but instead of executing it, it returns a scriptblock object that represents the compiled script. Let’s use this method to turn the string '1..4 | foreach

{$_ * 2}' into a scriptblock.
PS (8) > $sb = $ExecutionContext.InvokeCommand.NewScriptBlock( >> '1..4 | foreach {$_ * 2}') >> We saved this scriptblock into a variable, so let’s look at it. Since the ToString() on a scriptblock is the code of the scriptblock, we just see the code that makes up the body of the scriptblock. PS (9) > $sb 1..4 | foreach {$_ * 2} Now let’s execute the scriptblock using the "&" call operator. PS (10) > & $sb 2 4 6 8 The scriptblock executed, printing out the even numbers from 4 to 8. PowerShell version 2 introduced a simpler way of doing this by using a static method on the

ScriptBlock class. Here's how to use this static "factory" class:
PS (11) > $sb = [scriptblock]::Create('1..4 | foreach {$_ * 2}') PS (12) > & $sb 2 4 6 8 PS (13) > Using the [scriptblock] type accelerator, newer mechanism is significantly simpler than the rather long expression in the earlier example.

AUTHOR'S NOTE
Many people have asked why we (the PowerShell team) don’t allow you to simply cast a string to a scriptblock. The reason is that we want to make the system resilient against code injection attacks. We want to minimize the number of places where executable code can be injected into the system, and we particularly want code creation to be an explicit act. Casts are more easily hidden, leading to accidental code injections, especially when the system may prompt for a string. We don’t want those user-provided strings to be converted into code without some kind of check. See chapter 18 for more extensive discussions about security.

11.8.6 Creating functions using the function: drive
The final way to create a scriptblock is actually a side-effect of creating elements in the function drive. Earlier we saw that you can create a named function by assigning a scriptblock to a name in the function drive: PS (1) > $function:foo = {"Hello there"} PS (2) > foo ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

418 Hello there You could also use the New-Item cmdlet to do this: PS (3) > new-item function:foo -value {"Hi!"} New-Item : The item at path 'foo' already exists. At line:1 char:9 + new-item $y=6 PS (7) > $function:foo = "$x*$y" PS (8) > foo 30 PS (9) > $function:foo 5*6 The variables $x and $y expanded into the numbers 5 and 6 in the string, so the resulting scriptblock was {5*6} Now let’s define another function using foo, but adding some more text to the function. PS (10) > new-item function:bar -value "$function:foo*3" CommandType ----------Function Name ---bar Definition ---------5*6*3

PS (11) > bar 90 In the expanded string, $function:foo expanded into “5*6” so the new function bar was assigned a scriptblock {5*6*3}. This finishes our discussion of the techniques PowerShell provides for compiling script code at runtime. In the next section we'll look at how to embed static languages like C# and Visual Basic in our scripts. This ability to embed fragments of C# or Visual Basic vastly increases what can be done directly with scripts but at the cost of some increase in complexity.

11.9 Compiling code with Add-Type
In the previous section, we covered techniques for compiling script code at runtime. In this section, we'll look at how to inline code written in static languages into our scripts. The key to doing this is the Add-Type cmdlet, introduced in PowerShell V2. With the Add-Type cmdlet, we can embed code fragments written in compiled languages like C# or Visual Basic in our scripts then compile that code when the scripts are loaded. A ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

419 particularly interesting application of this technique is that we can create dynamic binary modules. This combines some of the best aspects of script modules with binary modules. Add-Type also fills in another other hole in the PowerShell V1 functionality. We can use it to dynamically load existing assemblies at runtime. Finally, this cmdlet can be used to simplify writing scripts that compile static language code into libraries or executables.

11.9.1 Defining a new .NET class: C#
Let's jump into an example. We'll dynamically add a new .NET class at runtime. We'll write the code for this class using C#. It's a simple class so even if you aren't a C# programmer, you should be able to follow along. The fragment looks like: Add-Type @' using System; public static class Example1 { public static string Reverse(string s) { Char[] sc = s.ToCharArray(); Array.Reverse(sc); return new string(sc); } } '@ Let's run this code: PS (STA) (7) > Add-Type @' >> using System; >> >> public static class Example1 >> { >> public static string Reverse(string s) >> { >> Char[] sc = s.ToCharArray(); >> Array.Reverse(sc); >> return new string(sc); >> } >> } >> '@ >> This command should have run with no errors. Once this has run, let’s use the new type that we've added. PS (STA) (8) > [example1]::Reverse("hello there") ereht olleh PS (STA) (9) > And there we go. We now have a new method for reversing strings. We could also have saved the file externally and then load it at runtime. We'll put the C# code into a file example1.cs which looks like: PS (MTA) (14) > gc example1.cs using System; public static class Example1 { public static string Reverse(string s) { ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

420 Char[] sc = s.ToCharArray(); Array.Reverse(sc); return new string(sc); } } And now we can add this to our session: PS (MTA) (15) > add-type -Path example1.cs

11.9.2 Defining a new enum at runtime
An enum type in .NET is a way of creating a fixed set of name/value constants. The ability to define these types is missing from PowerShell but we can work around this by using Add-

Type. Let's define an enum that can be used to specify a coffee order. We'll constrain the types of coffee orders we'll permit to "Latte", "Mocha", "Americano", "Cappuccino" or "Espresso". First we'll set a variable to the list of drink types. PS (1) > $beverages = "Latte, Mocha, Americano, Cappuccino, Espresso" We pass a string to Add-Type that contains the fragment of C# needed to define a enum type. PS (2) > Add-Type "public enum BeverageType { $beverages }" It should be pretty easy to see what's going on. We're defining a public type called BeverageType using the list of drinks in $beverages. Now that we have the type defined, we can use it in a function to create new drink orders. PS (3) > function New-DrinkOrder ([BeverageType] $beverage) >> { >> "A $beverage was ordered" >> } >> This function uses the enum to constrain the type of the argument to the function and then return a string showing what was ordered. We'll use the function to order a latte. PS (4) > New-DrinkOrder latte A Latte was ordered And the order goes through. Notice that casing of the drink name matches what was in the DrinkOrder enum definition, not what was in the argument. This is because the argument contains an instance of the DrinkOrder type not the original string. Let's try to order some other than a coffee and see what happens. PS (5) > New-DrinkOrder coke New-DrinkOrder : Cannot process argument transformation on parameter 'be verage'. Cannot convert value "coke" to type "BeverageType" due to inva lid enumeration values. Specify one of the following enumeration valu es and try again. The possible enumeration values are "Latte, Mocha, Americano, Cappuccino, Espresso". At line:1 char:10 + New-DrinkOrder $cmd = Get-Command Write-InputObject PS (5) > $cmd | fl

Name CommandType Definition

: Write-InputObject : Cmdlet : Write-InputObject [-Parameter1 ] -Inp utObject [-Verbose] [-Debug] [-Error Action ] [-WarningAction ] [-ErrorVariable ] [ -WarningVariable ] [-OutVariable ] [-OutBuffer ]

: : : : -Help.xml : {[-Parameter1 ] -InputObject [-Verbose] [-Debug] [-ErrorAction ] [-WarningAction ] [-ErrorVariable ] [-WarningVariable ] [-OutVariable ] [-OutBuffer ]} ImplementingType : MyWriteInputObjectCmdlet Verb : Write Noun : InputObject Notice that the Path, DLL and AssemblyInfo fields for this command are empty. Since the assembly for a dynamic binary module is in-memory only, these items are empty. They need an assembly that was loaded from disk in order to be defined. Dynamic binary modules make it possible to get the advantages of a script module (being able to read the script), with the advantages of compiled code (speed and static type checking.) The only disadvantage to the user compared with regular binary modules is that the load time may be a bit longer.

Path AssemblyInfo DLL HelpFile ParameterSets

This concludes our discussion on how to build code at runtime in PowerShell. We've covered quite a few ways to dynamically compile script code and, with Add-Type, we looked at ways to compile C# and friends in memory. We've also pretty much exhausted the "metaprogramming in PowerShell" topic. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

423

11.10 Summary
In chapter 11, we covered advanced topics in programming and metaprogramming with PowerShell. Although many of the techniques covered in the chapter are quite advanced, used appropriately they can significantly improve your productivity as a scripter. We’ll also see in later chapters how language elements such as scriptblocks make graphical programming in PowerShell easy and elegant. In this chapter we covered the following topics:  Metaprogramming is a set of powerful techniques that essentially “crack open” the PowerShell runtime. They allows us to extend the runtime with new keywords and control structures. We can directly add properties and methods to objects in PowerShell; this is useful because it let's us adapt or extend objects logically in specific problem domains. The fundamental unit of PowerShell code, including the content of all functions, scripts and modules, is actually scriptblocks. Scriptblocks also let us define methods that can be added to objects as script methods. Scriptblocks don’t necessarily need to be named, and can be used in many situations, including as the content of variables. While scriptblocks are the key to all of the metaprogramming features in PowerShell, they’re also an “everyday” feature that users work with all the time when they use the Foreach-Object and Where-Object cmdlets. The call operator & allows us to invoke commands indirectly; that is, by reference rather than by name (since a scriptblock is just a reference). This also works with the CommandInfo objects returned from Get-Command. When using the Update-TypeData cmdlet, we can load type configuration files which allow us to extend a type instead of a single instance of that type. PowerShell supports the use of “little language” or Domain Specific Language techniques to extend the core language. This allows us to more naturally specify solutions for problems in a particular domain. There are a variety of techniques for compiling and executing code at runtime. You can use the Invoke-Expression cmdlet, engine invocation intrinsics on the $ExecutionContext variable or the CreateScriptBlock() static method on the [scriptblock] type. Dynamic modules allow us to do local isolation in a script. They are also underlie the implementation of closures in PowerShell and provide a simpler way to create custom objects. The Add-Type cmdlet lets us work with compiled languages from within PowerShell. It also provides a means to embed code in these languages directly in our scripts. This ability adds significant power the environment at some cost in complexity.





 







Add-Type also makes it possible to create dynamic binary modules allowing us to combine some of the benefits of both static and dynamic coding techniques.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

424

12
Remoting and Background Jobs

In a day when you don't come across any problems - you can be sure that you are traveling in the wrong path. Swami Vivekananda

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

425 A tool intended for enterprise management that can't actually manage distributed systems is not useful. Unfortunately, in PowerShell V1, there was very little support for remote management built into PowerShell. This was fixed in PowerShell V2 by adding a comprehensive built-in remoting subsystem. This facility allows us to handle pretty much any kind of remoting task in pretty much any kind of remoting configuration we might encounter. Another related feature that V2 introduced was built-in support for background jobs. Background jobs allow multiple tasks to be executed within a single session, including mechanisms for starting, stopping and querying these tasks. Again, this is an important feature in the enterprise environment where we frequently have to deal with more than one task at a time. In this chapter we're going to cover the various features of remoting and how they can be applied. We'll use an extended example showing how to combine the various features to solve a non-trivial problem. Then we'll look at background jobs and how to apply them to create concurrent solutions. Finally we'll end the chapter by looking at some of the configuration considerations we need to be aware of when using PowerShell remoting.

12.1 Getting started with remoting
In this section, we'll go through the basic concepts and terminology used by PowerShell remoting. The ultimate goal for remoting is to be able to execute a command on a remoting computer. There are two ways to approach this. First, we could have each command do its own remoting. In this scenario, the command is still executed locally but uses some system-level networking capabilities like DCOM to perform remote operations. There are, in fact, a number of commands that do this which we'll cover in the next section. The negative aspect of this approach is that each command has to implement and manage its own remoting mechanisms. As a result, PowerShell includes more general solution allowing us to send the command (or pipeline of commands or even a script) to the target machine for execution and then retrieve the results. With this approach, we only have to implement the remoting mechanism once and then it can be used with any command. This second solution is where we'll spend most of our time. But first, let's look at the commands that do implement their own remoting.

12.1.1 Commands with built-in remoting
A number of commands in PowerShell V2 have been extended to have a -ComputerName parameter which allows them to specify the target machine to access. This list is shown in table 13.1.

Table 13.1 The PowerShell V2 commands with built-in remoting
Name Clear-EventLog Synopsis Deletes all entries from specified event logs on the local or remote computers. Get-Counter Get-EventLog Gets performance counter data from local and remote computers. Gets the events in an event log, or a list of the event logs, on the local or remote computers. Get-HotFix Gets the hotfixes that have been applied to the local and remote http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

426 computers. Get-Process Gets the processes that are running on the local computer or a remote computer. Get-Service Get-WinEvent Gets the services on a local or remote computer. Gets events from event logs and event tracing log files on local and remote computers. Get-WmiObject Gets instances of Windows Management Instrumentation (WMI) classes or information about the available classes. Get-WSManInstance Displays management information for a resource instance specified by a Resource URI. Invoke-WmiMethod Invoke-WSManAction Calls Windows Management Instrumentation (WMI) methods. Invokes an action on the object that is specified by the Resource URI and by the selectors. Limit-EventLog Sets the event log properties that limit the size of the event log and the age of its entries. New-EventLog Creates a new event log and a new event source on a local or remote computer. New-WSManInstance Remove-EventLog Remove-PSSession Remove-WmiObject Creates a new instance of a management resource. Deletes an event log or unregisters an event source. Closes one or more Windows PowerShell sessions (PSSessions). Deletes an instance of an existing Windows Management Instrumentation (WMI) class. Restart-Computer Restarts ("reboots") the operating system on local and remote computers. Set-Service Set-WmiInstance Starts, stops, and suspends a service, and changes its properties. Creates or updates an instance of an existing Windows Management Instrumentation (WMI) class. Show-EventLog Displays the event logs of the local or a remote computer in Event Viewer. Stop-Computer Test-Connection Stops (shuts down) local and remote computers. Sends ICMP echo request packets ("pings") to one or more computers. Write-EventLog Writes an event to an event log.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

427

Of particular interest are the WMI commands because, as we'll see in chapter 18, they allow us to access a wide range of management information. By design, WMI is inherently remotable which makes it easy for the WMI cmdlets to implement remoting. Let's look at a short example. The Get-WMIObject cmdlet has a -ComputerName parameter that allows us to get information from another machine. We'll use it in the following to retrieve information about the BIOS on a particular machine: PS (4) > Get-WmiObject -Computer brucepayx61 -cred $(get-credential) ` >> WIN32_Bios >> cmdlet Get-Credential at command pipeline position 1 Supply values for the following parameters: Credential SMBIOSBIOSVersion Manufacturer Name SerialNumber Version : : : : : 7SET33WW (1.19 ) LENOVO Ver 1.00PARTTBL LVB7KY3 LENOVO - 1190

In this example, we had to specify the computer name to connect to and the credentials to use when connecting before the operation could be performed. The command then connected to the target machine and used the WMI provider for the Win32_BIOS class to retrieve the BIOS information. (We'll save covering additional details until chapter 18.) One limitation when using the WMI cmdlets for remote access results from the fact that WMI uses

DCOM and DCOM is not a "firewall friendly" protocol. This means that there may be network configuration issues to address before we can use WMI. On the other hand, WMI doesn't require PowerShell or even .NET to be available on the target node which makes it very useful for accessing older Windows machines or minimal Windows configurations like the Server Core configuration introduced with Windows Server 2008. While being able to remote individual commands like Get-WmiObject can be handy, we really need a more general remoting solution similar to the Secure Shell (ssh) used on UNIX or Linux machines. This solution is provided by the PowerShell remoting subsystem and is where we'll spend the rest of this chapter (and chapter 13 as well). Let's start by looking at the basic modes of operation for the remoting infrastructure.

12.1.2 The PowerShell remoting subsystem
Way back in chapter 1, we saw a few brief examples of how remoting works. You may remember that all of those examples used the same basic cmdlet: Invoke-Command. This cmdlet allows us to remotely invoke a scriptblock on another computer and is the building block for most of the features in remoting. The syntax for this command is shown in figure 12.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

428

The Invoke-Command cmdlet is the core of PowerShell’s remoting model.
Invoke-Command [[-ComputerName] ] [-Port ] [-ApplicationName ] [-ThrottleLimit ] [-UseSSL] [-Authentication ] [-ConfigurationName ] [-Credential ] [-HideComputerName] [-JobName ] [-AsJob] [-SessionOption ] [-ScriptBlock] [-ArgumentList ] [-InputObject ]

Figure 12.1 This figure shows the syntax for the Invoke-Command cmdlet. This cmdlet is used to execute commands and scripts on one or more computers. It can be used synchronously or asynchronously as a job.

The Invoke-Command cmdlet is used to programmatically invoke a scriptblock on one or more computers. This is done by specifying a computer name (or list of names) for the machines on which we want to execute the command. For each name in the list, the remoting subsystem will take care of all of the details needed to open the connection to that computer, execute the command, retrieve the results then shut the connection down. If we're going to run the command on a very large set of computers,

Invoke-Command will also take care of all of resource management details like managing the number of concurrent remote connections and so on. This is a very simple but powerful model if we only need to execute a single command or script on the target machine. However, if we want to execute a series of commands on the target, the overhead of setting up and taking down a connection for each command becomes very expensive. PowerShell remoting addresses this situation by allowing us to create a persistent connection to the remote computer called a session. This is done using the New-PSSession cmdlet. Both of the scenarios we've discussed so far involve what is called non-interactive remoting because we're just sending commands to the remote machines then waiting for the results. We don't actually interact with the remote commands while they are executing. Another standard pattern in remoting occurs when we want to set up an interactive session where every command we type is sent transparently to the remote computer. This is the style of remoting implemented by tools like Remote Desktop, telnet or ssh (the secure shell). PowerShell allows us to start an interactive session using the Enter-PSSession cmdlet. Once we've entered a remote session we can suspend the session without closing the remote connection by using Exit-PSSession. Since the connection is not closed, we can later reenter the session with all session data preserved by using

Enter-PSSession again. These cmdlets: Invoke-Command, New-PSSession and Enter-PSSession are the basic remoting tools we'll be using. But before we can use them, we need to make sure remoting is enabler so we'll look at that next.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

429

12.1.3 Enabling remoting
Before we can use PowerShell remoting to access a computer, the remoting service has to be enabled on the computer. This is done using the Enable-PSRemoting cmdlet. To be able to run this command, you have to have Administrator privileges on the machine you are going to enable. To enable remoting, from a PowerShell session that is running with Administrator privileges, execute the Enable-PSRemoting command. The resulting sequence of interactions will look something like: PS C:\Windows\system32> Enable-PSRemoting WinRM Quick Configuration Running command "Set-WSManQuickConfig" to enable this machine for remote management through WinRM service. This includes: 1. Starting or restarting (if already started) the WinRM service 2. Setting the WinRM service type to auto start 3. Creating a listener to accept requests on any IP address 4. Enabling firewall exception for WS-Management traffic (for http only). Do you want to continue? [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): y WinRM has been updated to receive requests. WinRM service type changed successfully. WinRM service started. Configured LocalAccountTokenFilterPolicy to grant administrative rights remotely to local users. WinRM has been updated for remote management. Created a WinRM listener on HTTP://* to accept WS-Man requests to any IP on this machine. WinRM firewall exception enabled. PS C:\Windows\system32> At various points in the process, we'll be prompted to confirm each step. Respond 'y' to all of these steps to make sure everything is enable properly. (If you want to skip all the questions and just get it done, use the -Force option.) The Enable-PSRemoting command takes care of all of the configuration steps needed to connect remotely to this computer in a domain environment. In a nondomain or workgroup environment, there are some additional steps required for remoting to work.

12.1.4 Additional setup steps for workgroup environments
If we're working in a workgroup environment (e.g. at home), there are a few additional steps that must be taken before we can connect to a remote machine. With no domain controller available to handle the various aspects of security and identity, we have to manually configure the names of the computers we trust. For example, if we want to connect to the computer 'computerItrust', then we have to add it to the list of trusted computers (or trusted hosts list.) We can do this through the WSMan: provider as shown in table 12.2. Note that we need to be running as administrator to be able to use the WSMan: provider.

Table 12.2 This table lists the additional steps needed to enable remote access to a computer in a workgroup environment
Step Command Description

1

cd wsman:\localhost\client

Cd'ing into the client configuration

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

430 node in the wsman: provider allows us to access the WSMan configuration for this computer using the provider cmdlets.

2

$old = (get-item .\TrustedHosts).Value
We want to update the current value of the TrustedHosts item so we get it and save the value in a variable.

3

$old += ',computerItrust'

The value of TrustedHosts is a string containing a comma-separated list of the computers consider trustworthy. We add the new computer name to the end of this list, prefixed with a comma.

4

set-item .\TrustedHosts $old

Once we've verified that the updated contents of the variable are correct, we assign it back to the TrustedHosts item which updates the configuration.

Once we've completed these steps, we're ready to start playing with some examples.

SECURITY NOTE
The computers in the

TrustedHosts list are implicitly trusted simply by adding their names to this

list. The identity of these computers won't be authenticated when we connect to them. Since the connection process requires sending credential information to these machines, we need to be sure that we can trust these computers. Also be aware that the

TrustedHosts list on a machine applies

to everyone who uses that computer, not just the user who changed the setting.

12.1.5 Enabling remoting in the enterprise
As we saw in the last section, we enabled PowerShell remoting on a single computer using the EnablePSRemoting cmdlet. In the enterprise scenario, enabling machines one by one is not a great solution since we may be dealing with tens, hundreds or thousands of machines. Obviously we can't use PowerShell remoting to turn on remoting so we need another way to push configuration out to a collection of machines. This is exactly what Group Policy is designed for. We can use Group Policy to enable and configure remoting as part of the machine policy that gets pushed out. PowerShell depends on the WinRM (Windows Remote Management) service for its operation. To enable the WinRM listener on all computers in a domain, enable the "Allow automatic configuration of listeners" policy in the following Group Policy path: Computer Configuration\Administrative Templates\Windows Components\Windows Remote Management (WinRM)\WinRM service

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

431 This allows WinRM to accept remoting requests. To enable a firewall exception for in all computers in a domain, enable the "Windows Firewall: Allow local port exceptions" policy in the following Group Policy path: Computer Configuration\Administrative Templates\Network\Network Connections\Windows Firewall\Domain Profile This policy allows members of the Administrators group on the computer to use Windows Firewall in Control Panel to create a firewall exception for the WinRM service. We also need to ensure that the WinRM service is actually running on all machines we want to access. On server operating systems like Windows Server 2003, Windows Server 2008, and Windows Server 2008 R2, the startup type of the WinRM service is set to Automatic by default so nothing needs to be done for these environments. (This makes sense since the whole point of PowerShell is server management.) However, on client operating systems, (Windows XP, Windows Vista, and Windows 7), the WinRM service startup type is Disabled by default so we need to start the service or change the startup type to Automatic before remote commands can be sent to these machines.

AUTHOR'S NOTE
Even if WinRM is not started, you can still remote from the machine - outbound connections don't go through the WinRM service. If we're using a client machine and only want to connect from the client to the server, it makes sense to leave WinRM disabled on this machine.

To change the startup type of a service on a remote computer, we can use the Set-Service cmdlet. This cmdlet can change the state of a service on a remote machine and doesn't depend on the WinRM service so it can be used to "boot-strap" remoting. Here's an example showing how this would be done for a set or computers. First we need a list of the names of the computers to change the state on. In this example, we'll get the names from a text file: PS (1) > $names = Get-Content MachineNames.txt Now we can pass this list of names to the Set-Service cmdlet to change the startup type of the WinRM to be Automatic. PS (2) > Set-Service -Name WinRM -ComputerName $names ` >>> -Startuptype Automatic To verify the effect use the Get-Service cmdlet to verify that the configuration has been updated. At this point, we've done everything necessary to enable remoting on the target machines and we can get on with using the remoting feature to solve our remote management problems. We'll see what we can do in the next section.

12.2 Applying PowerShell remoting
With remoting services enabled, we can start to use them to get our work done. In this section, we're going to look some of the ways we can apply remoting to solve management problems. We'll start with some very simple remoting examples. Next, we'll work with some more complex examples where we introduce concurrent operations. Then we'll apply the principles we've learned to solve a specific problem: how to implement a multi-machine configuration monitor. We'll work through this problem in a

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

432 series of steps adding more capabilities to our solution finally resulting in a simple but fairly complete configuration monitor. Let's start with the most basic examples.

12.2.1 Basic remoting examples
Back in chapter 1, we saw the most basic examples of remoting: Invoke-Command servername {"hello world"} The first thing to notice is that Invoke-Command takes a scriptblock to specify the actions. This pattern should be familiar by now - we've seen it with ForEach-Object and Where-Object many times. The Invoke-Command does operate a bit differently however. It's designed to make remote execution as transparent as possible. For example, if we want to sort objects, the local command looks like: PS (1) > 1..3 | Sort -Descending 3 2 1 Now if we want to do the sorting on the remote machine, we would do this: PS (2) > 1..3 | Invoke-Command localhost { Sort -Descending } 3 2 1 We're essentially splitting the pipeline across local and remote parts and the scriptblock is used to demarcate which part of the pipeline should be executed remotely. This works the other way as well: PS (3) > Invoke-Command localhost { 1..3 } | sort -desc 3 2 1 Here we're generating the numbers on the remote computer and sorting them locally. Of course scriptblocks can contain more than one statement. This implies that the semantics need to change a bit. Where in the simple pipeline case, streaming input into the remote command was transparent, when the remote command contains more than one statement, we have to be explicit and use the $input variable to indicate where we want the input to do. This looks like: PS (4) > 1..3 | Invoke-Command localhost { >> "First" >> $input | Sort -Descending >> "Last" >> } >> First 3 2 1 Last The scriptblock argument to Invoke-Command in this case contains three statements. The first statement emits the string "First", the second statement does the sort on the input and the third statement emits the string "Last". What happens if we don't specify input? Let's take a look: PS (5) > 1..3 | icm localhost { >> "First" >> Sort -Descending >> "Last" >> } >> First ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

433 Last Nothing was emitted between "First" and "Last". Because $input was not specified, the input objects were never processed. We'll need to keep this in mind when we start to build or monitoring solution. Now let's look at how concurrency - multiple operations occurring at the same time - impact our scripts.

12.2.2 Adding concurrency to our examples
Back in chapter 2, we talked about how each object passed completely through all states of a pipeline, one by one. This changes with remoting because the local and remote commands run in separate processes which are executing concurrently. This means that we now have two threads of execution local and remote - and this can have an effect on the order in which things are executed. Consider the following statement: PS (12) > 1..3 | foreach { Write-Host $_; $_; Start-Sleep 5 } | >> write-host >> 1 1 2 2 3 3 This statement sends a series of numbers down the pipeline. In the body of the foreach scriptblock, the value of the current pipeline object is written to the screen then passed to the next state in the pipeline. This last stage also writes the object to the screen. Given that we know each object is processed completely by all stages of the pipeline, the order of the output is as expected. The first number is passed to the foreach where it is displayed then passed to Write-Output and it's displayed again so we see the sequence 1,1,2,2,3,3. Now let's run this command again using Invoke-

Command in the final stage.
PS (10) > 1..3 | foreach { Write-host $_; $_; Start-Sleep 5 } | >> Invoke-Command localhost { Write-Host } >> 1 2 1 3 2 3 Now the order has changed - we see '1' and '2' from the local process, then we see '1' from the remote process, and so on. The local and remote pipelines are executing at the same time which is what's causing the changes to the ordering. This is made more complicated by the use of buffering and timeouts in the remoting protocol, the details of which we'll which cover in chapter 13. We used the Start-Sleep command in these examples to force these visible differences. If we take the sleeps out, we get a different pattern: PS (13) > 1..3 | foreach { Write-Host $_; $_ } | >> Invoke-Command localhost { Write-Host } >> 1 2 3 1 2 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

434 3 This time, all of the local objects are displayed then passed to the remoting layer where they are buffered until they can be delivered to the remote connection. This allows the local side to process all objects before the remote side starts to operate. Concurrent operation and buffering make it appear a bit unpredictable but if we didn't have the Write-Hosts in place, it would be essentially unnoticeable. The important thing to understand is that objects being sent to the remote end will be processed concurrently with the local execution. This means that the remoting infrastructure doesn't have to buffer everything send from the local end before starting execution. Up until now, we've only been passing very simple commands to the remote end. However, since

Invoke-Command takes a scriptblock, we can, in practice, send pretty much any valid PowerShell script. We'll take advantage of this in the next section when we start to build our multi-machine monitor.

AUTHOR'S NOTE
So why does remoting require scriptblocks? This is done for a number a couple of reasons. First, scriptblocks are always compiled locally so you'll catch syntax errors as soon as the script is loaded. Second, it limits the vulnerability to "code injection attacks" by validating the script before sending it.

12.2.3 Solving a real problem: multi-machine monitoring
In this section, we're going to build a solution for a real management problem - multi-machine monitoring. With this solution, we're going to gather some basic health information from the remote host. The goal is to use this information to determine when a server may have problems such as out of memory, out of disk or reduced performance due to a high faulting rate. We'll gather the data on the remote host and return it as a hashtable so we can look at it locally. M ONITORING A SINGLE MACHINE To simplify things, we'll start by writing a script that can work against a single host. Listing 12.1 shows this script.

Listing 12.1 Defining the data acquisition ScriptBlock
$gatherInformation ={ @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first 5 TopWS = Get-Process | sort -desc WS | select -first 5 } } Invoke-Command servername $gatherInformation This script uses a number of mechanisms to get the required data and returns the result as a hashtable. This hash table contains the following pieces of performance-related information:   the amount of free space on the C drive from the Get-PSDrive command the page fault rate retrieved using WMI (see chapter 18)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

435   the processes consuming the most CPU from Get-Process with a pipeline the processes that have the largest working set also from Get-Process

We've written the information gathering scriptblock in exactly the same way we would write it for the local host - the fact that it is being remoted to another computer is entirely transparent. All we had to do was wrap the code in an extra set of braces and pass it to Invoke-Command. M ONITORING MULTIPLE MACHINES Working against one machine was pretty simple but our goal was multi-machine monitoring. So we need to change the script to run against a list of computers. We don't want to hardcode the list of computers in the script - that interferes with reuse, so we'll design it to get the data from a file called servers.txt. The content of this file is simply a list of host names, 1 per line which might look like: Server-sql-01 Server-sql-02 Server-sql-03 Server-Exchange-01 Server-SharePoint-Markerting-01 Server-Sharepoint-Development-01 Adding this functionality only requires a small, one-line change to the call to Invoke-Command. This revised command looks like: Invoke-Command (Get-Content servers.txt) $gatherInformation We could make this more complex - say we only wanted to scan certain computers on certain days. We'll update the servers.txt file to be a CSV file. This would look like: Name, Day Server-sql-01,Monday Server-sql-02,Tuesday Server-sql-03,Wednesday Server-Exchange-01, Monday Server-SharePoint-Markerting-01, Wednesday Server-Sharepoint-Development-01, Friday Now when we load the servers, we'll do some processing on this list which looks like: $servers = Import-CSV servers.csv | where { $_.Day -eq (get-date).DayOfWeek } | foreach { $_.Name } Invoke-Command $servers $gatherInformation There are still no changes required in the actual data-gathering code. Let's move on to the next refinement. RESOURCE MANAGEMENT USING THROTTLING In a larger organization, this list of servers is likely to be quite large, perhaps hundreds or even thousands of servers. Obviously it isn't possible to establish this many concurrent connections - it would exhaust the system resources. To manage the amount of resources consumed, we can use throttling. In general, throttling allows us to limit the amount of resources that an activity can consume at one time. In this case, we're limiting the number of connections that command makes at one time. In fact, there is a built in throttle limit to prevent accidental resource exhaustion. By default, Invoke-Command will limit the number of concurrent connections to 32. To override this, we use the -Throttle parameter on Invoke-Command to limit the number of connections. In this example we're going to limit the number of concurrent connections to 10 as shown: $servers = Import-CSV servers.csv | ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

436 where { $_.Day -eq (get-date).DayOfWeek } | foreach { $_.Name } icm -throttle 10 $servers $gatherInformation At this point, let's consolidate the incremental changes we've made and look at the updated script as a whole. This is shown in listing 12.2:

Listing 12.2 Data acquisition script using servers CSV file
$gatherInformation ={ @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first 5 TopWS = Get-Process | sort -desc WS | select -first 5 } } $servers = Import-CSV servers.csv | where { $_.Day -eq (get-date).DayOfWeek } | foreach { $_.Name } icm -throttle 10 $servers $gatherInformation This has become a pretty capable script - it gathers a useful set of information from a network of servers in a manageable scalable way. It would be nice if we could generalize it a bit more so we could use different lists of servers or change the throttle limits. PARAMETERIZING THE SOLUTION We can increase the flexibility of our tool by adding some parameters to it. Here are the parameters we want to add: param ( [Parameter()] [string] $serverFile = "servers.txt", [Parameter()] [int] $throttleLimit = 10, [Parameter()] [int] $numProcesses = 5 ) The first two are obvious - the name of the servers file and the throttle limit, both with reasonable defaults. The last one is less obvious. This parameter will control the number of process objects to include in the TopCPU and TopWS entries in the table returned from the remote host. While we could, in theory, trim the list that gets returned locally, we can't add to it so we really need to evaluate this parameter on the remote end to get full control. This means it has to be a parameter to the remote command. This is another reason scriptblocks are useful. We can add parameters to the scriptblock that's executed on the remote end. We're finally modifying the data collection scriptblock. The modified scriptblock looks like: $gatherInformation ={ param ($procLimit = 5) @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first $procLimit ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

437 TopWS = Get-Process | sort -desc WS | select -first $procLimit } } and the updated call to Invoke-Command looks like: Invoke-Command -Throttle 10 -ComputerName $serves ` -ArgumentList $numProcesses ` -ScriptBlock $gatherInformation Once again, let's look at the complete updated script which is shown in listing 12.3

Listing 12.3 Parameterized data acquisition script param ( [parameter] [string] $serverFile = "servers.txt", [parameter] [int] $throttleLimit = 10, [parameter] [int] $numProcesses = 5 ) $gatherInformation ={ param ($procLimit = 5) @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first $procLimit TopWS = Get-Process | sort -desc WS | select -first $procLimit } } $servers = Import-CSV servers.csv | where { $_.Day -eq (get-date).DayOfWeek } | foreach { $_.Name } Invoke-Command -Throttle 10 -ComputerName $servers ` -ArgumentList $numProcesses ` -ScriptBlock $gatherInformation This is script is starting to become a bit complex. At this point, it's a good idea to separate the script code in $gatherInformation that gathers the remote information from the "infrastructure" script which orchestrates the information gathering. We'll put this information gathering part into its own script. We'll call this new script BasicHealthModel.ps1 since it gathers some basic information about the state of a machine. This script is shown in listing 12.4.

Listing 12.4 Basic Health Model script param ($procLimit = 5) @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first $procLimit TopWS = Get-Process | sort -desc WS | select -first $procLimit ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

438 } The orchestration code has to be changed to invoke this script. This turns out to be very easy - we can just use the -File option on Invoke-Command to do this: Invoke-Command -Throttle 10 -ComputerName $servers ` -ArgumentList $numProcesses ` -Script BasicHealthModel.ps1 We'll put the final revised orchestration script into a file called Get-HealthData.ps1 shown in listing 12.5

Listing 12.5 Data acquisition driver script param ( [parameter] [string] $serverFile = "servers.txt", [parameter] [int] $throttleLimit = 10, [parameter] [int] $numProcesses = 5 [parameter] [int] $model = "BasicNealthModel.ps1" ) $servers = Import-CSV servers.csv | where { $_.Day -eq (get-date).DayOfWeek } | foreach { $_.Name } Invoke-Command -Throttle 10 -ComputerName $servers ` -ArgumentList $numProcesses ` -Script $model This separation of concerns allowed us to add a parameter for specifying alternative health models. The end result of all of this is that, with a very small amount of code, we've created a very flexible framework for an "agentless" distributed health monitoring system.

AUTHOR'S NOTE
What we're doing here isn't really what most people would call monitoring. Monitoring usually implies a continual semi-real-time mechanism for noticing a problem and then generating an alert. This system is certainly not real-time and it's a pull model not a push. This solution is more appropriate for configuration analysis. In fact the Microsoft Baseline Configuration Analyzer has a very similar (though much more sophisticated) architecture.

We now have an idea of how to use remoting to execute a command on a remote server. This is a powerful mechanism but sometimes we need to send more than one command to a server - for example, we might want to run multiple data-gathering scripts, one after the other on the same machine. Since there is a significant overhead in setting up each remote connection, so we don't want to be creating a new connection for every script we execute. Instead we want to be able to establish a persistent connection to a machine, run all of the scripts and then shut the connection down. In the next section we'll look at how this is accomplished with sessions. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

439

12.3 Sessions and persistent connections
In the previous section, we looked at how to run individual scriptblocks on remote machines. For each scriptblock we sent, a new connection was set up, the scriptblock was executed and then the connection was torn down. This is simple but inefficient. It also means that we can't maintain any state on the remote host like variable settings or function definitions. In this section, we'll look at how to create persistent connections called sessions which will give us much better performance when we want to perform a series of interactions with the remote host as well as allowing us to maintain remote state. In simplest terms, a session is the environment where PowerShell commands are executed. This is true even when we run the console host: powershell.exe. The console host program creates a local session that it uses to execute the commands we type. This session is remains alive until we exit the program. When we use remoting to connect to another computer, we're also creating one remote session for every local session we remote from as shown in figure 12.2.

Local machine 1 Local session for user 1

Remote machine Remote session for user 1

User 1

Local machine 2

Each local session has a corresponding remote session on the remote machine.
Remote session for user 2

User 2

Local session for user 2

Figure 12.2 Each local session that connects to a remote target requires a corresponding session on the target machine. This session can be transient and go way at the end of the command or remain open until explicitly closed.

As mentioned earlier, each session contains all of the things we work with in PowerShell - all of the variables, all of the functions that are defined and the history of the commands we typed - and each session is independent of any other session. If we want to work with these sessions, then we need a way to manipulate them. This is done in what we should by now consider to be the "usual way": through objects and cmdlets. PowerShell represents sessions as objects which are of type PSSession. These PSSession objects play an important role in remote computing as they hold the state on the remote computer. By default, every time we connect to a remote computer by name with Invoke-

Command, a new PSSession object is created to represent the connection to that remote machine. For example, when we run the command Invoke-Command mycomputer { "hello world" } PowerShell connects to the remote machine, creates a PSSession on that computer, executes the command and then discards the session. This is pretty inefficient and will be quite slow because setting up a connection and then creating and configuring a session object is a lot of work. This pattern of operations is shown in figure 12.3.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

440

Each command sent from the local machine to the remote machine results in a new session being created on the remote machine.
Local Machine Remote Machine

Invoke-Command creates connection 1 Session created Scriptblock 1 is sent for processing Return results Close connection 1 Session destroyed

Session 1 Lifetime

Invoke-Command creates connection 2 Session created Scriptblock 2 is sent for processing Return results Close connection 2 Session destroyed

Figure 12.3 This figure shows the sequences of operations between the local and remote machines when the name of the computer is specified for each command instead of using a PSSession object.

In this figure, we see the local machine setting up then tearing down a session for each call to InvokeCommand. If we're going to run more than one command on a computer, we need a way to create persistent connections to that computer. We do this with New-PSSession. The syntax for NewPSSession is shown in figure 12.4.

Session 2 Lifetime

The syntax of the New-PSSession cmdlet
New-PSSession [[-ComputerName] ] [-Port ] [-Credential ] [-UseSSL] [-Authentication ] [-CertificateThumbprint ] [-Name ] [-ConfigurationName ] [-ApplicationName ] [-ThrottleLimit ] [-SessionOption ]

Figure 12.4 This figure shows the syntax for the New-PSSession cmdlet. This cmdlet is used to create persistent connections to a remote computer.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

441 This command has many of the same parameters that we saw in Invoke-Command. The difference is that, for New-PSSession, these parameters are used to configure the persistent session instead of the transient sessions we saw being created by Invoke-Command. The PSSession object returned from

New-PSSession can then be used to specify the destination for the remote command instead of the computer name. When we use a PSSession objects with Invoke-Command, we see the pattern of operations as shown in figure 12.5.

When New-PSSession is used, multiple commands are processed before the sessions is removed with Remove-PSSession
Local Machine Remote Machine

New-PSSession creates connection PSSession is created Invoke-Command sends scriptblock 1 for processing Return results

Session Lifetime

Invoke-Command sends scriptblock 2 for processing Return results Invoke-Command sends scriptblock 3 for processing Return results Remove-PSSession closes the connection PSSession is destroyed

Figure 12.5 This figure shows the sequences of operations between the local and remote machines when a NewPSSession is used to create a persistent session. Using a persistent session allows multiple commands to be executed before the session is removed with Remove-PSSession

In contrast to the pattern shown in figure 12.3, in this figure, the lifetime of the session begins with the call to New-PSSession and persists until it is explicitly destroyed by the call to Remove-PSSession. As we've seen in this section, the basic patterns of operation with PSSessions are pretty straightforward. There are a few additional details that we'll clarify next.

12.3.1 Additional session attributes
There are a few more PSSession characteristics that we should to be aware of. These attributes can have an impact on the way we write our scripts so we'll go over them in this section. Technically, a session is an environment or execution context where PowerShell executes commands. Each PowerShell session includes an instance of the PowerShell engine and is associated with a host program which manages the interactions between PowerShell and the end-user. These interactions include how text is displayed, how prompting is done and so on. There are two host applications included with PowerShell Version 2: the PowerShell console host and the PowerShell ISE. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

442

AUTHOR'S NOTE
These are the default hosts however there is an impressive number of other third-party hosts available. As of this writing, these third-party hosts include PowerShell Plus from Idera Software, PowerGUI from Quest Software, PowerWF from DevFarm Software Corporation and Citrix Workflow Studio from Citrix. These products present a wide variety of different approaches to hosting the PowerShell engine. There are also a number of open source PowerShell host projects available on

http://www.codeplex.com that you may choose to explore.
SESSIONS AND HOSTS The host application running our scripts can impact the portability of our scripts if we become dependent on specific features of that host. (This is why PowerShell module manifests include the

PowerShellHostName and PowerShellHostVersion elements.) The reason this is important to remoting is that, when running remotely, a different remote host is used instead of the default host. This is necessary to manage the extra characteristics of the remote or job environments. This host shows up as a process named wsmprovhost corresponding to the executable wsmprovhost.exe. This host only supports a subset of the features available in the normal interactive PowerShell hosts. SESSION ISOLATION Another point is the fact that each session is configured independently when it's created and once it's constructed, it has its own copy of the engine properties, execution policy, function definitions and so on. This independent session environment exists for the duration of the session and is not affected by changes make other sessions. ONLY ONE COMMAND RUNS AT A TIME A final characteristic of a session instance is that we can run only one command (or command pipeline) in a session at one time. If we try to run more than one command at a time, a "session busy" error will be raised. There is, however, some limited command queuing: if there is a request to run a second command synchronously (one at a time), the command will waits up to four minutes for the first command to be completed before generating the "session busy" error. However, if a second command is requested to run asynchronously, that is without waiting, the busy error will be generated immediately. With some knowledge of about characteristics and limitations of PowerShell sessions, we'll start to look at how we can use them. This principle is called isolation - each session is isolated from, and therefore not affected by, any other session.

12.3.2 Using the New-PSSession cmdlet
In this section, we'll see how to use the New-PSSession cmdlet. Let's start with an example. First we'll create a PSSession on the local machine by specifying "localhost" as the target computer. PS (1) > $s = New-PSSession localhost

AUTHOR'S NOTE
On Windows Vista, Windows Server 2008 R2 or above, you have to be running with elevated privileges for this to work. This is not the case on earlier platforms like XP.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

443 We now have a PSSession object in the $s variable that we can use to execute "remote" commands. Earlier we said each session runs in its own process. We can confirm this by using the $PID session variable to see what the process id of the session process is. First we'll run this in the remote session. PS (2) > icm $s { $PID } 7352 And we see that the process id is 7352. When we get the value in the local session by simply typing "$PID" at the command line, PS (3) > $PID 5076 we see that the local process ID is 5076. Now let's define a variable in the remote session. PS (4) > icm $s {$x=1234} With this command, we've set the variable $x in the remote session to 1234. Now let's invoke another command to retrieve the value. PS (5) > icm $s { $x } 1234 and we get the expected value back. If we had done this without using the session, we would not have gotten the correct value back because the value of $x would have been lost when the remote session was closed down. Now let's define a function "hi": PS (6) > icm $s { function hi { "Hello there" } } then use Invoke-Command to call it: PS (7) > icm $s { hi } Hello there This works in pretty much the same way as it does in the local case - changes to the remote environment are persisted across the invocations. We can redefine the function and make it reference the $x variable we defined earlier PS (8) > icm $s { function hi { "Hello there, x is $x" } } PS (9) > icm $s { hi } Hello there, x is 1234 and we get the preserved value.

AUTHOR'S NOTE
We've had people ask if other users on the computer can see the sessions we are creating. As mentioned earlier, this is not the case. Users only have access to the remote sessions they create and only from the sessions they were created from. In other words, there is no way for one session to connect to another session that it did not itself create. visible to another user is the existence of the The only aspect of a session that may be

wsmprovhost process hosting the session.

As we've seen, remote execution is just like the local case - well - almost - we have to type InvokeCommand every time. If we are executing a lot of interactive commands on a specific machine, this becomes annoying very quickly. PowerShell provides a much better way to accomplish this type of task as we'll see in the next section.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

444

12.3.3 Interactive sessions
In the previous sections, we've looked at how to issue commands to remote machines using Invoke-

Command. This was effective but gets annoying for more interactive type of work. To make this scenario easier, we can start an interactive session using the Enter-PSSession cmdlet. Once we're in an interactive session, the commands that we type are automatically passed to the remote computer and executed without having to use Invoke-Command. Let's try this out. We'll reuse the session we created in the previous section. In this session, we defined the variable $x and the function "hi". To enter interactive mode in with this session, we'll call Enter-PSSession, passing in the session object. PS (10) > Enter-PSSession $s [localhost]: PS C:\Users\brucepay\Documents> As soon as we enter interactive mode, we see that the prompt changes - it now displays the name of the machine we're connected to and the current directory.

AUTHOR'S NOTE
This is the default prompt. This can be changed in the remote session in the same way it can be changed in the local session. If you have a prompt definition in you profile, you may be wondering why that wasn't used. We'll get to that later on in section 12.6.3 when we look at some of the things we need to keep in mind when using remoting.

Now let's check out the value of $x: [localhost]: PS C:\Users\brucepay\Documents> $x 1234 and it's 1234 which is the last value we set it to using Invoke-Command in the previous section. The remote session state has been preserved and is now available to us interactively. Let's try running the "hi" function we defined. [localhost]: PS C:\Users\brucepay\Documents> hi Hello there, x is 1234 and it also works. Of course as well as examining the state of things, we can also change them. We'll change the value of $x then rerun the "hi" function which uses $x in its output. [localhost]: PS C:\Users\brucepay\Documents> $x=6 [localhost]: PS C:\Users\brucepay\Documents> hi Hello there, x is 6 and the changed value is displayed in the output. We can exit an interactive remote session using the "exit" command, just like we'd exit an interactive local session. [localhost]: PS C:\Users\brucepay\Documents> exit We see that the prompt changed back, and when we try to run the "hi" function, PS (11) > hi The term 'hi' is not recognized as the name of a cmdlet, functio n, script file, or operable program. Check the spelling of the n ame, or if a path was included, verify that the path is correct and try again. At line:1 char:3 + hi icm $s { Get-Bios } SMBIOSBIOSVersion Manufacturer Name SerialNumber Version PSComputerName : : : : : : 3.34 Phoenix Technologies, LTD Phoenix - Award BIOS v6.00PG MXK5380BK3 NA580 HP-CPC - 42302e31 brucepay64h

It returns a set of information about the BIOS on the remote machine. Now we're set up to use

Import-PSSession to create a local proxy for this command.
PS (5) > Import-PSSession -session $s -CommandName Get-Bios ModuleType Name ExportedCommands ---------- ------------------Script tmp_00288002-bcec-43fd... Get-Bios You might recognize this output from this command - it's the same thing we see when we do Import-

Module. We'll discuss what that means in a minute but first let's see if we now have a local Get-Bios command by running it: PS (6) > Get-Bios SMBIOSBIOSVersion Manufacturer Name SerialNumber Version : : : : : 3.34 Phoenix Technologies, LTD Phoenix - Award BIOS v6.00PG MXK5380BK3 NA580 HP-CPC - 42302e31

and we get the same result we saw when we did the explicit remote invocation. This is the goal of implicit remoting - to make the fact that the command is being executed remotely invisible. In the next section we're going to see how it all works.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

450

12.4.2 How implicit remoting works
Now that we've seen implicit remoting in action, let's take a look at how it's implemented. The sequence of operations performed by the implicit remoting mechanism is shown in figure 12.10.
How remote commands are imported by the implicit remoting mechanism
Local machine User calls Import-Session $s -Command c User Request to import ‘c’ is set to remote machine Lookup ‘c’ Remote machine

Local Command Table

Import Request Processor
Add ‘c’

Proxy Function Generator

Command Table on Remote Machine
Raw ‘c’ metadata

Processed ‘c’ metadata

Figure 12.10 Import-PSSession works by sending a message to the remote computer to retrieve the metadata for the requested command. This metadata is processed and returned to the local machine where a proxy advanced function is generated.

When the user requests that a command be imported, a message is set to the remote computer for processing. The import request processor looks up the command and retrieves the metadata (i.e. the

CommandInfo object) for that command. That metadata is processed to simplify it, removing things like complex type attributes. Only the core remoting types are passed along. This metadata is received by the local machine's proxy function generator. It uses this metadata to generate a script command that will implicitly call the remote command. Let's take closer look what the generated proxy looks like. We can look at the imported Get-Bios command using Get-Command. PS (7) > Get-Command Get-Bios CommandType ----------Function Name ---Get-Bios Definition ---------...

The output shows us that we have a local function called Get-Bios. We can look at the definition of that function by using the Definition property on the CommandInfo object returned by Get-

Command:
PS (8) > Get-Command Get-Bios | % { $_.Definition } param( [Switch] ${AsJob}) begin { try { $positionalArguments = & $script:NewObject collections.arraylist foreach ($parameterName in $PSBoundParameters.BoundPositionally) { $null = $positionalArguments.Add( $PSBoundParameters[$parameterName] ) $null = $PSBoundParameters.Remove($parameterName) } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

451 $positionalArguments.AddRange($args) $clientSideParameters = Get-PSImplicitRemotingClientSideParameters ` $PSBoundParameters $False $scriptCmd = { & $script:InvokeCommand ` @clientSideParameters ` -HideComputerName ` -Session (Get-PSImplicitRemotingSession ` -CommandName 'Get-Bios') ` -Arg ('Get-Bios', $PSBoundParameters, $positionalArguments) ` -Script { param($name, $boundParams, $unboundParams) & $name @boundParams @unboundParams}} $steppablePipeline = $scriptCmd.GetSteppablePipeline( $myInvocation.CommandOrigin) $steppablePipeline.Begin($myInvocation.ExpectingInput, $ExecutionContext) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } Even though the above output has been reformatted a bit to make it more readable, this is a pretty complex function and uses many of the more sophisticated features we've covered in previous chapters. It uses advanced functions, splatting, scriptblocks and steppable pipelines. Fortunately we never have to write these functions ourselves.

AUTHOR'S NOTE
We don't have to create proxy functions for this particular scenario but back in section 11.5.2 we saw how this technique can be very powerful in extending the PowerShell environment.

The Import-PSSession cmdlet does this for us. In fact it will create a proxy function for each command it's importing which could lead to a lot of commands. Managing a lot of new commands in our environment could be a problem so let's go back to looking at exactly what the output from Import-

PSSession was. We commented at the time that it looked like the output from Import-Module and in fact this is exactly what it is. As well as generating proxy functions on our behalf, Import-

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

452

PSSession also creates a module to contain these functions. We'll use Get-Module for verify this, formatting the output as a list to see all of the details: PS (9) > Get-Module | fl * ExportedCommands Name Path : {Get-Bios} : tmp_00288002-bcec-43fd-853f-8059edfd2430_y hv1i4f4.ogw : C:\Users\brucepay\AppData\Local\Temp\tmp_0 0288002-bcec-43fd-853f-8059edfd2430_yhv1i4 f4.ogw\tmp_00288002-bcec-43fd-853f-8059edf d2430_yhv1i4f4.ogw.psm1 : Implicit remoting for http://brucepay64h/w sman : 00288002-bcec-43fd-853f-8059edfd2430 : C:\Users\brucepay\AppData\Local\Temp\tmp_0 0288002-bcec-43fd-853f-8059edfd2430_yhv1i4 f4.ogw : {ImplicitRemoting} : 1.0 : Script : ReadWrite : {[Get-Bios, Get-Bios]} : {} : {} : {} : {} : {} : System.Management.Automation.SessionState : $sourceIdentifier = [system.management.automation.wildcardpattern]:: Escape( $eventSubscriber.SourceIdentifier) Unregister-Event -SourceIdentifier ` $sourceIdentifier -Force -ErrorAction ` SilentlyContinue if ($previousScript -ne $null) { & $previousScript $args } ExportedFormatFiles : {C:\Users\brucepay\AppData\Local\Temp\tmp_ 00288002-bcec-43fd-853f-8059edfd2430_yhv1i 4f4.ogw\tmp_00288002-bcec-43fd-853f-8059ed fd2430_yhv1i4f4.ogw.format.ps1xml} ExportedTypeFiles : {}

Description Guid ModuleBase

PrivateData Version ModuleType AccessMode ExportedFunctions ExportedCmdlets NestedModules RequiredModules ExportedVariables ExportedAliases SessionState OnRemove

PS (10) > Things to notice in this output (again reformatted for readability) include the fact that the module name and path are temporary generated names. This module also defines an OnRemove handler (see section 10.7.7) to clean up when the module is removed. To see the contents of the module, we can look at the temporary file that was created by doing opening it in an editor using the module's Path property. For example, to open the module file in PowerShell ISE, do: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

453 PS (14) > powershell_ise (get-command Get-Bios).Module.Path Alternatively, we can save the session to an explicitly named module for reuse with Export-

PSSession. We'll save this session as a module called 'bios'.
PS (15) > Export-PSSession -OutputModule bios -Session $s ` >> -type function -commandname Get-Bios -allowclobber >> Directory: C:\Users\brucepay\Documents\WindowsPowerShell\Mod ules\bios Mode ----a---a---a--LastWriteTime ------------11/29/2009 1:05 PM 11/29/2009 1:05 PM 11/29/2009 1:05 PM Length -----10359 99 532 Name ---bios.psm1 bios.format.ps1xml bios.psd1

Executing this command created a new module in our user module directory. It created the script module file (.psm1), the module manifest (.psd1) and a file containing format information for the command. Notice that we used the -AllowClobber parameter. This is because the export is using the remote session to gather the data. If it finds a command being exported that already exists in the caller's environment, this would be an error. Since Get-Bios already exists, we had to use -

AllowClobber. Now we'll try out the new module. First we need to clean the existing module and session. PS (32) > get-module | remove-module PS (33) > remove-pssession $s Now we can import the module. PS (34) > import-module bios and it returns right away. It can do this because it hasn't actually set up the remote connection yet. This will happen the first time we access one of the functions in the module. We'll run Get-Bios: PS (35) > get-bios Creating a new session for implicit remoting of "Get-Bios" comman d... The term 'Get-Bios' is not recognized as the name of a cmdlet, f unction, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is co rrect and try again. + CategoryInfo : ObjectNotFound: (Get-Bios:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException When we ran this command, we saw a message saying that a new connection was being created and then the credential prompt dialog popped up. This is pretty much as expected. But then we got an error saying the command Get-Bios wasn't found. This is because we dynamically added the function to the remote session. When we established a new session, since we didn't add the function, it wasn't there. In the next section, we'll cover how to create remote endpoints that always contain our custom functions. Let's briefly review where we are. We've looked at how PowerShell remoting allows us to execute commands on a remote machine. We saw how to use explicit remoting to send scriptblocks to the remote computer to be executed. We looked at interactive remoting where every command is sent to the remote machine making it look like we are working directly on that machine. Finally we looked at implicit remoting which makes it appear that all of our commands are being run locally. This greatly simplifies remoting for the casual user. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

454 One thing that's been constant across all of these remoting experiences is that we always had to wait for the remote commands to complete before issuing the next command. But early on in our discussion of remoting, we observed that, because there are two (or more) processes involved, things actually do happen concurrently. In the next section, we'll see how this characteristic is used to implement background jobs in PowerShell.

12.5 Background jobs in PowerShell
In the previous section, we saw that, while remote PowerShell sessions run in separate processes, the user is still prevented from running new commands until the remote command completes. If we change things so that the caller doesn't block then other commands can run in parallel. This is basically how PowerShell background jobs work. With background jobs, the arrangement of executing commands and processes are shown in figure 12.11

PowerShell Process #1
User

PowerShell Process #2
Background Command #1

Interactive cmds

Foreground Command

PowerShell Process #3

The user sends interactive commands to be executed by the foreground loop. Background commands are executed in separate processes. Each process has its own command loop.

Background Command #2

PowerShell Process #4

Figure 12.11 For each background job the user creates, a new instance of powershell.exe is run to host the command loop for that job. This means that, if there are 3 background jobs as shown, then there are 4 processes running - 3 for the background jobs and 1 for the interactive foreground job.

Of course there is more to background jobs than simply executing multiple things at the same time. Background jobs are designed to be commands that run asynchronously while we continue to do other things at the console. This means that there needs to be a way to manage these background jobs starting and stopping them as well retrieving the output in a controlled way. In this section we'll cover the cmdlets that are used to accomplish these tasks. We'll look at starting, stopping and waiting for jobs. We'll look at the PSJob objects used to represent a running job. Finally we'll look at how to combine remoting with jobs to run jobs on remote machines. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

455

12.5.1 The Job commands
As with remoting, jobs are managed with a set of cmdlets. These cmdlets are shown in table 12.2.

Table 12.2 This table lists the cmdlets for working with PowerShell jobs.
Cmdlet Description This cmdlet is used to start background jobs. It takes a scriptblock as the argument representing the job to execute.

Start-Job

Stop-Job Get-Job

This cmdlet stops a job based on the JobID. Get-Job returns a list of currently executing jobs associated with the current session.

Wait-Job Receive-Job Remove-Job

Waits for one or more jobs to complete. Gets the results for a specific job. Remove a job from the job table so the resources can be released.

A background job runs commands asynchronously. They are used to execute long-running commands in a way that the interactive session is not blocked until that command completes. When a synchronous command runs, PowerShell waits until that command has completed before accepting any new commands. When a command is run in the background, instead of blocking, the command returns immediately emitting an object that represents the new background job. While we get control back immediately with the job object, we obviously won't get the results of that job even if the job runs very quickly. Instead we use a separate command to get the job's results. You also have commands to stop the job, to wait for the job to be completed, and finally, to delete the job. Let's see how these commands are used.

12.5.2 Working with the Job cmdlets
We us the Start-Job command to start a background job on a local computer. Let's try a simple example: PS (1) > start-job { "Hi" } | fl HasMoreData StatusMessage Location Command JobStateInfo Finished InstanceId Id Name ChildJobs Output Error Progress Verbose Debug : : : : : : : : : : : : : : : True localhost "Hi" Running System.Threading.ManualResetEvent fefc87f6-b5a7-4319-9145-616317ac8fcb 1 Job1 {Job2} {} {} {} {} {}

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

456 Warning State : {} : Running

As with the remoting cmdlets, command to execute is specified by a scriptblock. When the command runs, we see that an object is returned containing a wealth of information about the job. We'll look at this object in a detail later on. For now, we'll keep looking at the cmdlets. Now that we've started a job, we can use the Get-Job cmdlet to get information about that job: PS (3) > get-job | fl HasMoreData StatusMessage Location Command JobStateInfo Finished InstanceId Id Name ChildJobs Output Error Progress Verbose Debug Warning State : : : : : : : : : : : : : : : : : True localhost "Hi" Completed System.Threading.ManualResetEvent fefc87f6-b5a7-4319-9145-616317ac8fcb 1 Job1 {Job2} {} {} {} {} {} {} Completed

This cmdlet returned the same job object that we saw returned from Start-Job. (We can tell it's the same object by looking at the instance id which is a GUID and is guaranteed to be unique for each job.) There is one significant different in this output however - if we look at the State field, we see that it has change from Running to Completed. So - the first thing to note is that a job remains in the job table even after it has completed and will remain there until it is explicitly removed using the Remove-Job cmdlet. To get the results of the job, we can use another cmdlet - Receive-Job. This cmdlet will return the results of the command that was executed: PS (5) > Receive-Job 1 Hi and this returns the string that was emitted in the job body. This is not a very interesting example. Let's try something that will take a bit longer to run. First we'll define the scriptblock we want to run in the

$jsb variable.
PS (9) > $jsb = { >> foreach ($i in 1..10) { Start-Sleep 1; "i is $i" } >> } >> Now start the job running. We'll let the job object that was returned use the default formatting which complains if the screen is too narrow for all of the columns to be displayed. This doesn't really matter since the only thing we want that this point is the jog's Id. PS (10) > Start-Job $jsb WARNING: column "Command" does not fit into the display and was removed. Id -Name ---State ----HasMoreData ----------Locat ion -----

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

457 5 Job5 Running True lo...

Now we can start calling Receive-Job with the job's Id. PS (11) > Receive-Job 5 i is 1 i is 2 The first call returned the first two items out of the ten we're expecting. Let's call it again PS (12) > Receive-Job 5 i is 3 i is 4 and we get another two items. Call it again quickly PS (13) > Receive-Job 5 i is 5 and we get one additional item. We'll keep calling it until we get all of the items. PS (14) > Receive-Job 5 i is 6 i is 7 PS (15) > Receive-Job 5 i is 8 PS (16) > Receive-Job 5 i is 9 PS (17) > Receive-Job 5 i is 10 PS (18) > Receive-Job 5 This last call didn't return anything since the job has completed and all items have already been returned. We can verify this by calling Get-Job: PS (19) > get-job 5 WARNING: column "Command" does not fit into the display and was removed. Id -5 Name ---Job5 State ----Completed HasMoreData ----------False Locat ion ----lo...

and we see that it's state is Completed. Because the job is running asynchronously, the number of items that are returned depends on when you call Receive-Job. WAITING FOR JOBS TO COMPLETE So how do we wait until the job has completed? We could write a loop to keep checking the State field but that would be annoying and inefficient. Instead we can use the Wait-Job cmdlet: PS (21) > $jb = start-job $jsb; wait-job $jb ; receive-job $jb Id -9 i is i is i is i is i is i is i is i is i is Name ---Job9 1 2 3 4 5 6 7 8 9 http://www.manning-sandbox.com/forum.jspa?forumID=542 State ----Completed HasMoreData ----------True Locat ion ----lo...

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

458 i is 10 In this example, we're capturing the job object emitted by Start-Job in the $jb variable so we can use it in the subsequent Wait-Job and Receive-Job commands. Because of the Wait-Job, when we call Receive-Job we get all of the input. Notice that Wait-Job returns the object representing the job that has finished. We can use this to simplify the example a bit: PS (22) > start-job $jsb | wait-job | receive-job i is 1 i is 2 i is 3 i is 4 i is 5 i is 6 i is 7 i is 8 i is 9 i is 10 When Start-Job passes the job object to Wait-Job and, once the job has completed, passes the job object to Receive-Job, eliminating the need for an intermediate variable. REMOVING JOBS So far, we've been creating jobs but haven't removed any yet. This means that when we call Get-Job, we'll see that there are a number of jobs still in the job table: PS (23) > get-job WARNING: column "Command" does not fit into the display and was removed. Id -1 3 5 7 9 Name ---Job1 Job3 Job5 Job7 Job9 State ----Completed Completed Completed Completed Completed HasMoreData ----------False False False True False Locat ion ----lo... lo... lo... lo... lo...

Each time we start a job, it gets added to the job table. We can clean things up using the Remove-Job cmdlet. To empty the table, we can use Remove-Job with a wildcard: PS (24) > remove-job * Now when we call Get-Job, nothing is returned: PS (25) > get-job PS (26) > This is probably not the best way to clean things up however. A better solution would be to look for jobs that have completed and have no more data. This would look like: function CleanupJobs { Get-Job | where { $_.State -eq "Completed" -and -not $_.HasMoreData } | Remove-Job } This function calls Get-Job to get the list of all jobs, filters that list based on the State and HasMoreData fields and pipes the filtered list into Remove-Job. This allows us to clean up the job

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

459 table without worrying about losing information or getting errors. If we do want to kill all of the jobs immediately, we can use the -Force parameter on Remove-Job. In the next section, we'll look at ways we can apply concurrent jobs to solve problems.

12.5.3 Working with multiple jobs
So far we've just looked at simple patterns working with one job at a time. In fact we can run a number of jobs at the same time. This however, complicates things - we have to be able to handle the output from multiple jobs. Let's look at how to do this. The listing shown in 12.6 shows how to wait for a set of jobs and then receive the results.

Listing 12.6 Example of running multiple jobs
1..5| foreach { start-job -name "job$_" -scriptblock { param($number) $waitTime = Get-Random -min 4 -max 10 Start-Sleep -Seconds $waitTime "Job $number is complete; waited $waitTime" } -ArgumentList $_ > $null } wait-job job* | receive-job #1 Job is named 'job#' #2 Param receives the job number #3 Wait for between 4 and 10 seconds at random #4 Pass in the job number #5 Wait for the named jobs and get results then gets all of the results. Let's run this and see what happens. PS (1) > 1..5| foreach { >> start-job -name "job$_" -scriptblock { >> param($number) >> $waitTime = Get-Random -min 4 -max 10 >> Start-Sleep -Seconds $waitTime >> "Job $number is complete; waited $waitTime" >> } -ArgumentList $_ > $null } >> PS (2) > wait-job job* | receive-job Job 1 is complete; waited 4 Job 2 is complete; waited 4 Job 3 is complete; waited 8 Job 4 is complete; waited 5 Job 5 is complete; waited 7 and we see that all of the results are captured, ordered by the job name. Now let's look a more useful application of this pattern. Listing 12.7 shows a function that searches multiple directories in parallel looking for a specific pattern. #1 #2 #3

#4 #5

This example starts a number of jobs that will run concurrently, waits for all of them to complete and

Listing 12.7 A function that searches a collection of folders in parallel function Search-FilesInParallel { param ( [parameter(mandatory=$true, position=0)] $Pattern,

#1

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

460 [parameter(mandatory=$true, position=1)] [string[]] $Path, [parameter(mandatory=$false)] $Filter = "*.txt", [parameter(mandatory=$false)] [switch] $Any ) $jobid = [guid]::NewGuid().ToString() $jobs = foreach ($element in $path) { start-job -name "${jobid}$_" -scriptblock { param($pattern, $path, $filter, $any) Get-ChildItem -Recurse -Filter $filter | Select-String -list:$any $pattern } -ArgumentList $pattern,$element,$filter,$any } wait-job -any:$any $jobs | receive-job remove-job -force $jobs } #1 The pattern to search for #2 List of folder paths to search #3 The type of file to search #4 Stop searching if any match found #5 Generate GUID to use for job ID #6 Iterate through list of paths #7 start a search job for each path #8 Pass -any switch to Select-String #9 Wait for any or all jobs #10 Remove all of the jobs This function takes a list of folder paths to search along with a pattern to search for. By default it will only search .txt files. It also has a switch -any that controls how the search is performed. If the switch is not specified, all matches from all folders will be returned. If it is specified, only the first match will be returned and the remaining incomplete jobs will be cancelled. This seems like a pretty useful tool. Unfortunately, jobs are implemented by creating new processes for each jobs and this is a very expensive operation. So expensive in fact, that generally it's much slower that simply searching all of the files serially. In practice, PowerShell jobs are a way of dealing with latency - the time it takes for an operation to return a result - not throughput - the amount of data that gets processed. This is a good tradeoff for remote management tasks where we're talking to a lot of machines more or less at once. The amount of data, as we saw in the monitoring example in section 12.2, is frequently not large and the overall execution time is dominated by the time it takes to connect to a remote machine. With that in mind, let's look at how remoting and jobs work together. #5 #6 #7

#2 #3

#4

#8

#9 #10

12.5.4 Starting jobs on remote computers
So far we've only been looking at local jobs however, since the job infrastructure is based on the remoting framework, we can also create and manage background jobs on remote computers. The easiest way to is to use the -AsJob parameter on Invoke-Command. Alternatively, the scriptblock passed to Invoke-Command can call Start-Job explicitly. Let's take a look at how this works. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

461 CHILD JOBS AND NESTING So far we've always talked about job objects as being atomic - one Job object per job. In fact it's a bit more complicated than that. There are scenarios where we need to be able to aggregate collections of jobs into a single master or executive job. We'll get to those situations in a minute. For now, we'll simply call out the fact that background jobs always consists of a parent job and one or more child jobs. For jobs started using Start-Job or the -AsJob parameter on Invoke-Command, the parent job is the "executive". It does not run any commands or return any results.

AUTHOR'S NOTE
In other words, the executive does no actual work - it just supervises. All of the actual work is done by the subordinates. That sounds familiar somehow...

This collection of child jobs is stored in the ChildJobs property of the parent job object. The child job objects have a name, ID, and instance ID that differs from the parent job so that you can manage the parent and each child job individually or as a single unit. To see the parent and all of the children in a Job, we use the Get-Job cmdlet to get the parent Job object, and then pipe it to Format-List which displays the Name and ChildJobs as properties of the objects. This looks like: PS (1) > get-job | format-list -property Name, ChildJobs Name : Job1 ChildJobs : {Job2} We can also use a Get-Job command on the child job, as shown in the following command: PS (2) > get-job job2 Id -2 Name ---Job2 State ----Completed HasMoreData ----------True Location -------localhost Command ------get-process

and so on until we get to a Job that has no children. NESTED JOBS WITH INVOKE-COMMAND Let's look at the scenario where we need to have more than one nested job. When Start-Job is used to start a job on a local computer, the job always consists of the executive parent job and a single child job that runs the command. When we use the -AsJob parameter on Invoke-Command to start a job on multiple computers, we have the situation where the job consists of an executive parent job and one child job for each command running on a remote server as shown in figure 12.12

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

462
The user calls InvokeCommand to start a job with multiple nested jobs, one for each target node in $list

User

Invoke-Command -Computer $list { Get-Date } -AsJob

Parent Job (Executive)

Nested Job #1

Nested Job #1

Nested Job #3

Nested Job #4

Get-Date

Get-Date

Get-Date

Get-Date

Figure 12.12 This figure shows the relationship between the executive job and the nested jobs created when InvokeCommand -AsJob is used to run commands on multiple remote computers.

When we use Invoke-Command to explicitly run Start-Job one the remote machines, the result is the same as a local command run on each remote computer. The command returns a job object for each computer. The job object consists of an executive parent job and one child job that runs the command. The parent job represents all of the child jobs. When we manage a parent job, you also manage the associated child jobs. For example, if we stop a parent job, all child jobs are also stopped. Similarly, we get the results of a parent job, we're actually also getting the results of all child jobs. We can, however, manage child jobs individually. This is most useful when we want to investigate a problem with a job or get the results of only one of a number of child jobs started by using the -AsJob parameter of Invoke-Command. The following command uses Invoke-Command with -AsJob to start background jobs on the local computer and two remote computers. The command saves the job in the $j variable. PS (1) > $j = invoke-command -computername localhost, Server01, Server02 ` -command {get-date} -AsJob When we display the Name and ChildJob properties of the object in $j, it shows that the command returned a job object with three child jobs, one for each computer. PS (2) > $j | format-list name, childjobs Name : Job3 ChildJobs : {Job4, Job5, Job6} When you display the parent job, it shows that the overall job was considered to have failed. PS (3) > $j Id -1 Name ---Job3 State HasMoreData --------------Failed True Location Command -------------localhost,server... get-date

But on further investigation, when we run Get-Job on each of the child jobs, we find that only one of them has actually failed: PS (4) > get-job job4, job5, job6 Id -4 5 Name ---Job4 Job5 State ----Completed Failed HasMoreData ----------True False Location -------localhost Server01 Command ------get-date get-date

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

463 6 Job6 Completed True Server02 get-date

To get the results of all child jobs, use the Receive-Job cmdlet to get the results of the parent job. But you can also get the results of a particular child job, as shown in the following command. PS (5) > Receive-Job -job 6 -keep | format-table ComputerName,DateTime-auto ComputerName DateTime ------------ -------Server02 Thursday, March 13, 2008 4:16:03 PM By using child jobs, we much more granular control of the set of activities we have running. The way we've been working with jobs so far has been much like when we were using InvokeCommand and specifying the name of a computer. Each time we contacted the computer, InvokeCommand created a new session. We're doing pretty much the same thing when we use Start-Job. With Invoke-Command, we were able to improve our efficiency by creating sessions. In the next section we'll see how sessions work with jobs.

12.5.5 Running jobs in existing sessions
Each background job runs in its own PowerShell session, paralleling the way each remote command is also executed in its own session. As was the case with remoting, this can be a temporary session that only exists for the duration of the background job, or it can be run in an existing PSSession. But - how to do this is not obvious as the Start-Job cmdlet does not have a -Session parameter. Instead we have to use Invoke-Command with the -Session and -AsJob parameters. Here's what this looks like. First we create a PSSession object: PS (1) > $s = New-PSSession Now we'll pass that session object to Invoke-Command with -AsJob specified: PS (2) > $job = Invoke-Command -Session $s -AsJob {$PID} The scriptblock that we're passing in simply returns the process ID of the session. We'll use Receive-Job to retrieve it. PS (3) > Receive-Job $job 10808 We can call Invoke-Command without -AsJob with the same session object and scriptblock PS (4) > invoke-command -Session $s {$PID } 10808 and we get the same process ID back which is expected since the session is persistently associated with the same process.

AUTHOR'S NOTE
So why is there no

-Session parameter on Start-Job? In fact this parameter did exist at one

point in the development of PowerShell V2. That was when jobs and remoting used the same transport as well as the same basic infrastructure. This was found to be inefficient for communication with local jobs, required that the remoting service be enabled on the local machine just for jobs. It also meant that we needed admin privileges to start a job. As a consequence, the existing transport layer used by remoting (See chapter 12 for details) was replaced for communication with jobs and anonymous pipes are used instead. This change solved these problems but it had the unfortunate side effect that jobs could no longer be directly run with in

PSSession instances because the

PSSession object was tied to WSMan remoting.
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

464 One thing to keep in mind is that when a job is run in an existing PSSession, that session cannot be used to run additional tasks until the job has completed. This means that we have to create multiple

PSSession objects if we need to run multiple background tasks but want to avoid the overhead of creating new processes for each job. As always, it's up to the application designer to decide how best to manage resources for their application.

12.6 Considerations when running commands remotely
When we run commands on multiple computers we need to be aware, at least to some extent, of how the execution environment can differ on the target machines. For example, target machine may be running a different version of the operating system or they may have a different processor. There may also be differences in what applications are installed, how files are arranged or where things are placed in the registry. In this section, we'll look at a number of these issues.

12.6.1 Remote session startup directory
When a user connects to a remote computer, the system sets the startup directory for the remote session to a specific value. This value will change depending on the version of the operating system on the target machine:    If the machine is running Windows Vista, Windows Server 2003 R2 or later, the default starting location for the session is the user's home directory which is typically 'C:\Users\'. On Windows Server 2003, the user's home directory is also used but this resolves to a slightly different path: 'C:\Documents and Settings\' On Windows XP, the default user's home directory is used instead of the connecting user's. This resolves typically resolves to 'C:\Documents and Settings\Default User'.

The default starting location can be obtained from either the $ENV:HOMEPATH environment or the PowerShell $HOME variable. By using these variables instead of hardcoded paths in our scripts, we can avoid any problems related to these differences. Next we'll look at issues related to startup and profiles.

12.6.2 Profiles and remoting
Most PowerShell users eventually create a custom startup script or profile that is used to customize their environment. These customizations typically include defining convenience functions and aliases. While profiles are a great feature for customizing local interactive sessions, if the convenience commands they define are used in scripts that we want to run remotely, we'll encounter problems. This is because our profiles are not run automatically in remote sessions and that means that the convenience commands defined in the profile are not available in the remote session. In fact, the $PROFILE variable, which points to the profile file, is not even populated for remote sessions. As a best-practice, for production scripting, we should make sure our scripts never become contaminated with elements defined by our profiles. One way to test this is to run the script from

powershell.exe with the -noprofile option which looks like: powershell -noprofile -file myscript.ps1 This command will run the script without loading our profile. If the script depends on anything defined in the profile, it will generate errors. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

465 However, for remote interactive sessions, it would be nice to have the same environment everywhere. We can accomplish this by using Invoke-Command with the -FilePath parameter to send our profile file to the remote machine and execute it there. The set of commands we need to accomplish this are: $c = Get-Credential $s = New-PSSession -Credential $ -ComputerName targetComputer Invoke-Command -Session $s -FilePath $PROFILE Enter-PSSession $s First we get the credential for the target machine (this typically won't be needed in the domain environment). Next we create a persistent session to the remote computer. Then we use -FilePath on Invoke-Command to execute the profile file in the remote session. Finally, with the session properly configured, we can call Enter-PSSession to start our remote interactive session with all of our normal customizations. Alternatively, sometimes, we may want to run a profile on the remote machine instead of our local profile. Since $PROFILE is not populated in our remote session, we'll need to be a bit clever to make this work. The key is that, while $PROFILE isn't set, $HOME is. We can use this to compose a path to our profile on the remote computer. The revised list of commands looks like: $c = Get-Credential $s = New-PSSession -Credential $ -ComputerName targetComputer Invoke-Command -Session $s { . "$home\Documents\WindowsPowerShell\profile.ps1" } Enter-PSSession $s This command dot-sources the profile file in the user's directory on the remote machine into the session. Note that this script will not work on XP or Windows Server 2003 because the document directory on those versions of Windows is "Documents and Settings" instead of simply "Documents". For those operating systems, the set of steps would look like: $c = Get-Credential $s = New-PSSession -Credential $ -ComputerName targetComputer Invoke-Command -Session $s { . "$home\Documents and Setting\WindowsPowerShell\profile.ps1" } Enter-PSSession $s In this section we saw how to cause our profile to be used to configure the remote session environment. At the end of the section, we revisited the idea that some system paths will vary depending on the operating system version. In the next section we'll examine another area where these variations can cause problems.

12.6.3 Issues running executables remotely
PowerShell remoting allows us to execute the same types of commands remotely as we can locally. This includes external applications or executables. In fact the ability to remotely execute commands like

shutdown.exe to restart a remote host or ipconfig.exe to get network settings is critical for system management. For the most part, console-based commands will work properly since they only read and write to the standard input, output and error pipes. Commands that won't work are ones that directly call the Windows Console APIs like console-based editors or text-based menu programs. This is because there is ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

466 no console object available in the remote session. Since these applications are rarely used anymore, this typically won't have a big input. But there are some surprises. For example, the net.exe command will work fine most of the time, but if we do something like: PS (1) > net use p: '\\machine1\c$' /user:machine1\user1 * Type the password for \\machine1\c$: which prompts for a password, in a remote session we'll get an error: [machine1]: > net use p: '\\machine1\c$' /user:machine1\user1 * net.exe : System error 86 has occurred. + CategoryInfo : NotSpecified: (System error 86 has occ urred.:String) [], RemoteException + FullyQualifiedErrorId : NativeCommandError The specified network password is not correct. Type the password for \\machine1\c$: [machine1]: > This command prompted for a password which returned an empty string. The other kind of program that won't work properly are commands that try to open a user interface (also known as "try to pop GUI") on the remote computer. If the remote command starts a program that has a GUI interface, the program starts, but no window will appear. If the command eventually completes, then control will be returned to the caller and things will be more or less fine. However, if the process is blocked waiting for the user to provide some input to the invisible GUI, the command will hang and must be stopped manually by hitting ctrl-C. If this doesn't work, then some other mechanism will have to be used to terminate the process.. For example, if Invoke-Command is used to start notepad.exe on a remote machine, the process will start on the remote computer, but the notepad.exe window will never appear. At this point, the command will appear to be "hung" and we'll have to use ctrl-C to stop the command and get control back. Now let's look at some more places where accessing the console can cause problems and how to avoid these problems.

12.6.4 Reading and writing to the console
As we saw in the last section, executables that read and write directly to the console won't work properly. The same considerations apply for scripts that do things like call the Console APIs directly session: > [console]::writeline("hi") > > [console]::readline() > themselves. For example, let's call the [Console]::WriteLine() and [Console]::ReadLine() APIs in a remote [machine1]: [machine1]: [machine1]: [machine1]:

Neither of these calls worked properly. When we called the [Console]::WriteLine() API, nothing was displayed and when we called the [Console]::ReadLine() API, it returned immediately instead of waiting for input. It is still possible to write interactive scripts but we have to use the PowerShell Host cmdlets and APIs: [machine1]: > Write-Host Hi Hi [machine1]: > ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

467 [machine1]: > Read-Host "Input" Input: some input some input If we use these cmdlets as shown in the example, we can read and write to and from the host and the remoting subsystem will take care of making everything work.

SCRIPTING BEST-PRACTICE
To ensure that scripts will work in remote environments, don't call the console APIs direct. Use the PowerShell host APIs and cmdlets instead.

With console and GUI issues out of the way, let's look at how remoting affects the objects we're passing back and forth.

12.6.5 Remote output vs. local output
Much of the power in PowerShell comes from the fact that it passes objects around instead of strings. In this section we'll look at how introducing remoting affects these objects. When PowerShell commands are run locally, we're working directly with the "live" .NET objects which means that we can use the properties and methods on these objects to manipulate the underlying system state. This is not true when we're working with remote objects. Remote objects are serialized converted into a form that can be passed over the remote connection - when they are transmitted between the client and the server. While a small number of types are transmitted in such a way that they can be fully recreated by the receiving end, the majority of the types of objects we work with are not. Instead, when they are deserialized, they are turned into property bags - collections of data properties with the same names as the original properties. This difference is shown in figure 12.13.

Local machine User runs local Get-Process User Get-Process

Pipeline Processor

When a local command is invoked, that command generates “live” .NET objects which are returned to the invoker. When a remote command is invoked, the objects returned by the remote command are serialized on the remote machine and returned to the invoker as “property bags”: collections of properties attached to PSObject instances.
Remote machine

ProcessInfo .NET Objects

User runs remote Get-Process User

Invoke-Command machine { Get-Process }

PSObject Property Bags

Remoting Serializer

Pipeline Processor
ProcessInfo .NET Objects

Figure 12.13 This figure shows the differences in the way objects that are returned for the local and remote invocation cases. In the local case, live .NET objects are returned. In the remote case, the objects are serialized and returned as "property bags". ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

468 Typically, we can use deserialized objects just as we would use live objects, but we must be aware of their limitations. Another thing to be aware of is that the objects that are returned by through remoting will have had properties added that allow us to determine the origin of the command. POWERSHELL SERIALIZATION Since we can't have the complete set of types in an unbounded set on all computers, obviously we had to limit the number of types that serialized with fidelity where the remote type is the same type as the local type. To address the restrictions of a bounded set of types, types that aren't serialized with fidelity are serialized with as collections of properties, also called property bags. The serialization code takes each object, and adds all of its properties to the property bag. Recursively, it then looks at values of each the members. If the member value is not one of the ones supported with fidelity, a new property bag is created and the members of the members values are added to the new property bag and so on. This approach preserves structure if not the actual type and allows remoting to work uniformly everywhere. DEFAULT SERIALIZATION DEPTH The approach we just described allows any object to encoded and transferred to another system. There is, however another thing to consider. Objects have members that contain objects that contain members and so on. The full tree of objects and members can be very complex. Transferring all of the data all of the make the system unmanageably slow. This is addressed by introducing the idea of serialization depth. The recursive encoding of members stops when this serialization depth is reached. The default for objects as 1. The final source of "gottcha's" when writing portable, remotable scripts has to do with issues around processor architecture and the operating system differences they entail. We'll work through this final set of issues in the next (and last) section in this chapter and then we'll be done.

12.6.6 Processor architecture issues
We've looked at a number of aspects of the remote execution environment that may cause problems operating system differences, issue with session initialization and with GUI and console interactions. The last potential source of problems that we're going to look at is the fact that the target machine may be running on a different processor architecture (i.e. 64-bit vs. 32-bit) than the local machine. If the remote computer is running a 64-bit version of Windows, and the remote command is targeting a 32-bit session configuration, such as Microsoft.PowerShell32, the remoting infrastructure loads a WOW64 process and Windows automatically redirects all references to the $ENV:Windir\System32 directory to the $ENV:WINDIR\SysWOW64 directory. For the most part everything will still just work (that is the point of the redirection) unless to try to invoke an executable in the System32 directory that does not have a corresponding equivalent in the SysWow64 directory. Let's see what this looks like. We'll try to run defrag.exe on a 64-bit OS targeting the 32-bit configuration. This results in the following output: PS (STA) (1) > icm -ConfigurationName microsoft.powershell32 ` >> -computername localhost -command { defrag.exe /? } >> The term 'defrag.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

469 + CategoryInfo : ObjectNotFound: (defrag.exe:Strin g) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException Because there was no corresponding defrag.exe in the SysWoW64 directory, the command was not found. If we target the 32 bit configuration: PS (STA) (2) > icm -ConfigurationName microsoft.powershell ` >> -computername localhost -command { defrag.exe /? } >> Description: Locates and consolidates fragmented files on local volumes to improve system performance. Syntax: Defrag.exe -a [-v] Defrag.exe [{-r | -w}] [-f] [-v] Defrag.exe -c [{-r | -w}] [-f] [-v]

: : everything works properly. To find the processor architecture for the session, we can check the value of the

$ENV:PROCESSOR_ARCHITECTURE variable. The following command finds the processor architecture of the session in the $s variable. Let's try this, first with the 32-bit configuration PS (STA) (10) > icm -ConfigurationName microsoft.powershell32 ` >> -computername localhost { $ENV:PROCESSOR_ARCHITECTURE } >> x86 and we get the expected 'x86' result indicating a 32-bit session, and on the 64-bit configuration PS (STA) (11) > icm -ConfigurationName microsoft.powershell ` >> -computername localhost { $ENV:PROCESSOR_ARCHITECTURE } >> AMD64 we get 'AMD64' indicating a 64-bit configuration. This is the last remoting consideration we're going to look at in this chapter. Don't let these issues scare you however - they are mostly edge cases. With some attention to detail, the typical script should have no problems working as well remotely as it does locally. The PowerShell remoting system goes to great lengths to facilitate a seamless remote execution experience. But it's always better to have a heads up on some of the issues so we'll know where to start looking if we do run into a problem. In chapters 14 and 15, we'll dig further into the whole PowerShell diagnostic and debugging story. For now, let's review what we've covered in this chapter.

12.7 Summary
In this chapter we introduced PowerShell remoting and the kinds of things we can do with it. We looked at:      the basic concepts and terminology used in PowerShell remoting. how to enable remoting in both domain joined and private workgroup environments. how to apply remoting to build a multi-machine monitoring solution how to create and manage persistent connections to remote machines as well as transient ones. how to establish an interactive 1-1 session using the Enter-PSSession cmdlet.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

470 Then we moved on to look at PowerShell jobs which allow us to run tasks in the background. In this part of the chapter, we looked at:    the basics concepts behind jobs and the commands that are used to create and manage jobs. how to create jobs on remote machines. how to apply the job concept to implement concurrent solutions for problems we might encounter.

We finished this chapter looking into some of the issues we might encounter when using remoting to solve management problems. This to be aware of include:      differences in startup directories the fact that user profiles are not run by default in a remote session some issues using external applications or executables in a remote session differences in behavior due to the fact that remote objects are always serialized before returning them to the caller differences that occur due to different processor architectures being used on the remote end.

The information we've covered in this chapter is sufficient to apply PowerShell remoting effectively for most distributed management tasks. That said, our focus in the chapter has been confined to the "remoting client" user perspective. There are some additional advanced application and service scenarios that we didn't cover here. In chapter 13, we'll introduce a new role where we switch from being the user of existing remoting configurations to authoring our own custom applications and services. We'll look at how to create custom configurations and how these configurations can be used to build solutions for delegated administration tasks.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

471

13
Remoting: Configuring Applications and Services

He who is outside his door already has a hard part of his journey behind him. Dutch Proverb

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

472

In chapter 12, we looked at how PowerShell remoting capabilities are used to monitor and manage remote computers. Now we're going to switch from service consumer to remote service/application creator. Our goal is that, by the end of this chapter, we'll be able to build our own custom remoting services with PowerShell. But before we can start building those services, there is some additional background material required. The first thing we'll look at is how PowerShell remoting actually works. In particular, we'll pay special attention to Web Services for Management (WSMan) and see how it fits into this infrastructure. We'll look at how to manage the Windows WSMan implementation using cmdlets and the WSMan provider. This material will help us to understand and debug issues in the infrastructure. If we want our services to be usable, among other things, we need to be sure that they are secure and reliable. There are a variety of things to consider around securing services including authentication (who is connecting to the service) and authorization (what are they allowed to do). The other aspect - reliability means that we need to manage and control resource consumption. We looked at resource consumption from user perspective when we discussed command-level throttling. In this chapter we'll look at how to control resource consumption from the application end. Once we're done with the background material, we'll look at how to construct services by build custom named end-points that users can connect to. We'll look at how to configure these end-points, both to add commands to the default and, more importantly, how to restrict the set of visible commands. Now let's start with the infrastructure investigation.

13.1 Remoting infrastructure in depth
Before we start building services and applications with PowerShell remoting, we need to develop a better understanding for how everything works. There are a number of reasons for this. First of all, understanding how things work will help us design and deploy our services more effectively. Next, we'll have the knowledge to make sure our services are available to the service consumer. And finally, we'll be able to effectively debug what's gone wrong when something doesn't work the way it's expected. We'll begin our infrastructure exploration by looking at the protocol stack - the layers of networking technology used to enable PowerShell remoting. Since basic connectivity for PowerShell remoting is provided by the WSMan layer, the next thing we'll look are the cmdlets that we can use to configure and manage this layer. Then we'll take a look at how authentication is handled, both from the client and from the servers' perspective. We'll look at how targets are addressed and the concerns that we need to be aware of in this area. Finally we'll end the section by looking at managing connection related issues like resource consumption. These topics will help us to build our services in such a way that the end user can depend on them. Let's start by looking at the protocol stack.

13.1.1 The PowerShell remoting protocol stack
Most networked applications are built on top of multiple layers of software and PowerShell remoting is no different. In this section we'll look at the various components used by PowerShell remoting. For remoting to work there need to be layers of software to provide the transport mechanism for sending messages between the client and the server. These layers are called the protocol layers which ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

473 are organized in a protocol stack. Within this stack, higher layers build on the services provided by the lower layers. For PowerShell remoting, there are 5 layers to the stack. The top layer is [MS-PSRP] - the PowerShell Remoting Protocol. [MS-PSRP] is built on top of WSMan - Web Services for Management. WSMan is, in turn, built on top of SOAP - the Simple Object Access Protocol. SOAP builds on top of the Hypertext Transport Protocol (HTTP) or more precisely Secure HTTP or HTTPS and finally, the hypertext transfer protocols are in turn based on TCP/IP. The complete stack of protocols is shown in figure 13.1.

PowerShell Client

PowerShell Server

PowerShell Remoting Protocol (MS-PSRP) Web Services for Management (MS-WSMV) Simple Object Access Protocol (SOAP) (Secure) Hypertext Transport Protocol (HTTP/HTTPS) Transmission Control Protocol/ Internet Protocol (TCP/IP) Encoded Client Request

PowerShell Remoting Protocol (MS-PSRP) Web Services for Management (MS-WSMV) Simple Object Access Protocol (SOAP) (Secure) Hypertext Transport Protocol (HTTP/HTTPS) Transmission Control Protocol/ Internet Protocol (TCP/IP)

Encoded Server Response

Figure 13.1 This figure shows the PowerShell remoting protocol stack. The PowerShell Remoting Protocol (MS-PSRP) is based on industry-standard "firewall friendly" protocols simplifying deployment in an enterprise environment.

The PowerShell Remoting Protocol uses Microsoft's implementation of the WSMan protocol, designated Web Services Management Protocol Extensions for Windows Vista [MS-WSMV], to establish a connection and transfer data between the client and the server. [MS-WSMV] is built on top of the standard protocols shown in table 13.1.

Table 13.1 The standard protocols used by PowerShell remoting
Protocol SOAP (Version 1.2) The Hypertext Transfer Protocol (HTTP/1.1) HTTP over TLS The Transmission Control Protocol ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 Internet Engineering Taskforce (IETF) Internet Engineering Taskforce (IETF) [RFC2818] [RFC793] Standards Body World Wide Web Consortium (W3C) Internet Engineering Taskforce (IETF) Specification [SOAP1.2/1] [RFC2616]

Licensed to Andrew M. Tearle

474 The Internet Protocol Internet Engineering Taskforce (IETF) [RFC791]

While [MS-PSRP] is a proprietary protocol, it is fully documented and this documentation is publically available. The complete specification for the PowerShell remoting protocol is available at http://msdn.microsoft.com/en-us/library/dd357801(PROT.10).aspx Likewise, the complete documentation for the specific implementation of the WSMan protocol that PowerShell uses [MS-WSMV] is available at http://msdn.microsoft.com/en-us/library/cc251526(PROT.10).aspx

AUTHOR'S NOTE
These protocols are made available under the Microsoft Communications Protocol Program. The goal of this program is to facilitate to interoperation and communications natively with Windows Server operating systems.

PROTOCOL OPERATION When the user passes a PowerShell pipeline to the remoting cmdlets for execution, the pipeline will first be encoded by the remoting layers as structured XML text. (The structure (or schema) for this XML is documented in [MS-PSRP].) The encoded text is then sent to the target computer where it is decoded or rehydrated into a pipeline that can be executed on the remote machine. Along with the commands in the pipeline, the encoded pipeline also needs to include any arguments to the commands. Where encoding the commands is relatively easy, encoding the arguments is problematic as they can be any arbitrary type. Here's the issue we run into. As mentioned in chapter 12 (section 12.6.5), in order to do serialization with full type fidelity, the types we are working with must exists on both the sending and receiving ends of the connection. If the original type doesn't exist at the receiver, then the object can't be completely rehydrated because there is no type definition to rehydrate to. In the management world, this situation is quite common. For example, an Exchange server will have a different set of types than a database server and the client that is managing both of these servers may not have any of these types. In order to mitigate this problem PowerShell had to take a different approach to sharing objects between different computers.

AUTHOR'S NOTE
We say mitigation here because there really isn't a perfect solution with an unbounded set of types. If we restrict the types we lose functionality, and if we require all types everywhere, systems become unmanageably complex. By the way, we'll see this term mitigate again in chapter 19 when we discuss another problem that can't be completely resolved - security.

REPRESENTING OBJECTS AND TYPES IN THE PROTOCOL Instead of trying to accommodate all types, PowerShell defines a core set of types that are required to be on both ends of the connection. Any object that is one of these types will be rehydrated into the correct type. For any types that aren't in this core set, a different approach is used. These objects are "shredded" into property bags of type PSObject. (We discussed PSObject at length in chapter 11.) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

475 The algorithm for doing this is described in detail in section 2.2.5 of [MS=PSRP]. The types that are serialized with fidelity are: MS-PSRP 2.2.5.1.1 2.2.5.1.2 2.2.5.1.3 2.2.5.1.4 2.2.5.1.5 2.2.5.1.6 2.2.5.1.7 2.2.5.1.8 2.2.5.1.9 2.2.5.1.10 2.2.5.1.11 2.2.5.1.12 2.2.5.1.13 2.2.5.1.14 2.2.5.1.15 2.2.5.1.16 2.2.5.1.17 2.2.5.1.18 2.2.5.1.19 2.2.5.1.20 2.2.5.1.21 2.2.5.1.22 2.2.5.1.23 2.2.5.1.24 2.2.5.1.25 2.2.5.2.7 Protocol Element String Character Boolean Date/Time Duration Unsigned Byte Signed Byte Unsigned Short Signed Short Unsigned Int Signed Int Unsigned Long Signed Long Float Double Decimal Array of Bytes GUID URI Null Value Version XML Document ScriptBlock Secure String Progress Record Enums Corresponding PowerShell/.NET Type

[string] [char] [bool] [datetime] [duration] [byte] [sbyte] [uint16] [int16] [uint32] [int32] [int64] [uint64] [float] or [single] [double] [decimal] [byte[]] [guid] [uri] $null [version] [xml] [scriptblock] [System.Security.SecureString] [System.Management.Automation.ProgressRecord] [int32]

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

476 There are also some types of collections are that are serialize with some level of fidelity. These types are listed in table 13.2

Table 13.2 Collection types handled by the serializer
Protocol Type Stack Queue List Dictionaries Encoded type [System.Collections.Stack] [System.Collections.Queue] [System.Collections.IList] [System.Collections.IDIctionary] Decoded type [System.Collections.Stack] [System.Collections.Queue] [System.Collections.ArrayList] [hashtable]

Note that this table lists different types for encoding and decoding in two cases: List and

Dictionary. Here's what this means.
On the sender's side, any object that implements the IList interface will be encoded as List when serialized. On the receiver's side, PowerShell will always decode the collection into an instance of type System.Collections.ArrayList. For example: PS (1) > $r = icm localhost { >> ,(1,2,3) >> } >> In this example, we're sending a collection of integers through the remoting layer. Let's look at the type of object the receiver actually gets: PS (2) > $r.GetType().FullName System.Collections.ArrayList Even though we send a simple array of numbers, the remoting layer decoded it into an ArrayList. This applies to more complex list types as well. In this example, we're sending a generic collection of integers. This collection also comes back as System.Collections.ArrayList on the receiving end. PS (3) > $r = icm localhost { >> $l = new-object system.collections.generic.list[int] >> 1..10 | %{ $l.Add($_) } >> ,$l >> } >> PS (4) > $r.GetType().FullName System.Collections.ArrayList Similarly, any .NET type that implements the interface System.Collections.IDictionary will be encoded as Dictionary and decoded into [hashtable]. When we're sending an object that isn't in the known type pool, we get a property bag back instead of an instance of the original type. For example, let's return a process object through the remoting channel. PS (1) > $r = icm localhost { (get-process csrss)[0] } Now we'll look at the output: PS (2) > $r | fl Id Handles CPU : 568 : 956 : 39.0626504

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

477 Name : csrss PSComputerName : localhost It appears to return the same set of elements as a real Process object would but there are some differences. For example, let's look at the VM properly. PS (3) > $r | gm vm TypeName: Deserialized.System.Diagnostics.Process Name MemberType Definition ---- ------------------VM NoteProperty System.Int32 VM=77340672 Now compare this output to a real Process object. PS (4) > get-process | gm vm TypeName: System.Diagnostics.Process Name MemberType Definition ---- ------------------VM AliasProperty VM = VirtualMemorySize In the deserialized case, the type of the object is shown prefixed with "Deserialized" and the VM property is a Note property containing a fixed integer value. On the "live" Process object, we see that the VM property is actually an alias that points to the VirtualMemorySize property on the object. Another difference is that the deserialized object has fewer members on it: PS (5) > ($r | Get-Member).count 66 PS (6) > (Get-Process csrss | Get-Member).count 90 In particular, the deserialized object has only one method in it ToString(): PS (7) > @($r | Get-Member -type method).count 1 as opposed to the live object which has the full set of methods defined for the type. PS (8) > @(Get-Process csrss | Get-Member -type method).count 19 Since the object is not live (meaning that there is no connection to the Process object it was derived from on the remote machine) propagating the methods makes no sense. And, for performance reasons, not all of the properties are included and if an included property is itself a complex type, the deserialized version may only contain the ToString() of the value of the live property. This 'compression' of properties is done to reduce the amount of information transmitted thereby improving performance in the remoting layer. This approach works well in that it allows for easy operation between the various clients and servers but with the downside that a certain amount of information is lost. However, because of the way PowerShell parameter binding works, this "lossy serialization" isn't usually a problem. PowerShell functions and cmdlets don't really care what type the object is as long as the required properties are there. Now that we have an understanding of how the PowerShell protocol layer works, we'll move down the protocol stack to the WSMan layer. We won't actually be digging into the details of the protocol itself - that's beyond the scope of what we're trying to cover in this chapter. For our purposes, what we really need to know is how the WSMan layer is managed so let's look at that. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

478

13.1.2 Using the WSMan cmdlets and providers
Since PowerShell remoting is built on top of Microsoft's implementation of WSMan, in this section we'll look at how to configure and manage the WSMan service on a computer. As is always the case with PowerShell, the management interface for WSMan is a set of cmdlets and the WSMan provider as surfaced through the WSMan: drive. The WSMan cmdlets are listed in table 13.3.

Table 13.3 Cmdlets for working with WSMan
Cmdlet Name Description The Test-WSMan cmdlet submits an identification request that determines whether the WinRM service is running on a local or remote computer. If the tested computer is running the service, the cmdlet displays the WSMan identity schema, protocol version, product vendor, and the product version of the tested service.

Test-WSMan

Connect-WSMan Disconnect-WSMan

The Connect-WSMan cmdlet establishes a persistent connection to the WinRM service on a remote computer. Once the connection is established, the remote computer will appear as a node in the root directory of the WSMan: drive. Use the Disconnect-WSMan cmdlet to terminate the connection to the remote computer.

Get-WSManCredSSP Enable-WSManCredSSP Disable-WSManCredSSP

The Get-WSManCredSPP cmdlet gets the Credential Security Service Provider (CredSSP) status for the machine on which it is run. The output indicates shows the machines status for both the client role (will or will not forward credentials) and the server role (will or will not accept forwarded credentials.) Use the EnableWSManCredSSP cmdlet to enable the CredSSP delegation on either the client or server. Use the Disable-WSManCredSSP to disable CredSSP on a server.

New-WSManSessionOption

Creates a WSMan Session option hashtable which can be passed into WSMan cmdlets including Connect-WSMan.

We'll look at how to use these cmdlets in the next few sections. TESTING WSM AN CONNECTIONS We can use the Test-WSMan cmdlet test to see if the WSMan connection to our target host is working: PS (1) > Test-WSMan brucepay64h wsmid : http://schemas.dmtf.org/wbem/wsman/identity/1/ wsmanidentity.xsd ProtocolVersion : http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd ProductVendor : Microsoft Corporation ProductVersion : OS: 0.0.0 SP: 0.0 Stack: 2.0 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

479 In the output of the command we see that the WSMan service is running and available on the target machine. ESTABLISHING A REMOTE WSM AN CONNECTION Once we're verified that the service is available, we can use the Connect-WSman cmdlet to connect to the target service. We'll run PS (2) > Connect-WSMan brucepay64h and the prompt returned immediately without displaying any information. So how can we check to see if the connection was created? This is where the second part of the WSMan support comes in -the WSMan provider and the WSman: drive. This drive lets us examine and manipulate the WSMan service using the normal provider cmdlets. Start by looking at the contents of that drive using the dir command: PS (3) > dir wsman:\ WSManConfig: ComputerName -----------brucepay64h localhost computer: PS (5) > dir wsman:\brucepay64h | ft -auto WSManConfig: Microsoft.WSMan.Management\WSMan::brucepay64h Name ---MaxEnvelopeSizekb MaxTimeoutms MaxBatchItems MaxProviderRequests Client Service Shell Listener Plugin ClientCertificate Value ----150 60000 32000 4294967295 Type ---System.String System.String System.String System.String Container Container Container Container Container Container Type ---Container Container

and here we see the connection we just created. Now let's get more details about the connected

and digging further, we can get information about the Shell container: PS (6) > dir wsman:\brucepay64h\shell | ft -auto WSManConfig: Microsoft.WSMan.Management\WSMan::brucepay64h\Shell Name ---AllowRemoteShellAccess IdleTimeout MaxConcurrentUsers MaxShellRunTime MaxProcessesPerShell MaxMemoryPerShellMB MaxShellsPerUser Value ----true 180000 5 2147483647 15 150 5 Type ---System.String System.String System.String System.String System.String System.String System.String

A WSMan 'shell', while not identical to a PowerShell session, is used to implement PowerShell sessions when a user connects. It's worth spending some time exploring the WSMan: drive as we'll be returning ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

480 to it a number of times throughout the rest of this section. The topic we're going to cover is authentication where we'll see how both the target computer and connecting user are verified.

13.1.3 Authenticating the target computer
When we're performing remote operations on a machine, these operations may involve transmitting sensitive information to the target computer. As a consequence, we need ensure that the machine we are connecting to is really who we think it is. In this section we'll look at how to manage verifying server identity when working in a non-domain environment. In a domain environment, we have already established a trust relationship with the domain controller we are connected to. If the target computer is also connected to the same domain controller (i.e. has verified its identity to the domain controller), then we can rely on the domain controller to ensure that we are connecting to the correct machine. However, if we're either not working in a domain environment of the target computer is not connected to the same domain controller, then we need an alternate mechanism for establishing trust. We'll look at two mechanisms for addressing this problem: using HTTPS and using the TrustedHosts WSMan configuration item. First however, we need to make sure that passwords are set up on the machines we're connecting to. The system won't allow connecting to an account without a password. Once we have the account password, authenticating to the server is easy - we just use the Credential parameter and send our credentials to the target server. In this case, it's extremely important to know who we're connecting to as we certainly don't want to be sending passwords to the wrong machine. The first mechanism we'll look involves using HTTPS and certificates. USING HTTPS TO ESTABLISH SERVER IDENTITY One way of validating the identity of the target computers is to use HTTPS when connecting to that computer. This works because, in order to establish an HTTPS connection, the target server must to have a valid certificate installed where the name in the certificate matches the server name. As long as the certificate is signed by a trusted certificate authority, we know that the server is who it claims to be. Unfortunately, this does require that we have a valid certificate, issued either by a commercial or local certificate authority. This is an entirely reasonable requirement in an enterprise environment but may not always be practical in smaller or informal environments. If we aren't able to objecting a valid certificate, then we'll have to use a different mechanism of letting the remoting system know that we trust the target computer. This involves manually adding the target computer to the TrustedHosts list on the client. We'll review how to do this in the next section. USING THE TRUSTEDHOSTS LIST The TrustedHost list is a WSMan configuration item that lists the names of target computers we trust. We looked at this very briefly in chapter 12 in section 12.1.4. To review, The TrustedHosts list is simply a string containing a comma-separated list of computer names, IP addresses, and fully-qualified domain names. Wildcards are permitted to make it easy to specify groups of computers to trust. We'll repeat the steps that we used in chapter 12 but this time we'll explain things in more detail. The easiest way to view or change the TrustedHosts list is to use the WSMan: drive. The

TrustedHost configuration item for a machine is in the "WSMan:\\Client" node.
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

481 For the local machine this is " WSMan:\localhost\Client". To view the current trusted hosts list, use the Get-Item cmdlet: PS (1) > get-item wsman:\localhost\Client\TrustedHosts | ft -auto WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client Name Value Type ----------TrustedHosts brucepayx61,brucepay64h System.String To set this item to a new value, we'll use the Set-Item cmdlet. This looks like: PS (2) > set-item wsman:localhost\client\trustedhosts brucepay64h WinRM Security Configuration. This command modifies the TrustedHosts list for the WinRM client. The computers in the TrustedHosts list might not be authenticated. The client might send credential information to these computers. Are you sure that you want to modify this list? [Y] Yes [N] No [S] Suspend [?] Help (default is "Y"): y Since this operation affects the state of the system in a significant (i.e. has security implications) way, we are prompted to confirm this action. If we don't want this prompt, then we would use the -Force parameter. Now we'll use Get-Item to confirm the change: PS (3) > get-item wsman:\localhost\Client\TrustedHosts | ft -auto WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client Name Value Type ----------TrustedHosts brucepay64h System.String and we see that the old list was completely replaced by the new one. This may not be what we want we may whish to just edit or append to the list. To do this, first retrieve the current value: PS (5) > $cv = Get-Item wsman:\localhost\Client\TrustedHosts PS (6) > $cv.Value brucepay64h and it is what we'd set it too previously. Now add some additional host names to the list in the value key. PS (7) > $cv.Value = $cv.Value + ',brucepayx61, 192.168.1.13,bp3' PS (8) > $cv.Value brucepay64h,brucepayx61, 192.168.1.13,bp3 As mentioned earlier, both machine names and IP addresses are allowed in the list. Now we'll use Set-

Item to assign the updated value.
PS (9) > Set-Item wsman:localhost\client\trustedhosts $cv.value ` >> -force and finally verify the result. >> Get-Item wsman:\localhost\Client\TrustedHosts | ft -auto >> WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client Name Value Type ----------TrustedHosts brucepay64h,brucepayx61,192.168.1.13,bp3 System.String

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

482 If we want to add all of the computers in a particular domain, we can specify a wildcard (*) in the name we're adding to the list. For example, the following command sets the configuration element to a value that will trust all servers in the "yourdomain.com" domain. Set-Item wsman:localhost\client\trustedhosts *.yourdomain.com -whatif If you want to trust all computers, set the TrustedHosts configuration element to be '*' which matches all computers in all domains. This is not generally recommended. We can also use this technique to add a computer to the TrustedHosts list on a remote computer. We can use the Connect-WSMan cmdlet to add a node for the remote computer to the WSMan: drive on the local computer. Then use a Set-Item command to add update that machines TrustedHosts configuration element. Now we know how we should set things up so that the user can be confident that they are connecting to the correct service provider. Of course the service provider is also extremely interested in verifying the identity of the connecting user before allowing them to access any services. That's the topic of the next section.

13.1.4 Authenticating the connecting user
In the last section we looked at how the client verifies the identity of the target computer. Now we'll look at the converse of this - how the target computer verifies the identity of the connecting user. PowerShell remoting supports a wide variety of ways of authenticating a user - NTLM, Kerberos, LiveID and so on. Each of these mechanisms has their advantages and disadvantages. The authentication mechanism also has an important impact on how data is transmitted between the client and the service. Depending on how we authenticate to the server, the data passed between the client and server may or may not be encrypted. Encryption is extremely important in that it protects the contents of our communications with the server against tampering and preserves privacy. If encryption is not being used then we need to ensure the physical security of our network. No untrusted access to the network can be permitted in this scenario. The possible types of authentication are shown in table 13.4.

Table 13.4 The possible types of authentication available for PowerShell remoting
Auth Type Default Description Use the authentication method specified by the WSManagement protocol. Basic Use Basic Authentication , part of HTTP, where the user name and password are sent unencrypted to the target server or proxy. Digest Use Digest Authentication which is also part of HTTP. This mechanism supersedes Basic authentication and encrypts the credentials. Kerberos The client computer and the server mutually authenticate using the Kerberos network authentication protocol. Negotiate Negotiate is a challenge-response scheme that negotiates with Yes Yes Yes Encrypted Payload Depends on what was specified No, use HTTPS to encrypt the connection.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

483 the server or proxy to determine the scheme to use for authentication. For example, negotiation is used to determine whether the Kerberos protocol or NTLM is used. CredSSP CredSSP: Use Credential Security Service Provider (CredSSP) authentication, which allows the user to delegate credentials. This mechanism, introduced with Windows Vista, is designed to support the "second hop" scenario, where commands that run on one remote computer need to hop to another computer to do something. For all of the authentication types except Basic, the payload of the messages we send is encrypted directly by the remoting protocol. If Basic authentication is chosen, then we need to use encryption at a lower layer, for example by using HTTPS instead HTTP. In simple network configurations, we only have to worry about connecting directly to the target machine and only have to work about authenticating to that computer. There is a fairly common configuration where things are more complicated where we have to go through an intermediate machine before we can connect to the final target. This is the so-called "second-hop" scenario. We'll look at how authentication is handled in this configuration next. FORWARDING CREDENTIALS IN MULTI-HOP ENVIRONMENTS Normally, when a client connects to the server, the client sends the credentials to the server and the server validates them. This is fine when the client can connect directly to the server but it's often not the case in an enterprise environment, for security or performance reasons. If we can't directly connect to the server, then we need to talk to an intermediary to do the authentication for us. We send our credentials to this intermediate computer and it performs the "2nd hop" authentication for us. This process is shown in figure 13.2 Yes

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

484

Figure 13.2 This figure shows how second-hop authentication changes when credential delegation is used. Without delegation, the second hop from server 1 to server 2 authenticates as the user that the service is running under. With credential forwarding enabled, server 1 can use the client credentials to authenticate to server 2 as the client user.

To accomplish the necessary credential forwarding magic, PowerShell uses a mechanism introduced with Windows Vista called CredSSP (Credential Security Service Provider). This mechanism was originally designed for Windows Terminal Server to facilitate single sign on. Since then, it has been expanded to work with web services as well so PowerShell, through WS-Man, can take advantage of it. The CredSSP mechanism enables us to securely pass our credentials to a target machine via a trusted intermediary.

AUTHORS NOTE
CredSSP is another of the protocols that are publicly documented through the Microsoft Communications Protocol Program. The specification for available at:

CredSSP, designated [MS-CSSP], is http://msdn.microsoft.com/en-us/library/cc226764(PROT.13).aspx

For this delegation process to work, both the client and the server have to be enabled to allow this. We do this with the Enable-WSManCredSSP cmdlet. To enable the client-side, execute the following command on the client Enable-WSManCredSSP -Role client -DelegateComputer computername This command enables passing credentials from the client to the delegate computer for authentication. To enable the server side, run the following command Enable-WSManCredSSP -Role server ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

485 on the server. As one might expect, we have to run these commands from an elevated PowerShell session. There is one more variation in our discussion of user authentication that we have to cover before we more on and that's handling administrator privileges in cross-domain scenarios. ENABLING REMOTING FOR ADMINISTRATORS IN OTHER DOMAINS In a large enterprise with more than one domain, it is sometimes necessary for an administrator in one domain to perform administration tasks cross-domain. This runs into a problem with the way remoting works. Even when a user in another domain is a member of the Administrators group on the local computer, the user cannot connect to the local computer remotely with Administrator privileges. This is because, with the default settings, remote connections from other domains run with only standard user privilege tokens. To enable what we want, we can use the

LocalAccountTokenFilterPolicy registry entry to change the default behavior and allow remote users who are members of the Administrators group to run with Administrator privileges.
AUTHOR'S NOTE
This is not the default because making this change has a specific security concern. Setting the LocalAccountTokenFilterPolicy entry will disable user account control (UAC) remote restrictions for all users on all affected computers. Make sure you've considered the consequences of this change before proceeding with it

To change the policy we need to define a new value in a specific place in the registry. Use the provider commands and the registry provider, to make this change set the value of the

LocalAccountTokenFilterPolicy registry entry to 1. The command to do this looks like:
C:\PS> New-ItemProperty -name LocalAccountTokenFilterPolicy -path ` HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System ` -PropertyType DWord -value 1 This will create the new 'LocalAccountTokenFilterPolicy' value in the registry at the path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System Once this change has been made, administrators from another domain will be able to remotely access this computer through PowerShell. At this point, we're able to set things up so that the server can verify the identity of the connecting user and the user can verify that they are connected to the correct server. Now we need to look the various ways we can address the target computer.

13.1.5 Addressing the remoting target
In this section we'll look how to specify the address of the target remoting service that we want to connect to. With TCP/IP based protocols, there are two parts to a "service address": the IP address of the computer and the network port number that service is operating on. We'll look at both of these elements. We'll also look at how proxy servers fit into the overall scheme. DNS NAMES AND IP ADDRESSES The typical way to address a computer is to use its name. This name is resolved through DNS (the However, sometimes it's necessary to specify the IP Domain Name Service) into an IP address.

address of a computer explicitly instead of going through DNS. To permit this, the New-PSSession, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

486

Enter-PSSession and Invoke-Command cmdlets can also take the IP address of the computer.
However, because Kerberos authentication does not support IP addresses, NTLM authentication is used by default whenever we specify an IP address. In other words, we're looking at the same issues we encountered in out discussion on how to work in non-domain environment. The IP addresses fort these machines must be added to the Trusted Hosts list. See section 13.1.2 for details on how to do this. CONNECTING TO NON-DEFAULT PORTS By default, PowerShell connects to port 5985 for inbound remoting over HTTP and to port 5986 for connections over HTTPS. This information can also be retrieved directly from the WSMan: drive using the following command: PS (1) > dir WSMan:\localhost\Service\DefaultPorts | ft name,value Name ---HTTP HTTPS Value ----5985 5986

On the client end, when we need to connect to a non-default port use the -Port parameter as follows: Invoke-Command -ComputerName localhost -Port 5985 -ScriptBlock {hostname} On the server side, we can change the default port that the service listens on using the WSMan: provider. We use the Set-Item cmdlet to change the Port value in the path WSMan:\localhost\Listener\\Port. For example, the following command changes the

default port to 8080. set-item WSMan:\localhost\Listener\Listener_641507880\Port -value 8080 We can verify the change using the dir command on the path WSMan:\localhost\Listener\Listener_641507880\Port. ADDRESSING USING A URI Given all this talk about HTTP, IP addresses and ports, one might expect that a machine can be addressed using a URI (or URL) just like a web server and this is, in fact, the case. In the following example, we're using a URI to connect to the default service on 'machine1': PS (1) > Invoke-Command ` >> -ConnectionUri http://machine1:5985/WSMAN ` >> -Credential $c -ScriptBlock { hostname } >> machine1 PS (2) > This URI tells Invoke-Command the protocol to use (http), the machine name (machine1) and the port (5985) to connect to, all in a single string.

SPECIFYING DEFAULT SESSION OPTIONS
It should be apparent by now that the number of options we can specify for a connection is quite large. This eventually becomes cumbersome so PowerShell provides is a mechanism to predefine the options to use via the contains the default values to use. If this object is then stored in the variable

New-PSSessionOption cmdlet. This cmdlet will create an object that $PSSessionOption,

it will be used to set the defaults for subsequent connection. We'll see an example of this in a minute. The other approach for managing sets of options is to assign them to a hashtable and then use splatting to parameterize the command. Splatting is covered in chapter 11.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

487 This covers the basic addressing mechanisms. Now we'll look at how proxy servers come into all of this. WORKING WITH A PROXY SERVERS Since PowerShell remoting is built on top of the HTTP protocol, it is affected by HTTP proxy settings. In enterprises that have proxy servers, users may not be able to access the remote computer directly. To resolve this problem, we have to specify proxy setting options for the command remote command.

ProxyAccessType, ProxyAuthentication and There are three settings available: ProxyCredential. These can be specified either as explicit parameters to New-PSSession when creating a session, or can be configured in a SessionOption object and passed to the via the SessionOption parameter of the New-PSSession, Enter-PSSession or Invoke-Command cmdlets. The settings object can also be stored in the $PSSessionOption preference variable. If this variable is defined, then the cmdlets will use these settings as the default (explicit settings will always override this of course.) In the following example, we'll create a session option object with proxy session options and then use that object to create a remote session. First we create the session object PS (1) > $PSSessionOption = New-PSSessionOption ` >> -ProxyAccessType IEConfig ` >> -ProxyAuthentication Negotiate ` >> -ProxyCredential Domain01\User01 >> PS (2) > Running this command will have prompted you to enter the credentials for the user "Domain1\User01". Since this object now has the credentials needed to connect, we won't have to enter them again. Now we establish a connection to the remote server: PS (3) > New-PSSession -ConnectionURI https://www.myserver.com PS (4) > and it connects without any problems. We've now covered the identification, authentication and addressing issues that impact establishing a remote connections. Now we'll look at some additional issues that we might need to address depending on what versions of the Windows operation system we're running on the target computers.

13.1.6 Windows version-specific connection issues
When connecting to older client operating systems through remoting, there are a couple of additional things we'll need to look at:

AUTHOR'S NOTE
These settings affect all users on the system and they can make the system more vulnerable to a malicious attack. Use caution when making these changes.

WINDOWS XP WITH SP2 Windows XP SP2 is the oldest client operating system that PowerShell remoting is supported on. For remote access to work properly on a machine running this version of Windows, we have to change the default setting for the "Network

Access:

Sharing

and

security

model

for

local

accounts" security policy. This is necessary to allow remote logons using local account credentials to

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

488 authenticate using those credentials. To make this change, we'll use the Local Security Policy MMC snapin.

AUTHOR'S NOTE
Just to be clear, we're talking about a Microsoft Management Console (MMC) snapin, not a PowerShell V1 snapin.

To start this snapin, run secpol.msc. This can be done from the PowerShell command line or from the

Run item on the Start menu and will launch MMC with this snapin, as shown in figure 13.3.

Figure 13.3 This figure shows how to use the secpol.msc MMC snapin on Windows XP SP2 to enable PowerShell remoting to access this machine. The displayed policy item must be set to "classic" for remoting to work.

In the left-hand pane of the MMC window, navigate to the path "Security Settings\Local Policies\Security Options" to see the list of local security settings. From that list, double-click on "Network Access: Sharing and security model for local accounts". In the resulting property dialog, change the setting to be "Classic - local users authenticate as themselves" as shown in the figure. WINDOWS VISTA To enable remote access on Windows Vista clients, we need to set the Local Account Token

Filter policy. This requires setting the LocalAccountTokenFilterPolicy registry entry in
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System to 1. We can use PowerShell to do this. Run the following command to create the entry: New-ItemProperty ` –Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System ` –Name LocalAccountTokenFilterPolicy –PropertyType DWord –Value 1 With this change, it becomes possible to connect to a computer running Vista through PowerShell remoting. By this point, we are able to reliably connect our clients to our servers but there's one more consideration we need to address if we want to ensure the reliability of our network and that's resources ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

489 consumption. management. In the next section we'll look at the various options provided to allow us to do resource

13.1.7 Managing resource consumption
In chapter 12, we talked about throttling at the command level using the -Throttle parameter. The remoting cmdlets allow us to connect to many computers at once and send commands to all of those commands concurrently. It's also possible for many people to connect to the same machine at the same time. Allowing an arbitrary number of connections requires an arbitrarily large amount of resources so to prevent overloading either the client or the server, the remoting infrastructure supports throttling on both the client and server ends. Throttling allows us to limit the number of concurrent connections either outbound or inbound. We'll look at the outbound case first. To help us manage the resources on the local computer, PowerShell includes a feature that lets us limit the number of concurrent remote connections that are established for each command that is executed. The default is 32 concurrent connections, but we can use the -ThrottleLimit parameter change the default throttle setting for a particular command. The commands that support -

ThrottleLimit are shown in table 13.5. Table 13.5 PowerShell cmdlets supporting the -ThrottleLimit parameter
Name Synopsis Runs commands on local and remote computers. Creates a persistent connection to a local or remote computer. Gets instances of Windows Management Instrumentation (WMI) classes or information about the available classes.

Invoke-Command New-PSSession Get-WmiObject

Invoke-WmiMethod Remove-WmiObject

Calls Windows Management Instrumentation (WMI) methods. Deletes an instance of an existing Windows Management Instrumentation (WMI) class.

Set-WmiInstance

Creates or updates an instance of an existing Windows Management Instrumentation (WMI) class.

Test-Connection Restart-Computer Stop-Computer

Sends ICMP echo request packets ("pings") to one or more computers. Restarts ("reboots") the operating system on local and remote computers. Stops (shuts down) local and remote computers.

When using -ThrottleLimit to throttle the number of connections, remember that only applies to the current command. It doesn't apply to subsequent commands or other sessions running on the computer. This means that if commands are being run concurrently in multiple sessions, the number of active connections on the client computer is the sum of the concurrent connections in all the sessions.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

490 CONTROLLING RESOURCE CONSUMPTION WITH QUOTAS Controlling the number of connections is one way to limit resource consumption on the client. However, even when the number of connections is restricted, a single connection can still consume a large amount of resources if a lot of data is being transferred. To manage this, we can use quota settings to restrict the amount of data that gets transferred. These settings can be done at several different levels. On a per session level, we can protect the local computer by using the

MaximumReceivedDataSizePerCommandMB and MaximumReceivedObjectSizeMB parameters of the New-PSSessionOption cmdlet and the $PSSessionOption preference variable.
Alternatively, on the host end, can add restrictions to the endpoint configuration by using the

MaximumReceivedDataSizePerCommandMB and MaximumReceivedObjectSizeMB parameters of the Register-PSSessionConfiguration cmdlet.
Finally, at the WSMan layer, there are a number of settings we can use to protect the computer. We can use the MaxEnvelopeSizeKB and MaxProviderRequests configuration settings to limit the amount of data that can be sent in a single message and limit the total number of messages that can be sent. These settings are available in the settings in the

WSMan:\ node of the WSMan: drive. We can use the "Max*" settings in the WSMan:\\Service node to

control the maximum number of connections, concurrent operations on a global and current basis. The following command shows these settings for the local host: PS (1) > dir wsman:\localhost\service\maxc* | fl name,value Name : MaxConcurrentOperations Value : 4294967295 Name : MaxConcurrentOperationsPerUser Value : 15 Name : MaxConnections Value : 25 This output shows that the user is limited to 15 concurrent operations and a maximum of 25 connections is allowed. When these quotas are exceeded, an error will be generated. At that point, we need to take a look at what we're (or someone else) is trying to do. If the operation is legitimate, then it makes sense to increase the quotas. For example, the following command increases the object size quota in the Microsoft.PowerShell session configuration on the remote computer from 10 MB (the default value) to 11 MB. Set-PSSessionConfiguration -name microsoft.powershell ` -MaximumReceivedObjectSizeMB 11 -Force Again – this is something that needs to be configured based on the type of activity that we need to perform. Another kind of resource that we need to manage is the open connections to a target machine. We’ll look at how to manage this using timeouts in the next section. SETTING TIMEOUTS ON OPERATIONS We've looked at managing the number of connects and controlling the amount of data that can be sent or received. The last mechanism for managing resource consumption is to limit the amount of processor time that an operation is permitted to consume. This is done by setting timeouts on operations. If an ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

491 operation exceeds the timeout value then an error will occur and the operation will be terminated. Note that timeouts can be set at both the client and server ends of the connection. The shortest timeout of the two settings always takes precedence. Through the WSMan: drive, we have access to both client-side and servicer-side timeout settings. To protect the client, we can change the MaxTimeoutms setting in the node in the WSMan: drive that corresponds to the computer in question. To look at these settings for the local machine, first we cd into the appropriate node: PS (1) > cd wsman:\localhost and use Get-ChildItem to see the settings: PS (2) > Get-ChildItem | Format-Table -auto WSManConfig: Microsoft.WSMan.Management\WSMan::localhost Name ---MaxEnvelopeSizekb MaxTimeoutms MaxBatchItems MaxProviderRequests Client Service Shell Listener Plugin ClientCertificate Value ----150 60000 32000 4294967295 Type ---System.String System.String System.String System.String Container Container Container Container Container Container

On the server side, we need to be connected to remote machine via WSMan, so we'll use the Connect-

WSMan command with appropriate credentials to connect:
PS (1) > Connect-WSMan -ComputerName brucepay64h ` >> -Credential (Get-Credential) >> cmdlet Get-Credential at command pipeline position 1 Supply values for the following parameters: Credential The remote machine will now be visible as a node under the WSMan: drive. PS (2) > cd wsman:\ PS (3) > Get-ChildItem WSManConfig: ComputerName -----------localhost brucepay64h Type ---Container Container

Let's set our current directory to the Service sub-node of the remote machine PS (4) > cd brucepay64h\service and look at the contents: PS (5) > Get-ChildItem | Format-Table -auto WSManConfig: Microsoft.WSMan.Management\WSMan::brucepay64h\Service

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

492 Name ---RootSDDL MaxConcurrentOperations MaxConcurrentOperationsPerUser EnumerationTimeoutms MaxConnections MaxPacketRetrievalTimeSeconds AllowUnencrypted Auth DefaultPorts IPv4Filter IPv6Filter EnableCompatibilityHttpListener EnableCompatibilityHttpsListener CertificateThumbprint settings. Alternatively, we can protect the local computer by setting the various timeouts on the session when we create it. We can do this by creating a session options object using the NewValue ----O:NSG:BAD:P(A;;GA;;;BA)S:P(A... 4294967295 15 60000 25 120 false

* * false false

and we can see the EnumerationTimeoutms and the MaxPacketRetrievalTimeSeconds timeout

PSSessionOption cmdlet with the -CancelTimeout, -IdleTimeout, -OpenTimeout, and OperationTimeout parameters. Once we've created this object, we can pass explicitly pass it to NewPSSession using the -SessionOption parameter or implicitly by assigning it to the $PSSessionOption preference variable.
One problem with timeouts is that it's hard to tell if the operation timed out because there was a problem or because it just needed more time to complete the operation. If the timeout expires, the remoting infrastructure will simply terminate the operation and generate an error. It's up to us as users to investigate why the timeout occurred and either change the command to complete within the timeout interval or determine the source of the timeout (client or server) and increase the timeout interval to allow the command to complete. In this example, we'll use the New-PSSessionOption cmdlet to create a session option object with a -OperationTimeout value of 10 seconds. This parameter takes its value in milliseconds so we'll provide the value accordingly: PS (1) > $pso = New-PSSessionOption -OperationTimeout 10 Now we'll use the session option object to create a remote session. PS (2) > $s = New-PSSession -ComputerName brucepay64h ` >>> -SessionOption $pso Now let's try running a command in this session that takes longer than 10 seconds. We’ll use the

foreach cmdlet along with Start-Sleep to slowly emit some strings of stars. Here’s what happens:
PS (3) > Invoke-Command $s ` >> { 1..10 | foreach {"$_" + ('*' * $_) ; start-sleep 1}} 1* 2** 3*** 4**** Processing data from remote server failed with the followin g error message: The WinRM client cannot complete the opera tion within the time specified. Check if the machine name i s valid and is reachable over the network and firewall exce ption for Windows Remote Management service is enabled. For ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

493 more information, see the about_Remote_Troubleshooting Hel p topic. + CategoryInfo : OperationStopped: (System.Ma nageme...pressionSyncJob:PSInvokeExpressionSyncJob) [] , PSRemotingTransportException + FullyQualifiedErrorId : JobFailure When the timeout occurs, we get an error message explaining that the operation was terminated does to the timeout and suggests some remedial actions or way to get more information. This completes our in-depth coverage of the remoting infrastructure. At this point we have all the background needed to be able to setup and manage the remoting infrastructure so we can effectively deploy services. Now we can move on to the fun stuff! In the next section, we’ll look at how to create custom remoting services. Using the mechanisms provided by PowerShell, we can address a variety of sophisticated scenarios that normally require lots of hardcore programming. In PowerShell, this work reduces to a few lines of script code.

13.2 Building custom remoting services
In chapter 12, we looked at remoting from the service consumer perspective. It's time for use to on the role of service creator instead. In this section, we'll look at how to create and configure custom services instead of just using the default PowerShell remoting service. We'll see how we can configure our services to limit the operations a client can perform through these end-points in a very precise way.

13.2.1 Remote service connection patterns
Let's start with a short discussion about service architecture. There are two connection patterns used in remoting: fan-in and fan-out. What we looked at in chapter 12 were primarily fan-out patterns. When we create custom remoting services, we'll also be considering the fan-in pattern so we'll briefly describe these two patterns in this section. FAN-IN REMOTING The most common remoting scenario for administrators is the one-to-many configuration, in which one client computer connects to a number of remote machines in order to execute remote commands on those machines. This is called the "fan-out" scenario since the connections fan out for a single point and is shown in figure 13.4.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

494

Figure 13.4 This figure shows how "fan-out" remoting works. A single client fans out connections to manage multiple servers.

In this figure, we see a single client with concurrent connections to a number of servers that it wants to manage, usually as part of a group of operations. Custom services fit into the fan-out model when we want to restrict the access that the client has. For example, we may want to define a service that only exposes configuration data as we saw in chapter 12. In this scenario, there will typically only be a single client connecting to our service. FAN-OUT REMOTING In enterprises and hosted solution scenarios, we'll find the opposite configuration where many client computers connect to a single remote computer, such as a file server or a kiosk. This many-to-one is known as the "fan-in" configuration, shown in figure 13.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

495

Figure 13.5 This figure shows the "fan-in" arrangement where multiple client computers connect to a single server. This is usually a "delegated administration" scenario where a constrained endpoint is used to provide access to controlled services to a variety of clients. This module is used by Outlook.com.

Windows PowerShell remoting supports both fan-out and fan-in configurations. In the fan-out configuration, PowerShell remoting connects to the remote machine using the WinRM service running on the target machine. When the client connects to the remote computer, the WSMan protocol is used to establish a connection to the WinRM service. The WinRM service then launches a new process (wsmprovhost.exe) that loads a plug-in that hosts the PowerShell engine. This arrangement is shown in 13.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

496
When a user connects to the WinRM service, a new wsmprovhost process is created to host the powershell session for that user – one process per user per connection

wsmprovhost for user 1

PowerShell Session

User 1

Windows Remote Management (WinRM) Service

wsmprovhost for user 2

PowerShell Session WSMan Protocol

User 2 wsmprovhost for user 3

PowerShell Session

User 3

Figure 13.6 When multiple users connect to the WinRM service, a new wsmprovhost process is created to host the PowerShell session for that user. Even when the same user connects multiple times, each connection is still created for each connection.

Creating a new process for each session is fine if there aren't many users connecting to the service. However, if many connections are expected as is the case for a high-volume service, the one-processper-user model will not perform very well. To deal with this scenario, an alternate hosting model can be used. Instead of using the WinRM service to host WSMan and the PowerShell plugin, Internet Information Services (IIS) is used instead. In this model, instead of starting each user session in a separate process, all of the PowerShell sessions are run in the same process along with the WSMan protocol engine. This configuration is shown in figure 13.7

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

497
When IIS hosting is used, a single process contains all of the components – the WSMan protocol engine and all of the PowerShell sessions. There is still one session per user, but we aren’t creating a new process everytime a user connects to the service.
Internet Information Server (IIS) worker process

PowerShell Session

User 1

PowerShell Session WSMan Protocol

User 2
PowerShell Session

User 3

Figure 13.7 When IIS hosting is used for fan-in, each user still gets their own session but only one process is used to host all of the sessions. This is much more efficient since we're not creating a session per user.

Another feature of this setup is that there is a pluggable authentication mechanism which allows for alternate authentication services like LiveID to be used. A guide for setting up remoting is available on MSDN as part of the WinRM documentation under the topic " IIS Host Plug-in Configuration".

AUTHOR'S NOTE
Note that IIS hosting and fan-in remote management is not supported on Windows XP or Windows Server 2003. It requires Windows Vista or above when using a non-server operating system and Windows Server 2008 or later for a server OS.

Having all of the session running in the same process has certain implications. Since PowerShell lets us get at pretty much everything in a process, multiple users running unrestricted in the same process could interfere with each other. On the other hand, since the host process persists across multiple connections, it is possible to share process wide resources like database connections between sessions. Given the lack of session isolation, this approach is not intended for full-featured general purpose PowerShell remoting. Instead it's designed for use with constrained, special-purpose applications using PowerShell remoting. To build these applications, we need two things: 1. a way to create a constrained application environment 2. a way to connect to PowerShell remoting so the user gets the environment we've created instead of the default PowerShell configuration. We'll start with the second one first and look at how we specify custom remoting end-points in the next section. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

498

13.2.2 Working with custom configurations
In section 13.1.6, we talked about connecting to a computer by name (and optionally by port number) when using PowerShell remoting. This will always connect to the default PowerShell remoting service. In the non-default connection case, we also have to specific the endpoint on the target computer to connect to. An endpoint definition is made up of three elements: 3. the name we use to connect to the end-point 4. a script that will be run to configure the endpoint 5. an ACL used to control who has access to the end-point. When using the Invoke-Command, New-PSSession, or Enter-PSSession cmdlets, we can use the -

ConfigurationName parameter to specify the name of the session configuration we want to connect to.
Alternatively, connect to. When we connect to the named endpoint, a PowerShell session will be created and then the configuration script associated with the end-point will be executed. This configuration script should define the set of capabilities available when connecting to that endpoint. For example, there may be different endpoints for different types of management tasks - managing a mail server, managing a database server or managing a web server. For each task, a specific endpoint would be configured to surface the appropriate commands (and constraints) required for performing that task. In the next section we'll see how to create our own custom end-points. we can override the normal default configuration by setting the

$PSSessionConfigurationName preference variable to the name of the end-point we want to

13.2.3 Creating a custom end-point
In this section we'll look at how to create our own endpoint. Following on our theme of remote monitoring from chapter 12, we're going to create an endpoint with a new custom command Get-

PageFaultRate. This command will return the page fault rate from the target computer.
SESSION CONFIGURATION Every remoting connection will use one of the end-point configurations on the remote computer. These end-point configuration set up the environment for the session, determining the set of commands available in the session. When remoting is initially enabled using the Enable-PSRemoting cmdlet, a default end-point is created on the system called Microsoft.PowerShell (On 64-bit operating systems, there is also the

Microsoft.PowerShell32 end-point). This end-point is configured to load the default PowerShell configuration with all commands enabled. The Enable-PSRemoting cmdlet also set the security descriptor for this configuration so that only members of the local Administrators group can access the end-point. We can use the session configuration cmdlets to modify these default session configurations, to create new session configurations, and to change configurations. These cmdlets are shown in table 13.6. the security descriptors of all the session

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

499

Table 13.6 This table list the cmdlets for managing the remoting endpoint configurations
Cmdlet Description Denies access to the specified session configurations on local computer.

Disable-PSSessionConfiguration

Enable-PSSessionConfiguration

Enables existing session configurations on the local computer to be accessed remotely.

Get-PSSessionConfiguration

Gets a list of the existing, registered session configurations on the computer.

Register-PSSessionConfiguration Set-PSSessionConfiguration

Creates and registers a new session configuration. Changes the properties of an existing session configuration.

Unregister-PSSessionConfiguration

Deletes the specified registered session configurations from the computer.

In the next section we'll look at using these cmdlets to create and manage a custom end-point. REGISTERING THE ENDPOINT CONFIGURATION Now we'll look at what's involved in creating and configuring a custom endpoint. Endpoints are created using the Register-PSSessionConfiguration cmdlet and are customized by registering a startup script. In this example, we'll use a very simple startup script that only defines a single function Get-

PageFaultRate. This script looks like:
PS >> >> >> >> >> (1) > @' function Get-PageFaultRate { (Get-WmiObject Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec } '@ > configscript.ps1

Before we can use this function, we need to register the configuration. We'll call this configuration "wpai1". From an elevated PowerShell session, run the following command to create the end point: PS (2) > Register-PSSessionConfiguration -Name wpia1 ` >> -StartupScript $pwd/configscript.ps1 -Force >> WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin Name ---wpia1 Type ---Container Keys ---{Name=wpia1}

The output of the command shows that we have created an end point in the WSMan plug-in folder. To confirm this, we'll cd into that folder and run the 'dir' command. PS (3) > cd wsman:\localhost\plugin PS (4) > dir WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

500 Name ---Event Forwarding Plugin microsoft.powershell Microsoft.PowerShell32 WMI Provider wpia1 Type ---Container Container Container Container Container Keys ---{Name=Event Fo... {Name=microsof... {Name=Microsof... {Name=WMI Prov... {Name=wpia1}

This shows a list of all of the existing endpoints including the one we just created 'wpia1'. Now we'll test this endpoint with the Invoke-Command command and run the function defined by the startup script. PS (5) > icm localhost -Config wpia1 { Get-PageFaultRate } 58709002 This verifies that the endpoint exists and is properly configured. Now we'll clean up by unregistering the endpoint PS (6) > Unregister-PSSessionConfiguration -name wpia1 -force PS (7) > and verify that the endpoint has been removed. PS (8) > dir WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin Name ---Event Forwarding Plugin microsoft.powershell Microsoft.PowerShell32 WMI Provider Type ---Container Container Container Container Keys ---{Name=Event Fo... {Name=microsof... {Name=Microsof... {Name=WMI Prov...

We've now looked at the basic process needed to create a custom PowerShell remoting endpoint and use a configuration script to add additional functionality to the session. However, our overall goal was to create a custom endpoint that only exposed a task-specific limited set of commands. This requires another mechanism - controlling command visibility - which is the topic of the next section.

13.2.4 Access controls and endpoints
By default, only members of the Administrators group on a computer have permission to use the default session configurations. To allow users who are not part of the Administrators group to connect to the local computer, we have to give those users Execute permissions on the session configurations for the desired end-point on the target computer. For example, if we want to enable non-administrators to connect to the default remoting "Microsoft.PowerShell" endpoint, we can do this by running the following command: Set-PSSessionConfiguration Microsoft.Powershell -ShowSecurityDescriptorUI This will cause a dialog to be displayed as shown in figure 13.8.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

501

Figure 13.8 This figure shows the dialog that is used to enable the Execute permission on the default remoting configuration. This is necessary to allow a use who is not a member of the administrators group to connect to this computer using PowerShell remoting.

We add the name of the user we want to enable to the list then select the 'Execute (Invoke)' checkbox. Once this has been done, close the dialog. At this point, we'll get a prompt telling us that we need to restart the WinRM service for the change to take effect. We do this by running the Set-

PSSessionConfiguration as shown:
PS (1) > Set-PSSessionConfiguration Microsoft.Powershell -ShowSec urityDescriptorUI Confirm Are you sure you want to perform this action? Performing operation "Set-PSSessionConfiguration" on Target "Name: Microsoft.Powershell". [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help(default is "Y"): y

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

502 Confirm Are you sure you want to perform this action? Performing operation ""Restart-Service"" on Target "Name: WinRM". [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help(default is "Y"): PS (2) > Once the service is restarted, the user we've enabled will be able to the machine using remoting. SETTING SECURITY DESCRIPTORS ON CONFIGURATIONS When Enable-PSRemoting create the default session configuration, it doesn't actually create explicit security descriptors for the configurations. Instead, the configurations inherit the security descriptor of the RootSDDL. The RootSDDL is the security descriptor that controls remote access to the listener which is secure by default. To see the RootSDDL security descriptor, run the Get-Item command as shown: PS (1) > get-item wsman:\localhost\Service\RootSDDL WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Service WARNING: column "Type" does not fit into the display and was removed. Name ---RootSDDL Value ----O:NSG:BAD:P(A;;GA;;;BA)S:P(AU;FA;GA;;;WD...

The string format shown in the Value output in the example uses the syntax defined by the Security Descriptor Definition Language (SDDL), which is documented in the Windows Data Types specification

[MS-DTYP]in section 2.5.1. To change the RootSDDL, use the Set-Item cmdlet in the WSMan: drive. To change the security descriptor of an existing session configuration, use the Set-PSSessionConfiguration cmdlet with the -SecurityDescriptorSDDL or -ShowSecurityDescriptorUI parameters.
At this point, we know how to create and configure an end-point and how to control who has access to that end-point. However, in our configuration, all we've done is add new commands to the default PowerShell configuration. We haven't addressed the requirement to constrain the environment. In the next section, we'll introduce the mechanisms used to restrict which commands that are visible to the remote user.

13.2.5 Constraining a PowerShell session
On our way to creating custom constrained endpoints, we first need to know how to constrain a local session. In a constrained environment, we want to limit the variables and commands available to the session user. We do by controlling command and variable visibility. Let's look at command visibility first. Command and variable visibility is conceptually similar to exporting functions from a module but it uses a very different implementation. When a command is exported from a module, it is registered in the caller's command table. In the remoting scenario with Invoke-Command there is no "table" to copy the commands into and so we need a different mechanism to control visibility of the command.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

503

AUTHOR'S NOTE
This is true of explicit remoting and interactive remoting. With implicit remoting, we do copy commands into the caller's command table but these are local proxy commands instead of local references to exported commands.

Another more important consideration is security. Module exports are designed to prevent namespace collisions but don't actually prevent us from accessing the content of the module. In other words, it's not a security boundary. This is shown in figure 13.9.

Internal vs external command access in a session
PowerShell Session
User can’t call Get-Date directly because it’s not visible outside the session Call from inside

Get-Date

Get-Count

User can use the call operator to invoke a scriptblock which calls Get-Date indirectly

& { Get-Date }

& { Get-Date }

Figure 13.9 This figure shows the difference between an external call to a command in a session and an internal call. If the command is not visible across the session boundary, then the user can't call it. If the user can call a visible command, that visible command can call the private internal command.

In a constrained session, we need to establish a boundary and explicitly prevent access to the state of the session other than through the public or visible commands. Now that we've defined the boundary, let's look at controlling which commands are exposed.

AUTHOR'S NOTE
In local sessions, where the user operates in the same process as the session, the session boundary isn't sufficient to provide a true security. Only when we combine constrained session with remote (and therefore out of process) access do we actually get a security boundary.

CONTROLLING COMMAND VISIBILITY The mechanism used to control command visibility is the Visibility property on the CommandInfo object for that command. This mechanism isn't restricted to remoting, by the way - we can use this mechanism in the normal interactive PowerShell session. In fact this is a good way of testing our configuration without creating an endpoint. To make a command invisible or private, first we use Get-Command to get the CommandInfo object then set the Visibility property on that object to "Private". Let's try this. We'll modify our current session to make the Get-Date command private. This is a good place to start playing with

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

504 visibility since hiding Get-Date (typically) won't break anything (more on that later. First, we'll run the command as we might conventionally do: PS (1) > Get-Date Tuesday, November 17, 2009 9:04:20 PM and this returns today's date - nothing unexpected here. Now use Get-Command to retrieve the

CommandInfo object for Get-Date.
PS (2) > $gd = Get-Command Get-Date Let's look at the current setting of the Visibility property. PS (3) > $gd.Visibility Public It returns Public meaning it can be seen and therefore executed from outside the session. We'll change this to private. PS (4) > $gd.Visibility = "Private" Verify the change: PS (5) > $gd.Visibility Private Ok - now try calling the cmdlet again. PS (6) > Get-Date The term 'Get-Date' is not recognized as the name of a cmdlet, f unction, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is co rrect and try again. At line:1 char:9 + Get-Date function MyGetDate { [string] (get-date) } and when we call it, the default string representation of the date. PS (10) > MyGetDate 11/17/2009 22:11:39 Now we have officially constrained the session because the MyGetDate wrapper function has less functionality than the private Get-Date function. There are a couple of additional things we need to do to get a truly constrained environment. But first let's make Get-Date visible again. We can do this by setting the Visibility property back to "Public". PS (11) > $gd.Visibility = "public" PS (12) > Get-Date Tuesday, November 17, 2009 9:05:59 PM and everything is back to normal. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

506 Ok - this is not much of a security mechanism if we can just use the properties on the command objects to control the visibility of commands. Clearly there needs to be more to the story so in the next section we'll revisit the restricted language we mentioned in section XX when we discussed module manifests and the data language. SETTING THE LANGUAGE M ODE In the previous section, we looked at how to make a command private and then call it through a public function. Obviously if we allow external users to create their own functions, this won't be very secure. So - the next thing we need to do is constrain what PowerShell language features the user has access to. We do this through the $ExecutionContext variable. As we saw in chapter 11, $ExecutionContext captures - well - the execution context of the session. The property of interest in this case is SessionState. Let's look at what that property exposes: PS (1) > $ExecutionContext.SessionState

Drive

: System.Management.Automation.Dri veManagementIntrinsics Provider : System.Management.Automation.Cmd letProviderManagementIntrinsics Path : System.Management.Automation.Pat hIntrinsics PSVariable : System.Management.Automation.PSV ariableIntrinsics LanguageMode : FullLanguage UseFullLanguageModeInDebugger : False Scripts : {*} Applications : {*} Module : InvokeProvider : System.Management.Automation.Pro viderIntrinsics InvokeCommand : System.Management.Automation.Com mandInvocationIntrinsics And we see a lot of interesting things. Of particular interest are three properties: LanguageMode,

Scripts and Applications.
When we looked at constrain commands in the last section we used the Visibility property on the CommandInfo object to make Get-Date private. This works for commands that exist in memory but scripts and executables are loaded from disk each time which means that a new CommandInfo object is returned each time. Setting the Visibility property won't be very useful if we're returning a new object each time. Another mechanism was needed for this and this mechanism is a list of permitted commands and scripts. The default setting for these properties is a single element: '*' which means that any external command or script may be executed. If this element is deleted so that the list is empty, it means that no commands of this type may be executed. To permit only specific commands to be called, add the full path to the command or script to this list.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

507

AUTHOR'S NOTE
One might logically assume that wildcards would be permitted in this list. They aren't. The only pattern that has a special significance is when the list contains a single '*'. Pattern matching may be added in future releases but, while it was considered, it wasn't implemented in PowerShell V2.

Let's work through an example showing how these lists work. First we'll get the CommandInfo object for the external command ipconfig.exe. PS (1) > $ipp = (get-command ipconfig.exe).Definition This is an easy way of getting the full path to the command, regardless of which operating system you're on. Once we have that, we'll clear the Applications list. (We had to get the CommandInfo for ipconfig.exe before we cleared the list since after we cleared the list Get-Command would no longer be able to find the command. PS (2) > $ExecutionContext.SessionState.Applications.Clear() Now we'll use the CommandInfo to add the path to the command to the list. PS (3) > $ExecutionContext.SessionState.Applications.Add($ipp.Definition) And now try to run the command. PS (4) > ipconfig | Select-String ipv4 IPv4 Address. . . . . . . . . . . : 192.168.1.15 IPv4 Address. . . . . . . . . . . : 192.168.1.5 The command works as intended - this was the point of adding its path to the list after all. Now let's try a command that isn't on the list. PS (5) > expand /? | Select-String expand The term 'expand.exe' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:7 + expand $ExecutionContext.SessionState.Applications.Clear() PS (8) > $ExecutionContext.SessionState.Applications.Add('*') PS (9) > We now know how to use the Applications list to constrain the set of external commands that may be run. This is intended to be a security measure but we also saw that if we can create a scriptblock that scriptblock can "bypass" the security boundary. Clearly we'll also need to limit the ability to create functions and scriptblocks. This is done using the LanguageMode property on SessionState. Let's look at the default setting. PS (2) > $ExecutionContext.SessionState.LanguageMode FullLanguage The default setting is to allow all aspects of the PowerShell language to be used when sending commands to the engine. We'll change his in a minute, but first we need to set up a way to undo what we're doing. Once we've constrained the language, there won’t be a way to unconstrain it. We'll do this by creating a function to restore the LanguageMode to FullLanguage. PS (3) > function restore { >> $ExecutionContext.SessionState.LanguageMode = "FullLanguage" >> } >> When we run this function, it will reset the LanguageMode. Now we'll change the language mode to RestrictedLanguage. This is the subset of the PowerShell language that is used in PowerShell data files. We talked about this when we covered module manifests in chapter 10 (section 10.6). PS (4) > $ExecutionContext.SessionState.LanguageMode = >> "RestrictedLanguage" >> Let's look at what we can do now. First off, we can't set variables: PS (5) > $a=123 Assignment statements are not allowed in restricted language mod e or a Data section. At line:1 char:4 + $a= restore Now everything works again. PS (10) > $a=123 PS (11) > Now that we know how to constrain a session, we're halfway to our goal of being able to create constrained custom endpoints. In the next section, we'll look at the second piece - creating a custom remoting endpoint. before we switched over to RestrictedLanguage

13.2.6 Creating a constrained execution environment
The idea behind a constrained endpoint is that it allows us to provide controlled access to services on a server in a secure manner. This is the mechanism that the hosted Exchange product Outlook.com uses to constrain who gets to manage which sets of mailboxes. As mentioned earlier, for the environment to be secure, there needs to be both access restriction and a boundary. Remoting allows a boundary to be established as long as the only way to access the remote end is through the remoting layer. The restrictions can be established by controlling command visibility and restricting the language. We combine these into a custom remoting endpoint configured with a script the sets the configuration such that only a small subset of things are visible through the remoting layer. The code to do this is shown in listing 13.1.

Listing 13.1 constrainedconfigscript.ps1 foreach ($cmd in Get-Command) { $cmd.Visibility = "private" } foreach ($var in Get-Variable) { $var.Visibility = "private" } $ExecutionContext.SessionState.Applications.Clear() http://www.manning-sandbox.com/forum.jspa?forumID=542 #1

#2

#3

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

510 $ExecutionContext.SessionState.Scripts.Clear() $ExecutionContext.SessionState.LanguageMode = "NoLanguage" #4 #5

function Get-HealthModel { @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first 5 TopWS = Get-Process | sort -desc WS | select -first 5 } } #1 Hide all of the commands in the session #2 Hide all of the variables in the session #3 Hide all of the external programs #4 Hide all of the external scripts #5 Set to NoLanguage mode #6 Define public Get-HealthModel function

#6

The first part of this script is generic and reusable. It can be used as a preamble to any constrained endpoint configuration script. Any function defined after the line setting the session to NoLanguage mode will be publically visible and represents the "services" this endpoint provides. In this example, we are adapting the health model function we saw in section 12.2.4 to be run in this endpoint. Let's try this script. From an elevated PowerShell session, run the following commands PS (1) > Unregister-PSSessionConfiguration -name wpia1 -force PS (2) > Register-PSSessionConfiguration -name wpia1 ` >> -StartupScript $pwd/constrainedconfigscript.ps1 -force >>

WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin Name ---wpia1 Type ---Container Keys ---{Name=wpia1}

The first command removes the existing endpoint if definition in case it may be left from an earlier operation. The second line creates the new endpoint with the script from listing 13.1. With the endpoint installed, we can try it. We'll run the Get-HealthModel command: PS (3) > icm localhost -Config wpia1 {Get-HealthModel} Name ---Date TopWS PageFaults TopCPU FreeSpace Value ----2/24/2010 11:32:49 PM {System.Diagnostics.Process (svchos... 63217485 {System.Diagnostics.Process (svchos... 362546515968

and it executes properly. Let's try another command: PS (4) > icm localhost -Config wpia1 {Get-Date} The term 'Get-Date' is not recognized as the name of a cmdlet, functi on, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try a ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

511 gain. + CategoryInfo : ObjectNotFound: (Get-Date:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException This fails with the expected error. Let's try using Get-Command to see the list of the things Iwecan execute: PS (5) > icm localhost -Config wpia1 {Get-Command} The term 'Get-Command' is not recognized as the name of a cmdlet, fun ction, script file, or operable program. Check the spelling of the na me, or if a path was included, verify that the path is correct and tr y again. + CategoryInfo : ObjectNotFound: (Get-Command:String) [ ], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException and this also fails. Not unexpected, but it would be nice to see what's exposed. Let's try interactive remoting: PS (6) > Enter-PSsession localhost -Config wpia1 Enter-PSSession : The term 'Get-Command' is not recognized as the nam e of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:1 char:16 + enter-pssession $iss = [Management.Automation.Runspaces.InitialSessionState]: :CreateRestricted( >> "remoteserver") >> PS (2) > $iss.Commands | where { $_.Visibility -eq "Public" } | >> Format-Table name >> Name ---Get-Command Get-FormatData Select-Object Get-Help Measure-Object Exit-PSSession Out-Default The bodies of these functions are similar to the proxy functions used for implicit remoting that we saw in section 12.4.2. The goal here is a bit different as they are used to provide public facades for private commands. If you are interested in how these functions are defined, then look at the Definition property on the objects returned by Commands. The commands to do this are: PS (3) > $p = $iss.Commands | where { $_.Visibility -eq "Public" } PS (4) > $p[0] | fl name,definition Name : Get-Command Definition : [CmdletBinding()] param( [ValidateLength(0, 1000)] [ValidateCount(0, 1000)] [System.String[]] ${Name}, [ValidateLength(0, 1000)] [ValidateCount(0, 100)] [System.String[]] ${Module}, : : The output is truncated here as it's quite long. (Again, be happy we don't have to write these functions and you are welcome to explore them at your leisure but it we're not going to spend any more time looking at them.) Once we have this object, we can copy these functions into our session by doing: foreach ($cmd in $iss.Commands | where { $_.Visibility -eq "Public"}) { Set-Item "function:global:$($cmd.Name)" $cmd.Definition } This code fragment loops through the set of command definitions, looking for the public definitions and then uses the function provider to define new public functions in the session. It also defines some new private aliases that are used to bypass the proxies for internal calls. This is necessary because some of ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

513 the public functions we want to write may use the some of the cmdlets we've just proxied. Since the proxies don't implement all of the features of the wrapped cmdlets this could interfere with the operation of the new commands we want to write. This all works because of the way command resolution works. Remember that, when we're looking for a command, we look for aliases first, then functions and then cmdlets. This order is always followed when the call is coming from inside the session. If the call is coming from outside the session, then the look up order doesn't change, but if one of the things it finds is private, then that definition is skipped and the lookup moves on to the next type of command. So - if we define an alias that uses the modulequalified name of a cmdlet, internal look-ups will resolve to the alias. This alias uses module-qualified name for the cmdlet thereby skipping the function and going directly to the cmdlet. External lookups, on the other hand, won't see the alias since it's private. They can only see the public constrained proxy function. These lookup patterns are shown in figure 13.11.

Aliases, proxies and cmdlets in constrained sessions
Constrained PSSession

Users can call the public SelectObject proxy command but they can’t see either the private alias or the private cmdlet. The public proxy function can call the private cmdlet. Select-Object

Select-Object cmdlet

(visibility: private)

Select-Object proxy function

(visibility: public)

Session Boundary

Select-Object alias

(visibility: private)

Users can use the public GetHealthModel. When this calls Select-Object it resolves to the private alias and the private alias is hardcoded to bypass the function and call the cmdlet.

Get-HealthModel

Get-HealthModel function

(visibility: public)

Figure 13.11 This figure shows the call flow from the public constrained Select-Object proxy function to the private unconstrained Select-Object cmdlet. When the user calls the public Get-HealthModel and it calls Select-Object, the call resolves to the private Select-Object alias. This alias is hard-coded to bypass the constrained proxy function and call the cmdlet directly giving unconstrained access internally.

Now let's see what all of this looks like when we add these code fragments to our configuration script. Listing 13.2 shows the updated configuration script.

Listing 13.3 complexconstrainedconfigscript.ps1 foreach ($cmd in Get-Command) { $cmd.Visibility = "private" } #1

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

514 foreach ($var in Get-Variable) { $var.Visibility = "private" } $ExecutionContext.SessionState.Applications.Clear() $ExecutionContext.SessionState.Scripts.Clear() $ExecutionContext.SessionState.LanguageMode = "NoLanguage" #2

#3

#4

$iss = #5 [Management.Automation.Runspaces.InitialSessionState]::CreateRestricted( "remoteserver") foreach ($proxy in $iss.Commands | where { $_.Visibility -eq "Public"}) { $cmd = get-command -type cmdlet -ea silentlycontinue $proxy.name if ($cmd) { $a = Set-Alias "$($proxy.name)" ` "$($cmd.ModuleName)\$($cmd.Name)" -pass #6 $a.Visibility = "Private" } Set-Item "function:global:$($proxy.Name)" $proxy.Definition #7 }

function Get-HealthModel #8 { @{ Date = Get-Date FreeSpace = (Get-PSDrive c).Free PageFaults = (Get-WmiObject ` Win32_PerfRawData_PerfOS_Memory).PageFaultsPersec TopCPU = Get-Process | sort CPU -desc | select -first 5 TopWS = Get-Process | sort -desc WS | select -first 5 } } #1 Hide all existing commands #2 Hide all variables #3 Hide applications and scripts #4 Set NoLanguage mode #5 Get list of required proxies #6 Define private alias to cmdlet #7 Bind proxy function #8 Define public content This script is identical to the earlier script with the exception that it has the additional lines to needed to define the proxy functions. Let's test this out. First we can recreate the endpoint using the new configuration script. PS (1) > Unregister-PSSessionConfiguration -name wpia1 -force PS (2) > Register-PSSessionConfiguration -name wpia1 ` >> -StartupScript $pwd/complexconstrainedconfigscript.ps1 -force >> WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Plugin

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

515 Name ---wpia1 Type ---Container Keys ---{Name=wpia1}

Let's start an interactive remoting session with this new configuration: PS (3) > Enter-PSSession localhost -Config wpia1 and this time it succeeds without any problems. We can run Get-Command to get the list of available commands in the session: [localhost]: PS>get-command CommandType ----------Function Function Function Function Function Function Function Function Name ---Exit-PSSession Get-Command Get-FormatData Get-HealthModel Get-Help Measure-Object Out-Default Select-Object Definition ---------[CmdletBinding()]... [CmdletBinding()]... [CmdletBinding()]... ... [CmdletBinding()]... [CmdletBinding()]... [CmdletBinding()]... [CmdletBinding()]...

We'll try running the Get-HealthModel function: [localhost]: PS>Get-HealthModel Name ---FreeSpace PageFaults Date TopWS TopCPU and it works as expected. Now let's [localhost]: PS>exit The syntax is not supported t is in no-language mode. + CategoryInfo + FullyQualifiedErrorId Value ----359284412416 215576420 2/26/2010 10:05:30 PM {System.Diagnostics.Process (svchos... {System.Diagnostics.Process (sqlser... exit the session by calling exit. by this runspace. This might be because i : : ScriptsNotAllowed

and we get an error. This is because we're in NoLanguage mode and exit is a keyword. We haven't seen this before because we haven't done interactive remoting with a constrained session before. The solution to this is to use the Exit-PSSession function that was defined as part of the standard set of proxy functions. [localhost]: PS>Exit-PSsession Now let's import this session PS (6) > $s = New-PSSession localhost -ConfigurationName wpia1 PS (9) > import-pssession $s ModuleType Name ExportedCommands ---------- ------------------Script tmp_9bed4009-478e-4e91... Get-HealthModel The import worked so we can call Get-HealthModel: PS (10) > Get-HealthModel Name ---Date TopWS Value ----2/26/2010 10:52:36 PM {System.Diagnostics.Process (svchos... http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

516 PageFaults TopCPU FreeSpace 217681970 {System.Diagnostics.Process (sqlser... 360739450880

and it works just like a local command. The details of remoting are hidden. Let's step back and think about what we've accomplished here. With about 40 lines of script, we've defined a secure remoted service. And of these 40 lines, most of them are a boilerplate preamble. Just past them at the beginning of the configuration script then add the additional functions to define the services we want to expose. From the user's perspective, by using Import-PSSession, they are able to install the necessary "client" software to use this service simply by connecting to the service. Constrained session combined with implicit remoting result in a extremely flexible system allowing us to create precise service boundaries with very little server-side code and no client code. Consider how much code would be required to create an equivalent service using alternate technologies! And with this, we've come to end of our coverage of the remoting features in PowerShell.

13.3 Summary
In this chapter we looked at a PowerShell remoting from the service or application creators point of view. The goal of the chapter was to see how to create a custom service using the remoting capabilities.       In order to make sure we had the necessary background and context for building this service, we explored how PowerShell remoting actually worked. We looked at the PowerShell remoting protocol stack which includes HTTP and WSMan managing WSMan using cmdlets and the WSMan: drive authenticating both the target computer and the incoming user connecting to the service various issues around addressing the target computer ensuring the reliability of our services by managing resource consumption

In the second section of the chapter we started to build our service. We started by looking at the fan-in and fan-out service architectures. Then we covered Creating custom configurations and remoting end-points. How to configure an end-point with a startup script. How to control who has access to the endpoint using ACLs. How to constrain a session by controlling the set of commands visible to users connecting to the end point. Controlling the flavor of PowerShell that is available to the endpoint user. Using a startup script in a custom configuration the sets up the necessary constraints for the application. Configuring the session so that implicit remoting can be used with the end-point. PowerShell remoting and the underlying technologies are both broad and deep areas. In this chapter and the previous one, we've covered most of the features of the technologies and how they can be applied. This is also the last chapter in the first part of the book, which covered PowerShell on a featureby-feature basis. In the second part, we'll combining what we've learned to solve problems in a number of domains - text processing, creating graphical environments and so on. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

517

14
Errors and exceptions

Progress, far from consisting in change, depends on retentiveness. Those who cannot remember the past are condemned to repeat it. —George Santayana, The Life of Reason It’s always useful to keep in mind that PowerShell is not “just” a shell or scripting language. Its primary purpose is to be an automation tool for managing Microsoft Windows. And when we're depending on a script to perform some critical management task on a server, such as send software updates, inspect log files, or provision user accounts, we need to be sure that either the task is completed properly or the reason for failure is appropriately recorded. In this chapter, we’re going to focus on the latter topic: how PowerShell reports, records, and manages error conditions. This is one of the areas that really make PowerShell stand out from other scripting tools. The support for diagnostic tracing and logging is practically unprecedented in traditional scripting languages. Unfortunately, these features don’t come entirely free—there are costs in terms of complexity and execution overhead that aren’t there in other environments. All these capabilities are very much a part of PowerShell as a management tool; we’ve set a higher bar for PowerShell than has been set for most other language environments. We’ll begin the chapter by looking at the error processing subsystem. Errors in PowerShell are not simply error codes, strings, or even exceptions as found in languages such as C# and VB.Net. They are rich objects that include just about everything we could think of that might be useful in debugging a problem.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

518

AUTHOR'S NOTE
Some people dislike (okay, despise) the use of the word “rich” in this context. However, given the wealth of information that PowerShell error objects contain, rich really is the right word. So I’m going to use it several more times. So there.

We’re going to examine these ErrorRecord objects in detail, along with how they’re used by the various PowerShell mechanisms to manage error conditions. We’re also going to look at the other mechanisms that are available for solving script execution problems, including tracing and script debugging. PowerShell version 1 had fairly weak debugging tools. We'll look at how this was addressed by the new features in version 2.

14.1 Error handling
Error handling in PowerShell is very structured. As we said previously, errors are not simply bits of text written to the screen. PowerShell errors are rich objects that contain a wealth of information about where the error occurred and why. There is one aspect to error handling in PowerShell that is unique: the notion of terminating vs. non-terminating errors. This aspect is aligns with the streaming model that PowerShell uses to processing objects. Here’s a simple example that will help you understand this concept. Think about how removing a list of files from your system should work. You stream this list of files to the cmdlet that will delete the files. Imagine that you can’t delete all the files on the list for various reasons. Do you want the command to stop processing as soon as it hits the first element in the list? The answer is probably “No.” You’d like the cmdlet to do as much work as it can, but capture any errors so that you can look at them later. This is the concept of a non-terminating error—the error is recorded and the operation continues. On the other hand, there are times when you do want an operation to stop on the first error. These are called terminating errors. Of course, sometimes you want an error to be terminating in one situation and non-terminating in another and PowerShell provides mechanisms to allow you to do this. Since the architecture supports multiple non-terminating errors being generated by a pipeline, it can’t just throw or return an error. This is where streaming comes into play; non-terminating errors are simply written to the error stream. By default, these errors are displayed, but there are a number of other ways of working with them. In the next few sections, we’ll look at those mechanisms. But first we need to take at look at the error records themselves.

14.1.1 ErrorRecords and the error stream
As we delve into the topic of error handling, we’ll first take a look at capturing error records in a file using redirection, and then learn how to capture error messages in a variable. By capturing these errors instead of just displaying them, see can go back at a later time to analyze and hopefully fix what went wrong. First, let's review the normal behavior of objects in the pipeline. Output objects flow from cmdlet to cmdlet but error records are written directly to the default output processor. By default, this is the Out-

Default and the error records are simply displayed:
PS (1) > dir nosuchfile Get-ChildItem : Cannot find path 'C:\files\nosuchfile' because i t does not exist. At line:1 char:4 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

519 + dir foo.txt | B | C Error records output by A go to the input of Out-File Error records output by B go to the input of Out-Default Error records output by C go to the input of Out-Default

Out-File err.txt

Out-Default

A

B

C

Output of A goes to input of B Output of B goes to input of C Output of C goes to input of Out-Default

Figure 14.2 This diagram shows the output object and error record routing then the simple pipeline A | B | C is run from a PowerShell host process like powershell.exe or PowerShell_ISE.exe. Output objects go to the next command in the pipeline and error objects go directly to Out-Default.

but this has the downside that the error message is rendered to displayable text before writing it to the file. When that happens, we lose all the extra information in the objects. Take a look at what was saved to the file. PS (3) > Get-Content err.txt Get-ChildItem : Cannot find path 'C:\files\nosuchfile' because i t does not exist. At line:1 char:4 + dir &1 | B | C Error records output by A go to the input of B Error records output by B go to the input of Out-Default Error records output by C go to the input of Out-Default

Out-Default

A

B

C

Output of A goes to input of B Output of B goes to input of C Output of C goes to input of Out-Default

Figure 14.3 This diagram shows the output object and error record routing then the simple pipeline A | B | C is run from a PowerShell host process like powershell.exe or PowerShell_ISE.exe. Output objects go to the next command in the pipeline and error objects go directly to Out-Default.

Let's see how this actually works. First we'll use the stream merge operator to capture the error stream in a variable by using assignment. PS (4) > $err = dir nosuchfile 2>&1 Now use Get-Member to display the properties on the object. We’ll use the -Type parameter on Get-

Member to filter the display and only show the properties. (In order to be concise, we’ll use the gm alias instead of the full cmdlet name.) PS (5) > $err | gm -type property TypeName: System.Management.Automation.ErrorRecord Name ---CategoryInfo ErrorDetails Exception FullyQualifiedErrorId InvocationInfo TargetObject MemberType ---------Property Property Property Property Property Property Definition ---------System.Management.Automation... System.Management.Automation... System.Exception Exception {... System.String FullyQualified... System.Management.Automation... System.Object TargetObject {...

Although this shows you all of the properties and their definitions, some of the property names are a little tricky to figure out, so further explanation is in order. Table 14.1 lists all of the properties, their types, and a description of what the property is.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

522

Table 13.1 ErrorRecord properties and their descriptions
Property Name Property Type Description This string breaks errors into a number of broad categories.

CategoryInfo

ErrorCategoryInfo

ErrorDetails

ErrorDetails

This may be null. If present, ErrorDetails can specify additional information, most importantly ErrorDetails.Message, which (if present) is a more exact description and should be displayed instead of Exception.Message.

Exception

System.Exception

This is the underlying .NET exception corresponding to the error that occurred

FullyQualifiedErrorId

System.String

This identifies the error condition more specifically than either the ErrorCategory or the Exception. Use FullyQualifiedErrorId to filter highly specific error conditions.

InvocationInfo

InvocationInfo

This is an object that contains information about where the error occurred—typically the script name and line number.

TargetObject

System.Object

This is the object that was being operated on when the error occurred. It may be null, as not all errors will set this

field. Let’s look at the content of the properties for this error:PS (10) > $err | fl * force : System.Management.Automation.ItemNotFoun dException: Cannot find path 'C:\files\n osuchfile' because it does not exist. at System.Management.Automation.Sessi onStateInternal.GetChildItems(String pat h, Boolean recurse, CmdletProviderContex t context) at System.Management.Automation.Child ItemCmdletProviderIntrinsics.Get(String path, Boolean recurse, CmdletProviderCon text context) at Microsoft.PowerShell.Commands.GetC hildItemCommand.ProcessRecord() TargetObject : C:\files\nosuchfile CategoryInfo : ObjectNotFound: (C:\files\nosuchfile:Str ing) [Get-ChildItem], ItemNotFoundExcept ion FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Comman ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542 Exception

Licensed to Andrew M. Tearle

523 ds.GetChildItemCommand : : System.Management.Automation.InvocationI Nfo

ErrorDetails InvocationInfo

In this output, you can see the exception that caused the error was ItemNotFound-Exception. The

TargetObject member contains the full path the cmdlet used to locate the item. This overall error is placed in the broader category of ObjectNotFound. There are no additional error details for this object. Let’s take a closer look at the InvocationInfo member. This member provides information about where the error occurred. Here’s what it looks like: PS (6) > $err.InvocationInfo MyCommand ScriptLineNumber OffsetInLine ScriptName Line PositionMessage : : : : : : Get-ChildItem 1 11 $err = dir nosuchfile 2>&1

At line:1 char:11 + $err = dir $error[0].exception Attempted to divide by zero. Yes, it is. We’ll also verify that the previous error, the "file not found error", is now in position 1. PS (7) > $error[1].exception Cannot find path 'C:\working\book\nosuchfile' because it does no t exist. Again, yes it is. As you can see, each new error shuffles the previous error down one element in the array.

IMPORTANT TIP
The key lesson to take away from this is that when you are going to try to diagnose an error, you should copy it to a “working” variable so it doesn’t get accidentally shifted out from under you because you made a mistake in one of the commands you’re using to examine the error.

The $error variable is a convenient way to capture errors automatically, but there are two problems with it:   First, as we discussed earlier, $error only captures a limited number of errors so important information may fall off the end of the buffer Second, $error contains all of the errors that occur, regardless of where they came from or what command generated them, all mixed together in a single collection. This can make it hard to find the information you need to diagnose a specific problem. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

526 The first problem can be worked around using redirection to capture all the errors, but that still doesn’t address mixing all the errors together. To deal with this second issue, when you want to capture all the errors from a specific command, you use a standard parameter on all commands called -

ErrorVariable. This parameter names a variable to use for capturing all the errors that the command generates. Here’s an example. This command generates three error objects, since the files “nofuss”, “nomuss”, and “nobother” don’t exist. PS (1) > dir nofuss,nomuss,nobother -ErrorVariable errs Get-ChildItem : Cannot find path 'C:\Documents and Settings\bruc epay\nofuss' because it does not exist. At line:1 char:4 + dir trap { "Got it!" } 1/$null Got it! Attempted to divide by zero. At line:1 char:30 + trap { "Got it!" ; break } 1/$ div 1 0 2147483647 and we get the maximum integer value instead of the error we normally get when we divide by zero. PS (3) > 1/0 Attempted to divide by zero. At line:1 char:3 + 1/ dir variable:\nosuchvariable Get-ChildItem : Cannot find path 'nosuchvariable' because it doe s not exist. At line:1 char:4 + dir $nosuchvariable PS (2) > Now let's turn on strict mode version 1 and reference the variable again: PS (2) > Set-PSDebug -Strict PS (3) > $nosuchvariable The variable '$nosuchvariable' cannot be retrieved because it ha s not been set. At line:1 char:16 + $nosuchvariable divide(9, 3) Method invocation failed because [System.Object[]] doesn't conta in a method named 'op_Division'. At line:1 char:31 + function divide ($x,$y) { $x / ${a variable} = "foo" The other important use for delimited variables is in string expansion where the variable name being expanded could be confused with the rest of the string like: PS (3) > $prefix = "foo"; "The word is ${prefix}bar" The word is foobar Here the variable ${prefix} is expanded properly. Without the braces, the runtime would have tried to expand $prefixbar and failed. Now let's move onto the problematic scenario. Normally, outside of a string, if no names are specified between the braces, an error will occur, strict mode or not. PS (4) > ${} Empty ${} variable reference, there should be a name between the braces. At line:1 char:1 + $script = "function abc ($x) {dir; $x + 1}" The tokenizer actually returns two things - the tokens that make up the script and a collection of any errors encountered while parsing the script. Since the API is designed for use from languages that can't return multiple values, we also need to create a variable to hold these errors. PS (3) > $parse_errs = $null Now we're ready tokenize the script. We do this by calling the static method Tokenize() on the

PSParser class as follows:
PS (4) > $tokens = [System.Management.Automation.PSParser]::Tokenize( >> $script, ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

551 >> [ref] $parse_errs)

This will have put the list of tokens in the $tokens variable and any parse errors will be placed into a collection in $parse_errs. Let's dump these two variables - $parse_errs to the error stream and

$tokens to the output stream.
>> $parse_errs | Write-Error >> $tokens | Format-Table -Auto type,content,startline,startcolumn >> Type ---Keyword CommandArgument GroupStart GroupEnd GroupStart Command StatementSeparator Operator Number GroupEnd Content StartLine StartColumn ------- --------- ----------function 1 1 abc 1 10 ( 1 14 ) 1 15 { 1 17 dir 1 18 ; 1 21 + 1 24 1 1 26 } 1 27

Since the text being tokenized was valid PowerShell script, no errors were generated. We do get a list of all of the tokens in the text displayed on the screen. We can see that each token includes the type of the token, the content or text that makes up the token as well as the start line and column number of the token. Let's wrap this code up into a function to make it easier to call. We'll name our function

Test-Script.
PS (5) > function Test-Script ($script) >> { >> $parse_errs = $null >> $tokens = [system.management.automation.psparser]::Tokenize( >> $script, >> [ref] $parse_errs) >> $parse_errs | write-error >> $tokens >> } >> Let's try it out on a chunk of invalid script text. PS (6) > Test-Script "function ($x) {$x + }" | >> ft -auto type,content,startline, startcolumn >> Test-Script : System.Management.Automation.PSParseError At line:1 char:12 + Test-Script $script, >> [ref] $parse_errs) >> foreach ($err in $parse_errs) >> { >> "ERROR on line " + >> $err.Token.StartLine + >> ": " + $err.Message + >> "`n" >> } >> foreach ($token in $tokens) >> { >> if ($token.Type -eq "Command") >> { >> $gcmerr = Get-Command $token.Content 2>&1 >> if (! $? ) >> { >> "WARNING on line " + >> $gcmerr.InvocationInfo.ScriptLineNumber + ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

553 >> >> >> } >> } >> } >> } >> ": " + $gcmerr.Exception.Message + "`n"

The first part of the script hasn't change much - we tokenize the string and then display any errors, though in a more compact form. Then we loop through all of the tokens looking code commands. If we find a command, we check to see if it exists. If not, then we display a warning. Let's try it out. First, we'll define our test script with expected errors and an undefined command. PS (8) > $badScript = @' >> for ($a1 in nosuchcommand) >> { >> while ( ) >> $a2*3 >> } >> '@ >> Now run the test and see what we get: PS (9) > Test-Script $badScript ERROR on line 1: Unexpected token 'in' in expression or statement. ERROR on line 1: Unexpected token 'nosuchcommand' in expression or st atement. ERROR on line 3: Missing expression after 'while' in loop. ERROR on line 4: Missing statement body in while loop. WARNING on line 18: e name of a cmdlet, the spelling of the path is correct and The term 'nosuchcommand' is not recognized as th function, script file, or operable program. Check name, or if a path was included, verify that the try again.

In the output we see the expected syntax errors but we also get a warning for the undefined command. There are a lot of things we could do to improve this checker. For example we could look for variables that are only used once but we'll leave that as an exercise for the reader. So far we've looked a number of tools and approaches that we can use to to see what's wrong with our scripts. But how do we figure out what's going when other people are running our (or other peoples) scripts running in a different environment, possibly at a remote location? To help with this, the PowerShell console host includes a session transcript mechanism. We'll see how this works in the next section.

14.4 Capturing session output
When trying to debug what's wrong with someone's script at a remote location, being able to see the output and execution traces from a script run is extremely helpful. The PowerShell console host allows us to do this through a mechanism to capture console output in transcript files. This is exposed through the Start-Transcript and Stop-Transcript cmdlets shown in figure 14.17.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

554
Allows you to specify a path for the transcript file instead of using the default. Turn transcription on Force writing to the file even if it is read-only. If a writable file already exists don’t overwrite it.

Start-Transcript [[-Path] ] [-Append] [-Force] [-NoClobber] Stop-Transcript Append to the file instead of overwriting it.

Turn transcription off

Figure 14.17 This figure shows the cmdlets to start and stop console transcription. When transcription is turned on, all output to the console is written to the transcript file.

AUTHOR'S NOTE
Unfortunately, the implementation of these cmdlets is a feature of the console host (powershell.exe) and so is not available in other hosts, including the PowerShell ISE. But all is not lost - there is a way to effectively do the equivalent with the ISE as we'll see in the next chapter. Other host applications may have similar mechanisms.

To start a transcript, simply run Start-Transcript as shown in the next example. We'll begin the example by running a command before starting the transcript so we can see what is and is not recorded. We'll run Get-Date to get the current date: PS (1) > Get-Date Thursday, April 15, 2010 10:10:12 PM and now we'll start the transcript. PS (2) > start-transcript Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415221017.txt Since we didn't specify a filename for the transcript file, one will be automatically generated for us in our Documents directory. Now we'll run a couple of additional commands. PS (3) > 2+2 4 PS (4) > $psversiontable Name ---CLRVersion BuildVersion PSVersion WSManStackVersion PSCompatibleVersions SerializationVersion PSRemotingProtocolVersion Value ----2.0.50727.4200 6.0.6002.18111 2.0 2.0 {1.0, 2.0} 1.1.0.1 2.1

and stop the transcript. Again, it conveniently tells us the name of the file containing the transcript. PS (5) > stop-transcript ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

555 Transcript stopped, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415221017.txt Now let's see what was captured. PS (6) > Get-Content C:\Users\brucepay\Documents\PowerShell_transcript .20100415221017.txt ********************** Windows PowerShell Transcript Start Start time: 20100415221017 Username : brucepayquad\brucepay Machine : BRUCEPAYQUAD (Microsoft Windows NT 6.0.6002 Service Pack 2) ********************** Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415221017.txt PS (3) > 2+2 4 PS (4) > $psversiontable Name ---CLRVersion BuildVersion PSVersion WSManStackVersion PSCompatibleVersions SerializationVersion PSRemotingProtocolVersion Value ----2.0.50727.4200 6.0.6002.18111 2.0 2.0 {1.0, 2.0} 1.1.0.1 2.1

PS (5) > stop-transcript ********************** Windows PowerShell Transcript End End time: 20100415221038 ********************** The transcript file includes a header showing us the start time, the name of the user running the script and the name and OS information about the computer on which the command is being run. We see the filename yet again because it was written out after transcription was turned on and so is captured in the transcript. After that, we see the output of the commands we ran (including Stop-Transcript) and then finally a trailer showing the time the transcript stopped.

14.4.1 What gets captured in the transcript
It seems obvious that everything should get captured in the transcript file but this isn't the case. As mentioned earlier, the transcript captures everything written through the host APIs that were described in section 14.3. What doesn't get captured is anything that bypasses these APIs and writes directly to the console. The most important impact that this has is when running a native command like

ipconfig.exe or even cmd.exe. If these commands aren't redirected within PowerShell, then their output goes directly to the console and bypasses the host APIs. Let's see how this looks. We'll start a new transcript which writes to a different file (a new file name is generated each time). PS (1) > start-transcript Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415222650.txt and run two command, one of which uses cmd.exe to echo something directly to the console. PS (2) > cmd /c echo THIS WONT BE CAPTURED ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

556 THIS WONT BE CAPTURED PS (3) > "This will" This will Now stop the transcript and look at the output: PS (4) > stop-transcript Transcript stopped, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415222650.txt PS (5) > Get-Content C:\Users\brucepay\Documents\PowerShell_transcript .20100415222650.txt ********************** Windows PowerShell Transcript Start Start time: 20100415222650 Username : brucepayquad\brucepay Machine : BRUCEPAYQUAD (Microsoft Windows NT 6.0.6002 Service Pack 2) ********************** Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415222650.txt PS (2) > cmd /c echo THIS WONT BE CAPTURED PS (3) > "This will" This will PS (4) > stop-transcript ********************** Windows PowerShell Transcript End End time: 20100415222708 ********************** We see the same headers and trailers as before but notice that we see the cmd command being run, but there is no output shown for that command. Since cmd.exe wrote directly to the console buffer, the transcript mechanism didn't capture it. The way to make sure that we do capture the output of this kind of command is to pipe it through Write-Host, forcing it to go through the host APIs. This looks like: PS (7) > start-transcript Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415223336.txt PS (8) > cmd /c echo THIS WILL BE CAPTURED 2>&1 | Write-Host THIS WILL BE CAPTURED PS (9) > stop-transcript Transcript stopped, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415223336.txt PS (10) > Get-Content C:\Users\brucepay\Documents\PowerShell_transcri pt.20100415223336.txt ********************** Windows PowerShell Transcript Start Start time: 20100415223336 Username : brucepayquad\brucepay Machine : BRUCEPAYQUAD (Microsoft Windows NT 6.0.6002 Service Pack 2) ********************** Transcript started, output file is C:\Users\brucepay\Documents\PowerS hell_transcript.20100415223336.txt PS (8) > cmd /c echo THIS WILL BE CAPTURED 2>&1 | write-host THIS WILL BE CAPTURED PS (9) > stop-transcript ********************** Windows PowerShell Transcript End End time: 20100415223410 ********************** ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

557 This time, when we look at the transcript, we can see that the output of command 8 was captured in the transcript. Using the transcript cmdlets, it's easy to have the remote user capture the output of their session. Simply have the remote user call Start-Transcript, run their script and then call Stop-

Transcript. This will produce a transcript file that they can send to you for examination.
Session transcripts are a handy way to capture what's going on with a script but they require someone to actively call them to get the transcript. There is another place where activity is recorded continuously, including for PowerShell: the event log. The event log is the central store for log messages from the system as well as from all of the applications, services and drivers running on that machine. It's a "one-stop-shop" for diagnostic information. We'll see how to access this diagnostic treasure trove using PowerShell in the final section in this chapter.

14.5 The PowerShell and the event log
And now, the final topic in this chapter: exploring the Windows event log using PowerShell. The Windows event log provides a central place where applications and operating system components can record events like an operation starting and stopping, progress and especially, system and application errors. For system administration, having accessing to the event log is critical. Obviously, as an admin tool, PowerShell support for the event log is very important so that's what we're going to look at in this section.

14.5.1 The EventLog cmdlets.
PowerShell V1 had only a single, fairly limited command ( Get-EventLog) for working with the event log. More sophisticated operations required using the underlying .NET classes. PowerShell V2 filled in this gap and provides a comprehensive set of cmdlets for working with the event log as shown in table 14.2.

Table 14.2 The PowerShell event log cmdlets
Cmdlet Name Get-EventLog PowerShell Version V1, enhanced in V2 Description Gets the events in an event log, or a list of the event logs, on the local or remote computers. Clear-EventLog V2 Deletes all entries from specified event logs on the local or remote computers. Write-EventLog V2 Writes a new event log entry to the specified event log on the local or remote computer. Limit-EventLog V2 Sets the event log properties that limit the size of the event log and the age of its entries. Show-EventLog V2 Displays the event logs of the local or a remote computer using the event viewer MMC console. New-EventLog V2 Creates a new event log and a new event source on a local or remote computer. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

558 Remove-EventLog V2 Deletes an event log or unregisters an event source.

The Get-EventLog cmdlet is what we're going to focus our attention on here. This cmdlet allows us to retrieve a list of the available application and system event logs and then look at the content of each of the individual logs. To get a list of the available logs, run "Get-EventLog -List". The output will look something like this: PS (1) > get-eventlog -List Max(K) Retain OverflowAction ------ ------ -------------20,480 0 OverwriteAsNeeded 15,168 0 OverwriteAsNeeded 20,480 0 OverwriteAsNeeded 512 7 OverwriteOlder 20,480 0 OverwriteAsNeeded 8,192 0 OverwriteAsNeeded 16,384 0 OverwriteAsNeeded 15,360 0 OverwriteAsNeeded 16,384 0 OverwriteAsNeeded 20,480 0 OverwriteAsNeeded 20,480 0 OverwriteAsNeeded 15,360 0 OverwriteAsNeeded Entries ------42,627 0 0 0 0 11,695 80 101 790 34,798 47,875 11,988 Log --Application DFS Replication HardwareEvents Internet Explorer Key Management Service Media Center ODiag Operations Manager OSession Security System Windows PowerShell

As well as the names of the various logs, we can also see the configuration settings for the log like the amount of space an individual log might take and the what happens when the log fills up. We can use the Limit-EventLog cmdlet to change these limits for a log: PS (4) > Limit-EventLog -LogName Application -MaximumSize 256kb and verify that the limit has been changed. PS (6) > Get-EventLog -List | where {$_.Log -match "application"} Max(K) Retain OverflowAction ------ ------ -------------256 0 OverwriteAsNeeded Entries Log ------- --42,627 Application

As well as listing the available logs, Get-EventLog lets us see the events in any log. Because the even logs can be very large, the cmdlet supports a variety of options to control the amount of data returned. The parameters to Get-EventLog are shown in figure 14.18.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

559
The name of the event log to read from on the target computer A list of message resource IDs identifying the entries to retrieve Only return the newest events Get-EventLog [-LogName] [-Computer Name ] [[-InstanceId] ] [-Newest ] [-After ] [-Before ] [-UserName ] The index of a specific event in the log to get A list of the types of events to get [-Index ] [-EntryType ] [-Source ] [-Message ] The data range of records to retrieve Only get events where the user name matches one of the specified patterns Retrieve events with matching source identifiers. Only return log entries where the message text matches the specified pattern. The list of target computers to get events from

Figure 14.18 The Get-EventLog cmdlet has a large number of parameters that allow us to control where the events are retrieved from and which events are to be retrieved. We can use event type, number and date range to control the number of events retrieved and filter those events by user name or strings in the event message.

Table 14.3, describes the various the Get-EventLog filter parameters in more details.

Table 14.3 The types of filters provided by the Get-EventLog cmdlet
Filter Description The -Source parameter allows filtering log entries based on the name used to register the event source. This name is usually the name of the application logging the events however, for larger applications, it may be the name of a subcomponent within that application.

Source

Message

The -Message parameter allow the retrieved entries to be filtered based on the event's message text. The specified filter strings may contain wildcard patterns. (Note - since the text of a message is usually translated, using -Message filter may not be portable to different locations.)

InstanceID The InstanceId for an entry is the message resource identifier for the event. This identifier is used to retrieve the localized text for a message from the resource file for the registered event source. Since this identifier is not localized, the -InstanceID parameter provides a way to filter events by message that is portable across locations.

EntryType

The entry type (or severity level) is a way of classifying events based on the potential impact of the corresponding event on the system's behavior. The entry types are:

Information, Warning, Error or Critical. There are two additional event types
©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

560 that can occur in the security log: Success Audit and Failure Audit.

User

The -User parameter filters based on the name of the user on whose behalf the event occurred. Wildcards patterns can be used in arguments to this parameter.

Let's look at how these parameters are used by working through a few examples. We're going to look at the "Operations Manager" log.

AUTHOR'S NOTE
This log may not be present on your system since it requires that Microsoft Operations Manager be used. It doesn't really affect anything - just pick a log from the list we saw earlier (e.g. System) to work with instead.

We'll start by listing the newest 10 events in this log: PS (1) > Get-EventLog -LogName 'Operations Manager' -Newest 10 Index ----101 100 99 98 97 96 95 94 93 92 Time ---Apr 14 Apr 14 Apr 14 Mar 31 Mar 31 Mar 31 Mar 10 Mar 10 Mar 10 Mar 10 EntryType --------Information Error Information Information Error Information Information Error Information Information Source -----HealthService HealthService Health Service HealthService HealthService Health Service HealthService HealthService Health Service Health Service InstanceID ---------1073743827 3221227482 102 1073743827 3221227482 102 1073743827 3221227482 302 301

03:28 03:28 03:28 03:20 03:20 03:20 03:30 03:30 03:30 03:30

ES...

ES...

ES... ES...

The -Index parameter lets us retrieve a specific entry from the log. We'll save this entry in a variable then use format list to display additional properties of the entry. PS (2) > $e = Get-EventLog -LogName 'Operations Manager' -Index 99 PS (3) > $e | Format-List Index EntryType InstanceId Message : : : : 99 Information 102 HealthService (2264) Health Service Store: The d atabase engine (6.00.6002.0000) started a new in stance (0). General 1 {HealthService, 2264, Health Service Store: , 0. ..} Health Service ESE Store 4/14/2010 3:28:02 AM 4/14/2010 3:28:02 AM

Category : CategoryNumber : ReplacementStrings : Source TimeGenerated TimeWritten UserName : : : :

Using Format-List shows us, among other things, the InstanceID and text of the event message. Let's retrieve all of the events using this message resource ID: PS (4) > Get-EventLog -LogName 'Operations Manager' -Newest 10 ` >> -InstanceId 102 >>

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

561 Index Time ----- ---99 Apr 14 96 Mar 31 90 Mar 10 84 Feb 24 81 Feb 14 78 Feb 10 72 Jan 23 66 Jan 13 60 Dec 10 57 Nov 25 EntryType Source -------------03:28 Information Health 03:20 Information Health 03:29 Information Health 03:23 Information Health 17:55 Information Health 03:28 Information Health 03:21 Information Health 03:25 Information Health 03:27 Information Health 03:21 Information Health InstanceID ---------ES... 102 ES... 102 ES... 102 ES... 102 ES... 102 ES... 102 ES... 102 ES... 102 ES... 102 ES... 102

Service Service Service Service Service Service Service Service Service Service

We can use -Before and -After to retrieve messages around a specific date (and time is desired). PS (5) > Get-EventLog -LogName 'Operations Manager' ` >> -After 'Oct 15/2009' -Before "Oct 28/2009" >> Index ----28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 Time ---Oct 27 Oct 27 Oct 27 Oct 27 Oct 27 Oct 27 Oct 27 Oct 27 Oct 27 Oct 18 Oct 18 Oct 18 Oct 15 Oct 15 Oct 15 EntryType --------Information Error Information Information Error Information Information Error Information Information Error Information Information Error Information Source -----HealthService HealthService Health Service HealthService HealthService Health Service HealthService HealthService Health Service HealthService HealthService Health Service HealthService HealthService Health Service InstanceID ---------1073743827 3221227482 102 1073743827 3221227482 102 1073743827 3221227482 102 1073743827 3221227482 102 1073743827 3221227482 102

23:08 23:08 23:07 21:41 21:41 21:40 20:58 20:58 20:58 11:24 11:24 11:23 03:10 03:10 03:09

ES...

ES...

ES...

ES...

ES...

Here we've retrieved all of the messages between Oct. 15 and Oct. 28 in 2009. We can combine -

Before and -Newest to get a specific number of messages before a particular date.
PS (7) > Get-EventLog -LogName 'Operations Manager' ` >> -Before 'Oct 15/2009' -Newest 10 >> Index ----13 12 11 10 9 8 7 6 5 4 Time ---Oct 11 Oct 11 Oct 11 Oct 10 Oct 10 Oct 10 Oct 10 Oct 10 Oct 10 Sep 17 EntryType --------Information Error Information Information Error Information Information Information Information Information Source -----HealthService HealthService Health Service HealthService HealthService Health Service Health Service Health Service Health Service HealthService InstanceID ---------1073743827 3221227482 102 1073743827 3221227482 302 301 300 102 1073743827

03:09 03:09 03:08 08:48 08:48 08:48 08:46 08:46 08:46 15:18

ES...

ES... ES... ES... ES...

And finally, we can use -Message and -After to find all message matching a specific pattern that occurred after a specific date. We'll just use the month and day numbers and let the year default to this year. Here's what we get: PS (9) > Get-EventLog -LogName 'Operations Manager' ` >> -Message "*Data*6.0*" -After '4/1' | ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

562 >> Format-List Name,Time,EntryType,Message >> EntryType : Information Message : HealthService (2264) Health Service Store: The database e ngine (6.00.6002.0000) started a new instance (0). This command returned a single entry indicating that the database engine started a new instance. So why is all of this useful? Imagine we see a critical error in an application. This error shows up in the Application log. We suspect that it might be related to either a hardware issue or a bad device driver. Rather than manually pouring through hundreds of log entries, we can use the date from the

Application log entry to retrieve the events in the System log that occurred shortly before the application. Digging through the entries, we identify the problem that led to the failure. From this, we get the

Source and InstanceID identifying the problematic entry. We quickly write a script to remediate the problem on this machine but realize that there may be other machines in the organization with similar issues. We put together a list of potentially at-risk machines and pass this list to Get-EventLog using the -ComputerName parameter. We'll also specify the -Source and -InstanceID parameters of the problematic message. This will search the event logs of all of the at-risk machines, returning a list of event log entries matching the criteria. From this set of events, we can get the names of all of the computers that need to be fixed. Finally we can use PowerShell remoting to run the remediation script on all of the machines with the problem.

AUTHOR'S NOTE
While we need PowerShell remoting to run the remediation script on the target machines, PowerShell remoting is not used when

Get-EventLog access a remote computer. It uses its own remoting

protocol as mentioned in chapter 12. This means it can be used to examine the logs of the target computer to help diagnose what went wrong if remoting can connect to a computer.

The Get-EventLog filtering capabilities make this kind of "forensic" analysis very easy. One of the things we might want to analyze is PowerShell itself.

14.5.2 Examining the PowerShell event log
When PowerShell is installed, it creates a new event log for itself called “Windows PowerShell”. As PowerShell executes, it writes a variety of information to this log which we can see using the GetEventLog cmdlet. Let’s use the cmdlet to get the last few records from the PowerShell event log. As always, we can use the tools PowerShell provides to filter and scope the data we want to look at. We’ll use an array slice to get the last five records from the log. PS (2) > (get-eventlog 'windows powershell')[-5..-1] WARNING: column "Message" does not fit into the display and was remov ed. Index ----5 4 3 2 Time ---Dec 27 Dec 27 Dec 27 Dec 27 EntryType --------Information Information Information Information Source -----PowerShell PowerShell PowerShell PowerShell InstanceID ---------600 600 600 600

17:25 17:25 17:25 17:25

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

563 1 Dec 27 17:25 Information PowerShell 600

The default presentation of the event records doesn’t show much information. Let’s look at one event in detail and see what it contains. PS (3) > (get-eventlog "windows powershell")[0] | fl *
First, we get some basic event log elements common to all event log entries.

EventID MachineName Data Index

: : : :

400 brucepayquad {} 8915

Next, we see the event category. This is not the same as the error category discussed earlier. PowerShell event log entries are grouped into several large categories.

Category CategoryNumber

: Engine Lifecycle : 4

Next is the entry type and a message describing the entry. This is followed by a collection of detail elements, which include things such as the state transition for the engine, as well as some of the versioning information we saw on the hosts for a particular engine.

$host object earlier. This is included in case you have multiple

EntryType Message

: Information : Engine state is changed from None to Available. Details: NewEngineState=Available PreviousEngineState=None SequenceNumber=9 HostName=ConsoleHost HostVersion=2.0 HostId=29f2d25c-551c-4e8b-9c15-3cd2103c4a70 EngineVersion=2.0 RunspaceId=ffff212a-7e81-4344-aecd-c6bcab05b 715

The following fields are only populated when detailed logging is turned on for a PowerShell snap-in (but, unfortunately, not for modules).

Source

PipelineId= CommandName= CommandType= ScriptName= CommandPath= CommandLine= : PowerShell

The following fields specify the replacement strings that are available. These strings are substituted into the log message text.

ReplacementStrings : {Available, None, NewEngineState=Available PreviousEngineState=None SequenceNumber=9 HostName=ConsoleHost HostVersion=2.0 HostId=29f2d25c-551c-4e8b-9c15-3cd2103c4a70 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

564 EngineVersion=2.0 RunspaceId=ffff212a-7e81-4344-aecd-c6bcab05b715 PipelineId= CommandName= CommandType= ScriptName= CommandPath= CommandLine=}
Finally, some additional information for identifying the event log record and when it occurred.

InstanceId TimeGenerated TimeWritten UserName Site Container

: 400 : 1/10/2010 6:02:19 PM : 1/10/2010 6:02:19 PM : : :

Granted, the output is not all that interesting but, when we're trying to figure out what when wrong on our systems, being able to see when the PowerShell interpreter was started or stopped can be very useful. There are also certain types of errors that may cause a PowerShell session to terminate. These errors also will be logged in the PowerShell event log. That’s all we’re going to cover on event logs in this chapter. From these examples, we can see that the event logs provide a lot of information, much of which can help us manage and maintain our systems. The trick is being able to extract and correlate the information across the various logs and this is where PowerShell can be very useful.

14.6 Summary
This chapter focused on the diagnostic features of PowerShell: the error handling mechanisms and the various debugging, tracing, analysis and logging features. And, while this is a fairly long chapter, it still can't claim to be an exhaustive discussion of all of these features. Let’s summarize the areas that we did cover. We started with basic error handling covering:      The two types of errors in PowerShell: terminating and non-terminating The ErrorRecord object and the error stream The $error variable and -ErrorVariable parameter The $? and $LASTEXITCODE variables

$ErrorActionPreference and the -ErrorAction parameter
The trap statement and how to use it The try/catch/finally statements in PowerShell V2 Using the throw statement to generate our own terminating exceptions

Next, we covered how to work with terminating errors and exceptions:   

And then we covered tools and techniques for finding static and runtime errors in out scripts. We covered:   Using the host APIs to do "printf"-style debugging. Catching errors with "strict mode", both V1 and V2 versions.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

565  Using the PowerShell tokenizer API to do static analysis of a strict to try an catch problems before they happen.

Finally we looked at some techniques for examining the behavior of scripts that other people may be running by using the transcript feature in powershell.exe and by looking at the event log using the Get-

EventLog cmdlet.
This chapter looked at ways to deal with errors in PowerShell scripts after they have occurred (sometimes long after in the event log case.) In chapter 15, we'll look at ways of prevent errors in our script by using the PowerShell ISE and the built-in debugger.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

566

15
The PowerShell ISE and Debugger

Big Julie: I had the numbers taken off for luck, but I remember where the spots formerly were. —Guys and Dolls, words and music by Frank Loesser In the last chapter, we learned how PowerShell deals with errors and how those features help us find bugs in out scripts. In this chapter, we're going to look at more tools that help us create correct, reliable scripts. We'll cover the graphical Integrated Script Environment (ISE) as well as the debugger, both of which were introduced with PowerShell V2. We'll start by covering the basic operation and usage patterns of the ISE. Next we'll look at customizing and extending the ISE using the object model provided for that purpose. In the second part of the chapter we'll move on to the debugger and how it works within the ISE as well as how it's used on the command line. All together, this is a very subject area so this chapter is longer than most of the others in this book.

15.1 The PowerShell ISE
PowerShell version 2 significantly improved the PowerShell scripting experience with the introduction of the graphical Integrated Scripting Environment. This new PowerShell host application adds many of the features expected in a modern scripting environment: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

567

    

syntax highlighting in the editor and command windows multiple concurrent sessions multiple editor tabs within a session full globalization support an extension environment. mechanism allowing use to add our own functionality to the

The goal of the first part of this chapter is to learn to use the ISE effectively and adapt it to the individual's working style and environment. We'll begin with examining at the window layout and major features of the ISE.

15.1.1 Controlling the ISE window layout
The basic layout of the ISE consists of 3 panes - the editor pane, the output pane and the command pane. This layout is shown in figure 15.1.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

568

The basic ISE layout includes three panes – editor, output and command input.

Editor Pane

Output Pane

Command Pane
Figure 15.1 This figure shows the basic layout of the PowerShell ISE. The user interface is broken into three panes the command pane for command entry, the output pane and the editor pane.

As one would expect, the Editor pane holds all of the current files being edited and the Output pane is where the output of executed commands is displayed. The last pane is the Command pane for command entry. This is a bit different than most shells. Typically, shells interleave user commands with the output text all in the same pane. The ISE does display the commands as they are executed in context with the output but the commands themselves are entered into a separate window. This arrangement makes it possible to edit complex commands interactively while preserving the critical aspects of the traditional shell experience. Let's take a closer look at the elements in the command pane. THE COMMAND PANE The command pane actually composed of three separate pieces: the command entry area, the prompt line and the status time as shown in figure 15.2.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

569

The layout of the PowerShell ISE Command Pane Change command window position Prompt bar

Command input editor

Status informaton

Current position in file

Status Bar Font-size

Figure 15.2 This figure shows the pieces of the ISE command pane: the prompt bar, the command input editor and the status bar, as well as controls to change the font size and command window position.

In this figure we can see that there are two elements in the prompt bar: the text of the prompt and a control for changing the position of the command window relative to the editor window. As is the case with the console host, the prompt text is set by the user-definable prompt function. The position control is used to exchange the relative positions of the editor and command panes. This is shown in figure 15.3

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

570

The layout of the PowerShell ISE with the command pane “down” and “up” When the command pane is down, it appears below the output windows

Script Editor

Command Input Pane When the command pane is up, it appears above the output windows Command Input Pane

Script Editor
Figure 15.3 This figure shows the layout of the ISE when the command pane is "up" - above the output window, or "down" - below the output window.

When the position icon (an up arrow) is clicked, the command pane moves from below the output window to above it and the icon changes to a down arrow as shown in the figure. When clicked again, the original positions are restored. THE ISE TOOL BAR Moving on in our coverage of the basic ISE elements, let's look at the tool bar. The tool bar for quick access to usual features (e.g. cut, copy and paste) but there are also some PowerShell specific features included. The toolbar is shown in figure 15.4.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

571 THE TOOL BAR CONTROLS

Open file

Save file

Clear output pane

New remote PowerShell Tab

Change layout

New file Cut, copy and paste Undo/redo edits Run script, run selection and stop Launch Show/hide powershell.exe script pane

Figure 15.4 This figure shows the ISE toolbar with controls to create, open and save files, run scripts, and control the layout of the ISE. It also allows the user to start a remote PowerShell session in a new tab as well as launch the console host powershell.exe.

In the ISE toolbar, along with the standard items, there are also ISE-specific controls for running scripts, for creating remote tabs and launching the console host powershell.exe. We'll cover remote tabs later on in section 15.2.2. Another thing that the toolbar is contains controls that allow additional layout options for the ISE. We can see one of these alternate layouts in figure 15.5.

The layout of the PowerShell ISE with the editor pane on the side.

Figure 15.5 The organization of the three major panes in the ISE can be controlled to a large extent by user settings. In this figure, the layout is configured so the editor window is to the right of the output window instead of above.

This side-by-side layout is most effective on a large monitor allowing us to have full-sized execution and editor windows beside each other. There are two other layout modes with the editor pane either hidden or maximized. These modes are shown in figure 15.6.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

572

Editor pane hidden

Editor pane mazimized

Figure 15.6 On the left hand side of this figure, the editor pane has been hidden and the full space used for the command and output windows. In the right hand side, the editor pane has been maximized hiding the other windows. These modes can also be toggled by pressing ctrl-R.

Where the side-by-side mode was ideal if there is a lot of screen real estate, this mode allows the ISE to be used effectively on smaller screen devices like netbooks or tablets. Of course all of these modes can be set from the menus. The other thing we can do is use hotkey sequences to switch configurations. The

View menu shown in figure 15.7 shows a number of these hotkeys.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

573

Figure 15.7 This figure shows the items in the view menu and their associated hot-key sequences for managing the layout of the panes in the ISE>

Now that we know how to use the layout settings to choose the best layout for our work, we'll see what benefits the ISE editor brings to the PowerShell user.

15.1.2 Using the ISE editor
The PowerShell ISE editor is built on the same editor control used in recent versions of Microsoft Visual Studio and the Expression Blend internet application development suite. As such it follows most to the key-binding sequences used by these tools and they in turn, follow the Windows Common User Access (CUA) bindings. Since the same control is used everywhere, the CUA guidelines apply everywhere and we can finally use Ctrl-C and Ctrl-V to copy and paste between windows, including the command window. The more interesting key bindings are shown in table 15.1. Where there are special behaviors associated with the keys in the ISE, they are marked as "ISE" in the left column.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

574

Table 15.1 The PowerShell ISE Editor Key Bindings
Key F1 Description Opens the PowerShell help viewer. This launches the hypertext help viewer which allows the contents of the PowerShell help system to be searched and navigated much more easily than the command line version of help. ISE Ctrl F Search forward in the current editor window from the current cursor position. This does not wrap and so will not find occurrences earlier in the text. F3 Search forwards in the current editor window to find the next occurrence of the pattern specified to Ctrl-F ISE Shift-F3 Search backwards in the current editor window to find earlier occurrences. As with the forward search, this will not wrap around and continue searching from the end of the file once the start has been reached. ISE ISE ISE Ctrl-H Ctrl-G Ctrl-F5 Replace strings in the current editor window. Go to a specific line number in the current file. Execute the contents of the file in the current editor window. The file will be saved to disk before execution. Note - file will be as though it where dotsourced modifying the global environment. ISE ISE F8 Ctrl-Shift P Dot-source the currently selected text. Start an instance of powershell.exe. This is useful for commands that require the console window environment. Ctrl-F4 Close the current editor window, prompting to save if the buffer has not been saved. Alt-F4 Exit the ISE. If there are no unsaved files, this will cause the ISE to immediately exit without prompting. ISE Ctrl-N Ctrl-S ISE ISE ISE ISE Ctrl-R Ctrl-1 Ctrl-2 Ctrl-3 Ctrl-O Open a new editor tab. Save the current editor tab. Toggle visibility of the script pane. Show the script pane above the command pane. Show the script pane on the right Show the script pane maximized. Open an existing file http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

575 ISE ISE ISE Ctrl-T Ctrl-W Ctrl-Shift-R Ctrl-A ISE Tab Start a new PowerShell session tab. Close the current PowerShell session tab. Open a new remote PowerShell session tab. Select everything in the current window. If nothing is selected, move to next tab stop. The ISE will insert 4 spaces by default. It does not insert tab characters. If more than one line is selected, then it will shift all of the selected lines one tab stop right. ISE Shift-Tab If more than one line is current selected in the editor window, the text will be shifter one tab stop (4 spaces by default) left. ISE Ctrl-Tab Home, End Cycle through the editor buffers in the current PowerShell tab. Move the cursor to the beginning or end of the line. The first time the Home key is pressed, it moves the cursor to first non-blank character in the line. Pressing it again will move to the cursor to the first character in the line. ISE Enter In an editor window, insert a new line. In the command window, execute the command. In the editor window, tab depth is maintained. Shift Down-Arrow , Extend the text selection up to the next or previous line. Shift Up-Arrow Shift Left-Arrow Shift Right-Arrow Shift-F10 Ctrl-Home, CtrlEnd In the table there are a couple of items that we'll explore in detail later in the chapter like what a "New remote runspace tab" (section 15.2.2) is. We'll also look at the key bindings used by the debugger. But for now, we'll finish our discussion of the basic operation of the editor. OPENING A FILE In table 15.1, we saw that one way to open a file is to select the File->Open menu item or just hit Add the next item/character to selection. Add the previous item/character to selection. Display the context menu for current window (like right-clicking) Go to the beginning or end of the current editor windows.

Ctrl-O. This is what we expect from an editor. From a shell, however - we expect to do things with commands and we can. There is a command available in the ISE called psEdit that will allow us to open files from the command line. Since it takes wildcards, it can be used to open multiple files at once. This command is actually a function and the default definition looks like: PS (STA) (2) > Get-Content function:psedit param([Parameter(Mandatory=$true)]$filenames) foreach ($filename in $filenames) { dir $filename | where {!$_.PSIsContainer} | %{ $psISE.CurrentPowerShellTab.Files.Add($_.FullName) > $null ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

576 } } The thing to notice in this function definition is the $psISE variable. This is the root of the ISE object model that lets us script against the ISE itself. We'll cover this in detail later in this chapter. CREATING A NEW FILE Opening existing files is fine but the goal of the ISE it to help use create new scripts. Again we can use the menu items File->New or Ctrl-N but there is an annoyance here. The Windows file browser dialog has its own idea of what the current directory should be and it's not the same as the current directory we see in the command window.

AUTHOR'S NOTE
Sometime this is a feature - all new files are created in the same place, but mostly I find it annoying and create files from the command line.

If we want to create a file in the current directory, we'll have to do it from the command line. Unfortunately, psEdit will just complain if the tell it to open a file that doesn't exist. Instead we have to create the file ourselves with New-Item or just doing: PS (STA) (4) > "" > newfile.ps1 This will create a file with one empty line in it. We verify that it was created: PS (STA) (5) > dir .\newfile.ps1 Directory: C:\Users\brucepay Mode ----a--LastWriteTime ------------5/6/2010 10:02 PM Length Name ------ ---6 newfile.ps1

and then open it. PS (STA) (6) > psEdit .\newfile.ps1

WHY ARE THERE 6 CHARACTERS IN A 1 LINE FILE?
You might have noticed something funny in the output when we looked at the file using dir - it was supposed to be single empty line which implies 2 characters (CR and LF) not 6. The other 4 characters are the Unicode Byte order mark or BOM, indicating that this is a Unicode text file. This is something to keep in mind because by editing files in the ISE may turn an ASCII file into a Unicode file.

TAB-EXPANSION IN THE EDITOR WINDOW One of the most useful tools for learning and working with PowerShell is the tab completion feature. This feature is available in the console host but it really shines in the ISE because it works in the editor as well as the command window. It also doesn't suffer from the limitation that the console shell has where hitting tab is treated as the end of line and everything after it is deleted. In the ISE, tabcompletion can be used anywhere in the editor windows. When the tab completion function executes, it operates against the state of the live shell session in the current tab. This means that if we have a variable defined in out session, the editor will tabcomplete against the existing definition of the variable, even if it is used in a different way in the script we're editing. Likewise, it will resolve command parameters against existing commands, not the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

577 commands that are defined in a particular file. At times this can be confusing so. The fact that the ISE dot-sources scripts when run with F5 mitigates this to some extent since the script variables become global variables. We'll discuss this a bit more later on. Now let's look at the other significant feature that the ISE offers for writing scripts. SYNTAX HIGHLIGHTING IN THE ISE WINDOWS Another very useful feature of the ISE is that is does syntax highlighting everywhere - both in the editor and command windows. As text is entered in either of the windows, the ISE will color the content of the buffer based on the syntax of what we've typed. If there is an error at some point in what we type, then the highlighting will stop. This is a way to catch syntax errors while we're entering our code. Note that syntax highlighting is limited to PowerShell script files with .ps1, .psm1 and .psd1 extensions.

AUTHOR'S NOTE
The syntax highlighting is done using the tokenizer API introduced in chapter 14. We (the PowerShell team) made this a public API to make it easier for other editor environments hosting PowerShell to support syntax highlighting.

At this point, let's switch from talking about using the ISE to create code and look at what it brings to the table as far as running code is concerned.

15.1.3 Executing commands in the ISE
Since this is an "integrated" scripting environment, we want to be able to run our scripts as well as create them. As with the console host, we can run a script simply by typing the name of the script and hitting return. But the ISE offers some additional ways to run scripts as we'll see in the next few sections. RUNNING CURRENT EDITOR WINDOWS To run the contents of the current editor window, we simply press F5. This is consistent with most other Microsoft products that have an execution capability like Visual Studio or PowerPoint to run a slide show. There is one very significant difference between running from the command window and running by hitting F5 and that's how the script gets run. When run from the command window, a script runs in its own scope as described in chapter 9. But when it's run using F5, it runs in the current scope. In other words, it dot-sources the script into the environment.

IMPORTANT: RUNNING SCRIPTS WITH F5 DOT-SOURCES THE SCRIPT
This point is important enough to be repeated. When you use F5 to run a script, it is executed in the global environment of the current session tab. This means that any scripts or variables defined in the script will persist after the script has exited. This is good for tab-completion and debugging but can also be confusing as more things get added to the global environment. When testing a script in the ISE, it is recommended to do the testing in a separate PowerShell tab using the command line to launch the script instead of F5.

This has significant consequences, both positive and negative. On the positive side, because the script is dot-sourced, we can examine the state of variables and functions it defined since they are still available ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

578 in the global session state. On the negative side, all of these things that are being defined clutter up the global state and may possibly overwrite existing variables or functions. EXECUTING SELECTED TEXT As well as executing the contents of the current editor, we can also executed selected regions of script. Simply select the region of text then press F8. This is illustrated in figure 15.8.

Figure 15.8 Fragments of code can be executed by selecting the text and hitting F8. This allows you to test fragments of a script as it's being developed.

The figure shows selecting some lines in a file 'snippets.ps1' then executing that text. As was the case with F5, fragments executed with F8 are run in the global scope. This makes sense with fragments - we want to be able to execute incrementally then examine the effects at each step.

AUTHOR'S NOTE
You may wonder why the key to execute the selected region is F8. Originally it was F6, It changed when a very senior person (who will remain nameless) was doing a demonstration and accidently hit

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

579
F5 (execute the entire script) instead of F6 (execute the selection.) The results were decidedly nonoptimal. So we moved "execute selected" from F6 to F8.

15.1.4 Considerations when running scripts in the ISE
Up until now, we've focused on how to use the ISE and what we can do with it. However, there are also a few things to be aware of that can lead to different behaviors between the ISE and the console host. In this section, we're going to take a look at a couple of these issues. Both of these concerns involve aspects of the Windows operating system and how it executed programs. Let's take a look. ISSUES WITH NATIVE COMMANDS The things that are most likely to cause problems with the ISE are external executables or "native commands" compiled as console applications. To understand the possible issues, we'll start with some background. In Windows, executables are either "console applications" or "Win32 applications". Windows itself treats the two types differently when they are executed, automatically allocating console objects for console applications. While this feature was intended to simplify things, in practice it can actually make things more complicated. If a console application is launched from a Win32 application, the system will allocate a console window for the application, even if that application will never actually use that window. As a result, the user sees a console window suddenly appear. If the user is unaware of where the window is coming from, they are likely to close it causing the console application to exit. Now let's look at how this impacts the ISE. As mentioned, a console application has a "console object" that the application can use to perform "full-screen" interactions with the user. For most console applications, all they need to do is perform simple interactions like writing out some strings or reading from a redirected input. In these cases, the command works fine with the ISE since the ISE properly handles redirection. However, if the command tries to perform an interaction that requires calling one of the console APIS, especially if it tries to read from the console object (as opposed to the standard input stream), it will appear to "hang" in the ISE. Of course it's not really hung and can still be stopped by hitting CtrlC/Ctrl-Break but it's generally better to prevent the hang in the first place. To address this, the ISE maintains a list of applications that are known not to work properly. For example, the full-screen editor

edit.com doesn't work in the ISE and trying to run it will result in the error message displayed in figure 15.9.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

580

Figure 15.9 Interactive console applications that try to read from the console object are not supported in the ISE and will result in an error.

The list of applications that are blocked by the ISE is stored in a preference variable $

psUnsupportedConsoleApplications and can be updated or edited by the user. The default list is as follows: PS (STA) (5) > $psUnsupportedConsoleApplications cmd cmd.exe diskpart diskpart.exe edit.com netsh netsh.exe nslookup nslookup.exe powershell powershell.exe You might be surprise to see PowerShell on this list but it uses the console object to read commands from the user so that tab-completion and editing can work. So, if powershell.exe is just run as a command, reading and writing to pipes or with input redirected, it's allowed to be run from the ISE. However, if we try to run it interactively (which is determined by the fact that it's at the end of the pipeline) then it will be blocked. THREADING DIFFERENCES BETWEEN THE CONSOLE AND THE ISE Another difference between the console host environment and the ISE is the default threading apartment model. This primarily affects applications using COM (chapter 18).

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

581

WHAT IS THE APARTMENT THREADING MODEL?
In simplest terms, the threading apartment model controls the number of threads of execution in any single object at any given time. In the multithreaded apartment (MTA), multiple threads can be accessing an object at the same time so if the object hasn't been carefully designed, it could suffer from race conditions. For example, two threads may be trying to update the same property at the same time and it becomes a race to see whose change takes effect. These race conditions lead to very hard to track down bugs. In the single threaded apartment (STA), the system guarantees that only one thread is updating an object at a time making everything much simpler. So why have MTA at all? Because for a lot of objects, multiple threads in the object may be fine - for example, they may only be reading data but not changing state. This type of concurrent access, in turn, leads to better performance characteristics, especially in this day of multi-core processors.

The console host runs MTA by default however the ISE runs STA. This is where we may see occasional behavioral differences between the two environments when using COM. If you run into a situation where you see different behavior if a script is run in the console host as opposed to the ISE, the difference the threading module is probably the culprit. While the ISE always runs in STA mode but the console host in Version 2 has new arguments -sta and -mta that allow us to select the mode to use for the session. This is one way to track down these COM problems - run the script from the console host specifying -

sta and then again specifying -mta and see if there is a difference. We'll discuss this issue in more detail in chapter 17 when we look at using .NET directly from PowerShell. For now, let's resume our exploration of the features in the ISE.

15.2 Using multiple PowerShell tabs
Easily one of the most popular features in the ISE is the ability to have, not just multiple editor windows, but multiple concurrent PowerShell sessions available as tabs. Each tab represents its own isolated environment with its own variables, functions and thread of execution. With multiple tabs, we can execute multiple tasks from the same ISE process. If we're running a task that is taking a long time, we can simply start an additional tab in the ISE by pressing -T. In fact there are actually two types of tabs as we'll see in the next two sections.- local tabs in 15.2.1 and remote tabs in 15.2.2.

15.2.1 Local in-memory session tabs
For the most part sessions in separate tabs are isolated however they are, in fact, all executing in the same process. Given what we've seen with remoting and jobs, this might seem somewhat surprising. Up until now, we've pretty much implied there is one PowerShell session per process. In fact, it's possible to have an arbitrary number of session-like objects called "runspaces" in a single process, limited only by system resources.

OK - WHAT'S A RUNSPACE?
You may (or may not) have heard the term "runspace" before. It shows up in discussions periodically but is mostly not mentioned in the end-user documentation. Simply put, a runspace is a "space where PowerShell commands can be run." This sounds pretty similar to how we described a PowerShell session in our discussion of remoting. In fact, each session contains an instance of a runspace object, visible as a property on the session object. So how did we end up with two terms? Here's what

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

582 happened. Originally there was only the runspace. Then we did some usability testing on remoting and discovered that the term really seemed to confuse users with the common response being "so a runspace is just like a session - right?". Eventually, to minimize confusion, we added the PSSession object for end-users and kept the runspace term for developers. (We also had to keep "runspace" because it's part of the PowerShell Application Programmer's Interface or API.)

The runspace feature is primarily targeted at programmers and is not normally exposed to the end user. In fact, the only place you are likely to encounter a runspace is with tabs in the ISE and even there it's pretty much invisible. For the most part, we don't need to care about this much except when we're doing things that affect the ISE's environment at the process level. In fact the most obvious instance of a process-wide resource are environment variables and $ENV:. Since there is only one environment table per process, changes to the $ENV: in one tab will affect the other tabs. While this in-process tab model is efficient, to get real isolation between our sessions, we need to create additional processes. Fortunately we already have a way to do this through remoting.

15.2.2 Remote session tabs in PowerShell ISE
In the previous section, we looked at how in-memory sessions work. The ISE also supports interactive remoting from within a tab (incidentally giving use process isolation between tabs). This means that a tab can be connected to a remote computer. This is effectively equivalent to calling the Enter-

PSSession cmdlet in a local table, but rather than make force us to start a local session and then create a remote session, the ISE treats this as a first-class experience allowing us to create a remote tab directly. To start a remote connection, from the File menu select the "New

Remote PowerShell

Tab..." item. This is shown in figure 15.10.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

583

Figure 15.10 This figure show the menu item under the file menu that lets us directly connect to a remote machine.

Clicking on this item will pop up a dialog asking for the name of the target computer and who to connect as when connecting to that computer. This dialog is illustrated in figure 15.11.

Figure 15.11 When using the ISE to connect to a remote computer, you will be prompted to enter the name of the computer to connect to and the user name to use when connecting.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

584 Fill in the name of the computer and the user to connect as, and then click Connect. At this point a second credential dialog will be displayed asking for the password for the specified user account.

AUTHOR'S NOTE
So why are there two dialogs? The credential dialog is a system dialog that provides for enhanced security and, as a result, has to be displayed separately.

Once we're connected to the remote endpoint, we'll see a new tab in the ISE that displays the name of the computer we're connected to. Figure 15.12 shows what this looks like:

Figure 15.12 The PowerShell ISE allows you to have multiple sessions running at the same time. These sessions can be either local or remote. In this figure, the first tab is a local session and the second tab is a remote session connected to another computer.

Notice the content of the output pane - it shows the actual commands that were run to establish the remote connection. The Enter-PSSession is nothing new but the following line: if ($?) {$psISE.CurrentPowerShellTab.DisplayName = 'brucepayquad'} is something we haven't seen so far. Here's what's going on: If the Enter-PSSession command succeeds and a remote connection is established, then the value of the variable $? will be $true. When that happens, the PowerShell ISE object model is used to change the display name of the tab to the name of the remote computer. The object stored in $psISE is a .NET object that lets which scripts that manipulate the ISE itself. We'll look at using this object model in the next section. In chapter 12, we discussed how interactive remoting works in the case where we're just using the console host. This is a relatively simple situation since the console host is effectively limited to one ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

585 interactive session at a time. With the ISE things are more sophisticated and therefore somewhat more complex. A graphical environment allows us to work with multiple sessions, including multiple local and remote sessions, where each session has its own tab in the interface. Figure 15.13 shows the arrangement of connections that we can set up with the ISE.

Each PowerShell ISE tab has an associated session. Local tabs have local sessions and remote tabs have remote sessions.
Local machine Remote machine 1 Tab 1: Remote session Tab 2: Remote session Tab 3: Local session Remote machine 2 Tab 4: Remote session Remote session 1 Remote session 1

Remote session 2

Figure 15.13 In the PowerShell Integrated Scripting Environment, the user may have multiple tabs open. Each tab has a session. Remote tabs also have a corresponding remote session.

In this figure, we see that the ISE running on the local machine has three tabs open: two tabs connected to one remote machine, one local table and one remote tab connected to machine 2. This illustrates the flexibility that the ISE provides allowing use to easily work with multiple machines from a single ISE instance. At this point, we have a pretty good understanding of the ISE from a user perspective. In the next section, we look at the object model, mentioned earlier in section 15.2.2, and see how we can customize and extend the environment through scripting. This mechanism will allow us to bind out favourite commands to menu items and hot-key sequences.

15.3 Extending the ISE
The ISE provides a great many features but there are always more features or tools we can add to any development environment. Because of this, the ISE provides a surprisingly easy-to-use (at least compared to a lot of other IDE's) extension model that allows us to add our own features to the environment.

15.3.1 The $psISE variable
All extensibility points are available through the $psISE variable. This makes the object model easy to discover since we can use tab-completion to navigate and Get-Member to explore the objects. Here's what the output of Get-Member looks like for this object: PS (STA) (3) > $psISE | Get-Member -MemberType Property | Format-List name,definition

Name : CurrentFile Definition : Microsoft.PowerShell.Host.ISE.ISEFile CurrentFile {get;} ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

586

Name : CurrentPowerShellTab Definition : Microsoft.PowerShell.Host.ISE.PowerShellTab CurrentPowerShellTab {get;} Name : Options Definition : Microsoft.PowerShell.Host.ISE.ISEOptions Options {get;} Name : PowerShellTabs Definition : Microsoft.PowerShell.Host.ISE.PowerShellTabCollection PowerShellTabs {get;} Table 15.2 shows each of these properties and provides a brief description of their purpose:

Table 15.2 The top-level properties in the ISE Object Model
Name Description This object represents the file shown in the current editor window. With this property, we can access the text in the buffer, save the file and so one.

CurrentFile

CurrentPowerShellTab

This gives us access to the tab currently in use - in other words, the one where we are typing our commands. Through this member, we can access file objects for all of the files open in this table, add custom menu items and so on.

Options

We looked at some of the various options that can be set for the ISE earlier in this chapter. Through the options menu, we can set additional options as well as perform the customizations we saw earlier from a script.

PowerShellTabs

This gives list of all of the tabs in the session. We can perform the same options from these tabs as we could for the current tab, but work with all of the tabs in the process.

In the next few sections, we'll walk though these members and look at the kinds of things we can do with them. To help us orient our discovery, figure 15.14 provides a map showing hierarchy of objects in the object model.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

587
$psISE The ISE Object Model CurrentFile DisplayName Encoding FullPath IsUntitled Editor CurrentPowerShellTab CanInvoke DisplayName ExpandedScript Files Prompt StatusText CommandPane AddOnsMenu Output

Options PowerShellTabs

Figure 15.14 This figure shows the hierarchy of objects in the ISE object model. Using these objects you can extend and customized the ISE environment.

However, as we mentioned earlier, the most effective way to explore the contents of the object module is to start with $psISE and use tab-completion to step through the properties. Let's start our exploration with the Options property.

15.3.2 Using the option property
The Options member lets us set a number of ISE options that are not directly accessible from the menus. We'll use Get-Member display the contents of this property: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

588 PS (STA) (6) > $psISE.Options SelectedScriptPaneState : Top ShowToolBar : True TokenColors : {[Attribute, #FFADD8E6], [Command, #FF0000FF], [CommandArgument, #FF8A2B E2], [CommandParameter, #FF000080]...} DefaultOptions : Microsoft.PowerShell.Host.ISE.ISEOptions FontSize : 12 FontName : Lucida Console ErrorForegroundColor : #FFFF0000 ErrorBackgroundColor : #00FFFFFF WarningForegroundColor : #FFFF8C00 WarningBackgroundColor : #00FFFFFF VerboseForegroundColor : #FF0000FF VerboseBackgroundColor : #00FFFFFF DebugForegroundColor : #FF0000FF DebugBackgroundColor : #00FFFFFF OutputPaneBackgroundColor : #FFF0F8FF OutputPaneTextBackgroundColor : #FFF0F8FF OutputPaneForegroundColor : #FF000000 CommandPaneBackgroundColor : #FFFFFFFF ScriptPaneBackgroundColor : #FFFFFFFF ScriptPaneForegroundColor : #FF000000 ShowWarningForDuplicateFiles : True ShowWarningBeforeSavingOnRun : False UseLocalHelp : True CommandPaneUp : False We can see that there are a number of options that correspond to the toolbar and menu items:

CommandPaneUp, ShowToolBar and so on. There are also a number of new options that let us set the colors of the various things. We can control the foreground and backgrounds of each of the panes, as well as changing the colors used to display tokens. Through the Options property, we can write scripts that can automatically configure the ISE the way we want. This stuff is pretty obvious so we'll move on to something a bit more exotic (and useful!).

15.3.3 Managing tabs and files
The top level objects in the ISE are tabs and which, in turn, contain editor instances. To get a list of the tabs, we use the PowerShellTabs property. We can do this interactively from the command window. We'll try this from an ISE instance with the arrangement of tabs as shown in figure 15.15

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

589

Figure 15.15 This figure shows the arrangement of windows in the ISE we'll use in these examples. In this case, the ISE has two tabs open and the current tab is labeled 'PowerShell 1".

Open the ISE and then use Ctrl-N to create a second tab. Then, in the command pane, run the following command: PS (STA) (13) > $psise.PowerShellTabs DisplayName AddOnsMenu StatusText ExpandedScript Prompt CommandPane Output Files CanInvoke DisplayName AddOnsMenu StatusText ExpandedScript Prompt CommandPane Output Files CanInvoke : : : : : : : : : : : : : : : : : : PowerShell 1 Microsoft.PowerShell.Host.ISE.ISEMenuItem Running script / selection. Press Ctrl+Break to stop. True PS (STA) (13) > Microsoft.Windows.PowerShell.Gui.Internal.CommandEditor Microsoft.Windows.PowerShell.Gui.Internal.OutputEditor {Untitled1.ps1*} False PowerShell 2 Microsoft.PowerShell.Host.ISE.ISEMenuItem False PS (STA) (1) > Microsoft.Windows.PowerShell.Gui.Internal.CommandEditor Microsoft.Windows.PowerShell.Gui.Internal.OutputEditor {} True

In the output of from this command, we see the list of the tabs currently open in the ISE. This output shows us the DisplayName of the tab, the current prompt value, the text in the status window and so on. In the "PowerShell 1" tab, where the command was run, we see the status of this tab is "running a script" which, of course is the command we just typed. Since it is running a command, we also see that the CanInvoke property is false indicating that this tab is busy executing a command. If we look at the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

590 output for "PowerShell 2", the status text is blank and the CanInvoke property is true indicating that there is no command running in that tab. Now let's look at the methods on this tab object. Since the output from the previous command returned a collection, we'll look at the second item (representing the tab we're not currently using) with

Get-Member:
PS (STA) (17) > $psISE.PowerShellTabs[1] | gm -type method TypeName: Microsoft.PowerShell.Host.ISE.PowerShellTab Name ---Equals GetHashCode GetType Invoke ToString MemberType ---------Method Method Method Method Method Definition ---------bool Equals(System.Object obj) int GetHashCode() type GetType() System.Void Invoke(scriptblock script) string ToString()

Notice that this object has an Invoke() member which will invoke a command in that tab. Let's try it out. Since it takes a scriptblock as an argument, we'll use the Create() method on [ScriptBlock] to compile the code we want to run: PS (STA) (27) > $sb =[ScriptBlock]::Create('1..20 | %{sleep 1; get-date} ') The scriptblock contains a number of calls to sleep (the alias for Start-Sleep) to make sure it's running long enough to see what's going on. Let's verify that we can invoke our scriptblock in the other tab: PS (STA) (28) > $psISE.PowerShellTabs[1].CanInvoke True Checking the CanInvoke property for that tab confirms that we can execute so let's call Invoke() on our scriptblock: PS (STA) (30) > $psISE.PowerShellTabs[1].Invoke($sb) The call to Invoke() starts the scriptblock executing in the other tab then returns immediately rather than blocking for the command to complete. We'll check the CanInvoke property again to verify that the command is running. This time we expect the property to return false: PS (STA) (31) > $psISE.PowerShellTabs[1].CanInvoke False and it does. Now let's switch to the "PowerShell 2" tab and look at the output. This is shown in figure 15.16.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

591

Figure 15.16 This shows the output of our script. While we started the script in the first tab, it is actually run in the second tab with the output displayed in that tab,

In the output window of the "PowerShell 2" tab, we see the results of the command we ran. In effect, we're using the second tab to do background processing. It would be nice if we could have a better name for this tab. We can use the DisplayName property on the tab object to do this as shown in figure 15.17.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

592

Figure 15.17 This figure shows the command to change the display name of the current tab to be "(Bkgn tasks)".

Finally, we can put the pieces together into a function to automatically invoke a scriptblock in a the

'(Bkgn tasks)' tab. The code for this function is shown in listing 15.1. Listing 15.1 function Invoke-InBackgroundTab { param ( [parameter(mandatory=$true)] $ScriptBlockToInvoke ) $bkgnTab = '(Bkgn tasks)' $tab = $null $needNewTab = $true foreach ($tab in $psISE.PowerShellTabs) { if ($tab.DisplayName -eq $bkgnTab) { $needNewTab = $false break } } if ($needNewTab)

#1

#2

#3

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

593 { $tab = $psISE.PowerShellTabs.Add() $tab.DisplayName = $bkgnTab do { Start-Sleep -Milliseconds 200 } while (-not $tab.CanInvoke) } if (! $tab.CanInvoke) { Write-Error ` "Background tab is currently busy. Try again later" } $tab.Invoke($ScriptBlockToInvoke) } Set-Alias ibg Invoke-InBackgroundTab #1 Requires a scriptblock to execute #2 The name of background tab #3 Search to see if background tab exists #4 If it does not create it #5 Need to wait until it can be used #6 Only one background task can be run #7 Invoke the scriptblock #8 Define a convenience alias #4

#5

#6 #7 #8

First this function searches for the target tab and, if it doesn't exist, then it creates a new tab. When it finds the tab, if it's not already busy, the scriptblock is invoked and the output is shown in the target tabs output pane. This relatively simple example gives us an idea of how we can work with tabs. We can perform sophisticated operations with very little code. In the next section, we're going to look at how to work with the elements inside a tab - the editor and output panes.

15.3.4 Working with the text windows
During script creation, we spend most of our time in the command and editor windows with the results of executing commands being shown in the output window. As was the case with tabs, the ISE object model allows us manipulate the contents of these windows using scripts. Using this capability, we can add custom tools to extend the built-in set of editor functions. Let's start by exploring what we can do with the Output pane. SAVING THE OUTPUT PANE CONTENTS In chapter 14, we discussed how we could use the transcript facility in the console host to capture the output of our commands for later examination. The ISE doesn't have a corresponding transcript mechanism but we can still copy the contents of the output pane to a file. As before, we start with the

$psISE variable to access the contents of the output pane. Let's use Get-Member to see what this looks like: PS (STA) (94) > $psISE.CurrentPowerShellTab.Output | Get-Member -MemberType method,property TypeName: Microsoft.Windows.PowerShell.Gui.Internal.OutputEditor Name ---Clear EnsureVisible MemberType ---------Method Method Definition ---------System.Void Clear() System.Void EnsureVisible(int lineNumber)

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

594 Equals Method Focus Method GetHashCode Method GetLineLength Method GetType Method InsertText Method Select Method endLine, int endColumn) SetCaretPosition Method columnNumber) ToString Method CaretColumn Property CaretLine Property LineCount Property SelectedText Property Text Property bool Equals(System.Object obj) System.Void Focus() int GetHashCode() int GetLineLength(int lineNumber) type GetType() System.Void InsertText(string text) System.Void Select(int startLine, int startColumn, int System.Void SetCaretPosition(int lineNumber, int string ToString() System.Int32 CaretColumn {get;} System.Int32 CaretLine {get;} System.Int32 LineCount {get;} System.String SelectedText {get;} System.String Text {get;set;}

Looking at the members on this object, we see that there are members available to select text in the window, insert text, etc. There is one specific member that we are looking for - the Text property itself. This property gives us access to the full text in the output pane which we can trivially save to a file by using redirection: PS (STA) (22) > $psISE.CurrentPowerShellTab.Output.Text > output.txt This command copies the contents of the output pane to a file called "output.txt". We can then use notepad to look at this file: PS (STA) (23) > notepad output.txt The results of this command are shown in figure 15.18.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

595

Figure 15.18 This figure shows the contents of the output buffer, which were saved to a fine, displayed in notepad.

We used notepad to display this rather than use the ISE so we can see both the output and the contents of the save file at the same time. Next, , we'll look at the object model for the editor pane and see what we can do with that M AKING CHANGES IN THE EDITOR PANE We've seen how to save the contents of the output pane to a file but we're limited with what we can do in this pane because it's read-only. With the editor pane, we are also able change the contents of the

Text property. Let's look at how to do this by working through an example where we'll rename a variable in the editor buffer. Our task will be to replace the $tab variable name in listing 15.1 with the more descriptive $currentTab.
Assuming the listing is loaded in the current editor pane, we'll see how lines of the function reference this variable. This can be done with the following command: PS (STA) (110) > $psISE.CurrentFile.Editor.Text -split "`n" -match '\$tab' $tab = $null foreach ($tab in $psISE.PowerShellTabs) if ($tab.DisplayName -eq $bkgnTab) $tab = $psISE.PowerShellTabs.Add() $tab.DisplayName = $bkgnTab while (-not $tab.CanInvoke) ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

596 if (-not $tab.CanInvoke) $tab.Invoke($ScriptBlockToInvoke) In this command, because the text in the editor buffer is returned as a single file, we used the -split operator to split the text at newline characters and then used the -match operator to see the lines containing the pattern. To get a simple count, just do PS (STA) (111) > ($psISE.CurrentFile.Editor.Text -split "`n" -match '\$tab').Count 8 We'll look at modifying the text in the editor using the object model next. But before we do this, we'll copy the text to another editor buffer. We're doing this because direct updates of the contents of the editor window are tracked by the normal undo mechanism. By making a copy, we're providing our own undo mechanism. First, we'll get a reference to the current tab: $curFile = $psISE.CurrentFile We'll use this to get at the contents of the editor. Now we'll create a new file tab and save the result in to the $newFile variable. $newFile = $psISE.CurrentPowerShellTab.Files.Add() When we run this command, the tab we've just created will become the currently displayed file tab. Now we can use the reference to the original file tab to get the text source text. We'll the -replace operator, we can make our replacement and assign the result to the Text property on the editor object in the new file tab. $newFile.Editor.Text = $curFile.Editor.Text -replace '\$tab', '$currentTab' This command will also set the new buffer to be the current buffer so we can see the results of our operation. Finally, if we wanted to set current file back to the original file, we would do: $psISE.CurrentPowerShellTab.Files.SetSelectedFile($curFile) which changes the current file tab back to the original. In the next section we'll look at an example where we work with both tabs and editor panes to save the state of the ISE. SAVING THE LIST OF OPEN FILES With the ability of have multiple session tabs with multiple files open, we can end up with a lot of files open working on different parts of a project. Unfortunately the ISE doesn't have a project mechanism to keep track of all of these files. However, now that we know how to work with both session tabs and file tabs, we can write our own tool to save a list of the files we're working with. A function to do this is shown in listing 15.2:

Listing 15.2 A function to save the list of currently open files in the ISE
$PathToSavedISEState = "~/documents/WindowsPowerShell/SavedISEState.ps1xml" function Save-ISEState { $state = foreach ($tab in $psISE.PowerShellTabs) { foreach ($file in $tab.Files) { if ($file.FullPath) { @{ Tab = $tab.DisplayName DisplayName = $file.DisplayName #1

#2 #3 #4

#5

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

597 FullPath = $file.FullPath } } } } $state | Export-Clixml $PathToSavedISEState } #1 #2 #3 #4 #5 #6 Path to save list of files Loop through each tab Loop through each file in the tab Skip unsaved files Use a hashtable for the data Write data to the file #6

This function loops through all of the ISE tabs and then the files in each tab. For the file tabs that have been saved (and therefore have a file path), build a hashtable containing the information we need to restore the tabs. This data is saved to the target file using the CliXML format (essentially equivalent to the way remoting serialization works). Now we need a way to read the data back. This turns out to be quite simple as shown by the following function: function Get-SavedISEState { Import-Clixml $PathToSavedISEState } Using the Import-Clixml cmdlet, we can recreate the collection of hashtables we saved. Reading the data is not as interesting as actually reloading the files so listing 15.4 shows a function that does this as well:

Listing 15.4 A function to restore the file tabs function Restore-ISEState { $tab = "" Import-Clixml $PathToSavedISEState | foreach { if ($_.Tab -ne $tab) { $targetTab = $psise.PowerShellTabs.Add() $tab = $_.Tab } $targetTab.Files.Add($_.FullPath) } } #1 Reload the save hashtabls #2 Create a new tab as needed #3 Add the file to the current tab

#1 #2

#3

This function loads the data then loops through the hashtables. If the name of the target tab in the hashtable doesn't match the current tab name, a new tab is created and the file is added to the new tab. This isn't a perfect solution - it always creates new tabs - but it shows how this type of operation can be performed. So far, all of our ISE object model examples have required that we type commands in the command pane to do things. In the next section, we'll see how we can speed things up by adding menu items and hot keys to execute our extensions. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

598

15.3.5 Adding a custom menu
In this section, we see how to add custom menu items and hot keys to the ISE. This is done by accessing the AddOnsMenu property on the tab object: $psise.CurrentPowerShellTab.AddOnsMenu

AUTHOR'S NOTE
Note that this property is associated with a tab so it isn't possible to add a custom menu item that is present in all tabs. On the other hand, it means that different tabs can have different custom menus allowing for specialized task tabs.

Custom menu items are always added as submenus of the "Add-ons" menu. By default, the "Add-ons" menu isn't displayed but the first time a custom menu item is added, the "Add-ons" menu will appear. We'll start by adding a simple menu item that just prints some text to the output window. The command to do this is: $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Say hello!", {write-host "Hello there"}, "Alt+s") This command adds a submenu item with the name "Say hello!", a scriptblock that defines the action to take and a hot key sequence to invoke the item from the keyboard. Figure 15.19 shows what the added menu item looks like.

Figure 15.19 When a custom menu item is added, it always appears under the "Add-ons" menu. The first time a custom item is added, the "Add-ons" item appears on the menu bar.

In this figure we can see that executing this command returns the object representing this menu item. If we try to add another item with the same hot key sequence, we'll get an error: PS (STA) (26) > $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add( "Say hello!", {write-host "Hello there"}, "Alt+s") Exception calling "Add" with "3" argument(s): "The menu 'Say hello!' uses shortcut 'Alt+S', which is already in use by the menu or editor functionality." At line:1 char:52 + $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add $mi = $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus[0] PS (STA) (37) > $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Remove($mi) True In this example, first we retrieved the menu item and then passed that object to the Remove() method. If we check the Add-ons menu item, we'll see that the item has been removed. If we want to remove all of the items, then we can do this by running the following code: foreach ($item in @($psISE.CurrentPowerShellTab.AddOnsMenu.Submenus)) { $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Remove($item) } This code gathers the collection of menu items to remove and then loops over the collection removing them one at a time.

AUTHOR'S NOTE

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

600
There is a trick in this example that is useful to know. We would normally expect to be able to use the Submenus collection directly in the foreach loop. There is a problem, however, when we are removing items from the collection we are looping over. This is because the foreach statement uses a .NET enumerator to keep track of where we are in the collection. If we removed an item from the collection, the enumerator won't match the collection any more. To avoid problems with inconsistency in the collection, an error is raised. To get around this problem, we need to create an intermediate collection of the submenu items. In PowerShell this is very easy - simply use the @( ...expression... ) operator. This operator evaluates the expression it contains and returns a new collection containing the results of that evaluation. Since the new collection is a snapshot of the collection we're changing, the problem goes away.

Let's make our example a little more sophisticated. We'll add some code to prompt the user for a name to display. We can use the host object's prompt function to do this: $sb = { $name = Read-Host "Enter the name of the person to say hi to" Write-Host "Hello name!" } $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Say hello!", $sb, $null) This will cause a message box to be displayed as we see in figure 15.21.

Figure 15.21 This figure shows what the prompt from the example menu item looks like,

The ability to prompt the user for input significantly increases what we can do with our extensions. For example, we could add a search capability that does loop around in the editor buffer. But we'll leave that as an exercise for the reader and move on to something else which is also quite useful. ADDING A FILE CHECKER MENU ITEM Let's look at a rather more useful example. In chapter 14, we saw how we could use the PowerShell tokenizer API to check a script for certain kinds of errors. This is pretty handy in the ISE so we'll add it as a menu item. The code for this is shown in listing 15.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

601

Listing 15.5 Script code to add a syntax checker to the ISE function Test-Buffer { $text = $psISE.CurrentFile.Editor.Text $out=$null $tokens = [management.automation.psparser]::Tokenize($text, [ref] $out) $out | fl message, @{ n="line"; e = { "{0} char: {1}" -f $_.Token.StartLine, $_.Token.StartColumn } } } $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("Test Buffer", {Test-Buffer}, "Control+F5") The code in listing 15.5 adds a new custom menu item that will run the script-checking code when control-F5 is pressed (as opposed to F5 with actually runs the script). This turns out to be a surprisingly handy feature. While the ISE does syntax highlighting, when there is an error it just stops the highlighting without showing us the error. Using the extension we've just added, we can find the syntax errors without having to actually run the script. A SUBMENU FOR SNIPPETS We'll look at one final example where we create a custom nested menu. In this example, we want to create a "snippets" menu showing various PowerShell examples. Since there are multiple items, we'll add this as a submenu of the Add-ons menu. Listing 15.6 shows the code necessary to do this.

Listing 15.6 Add a snippets menu
$snippetText = @' #1 if ($true) { "if-Part" } elseif { "elseif-Part" } else { "else-Part" } while ($true) { Write-Host "in while loop" } foreach ($i in 1..10) { $i; Write-Host "i is $i" } for ($i=1; $i -lt 20; $i++) { $i } function basicFunc ($p1, $p2) { $p1 + $p2 } function advFunc { [CmdletBinding()] param ([parameter(mandatory=$true)] $p1) $p1 } '@ -split "`n" #2 $addOns = $psISE.CurrentPowerShellTab.AddOnsMenu $snippets = $addOns.Submenus.Add("snippets", $null, $null) foreach ($snip in $snippetText ) { [void] $snippets.SubMenus.Add($snip, { $psise.CurrentFile.Editor.InsertText($snip) }.GetNewClosure(), $null) } #1 Here-string defining the snippets #2 Split on newlines #3 Add top-level Snippets menu #4 Add each line as a submenu #5 Use a closure to capture text

#3

#4 #5

Most of this function is straightforward. We used a here-string to define the snippets and then split it into individual lines with the -split operator. Then we defined a top-level "snippets" menu that has no action and added each line as a submenu of the top-level menu. The one somewhat unusual thing is the ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

602 use of a closure (see chapter 11) then defining the action. This closure captures the value of the $snip variable so the code to insert the text is very simple. In the first part of this chapter, we introduced the ISE and spent a fair amount of time on how to customize the ISE using the object model. Having a more modern environment like the ISE makes scripting in PowerShell much easier but the biggest advantage really is the integrated debugger. In the remainder of this chapter we'll look at how to use the debugging feature built into the in the ISE to debug our scripts. We'll also see how to use the debugger from the command line and the additional features that offers. Combined, they provide powerful tools for debugging scripts in a variety of environments.

15.4 PowerShell script debugging features
This section covers the various tools and techniques for debugging PowerShell scripts. PowerShell V1 did not include a debugger but did have some limited tracing capabilities. V2 introduced a much more comprehensive debugger along with graphical debugging support in the ISE. We'll start by looking at the limited (but still useful) tracing features carried over from V1. Then we'll look at how to debug from the ISE and finally we'll look at the command line debugger and the additional capabilities it has to offer.

15.4.1 The Set-PSDebug cmdlet
In chapter 14, we introduced the Set-PSDebug cmdlet and talked about using it to the PowerShell V1 “strict-mode”. In this section, we'll cover the remaining features this cmdlet offers - tracing and stepping through scripts. The syntax for this command is shown in figure 15.22.
Set Script Tracing Level 0 = off 1 = basic 2 = full Set-PSDebug Set-PSDebug Turn on stepping Turn on Strict Mode (see chapter 14)

[-Trace ] -Off

[-Step]

[-Strict]

Turn all debugging features off.

Figure 15.22 The Set-PSDebug cmdlet parameters. This cmdlet can be used to enable tracing and stepping through an executing script.

The details of each of these features are covered in the following sections. TRACING STATEMENT EXECUTION Basic script tracing is turned on by setting as follows: PS (1) > Set-PSDebug -trace 1 In this trace mode, each statement executed by the interpreter will be displayed on the console as shown. PS (2) > 2+2 DEBUG: 1+ 2+2 4 PS (3) > $a=3 DEBUG: 1+ $a=3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

603 PS (4) > pwd DEBUG: 1+ pwd Path ---C:\files The debugging output is prefixed with the DEBUG: tag and is typically shown in a different color than normal text. Note that the entire script line is displayed. This means that if you have a loop all on one line, you’ll see the line repeated: PS (5) > foreach ($i in 1..3) {"i DEBUG: 1+ foreach ($i in 1..3) DEBUG: 1+ foreach ($i in 1..3) i is 1 DEBUG: 1+ foreach ($i in 1..3) i is 2 DEBUG: 1+ foreach ($i in 1..3) i is 3 is $i"} {"i is $i"} {"i is $i"} {"i is $i"} {"i is $i"}

In this example, you’ll see the line repeated four times: once for evaluating the expression 1..3 in the foreach loop and then once for each iteration of the loop, for a total of four times. This is a good reason, even though PowerShell doesn’t require it, to write scripts with one statement per line - it can help with debugging, both when tracing and when using the debugger to set breakpoints. Basic tracing doesn’t show you any function calls or scripts you’re executing. First, define a function

foo:
PS (6) > function foo {"`$args is " + $args} DEBUG: 1+ function foo {"`$args is " + $args} And run it in a loop: PS (7) > foreach ($i in 1..3) {foo $i} DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: 1+ function foo {"`$args is " + $args} $args is 1 DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: 1+ function foo {"`$args is " + $args} $args is 2 DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: 1+ function foo {"`$args is " + $args} $args is 3 In this output, we can only see the line that’s being executed. We don't see when we enter the actual function. To get this extra information, we need to turn on full tracing. PS (8) > Set-PSDebug -trace 2 DEBUG: 1+ Set-PSDebug -trace 2 In this mode, PS (9) > DEBUG: DEBUG: DEBUG: DEBUG: $args is DEBUG: DEBUG: DEBUG: $args is DEBUG: DEBUG: we also see the function calls: foreach ($i in 1..3) {foo $i} 1+ foreach ($i in 1..3) {foo $i} 1+ foreach ($i in 1..3) {foo $i} ! CALL function 'foo' 1+ function foo {"`$args is " + $args} 1 1+ foreach ($i in 1..3) {foo $i} ! CALL function 'foo' 1+ function foo {"`$args is " + $args} 2 1+ foreach ($i in 1..3) {foo $i} ! CALL function 'foo'

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

604 DEBUG: 1+ function foo {"`$args is " + $args} $args is 3 In addition to function calls, full tracing adds to the display by showing variable assignments. Let’s redefine our function so that it performs a variable assignment. We’ll split it across multiple lines so the trace is a bit clearer: PS (10) > function foo { >> $x = $args[0] >> "x is $x" >> } >> DEBUG: 1+ function foo { And run it again. PS (11) > foreach ($i in 1..3) {foo $i} DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: ! CALL function 'foo' DEBUG: 2+ $x = $args[0] DEBUG: ! SET $x = '1'. DEBUG: 3+ "x is $x" } x is 1 DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: ! CALL function 'foo' DEBUG: 2+ $x = $args[0] DEBUG: ! SET $x = '2'. DEBUG: 3+ "x is $x" } x is 2 DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: ! CALL function 'foo' DEBUG: 2+ $x = $args[0] DEBUG: ! SET $x = '3'. DEBUG: 3+ "x is $x" } x is 3 You can see that for each iteration in the loop, tracing shows the      Loop iteration Function call Statement doing the assignment Actual assignment to $x including the value assigned Statement that emits the value

The value displayed is the string representation of the object being assigned, truncated to fit in the display. It depends on the ToString() method defined for that object to decide what to display. This isn’t always as useful as one would like. For example, with the hashtable: PS (12) > $a = @{x=1; y=2} DEBUG: 1+ $a = @{x=1; y=2} DEBUG: ! SET $a = 'System.Collections.Hashtable'. It shows you the type of the object, but nothing about its actual value. For arrays and other collections, it shows you a truncated representation of the elements of the list. So, for an array of one hundred numbers, you see: PS (13) > $a = 1..100 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

605 DEBUG: 1+ $a = 1..100 DEBUG: ! SET $a = '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23...'. Overall, script tracing is pretty effective, but sometimes you still need to add calls to Write-Host to your script to help with debugging as we mentioned in chapter 14.

DEBUGGING SCRIPTS RUN BY OTHER PEOPLE
The other thing we mentioned in chapter 14 was the transcript capability. Transcripts combined with tracing provide a valuable tool to help with debugging scripts that are being run by other people in your organization. By capturing the trace output in a transcript file, we can get a much better idea of what a script is going in the other user's environment. Tracing is also valuable in debugging remote scripts where we can't use the ISE debugger as we'll see later on in this chapter.

STEPPING THROUGH STATEMENT EXECUTION The next debugging feature we’ll look at is the mechanism that PowerShell provides for stepping through a script.

AUTHOR'S NOTE
Like the tracing mechanism, this stepping feature is also a carry-over from PowerShell V1. It is largely subsumed by the ISE debugger but there are some advanced scenarios, like debugging dynamically generated code, where it's still very useful. For example, if we use [ScriptBlock]::Create() to dynamically generate a scriptblock, we can't set a breakpoint because we don't have a line number in a file to use to set the breakpoint. More on this later.

We turn stepping on by calling the Set-PSDebug cmdlet with the -Step parameter. PS (14) > Set-PSDebug -step DEBUG: 1+ Set-PSDebug –step Rerun the foreach loop and take a look at the prompt that’s displayed: PS (15) > foreach ($i in 1..3) {foo $i} Continue with this operation? 1+ foreach ($i in 1..3) {foo $i} [Y] Yes [A] Yes to All [N] No [L] No to All [?] Help(default is "Y"): y DEBUG: 1+ foreach ($i in 1..3) {foo $i} Continue with this operation? 1+ foreach ($i in 1..3) {foo $i} [Y] Yes [A] Yes to All [N] No [L] No to All [?] Help(default is "Y"): y DEBUG: 1+ foreach ($i in 1..3) {foo $i} DEBUG: ! CALL function 'foo' Continue with this operation? The interpreter displays the line to be executed, then asks the user to select one of Yes, Yes to All, No, or No to All. The default is “Yes”. If you answer “Yes”, then that line will be executed and you will be prompted as to whether you want to execute the next line. If you answer “Yes to All”, then step mode will be turned off and execution will continue normally. If you answer either “No” or “No to All”, the current execution will be ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

[S] Suspend

[S] Suspend

Licensed to Andrew M. Tearle

606 stopped and you will be returned to the command prompt. There is no difference in the behavior between “No” and “No to All”. The following shows the message you will see if you enter “No”. Continue with this operation? 2+ $x = $args[0] [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help(default is "Y"): y DEBUG: 2+ $x = $args[0] DEBUG: ! SET $x = '2'. Continue with this operation? 3+ "x is $x" } [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help(default is "Y"): l WriteDebug stopped because the DebugPreference was 'Stop'. At line:1 char:23 + foreach ($i in 1..3) {f > PS (4) > $i 2 In fact, it is. But we’re not limited to inspecting the state of the system. We can actually change it. In this case, let’s make the loop end early by setting the value to something larger than the terminating condition. We’ll set it to 100. 1>> PS (5) > $i=100 1>> PS (6) > $i 100 Now exit the nested prompt session with the normal exit statement. This returns you to the previous level in the interpreter where, since we’re stepping, you’re prompted to continue. Respond with “a” for “[A] Yes to All” to get out of stepping mode. 1>> PS (7) > exit Continue with this operation? 1+ $i=0; while ($i++ -lt 10) { $i } [Y] Yes [A] Yes to All [N] No [L] No to All [?] Help(default is "Y"): a DEBUG: 1+ $i=0; while ($i++ -lt 10) { $i } 100

[S] Suspend

There are two things to notice here: the loop terminates, printing only one number, and that value is the value we set $i to, which is 100. We’ll check one more time to verify that $i is actually 100. PS (8) > $i 100 Using this suspend feature, you stop a script at any point and examine or modify the state of the interpreter. You could even redefine functions in the middle of execution (although you can’t change the function that is currently executing). This makes for a powerful debugging technique, but it can be annoying to use stepping all the time. This is where having a real debugger makes all of the difference. But before we get to that, there's one more thing we need to look at - the mechanics of how a breakpoint works. CREATING A BREAKPOINT COMMAND In section 14.3.1, we introduced the $host variable and talked about using it to write our debugging messages. The $host variable has another method that can be used for debugging called EnterNestedPrompt(). This is the method we mentioned earlier when we looked at the stepping feature on Set-PSDebug. Using this method, we can start a nested session at any point we want and this can be used approximately like a breakpoint. (Don't worry - there are real breakpoints - we will get there.) To cause our script to break, we insert a call to EnterNestedPrompt() at the desired location and when it’s hit, a new interactive session starts. Let’s try it out. We’ll execute a loop that counts from 0 to 9. In this loop, when the loop counted is equal to 4, we’ll call EnterNestedPrompt(). PS (1) > for ($i=0; $i -lt 10; $i++) >> { ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

609 >> >> level. >> >> >> >> } >> i is i is i is i is i is "i is $i" if ($i -eq 4) {

When execution gets to this point, we’ll output the string “*break*” and then enter a nested prompt "*break*" $host.EnterNestedPrompt() }

0 1 2 3 4

Now $i is equal to four, so we hit the “breakpoint” code. As in the stepping case, we can examine and change the state of the interpreter, *break* 1>> PS (2) > $i 4 1>> PS (3) > $i=8 and use exit to resume the top-level execution thread. 1>> PS (4) > exit i is 9 PS (6) > Let's see how we can use this feature to create a "breakpoint" command. Once again, we’ll take advantage PS (1) >> { >> >> >> >> breakpoint >> >> >> >> >> >> >> >> } >> PS (2) >> { >> of scriptblocks to add a way to trigger the breakpoint based on a particular condition. > function bp ([scriptblock] $condition) if ($condition) { if (. $condition) { and enter a nested shell level. $host.UI.WriteLine("*break*") $host.EnterNestedPrompt() } } else { $host.UI.WriteLine("*break*") $host.EnterNestedPrompt() }

If the $condition parameter to bp is not null, evaluate it. If it evaluates to $true, then execute the

> for ($i=0; $i -lt 10; $i++) . bp {$i -eq 5}

Here we’re inserting a breakpoint that will cause execution to break when $i is equal to 5. Note that we’re dotting the bp function. This is because we want it to be executed in the current scope, allowing us to change the state of the loop variable. >> "`$i is $i" >> } >> $i is 0 $i is 1 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

610 $i is 2 $i is 3 $i is 4 *break* We hit the breakpoint. Increment the loop variable so that 5 is never displayed, and exit the nested prompt level and resume execution. 1>> PS (3) > $i++ 1>> PS (4) > exit $i is 6 $i is 7 $i is 8 $i is 9 PS (5) > The loop exits, never having printed 5. This gives us a basic idea of how the debugging environment works so let's move on to the main event and look at debugging from the ISE.

15.5 The PowerShell V2 Debugger
With PowerShell V2, a powerful new debugger was added to the product. In this section we'll start with how to use the debugger from the ISE and then move on to what we can do with command line debugging.

15.5.1 The Graphical Debugger
As we've seen the ISE provides an integrated environment for editing and running scripts. It is also the easiest and frequently most effective way to debug scripts. This is because the integrated debugger makes it much easier to see where we are and what's going on in our scripts. The major debugging features can be found in the Debug menu shown in figure 15.24

Figure 15.24 This figure shows the debug menu options. These options allow you to set break points and step through your script, line by line. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

611 If you've used Microsoft Visual Studio, you should notice that the keyboard shortcuts are the same as the Visual Studio debugger. A description for each of these menu items is shown in table 15.3.

Table 15.3 Menu items in the ISE Debug menu
Menu Item Step Over Key F10 Command Equivalent Description Step over the current function call rather than stepping into the function. Step Into F11

S (Step-into)

V (Step-Over)

Step the script, stepping into functions and scripts as they are encountered.

Step Out

Shift-11

O (Step-Out)

Execute script lines until the end of the current function has been reached.

Run/Continue

F5

C (Continue)

If no command is running, the contents of the current editor buffer are executed. If the interpreter is paused at a break point, execution will resume.

Stop Debugger

F5

Q (Stop)

Stop debugging session and the script being debugged.

Toggle Breakpoint

F9

Set-PSBreakpoint, Remove-PSBreakpoint

If there is no breakpoint on the current line, then one will be added. If there is not, then it will be removed. All breakpoints that are set in the current session (current tab) will be deleted. Breakpoints in other tabs are unaffected.

Remove All Breakpoints

Ctrl-ShiftF9

Remove-PSBreakpoint

Enable All Breakpoints

n/a

Enable-PSBreakpoint

All breakpoints in the current session (tab) are enabled. If a breakpoint is currently enabled it will remain active. If a breakpoint is disabled, it will be enabled.

Disable All Breakpoints

n/a

Disable-PSBreakpoint

All existing breakpoints in the current session (tab) are left in place but disabled. They may be re-enabled at a later time.

List-Breakpoints

Ctrl-Shift-L

Get-PSBreakpoint

List all defined breakpoints in the current session (tab).

Display Call Stack

Ctrl-Shift-D

Get-PSCallStack K

When a break point has been hit, it is useful to be able to see a list of the currently active function and script

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

612 contexts. This table show the menu item name, the keyboard shortcut if there is one available and the description of the corresponding functionality. What's a bit unusual for a graphical debugger is that the table also lists command line equivalents to the menu functionality. In fact there are two types of command equivalents - an actual cmdlet that can be called at any time and special single-character short-cut commands that can only be used while debugging a script. We'll walk through an example to see how this all works. In this example we're debugging a trivial script that just outputs number. We want to put a breakpoint on line 4 so we move the cursor to line 4 and hit F9. The line will be highlighted in red indicating that there is a breakpoint set on that line. We press F5 and execution will begin (if the script has been modified, we'll be prompted to save it first.) Execution proceeds outputing numbers until line 4 in the script is hit and the breakpoint triggers. At this point, the line where the breakpoint was hit is highlighted in yellow and the command pane displays the debugging prompt. This is shown in figure 15.25.

The breakpoint on line 4 has been hit. This is shown by highlighting the relevant line in the editor pane. The output pane shows the output of the script so far (the numbers 1,2,3) and then a message indicating that a breakpoint has been hit.

Command Output Pane Pane

Editor Pane

The command prompt changes to indicate that we are in debugging mode and that the special debugger commands are now active.

Figure 15.25 This figure shows how the command pane changes once a breakpoint has been hit. The prompt is prefixed with ">>" to indicate that we are debugging and the debugger commands are enabled.

When the debugging prompt is displayed, this indicates that the special debug-mode only commands are now available.

AUTHOR'S NOTE
Earlier we said that the way debugging worked in PowerShell, we didn't need a new language. This is definitely true - but we did need to some convenience aliases otherwise things were too verbose to use easily. That's what we're seeing here.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

613 For example, to step the next line, we could either type the command 's' or 'Step-Over' or use the 'Step Over' menu item. Regardless of how the command is entered, the line where we had hit the breakpoint is executed and then breaks at the next line in the script. This line is now highlighted in yellow. The previous line is highlighted in red since it has an actual breakpoint on it as we see in figure 15.26.

When the StepOver command is issue, the highlight moves to the next statement. The output pane shows updated output including the ‘4’ from the line where the breakpoint was hit and then a message indicating that a new breakpoint has been hit.

Figure 15.26 This figure shows how the command pane changes once a breakpoint has been hit. The prompt is prefixed with ">>" to indicate that we are debugging and the debugger commands are enabled.

The full set of debugger short-cut commands is shown in table 15.4.

Table 15.4 This table lists the special commands that are available in debug mode
Command Full Name Description Steps execution to the next statement. If the statement to execute is a function or script, the debugger will step into that command. If the script or function is not currently displayed in an editor tab, a new editor tab will be opened.

Command Output Pane Pane

Editor Pane

S

Step-Into

V

Step-Over

Basically the same as Step-Into except that function/script calls are not followed.

O

Step-Out

Executes statement until the end of the current function or script is reached.

C

Continue

Resume execution of the script. Execution continues until another breakpoint is reached or the script exits.

L [ []]

List

List the portion of the script around the line where we are currently stopped. By default, the current line is

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

614 displayed, preceded by the 5 previous lines, and followed by the 10 subsequent lines. To continue listing the script, press the Enter key. The list command can be optionally followed by a number specifying the number of lines to display before and after the current line. If two numbers are specified, the first number is the number of preceding lines and the second is the number of following lines.

Q K

Stop GetPSCallSt ack

Stop execution which also stops the debugger. Display the up to the current execution point. This command is not specific to debugger mode and may be used anywhere.

Hitting the enter key on an empty line repeats the last command entered. This makes it easy to continue stepping or list the script.

?, h

This will display all of the special debugger commands in the output window.

EXECUTING OTHER COMMANDS IN DEBUG MODE If you look at the various tables so far, there is clearly something missing. Knowing where we are is great, but how can we see the values of variables and so on. And the answer is that we don't need anything specific to debugging here - we just use the same cmdlets we've used all along. To see a variable use Set-Variable. Change a variable just use assignment or the Set-Variable command. Debug mode just adds some new commands to the environment - all of the existing commands are still available so we have the full power of PowerShell available in this environment. HOVERING OVER VARIABLES TO SEE THEIR VALUES Some users don't want to type a command to look at variables, so like Visual Studio, the ISE allows you to hover the mouse pointer over a variable to see its value. Simply place the mouse pointed over the variable you want to see in the editor and its value will be displayed. Figure 15.27 shows what this looks like.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

615

Figure 15.27 This figure shows the use of hover to display the value of a variable. The mouse pointer is placed over the variable the the variable's value is displayed in a pop-up.

Note that there is one caveat to this - if the script isn't running then the variable won't have a value. Or, more confusingly, if there is another variable in an active scope with the same name, we'll see that value rather than the one we were expecting. This is one of the tricky aspects of dynamically scoped environments.

AUTHOR'S NOTE
By now you may have noticed that we aren't passing parameters to the scripts we're running. In fact, when run with F5 or from the menu, there is no way to specify parameters to the script we're about to run. The workaround for his is simply to run the script from the ISE command line where we can specify parameters.

Because PowerShell is shell, we also have to be able to debug directly from the command line which is our next topic.

15.6 Command-line Debugging
Given the nature of the PowerShell environment, we need to support debugging in a variety of environments. The most effective way to do this is to enable debugging scripts from the command line. This makes it possible to use the debugger from the console host as well. As always, these debugging features are surfaced though a set of cmdlets. These cmdlets are listed in table 15.5.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

616

Table 15.5 The PowerShell debugger cmdlets
Cmdlet Description Gets the current call stack Enables an existing breakpoint Disables a breakpoint without removing it. Sets a breakpoint Gets the list of breakpoints Remove an existing breakpoint.

Get-PSCallStack Enable-PSBreakPoint Disable-PSBreakPoint Set-PSBreakPoint Get-PSBreakPoint Remove-PSBreakPoint

Command line debugging is also important for another reason - there are many more things we can do using these cmdlets, including writing scripts to debug scripts. In other words, all of the features we've seen in the GUI debugger are available from the command line, but not all of the command line features are available from the GUI.

AUTHOR'S NOTE
There is, of course, no deep technical reason why this is so. We just ran out of time before the release. Using the ISE object model it's possible to backfill many of the missing features.

In fact, the GUI debugger only surfaces a portion of the functionality of what can be done with the PowerShell debugger. In the next few sections we’ll dig into these capabilities.

15.6.1 Working with breakpoint objects
Let's begin our discussion by taking a detailed look how breakpoints are implemented: So far we've seen a fairly conventional debugger experience but the introspective nature of PowerShell allows us to do much more when working with breakpoints. As with most everything else, breakpoints in PowerShell are objects that we can script against. As always, we'll use Get-Member to examine these objects: PS (STA) (42) > get-psbreakpoint | gm TypeName: System.Management.Automation.LineBreakpoint Name ---Equals GetHashCode GetType ToString Action Column Enabled HitCount Id Line Script MemberType ---------Method Method Method Method Property Property Property Property Property Property Property Definition ---------bool Equals(System.Object obj) int GetHashCode() type GetType() string ToString() System.Management.Automation.ScriptBlock Action {get;} System.Int32 Column {get;} System.Boolean Enabled {get;} System.Int32 HitCount {get;} System.Int32 Id {get;} System.Int32 Line {get;} System.String Script {get;}

In this output see members we've looked at before - the breakpoint Id, the line and script where it applies. Much more interesting are things like HitCount and especially the Action property. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

617 The HitCount property records the number of times a breakpoint has been hit - not terribly interesting but useful sometimes. The really interesting property is Action holds instances of our old friend the scriptblock. By specifying actions in scriptblocks, breakpoints can do much more than simply interrupting execution when the break point is hit. Using scriptblocks allows us to perform arbitrary actions controlling when or even if the breakpoint actually fires. Let's see how this works with a simple test script: PS (1) > Get-Content testscript2.ps1 "Starting" $sum = 0 foreach ($i in 1..10) { $sum += $i } "The sum is $sum" This script loop over the numbers from 1 to 10, summing them up and then printing the result. Now let's define a breakpoint for this script using the Set-PSBreakPoint command. PS (2) > $firstBP = Set-PSBreakpoint -Script testscript2.ps1 -Line 5 ` >> -Action { >> if ($i -gt 3 -and $i -lt 7) >> { >> Write-Host ">>> DEBUG ACTION: i = $i, sum = $sum" >> } >> } >> This command specifies that a scriptblock will be executed every time we hit line 5 in the test script. In the body of the scriptblock, we're checking to see if the value of $i is greater than 3 and less than 7. If so, we'll display a message. We have to use Write-Host to display this message since the results of the scriptblock are not displayed. The Set-PSBreakpoint command returns an instance a breakpoint object. Let's display it as a list so we can see its members. PS (3) > $firstBP | Format-List Id Script Line Column Enabled HitCount Action : : : : : : : 1 C:\wpia_v2\text\chapter15\code\testscript2.ps1 5 0 True 0 if ($i -gt 3 -and $i -lt 7) { Write-Host ">>> DEBUG ACTION: i = $i, sum = $sum" } This shows the full path to the script and the line in the script that will trigger the action as well as the action itself. Now let's run the test script to see how it works. PS (4) > ./testscript2.ps1 Starting >>> DEBUG ACTION: i = 4, sum = 6 >>> DEBUG ACTION: i = 5, sum = 10 >>> DEBUG ACTION: i = 6, sum = 15 The sum is 55 The output shows the value of $i and $sum as long as $i is between 3 and 7 as intended.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

618 Before we move on to the next example, we'll remove all of the breakpoints so they don't confuse the results in the example. PS (5) > Get-PSBreakpoint | Remove-PSBreakpoint This time, instead of just displaying a message, we're going to use the break keyword to break the script under specific conditions. Here is the command to define the new breakpoint. PS (6) > $firstBP = Set-PSBreakpoint -Script testscript2.ps1 -Line 5 Action { >> if ($i -eq 4) >> { >> Write-Host ">>> DEBUG ACTION: i = $i, sum = $sum" >> break >> } >> } >> For this breakpoint, we'll only fire the action on line 4 of the test script. In the scriptblock body, we display the message as before and then call break which will break the execution of the script: PS (7) > ./testscript2.ps1 Starting >>> DEBUG ACTION: i = 4, sum = 6 Entering debug mode. Use h or ? for help. Hit Line breakpoint on 'C:\wpia_v2\text\chapter15\code\testscript2.ps1:5' testscript2.ps1:5 $sum += $i

As was the case in the graphical debugger, we have same set of options available at the break prompt: PS (8) > ? s, stepInto Single step (step into functions, scripts, etc.) v, stepOver Step to next statement (step over functions, scri pts, etc.) o, stepOut Step out of the current function, script, etc. c, continue Continue execution q, quit Stop execution and exit the debugger k, Get-PSCallStack Display call stack l, list List source code for the current script. Use "list" to start from the current line, "list " to start from line , and "list " to list lines starting from line Repeat last command if it was stepInto, stepOver or list Displays this help message

?, h

For instructions about how to customize your debugger prompt, type "he lp about_prompt". We'll use the 'c' command to continue execution: PS (9) > c The sum is 55 and the script complete, displaying the sum. And, as before, we'll clean up the breakpoint: PS (10) > Get-PSBreakpoint | Remove-PSBreakpoint and move on to the next example.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

619

15.6.2 Setting breakpoints on commands
While the most common scenario using the debugger involves setting breakpoints on lines in a file, it is also possible to break on a specific command. We'll define a simple function: PS (15) > function hello { "Hello world!" } and set a breakpoint on that function. PS (16) > Set-PSBreakpoint -Command hello ID Script -- -----0 Line Command ---- ------hello Variable -------Action ------

This time we won't associate an action and allow the default behavior - causing a break in execution - to occur. Now let's execute the function: PS (17) > hello Entering debug mode. Use h or ? for help. Hit Command breakpoint on 'hello' hello When the command is run, we immediately hit the breakpoint. We'll enter 'c' and allow the function to complete. PS (18) > c Hello world! Among other things, the ability to set breakpoints on commands as opposed to specific lines in a script allows us to debug interactively entered functions. Now let's move on to the final example in this section - setting breakpoints on variables.

15.6.3 Break execution on variable assignment
In the previous examples the breakpoints were triggered when execution reached a certain line in the script or entered a command. We can also cause a break when variables are read or written. In the following command, we'll specify an action to take when the $sum variable is written. PS (12) > $secondBP = Set-PSBreakpoint -Variable sum -mode Write ` >> -Action { >> if ($sum -gt 10) >> { >> Write-Host ">>> VARIABLE sum was set to $sum" >> } >> } >> In the action scriptblock, we'll use Write-Host as before to display the value of $sum, but only when it's greater than 10. Let's look at what this breakpoint looks like: PS (13) > $secondBP | Format-List Id Variable AccessMode Enabled HitCount Action : : : : : : 3 sum Write True 0 if ($sum -gt 10) { Write-Host ">>> VARIABLE sum was set to $sum" } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

620 We see the line and variable and access mode that will trigger the action and the scriptblock to execute when triggered. And, running the test script: PS (14) > ./testscript2.ps1 Starting >>> VARIABLE sum was set to 15 >>> VARIABLE sum was set to 21 >>> VARIABLE sum was set to 28 >>> VARIABLE sum was set to 36 >>> VARIABLE sum was set to 45 The sum is 55 We see the error messages displayed. One of the nice things is that a variable-based breakpoint is not tied to a specific line number in the script so it will continue to work even when you edit the script. While these examples are by no means exhaustive, give us a sense of the capabilities of the PowerShell command line debugger. We are able to do much more sophisticated debugging from the command line. However, even for the command line, there are a number of limitations to the debugging capabilities. We'll look at these limitations in the final section in this chapter.

15.6.4 Debugger limitations and issues
The PowerShell debugger, while powerful, does suffer from a number of limitations. Probably the biggest limitations is that remote debugging is not currently supported. Even in an interactive remoting session, the debugger won't work: [localhost]: PS (1) > Set-PSBreakpoint -script testfile2.ps 1 -line 5 Debugging is not supported on remote sessions. + CategoryInfo : + FullyQualifiedErrorId : SetPSBreakpoint:RemoteDebuggerNotSuppo rted,Microsoft.PowerShell.Commands.SetPSBreakpointCommand This is, in part, because the remote host environment doesn't support the required

EnterNestedPrompt() function we saw earlier:
[localhost]: PS (2) > $host.EnterNestedPrompt() Exception calling "EnterNestedPrompt" with "0" argument(s): "Remote h ost method get_Runspace is not implemented." At line:1 char:24 + $host.EnterNestedPrompt new-psdrive -name docs -PSProvider filesystem ` >> -Root (resolve-path ~/*documents) >> Name ---docs Provider -------FileSystem Root ---C:\Documents and Settings\brucep Current Location --------

Now we can cd into this drive PS (2) > cd docs: ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

625 then use Get-Location (to see where we are: PS (3) > Get-Location Path ---docs:\ We are, at least according to PowerShell, in the docs: drive. Let’s create a file here: PS (4) > "Hello there!" > junk.txt Next, try to use cmd.exe to display it (we’ll get to why we’re doing this in a second): PS (5) > cmd /c type junk.txt Hello there! Well, that works fine. Display it using Get-Content with the fully qualified path, including the docs: drive. PS (6) > get-content docs:/junk.txt Hello there! This works as expected. But when we try this with cmd.exe PS (7) > cmd /c type docs:/junk.txt The syntax of the command is incorrect. it fails! This is because non-PowerShell applications don’t understand the PowerShell drive fiction. Do you remember the earlier example, where we did a “cd” to the location first, that it did work? This is because when we’re “in” that drive, the system automatically sets the current directory properly to the physical path for the child process. This is why using relative paths from cmd.exe works. However, when we pass in a PowerShell path, it fails. There is another workaround for this besides doing a cd. You can use the Resolve-Path cmdlet to get the ProviderPath. This cmdlet takes the PowerShell “logical” path and translates it into the provider’s native physical path. This means that it’s the “real” file system path that non-PowerShell utilities can understand. We’ll use this to pass the real path to cmd.exe: PS (7) > cmd /c type (resolve-path docs:/junk.txt).ProviderPath Hello there! This time, it works. This is an area where we need to be careful and think about how things should work with non-PowerShell applications. If we wanted to open a file with notepad.exe in the doc: directory, we’d have to do the same thing we did for cmd.exe and resolve the path first: notepad (resolve-path docs:/junk.txt).ProviderPath If you frequently use notepad then you can create a function in your profile: function notepad { $args | %{ notepad.exe (resolve-path $_).ProviderPath } } You could even create a function to launch an arbitrary executable: function run-exe { $cmd, $files = $args $cmd = @(get-command -type application $cmd)[0].Definition $files | %{ & $cmd (resolve-path $_).ProviderPath } } This function resolves both the file to edit and the command to run. This means that you can use a PowerShell drive to map a command path to execute.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

626 EACH PROVIDER HAS A HOME DIRECTORY Another useful feature provider by the PowerShell path mechanism is a shortcut mechanism that allows us to easily access the default or "home" directory for that provide. Simply start the path with the tilde (~) character and the remaining path components will be resolved relative to the provider's home directory. In the file system, this will be the user's home directory allowing us easy access to the subdirectories off of our home directory. For example, to cd to the desktop folder, we just have to do cd ~/desktop This makes accessing these directories very convenient. Remember, however, the ~ always refers to the home directory for the current drive's associated provider. As a consequence, if our current location is somewhere in the registry provider, coding 'cd ~/desktop' won't work because the ~ will resolve to the home directory of the registry provider not the file system provider. Since we're on the topic of paths, let's look at some more features PowerShell provides for working with paths.

16.1.3 Working with paths that contain wildcards
Another great feature of the PowerShell provider infrastructure is universal support for wildcards (see chapter 4 for details on wildcard patterns). We can use wildcards any place we can navigate to, even in places such as the alias: drive. For example, say you want to find all of the aliases that begin with “gc”. You can do this with wildcards in the alias provider. PS (1) > dir alias:gc* CommandType ----------Alias Alias Alias Name ---gc gci gcm Definition ---------Get-Content Get-ChildItem Get-Command

We see that there are three of them. We might all agree that this is a great feature, but there is a downside. What happens when you want to access a path that contains one of the wildcard meta-characters: “?”, “*”, “[” and “]”. In the Windows file system, “*” and “?” aren’t a problem because we can’t use these characters in a file or directory name. But we can use “[” and “]”. In fact, they are used quite a bit for temporary Internet files. Working with files whose names contain “[” or “]” can be quite a challenge because of the way wildcards and quoting (see chapter 3) work. Square brackets are used a lot in filenames in browser caches to avoid collisions by numbering the files. Let’s run some experiments on some of the files in the IE cache.

AUTHOR'S NOTE
Here’s another tip. By default, the Get-ChildItem cmdlet (and its alias dir) will not show hidden files. To see the hidden files, use the -Force parameter. For example, to find the “Application Data” directory in our home directory, we try PS (1) > dir ~/app* but nothing is returned. This is because this directory is hidden. To see the directory, we use -Force as in:

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

627
PS (2) > dir ~/app* -Force Directory:Microsoft.PowerShell.Core\FileSystem::C:\Docum ents and Settings\brucepay Mode ---d-rhLastWriteTime ------------12/14/2006 Length Name

------ ---Application Data

9:13 PM

and now the directory is visible. We’ll need to use -force to get into the directory containing the temporary Internet files.

16.1.4 Suppressing wildcard processing in paths
In one of the directories used to cache temporary Internet files, we want to find all of the files that begin with “thumb*”. This is easy enough: PS (2) > dir thumb* Directory: Microsoft.PowerShell.Core\FileSystem::C:\Doc uments and Settings\brucepay\Local Settings\Temporary I nternet Files\Content.IE5\MYNBM9OJ Mode ----a---a---a---a---a--LastWriteTime ------------9/7/2006 10:34 PM 9/7/2006 7/8/2006 9/11/2006 9/11/2006 10:35 PM 7:58 PM 2:48 PM 2:48 PM Length Name ------ ---4201 ThumbnailServe r[1].jpg 3223 ThumbnailServe r[2].jpg 2066 thumb[1].jpg 12476 thumb[2].txt 11933 thumb[3].txt

We get five files. Now we want to limit the set of files to things that match “thumb[”. We try this directly using a wildcard pattern: PS (3) > dir thumb[* Get-ChildItem : Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard pattern is not valid: th umb[* At line:1 char:3 + ls cd hklm:\software\microsoft\powershell Now let's use dir to see what's there: PS (2) > dir Hive: HKEY_LOCAL_MACHINE\software\microsoft\powershell

SKC --4

VC Name -- ---2 1

Property -------{Install, PID}

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

630 Unfortunately, the default display for a registry entry is a bit cryptic. We'll Format-List to make it a bit more comprehensible. PS (3) > dir | Format-List Name ValueCount Property SubKeyCount : : : : 1 2 {Install, PID} 4

This is quite a bit clearer. In this path we find a single item named '1'. This item has two properties or values associated with it and 4 children or subkeys. Now we'll cd into the container and run dir again: PS (4) > cd ./1 PS (5) > dir Hive: HKEY_LOCAL_MACHINE\software\microsoft\powershell\1 SKC --0 0 1 2 VC -1 7 0 0 Name ---0409 PowerShellEngine PSConfigurationProviders ShellIds Property -------{Install} {ApplicationBase, PSCompati... {} {}

Now we see information about the subkeys. But what about accessing the properties? We do this using the Get-ItemProperty cmdlet. To get the value of the PowerShell Product ID property, we do: PS (6) > Get-ItemProperty -Path . -Name PID PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE \software\microsoft\powershell\1 PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE \software\microsoft\powershell PSChildName : 1 PSDrive : HKLM PSProvider : Microsoft.PowerShell.Core\Registry PID : 89383-100-0001260-04309

Notice that we need to specify both the path and the name of the property to retrieve. Properties are always relative to a path. By specifying '.' as the path, the property is relative to the current directory. There is another somewhat annoying thing about how Get-ItemProperty works. It doesn't actually return the value of the property, it returns a new object that has the property value as a member. So before we can do anything with this value, we need to extract it from the containing object: PS (9) > (Get-ItemProperty -Path . -Name PID).PID 89383-100-0001260-04309 By using the '.' operator to extract the member's value, we can get just the value.

AUTHOR'S NOTE
This is another one of those design tradeoffs the PowerShell team encountered as we developed this environment. If we returned just the value, then we would lose the context for the value (where it came from etc.) And so, in order to preserve this information we ended up with forcing people to write what appears to be redundant code. In retrospect, a better way to handle this would have been to return the value with the context attached as synthetic properties.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

631

16.2 File processing
Let’s step back for a minute and talk about files, drives and navigation. PowerShell has a provider abstraction that allows the user to work with system data stores as though they were drives. A provider is a piece of installable software that surfaces a data store in the form that can be mounted as a “drive”.

AUTHOR'S NOTE
By installable, we mean that the end user can install new providers or even write their own providers. This activity is outside the scope of this book, however. Refer to the PowerShell user documentation for information on how to install additional providers. The PowerShell Software Developer’s Kit includes documentation and examples that can help you write your own providers.

These drives are a PowerShell “fiction”; that is, they only have meaning to PowerShell as opposed to system drives that have meaning everywhere. Also, unlike the system drives, PowerShell drive names can be longer than one character. We’ve already seen some examples of non-filesystem providers in earlier chapters, where we worked with the variable: and function: drives. These providers let you use the New-Item and Remove-Item cmdlets to add and remove variables or functions just as if they were files. A key piece to making this provider abstraction is the set of core cmdlets listed in table 10.1. These cmdlets are the “core” set of commands for manipulating the system and correspond to commands found in other shell environments. Because these commands are used so frequently, short aliases—the canonical aliases—are provided for the commands. By canonical, we mean that they follow a standard form: usually the first letter or two of the verb followed by the first letter or two of the noun. Two additional sets of “user migration” aliases are provided to help new users work with the system. There is one set for cmd.exe users and one set for UNIX shell users. Note that these aliases only map the name; they don’t provide exact functional correspondence to either the cmd.exe or UNIX commands.

Table 16.1 List of core cmdlets, aliases and equivalents in other shells
Cmdlet Name Canon ical Cmd.exe UNIX sh Description

Get-Location

gl

cd

pwd

Get the current directory.

Set-Location

sl

cd, chdir copy del, rd

cd, chdir cp rm, rmdir

Change the current directory

Copy-Item Remote-Item

cpi ri

Copy files. Remove a file or directory. PowerShell has no separate command for removing directories as opposed to files.

Move-Item Rename-Item

mi rni

move ren

mv mv

Move a file. Rename a file

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

632

Set-Item Clear-Item New-Item

si cli ni

Set the contents of a file. Clear the contents of a file. Create a new empty file or directory. The type of object is controlled by the -Type parameter.

MkDir

md

mkdir

Mkdir is implemented as a function in PowerShell so that users can create directories with- out having to specify –type directory.

Get-Content

gc

type

cat

Send the contents of a file to the output stream.

Set-Content

sc

Set the contents of a file. UNIX and cmd.exe have no equivalent. Redirection is used instead. The difference between Set-

Content and Out-File is discussed later in this chapter. On-line help is available for all of these commands; simply type help cmdlet-name and you’ll receive detailed help on the cmdlets, their parameters, and some simple examples of how to use them. In the next few sections, we’ll look at some more sophisticated applications of these cmdlets, including how to deal with binary data. In traditional shell environments, binary data either required specialized commands or forced us to create new executables in a language such as C, because the basic shell model couldn’t cope with binary data. We’ll see how PowerShell can work directly with binary data. But first, let’s take a minute to look at the PowerShell drive abstraction to simplify working with paths.

16.2.1 Reading and writing files
In PowerShell, files are read using the Get-Content cmdlet. This cmdlet allows you to work with text files using a variety of character encodings. It also lets you work efficiently with binary files, as we’ll see in a minute. Writing files is a bit more complex, because you have to choose between Set-Content and Out-File. The difference here is whether or not the output goes through the formatting subsystem. We’ll also explain this later on in this section. One thing to note is that there are no separate open/read/close or open/write/close steps to working with files. The pipeline model allows you to process data and never have to worry about closing file handles—the system takes care of this for you. READING FILES WITH THE GET -CONTENT CMDLET The Get-Content cmdlet is the primary way to read files in PowerShell. Actually, it’s the primary way to read any content available through PowerShell drives. Figure 16.1 shows a subset of the parameters available on the cmdlet.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

633
The cmdlet name Path to the object to read. The number of objects to read in a block

Line or Record delimiter

Get-Content [-Path] [-ReadCount ] The total number of [-TotalCount ] objects to read [-Delimiter ] [-Wait] [-Encoding ]

A switch parameter – if specified, the cmdlet will wait polling input until stopped

The encoding to use when reading the while.

Figure 16.1 The Get-Content cmdlet parameters. This cmdlet is used to read content from a content providers store.

Reading text files is simple. The command Get-Content myfile.txt will send the contents of "myfile.txt" to the output stream. Notice that the command signature for -path allows for an array of path names. This is how you concatenate a collection of files together. Let’s try this. First we’ll create a bunch of files: PS (1) > 1..3 | %{ "This is file $_" > "file$_.txt"} PS (2) > dir Directory: Microsoft.PowerShell.Core\FileSystem::C:\Temp\fil es Mode ----a---a---a--LastWriteTime ------------7/6/2006 8:33 PM 7/6/2006 8:33 PM 7/6/2006 8:33 PM Length -----34 34 34 Name ---file1.txt file2.txt file3.txt

And now display their contents: PS (3) > cat file1.txt,file2.txt,file3.txt This is file 1 This is file 2 This is file 3 or simply PS (4) > cat This is file This is file This is file *.txt 1 2 3

In this example, the contents of file1.txt, file2.txt, and file3.txt are sent to the output stream in order. For cmd.exe users, this is equivalent to copy file1.txt+file2.txt+file3.txt con Let’s try this in cmd.exe: C:\Temp\files>copy file1.txt+file2.txt+file3.txt con file1.txt T h i s i s f i l e 1 file2.txt h i s i s f i l e 2 file2.txt h i s i s f i l e 3 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

634 1 file(s) copied. The output looks funny because the files were written in Unicode. You need to tell the copy command to write in ASCII, and try it again: C:\Temp\files>copy /a file1.txt+file2.txt+file3.txt con file1.txt This is file 1 file2.txt This is file 2 file2.txt This is file 3 1 file(s) copied. By default, PowerShell uses Unicode for text, but you can override this. We’ll see how to do this in the section on writing files. In the meantime, let’s look at how to work with binary files. EXAMPLE: THE GET -HEXDUMP FUNCTION Let’s look at an example that uses some of these features to deal with non-text files. We’re going to write a function that can be used to dump out a binary file. We’ll call this function Get-HexDump. It takes the name of the file to display, the number of bytes to display per line, and the total number of bytes as parameters. We want the output of this function to look like the following: PS (130) > Get-HexDump "$env:windir/Soap Bubbles.bmp" -w 12 -t 100 42 4d ba 01 01 00 00 00 00 00 ba 01 BMº.......º. 00 00 28 00 00 00 00 01 00 00 00 01 ............ 00 00 01 00 08 00 00 00 00 00 00 00 ............ 01 00 12 0b 00 00 12 0b 00 00 61 00 ..........a. 00 00 61 00 00 00 6b 10 10 00 73 10 ..a...k...s. 10 00 73 18 18 00 7b 21 21 00 84 29 ..s......... 29 00 84 31 31 00 6b 08 08 00 8c 39 ...11.k....9 31 00 84 31 29 00 8c 31 31 00 7b 18 1..1...11... 18 00 8c 39 ...9 In this example, we’re using Get-HexDump to dump out the contents of one of the bitmap files in the Windows installation directory. We’ve specified that it display 12 bytes per line and stop after the first 100 bytes. The first part of the display is the value of the byte in hexadecimal, and the portion on the right side is the character equivalent. Only values that correspond to letters or numbers are displayed. Nonprintable characters are shown as dots. The code for this function is shown in listing 10.1.

Get-HexDump function Get-HexDump ($path = $(throw "path must be specified"), $width=10, $total=-1) { $OFS="" 1 Get-Content -Encoding byte $path -ReadCount $width ` -totalcount $total | %{ 2 $record = $_ if (($record -eq 0).count -ne $width) 3 { $hex = $record | %{ 4 " " + ("{0:x}" -f $_).PadLeft(2,"0")} 4 $char = $record | %{ 4 if ([char]::IsLetterOrDigit($_)) 4 { [char] $_ } else { "." }} 4 "$hex $char" 5 } } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

635 } 1 2 3 4 5

Set $OFS to empty Read the file Skip record if length is zero Format data Emit formatted output

As required, the function takes a mandatory path parameter and optional parameters for the number of bytes per line and the total number of bytes to display. We’re going to be converting arrays to strings and we don’t want any spaces added, so we’ll set the output field separator character #1 to be empty. The Get-Content cmdlet #2 does all of the hard work. It reads the file in binary mode (indicated by setting encoding to byte), reads up to a maximum of -TotalCount bytes, and writes them into the pipeline in records of length specified by -ReadCount. The first thing we do in the foreach scriptblock is save the record that was passed in, because we’ll be using nested pipelines that will cause $_ to be overwritten. If the record is all zeros #3, we’re not going to bother displaying it. It might be a better design to make this optional, but we’ll leave it as is for this example. For display purposes, we’re converting the record of bytes #4 into two-digit hexadecimal numbers. We use the format operator to format the string in hexadecimal and then the PadLeft() method on strings to pad it out to two characters. Finally, we prefix the whole thing with a space. The variable $hex ends up with a collection of these formatted strings. Now we need to build the character equivalent of the record. We’ll use the methods on the [char] class to decide whether we should display the character or a “.”. Notice that even when we’re displaying the character, we’re still casting it into a [char]. This is needed because the record contains a byte value which, if directly converted into a string, will be a formatted as a number instead of as a character. Finally, we’ll output the completed record, taking advantage of string expansion to build the output string #5 (which is why we set $OFS to “”). This example illustrates the basic technique for getting at the binary data in a file. The technique has a variety of applications beyond simply displaying binary data, of course. Once you reach the data, you can determine a variety of characteristics about the content of that file. In the next section, we’ll take a look at an example and examine the content of a binary file to double-check on the type of that file. EXAMPLE: THE GET -M AGICNUMBER FUNCTION If you looked closely at the output from the .BMP file earlier, you might have noticed that the first two characters in the file were BP. In fact, the first few bytes in a file are often used as a “magic number” that identifies the type of the file. We’ll write a short function Get-MagicNumber that displays the first four bytes of a file so we can investigate these magic numbers. Here’s what we want the output to look like. First we’ll try this on a .BMP file PS (1) > get-magicnumber $env:windir/Zapotec.bmp 424d 3225 'BM2.' and then on an .EXE. PS (2) > get-magicnumber $env:windir/explorer.exe 4d5a 9000 'MZ..' This utility dumps the header bytes of the executable. The first two bytes identify this file as an MS-DOS executable.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

636

AUTHOR'S NOTE
Trivia time: As you can see, the ASCII representation of the header bytes (0x5A4D) is MZ. These are the initials of Mark Zbikowski, one of the original architects of MS-DOS.

The code for Get-MagicNumber is shown in listing 10.2.

Get-MagicNumber function Get-MagicNumber ($path) { $OFS="" $mn = Get-Content -encoding byte $path -read 4 -total 4 $hex1 = ("{0:x}" -f ($mn[0]*256+$mn[1])).PadLeft(4, "0") $hex2 = ("{0:x}" -f ($mn[2]*256+$mn[3])).PadLeft(4, "0") [string] $chars = $mn| %{ if ([char]::IsLetterOrDigit($_)) { [char] $_ } else { "." }} "{0} {1} '{2}'" -f $hex1, $hex2, $chars } 1 Set $OFS to empty string 2 Format as hex 3 Format as char 4 Emit output

1 2 3 4

There’s not much that’s new in this function. Again, we set the output field separator string to be empty #1. We extract the first four bytes as two pairs of numbers formatted in hex #2 and also as characters #3 if they correspond to printable characters. Finally, we format the output #4 as desired. From these examples, we see that Get-Content allows us to explore any type of file on a system, not just text files. For now, though, let’s return to text files and look at another parameter on Get-

Content: -Delimiter. When reading a text file, the default line delimiter is the newline character.
AUTHOR'S NOTE
Actually, the end-of-line sequence on Windows is generally a two-character sequence: carriage return followed by newline. The .NET I/O routines hide this detail and let us just pretend it’s a newline. In fact, the runtime will treat newline by itself, carriage return by itself, and the carriage return/newline sequence all as end-of-line sequences.

This parameter lets you change that. With this new knowledge, let’s return to the word-counting problem we had earlier. If we set the delimiter to be the space character instead of a newline, we can split the file as we read it. Let’s use this in an example. get-content about_Assignment_operators.help.txt ` -delimiter " " | foreach { $_ -replace "[^\w]+"} | where { $_ -notmatch "^[ `t]*`$"} | group | sort -descending count | select -first 10 | ft -auto name, count In this example, the -delimiter parameter is used to split the file on space boundaries instead of newlines. We’re using the same group, sort, and format operations as before; however, this time we’re sorting in descending order so we can use the Select-Object cmdlet instead of array indexing to extract the top 10 words. We’re also doing more sophisticated filtering. We’re using a foreach filter to ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

637 get rid of the characters that aren’t legal in a word. This is accomplished with the -replace operator and the regular expression “[^\w]+”. The \w pattern is a meta-character that matches any legal character in a word. Putting it in the square brackets prefixed with the caret says it should match any character that isn’t valid in a word. The where filter is used to discard any empty lines that may be in the text or may have been created by the foreach filter. At this point, we have a pretty good handle on reading files and processing their contents. It’s time to look at the various ways to write files.

16.2.2 Writing files
There are two major ways to write files in PowerShell—by setting file content with the Set-Content cmdlet and by writing files using the Out-File cmdlet. The big difference is that Out-File, like all of the output cmdlets, will try to format the output. Set-Content, on the other hand, will simply write the output. If its input objects aren’t already strings, it will convert them to strings by calling the .ToString() method. This is not usually what you want for objects, but it’s exactly what you want if your data is already formatted or if you’re working with binary data. The other thing you need to be concerned with is how the files are encoded when they’re written. In an earlier example, we saw that, by default, text files are written in Unicode. Let’s rerun this example, changing the encoding to ASCII instead. PS (48) > 1..3 | %{ "This is file $_" | >> set-content -encoding ascii file$_.txt } >> The -Encoding parameter is used to set how the files will be written. In this example, the files are written using ASCII encoding. Now let’s rerun the cmd.exe copy example that didn’t work earlier. PS (49) > cmd /c copy file1.txt+file2.txt+file3.txt con file1.txt This is file 1 file2.txt This is file 2 file3.txt This is file 3 1 file(s) copied. This time it works fine, because the encoding matches what cmd.exe expected. In the next section, we’ll look at using -Encoding to write binary files.

16.2.3 All together now—Reading and writing
Our next topic of interest is combining reading and writing binary files. First we’ll set up paths to two files: a source bitmap file: $src = "$env:windir/Soap Bubbles.bmp" and a destination in a temporary file. $dest = "$env:temp/new_bitmap.bmp" Now we’ll copy the contents from one file to the other: get-content -encoding byte -read 10kb $src | set-content -encoding byte $dest Now let’s define a (not very good) checksum function that simply adds up all of the bytes in the file. function Get-CheckSum ($path) { $sum=0 get-content -encoding byte -read 10kb $path | %{ foreach ($byte in $_) { $sum += $byte } ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

638 } $sum } We’ll use this function to verify that the file we copied is the same as the original file (note that this is a fairly slow function and takes a while to run). PS (5) > Get-CheckSum $src 268589 PS (6) > Get-CheckSum $dest 268589 The numbers come out the same, so we have some confidence that the copied file matches the original.

16.2.4 Performance caveats with Get-Content
PowerShell makes file processing easy however the pipeline processor (at least as of version 2) is rather slow and this makes certain types of processing on large files problematic. For example, a user at Microsoft had a script that processed a large file to remove the first three lines from the file. This was done by using the following series of steps: $lines = Get-Content $file -ReadCount 0 $lines = $lines | select -Skip 3 $lines | set-content temp.txt move temp.txt $file -Force This script read in all of the lines the file, used select to skip the first three lines, wrote the result to a temporary file and then did a forced rename to replace the original file. On the target file, this simple process was taking well over a minute due to pipeline processor overhead and it had to be run on a very large number of files. While there wasn't much that could be done in the pipeline to speed things up, there was a workaround available - use the raw .NET IO classes. The workaround looked something like this: function Skip3 ($file, $encoding = [System.Text.Encoding]::Unicode) { $input = Resolve-Path $file $output = Join-Path (Split-Path -parent $input) out.txt [io.file]::WriteAllLines( $output, [io.file]::ReadAllLines($input)[3..$text.length], $encoding) } In this function, the .NET methods are used to read and write the file and array indexing is used to strip of the first 3 lines. This script ran in less than a second for each file making the intended task feasible. This type of workaround should be the exception rather than the rule. PowerShell is fast enough for most typical applications. It is nice, however, to know that faster techniques are available directly from PowerShell (at the cost of some complexity) instead of having to switch to a different tool.. This wraps up our coverage of the file system provider. In the next section we'll look another very useful provider - the registry provider.

16.3 Processing unstructured text
While PowerShell is an object-based shell, it still has to deal with text. In chapter 4, we covered the operators (-match, -replace, -like, -split, -join) that PowerShell provides for working with text. We showed how to concatenate two strings together using the plus operator. In this section, we’ll cover some of the more advanced string processing operations. We’ll discuss techniques for splitting and

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

639 joining strings using the [string] and [regex] members, and using filters to extract statistical information from a body of text.

16.3.1 Using System.String to work with text
One common scenario for scripting is processing log files. This requires breaking the log strings into pieces to extract relevant bits of information. PowerShell V2 address this by using the -split operator but in PowerShell V1, if we needed to split a string into pieces, we had to use the Split() method on the [string] class. This is still fairly simple to do: PS (1) > "Hello there world".Split() Hello there world The Split() method with no arguments splits on spaces. In this example, it produces an array of three elements. PS (2) > "Hello there world".Split().length 3 We can verify this with the Length property. In fact, it splits on any of the characters that fall into the

WhiteSpace character class. This includes tabs, so it works properly on a string containing both tabs and spaces. PS (3) > "Hello`tthere world".Split() Hello there world In the revised example, we still get three fields, even though space is used in one place and tab in another. And while the default is to split on a whitespace character, you can specify a string of characters to use split fields. PS (4) > "First,Second;Third".Split(',;') First Second Third Here we specified the comma and the semicolon as valid characters to split the field. There is, however, an issue; the default behavior for “split this” isn’t necessarily what you want. The reason why is that it splits on each separator character. This means that if you have multiple spaces between words in a string, you’ll get multiple empty elements in the result array. For example: PS (5) > "Hello there world".Split().length 6 In this example, we end up with six elements in the array because there are three spaces between “there” and “world”. Now let's continue on paralleling the features of the -split operator. USING SPLIT STRINGOPTIONS The -split operator allows us to specify a number of options that are used to control the splitting process. Let's look at how we can do the same thing using the Split() method. We can get a list of all of the method overloads by using the OverloadDefinitions member on the PSMethod object for

Split:
PS (6) > string[] string[] string[] string[] "hello".split.OverloadDefinitions Split(Params char[] separator) Split(char[] separator, int count) Split(char[] separator, System.StringSplitOptions options) Split(char[] separator, int count, System.StringSplitOptions http://www.manning-sandbox.com/forum.jspa?forumID=542

©Manning Publications Co. Please post comments or corrections to the Author Online forum:

Licensed to Andrew M. Tearle

640 options) string[] Split(string[] separator, System.StringSplitOptions options) string[] Split(string[] separator, int count, System.StringSplitOptio ns options) The methods that take the options argument look promising. Let’s see what the SplitStringOptions are. We’ll do this by trying to cast a string into these options. PS (8) > [StringSplitOptions] "abc" Cannot convert value "abc" to type "System.StringSplitOptions" d ue to invalid enumeration values. Specify one of the following e numeration values and try again. The possible enumeration values are "None, RemoveEmptyEntries". At line:1 char:21 + [StringSplitOptions] [StringSplitOptions]::RemoveEmptyEntries) >> Hello there world It works as desired. Now we can apply this to a larger problem. ANALYZING WORD USE IN A DOCUMENT Given a body of text, we want to find the number of words in the text as well as the number of unique words, and then display the 10 most common words in the text. For our purposes, we’ll use one of the PowerShell help text files: about_Assignment_operators.help.txt. This is not a particularly large file (it’s around 17 kilobytes) so we can just load it into memory using the Get-Content (gc) cmdlet. PS (10) > $s = gc $PSHOME/en-US/about_Assignment_operators.help.txt PS (11) > $s.length 747 The variable $s now contains the text of the file as a collection of lines (747 lines, to be exact.) This is usually what we want, since it lets us process a file one line at time. But, in this example, we actually want to process this file as a single string. To do this, we could use the -join operator, but let's use the String.Join() method instead. We'll join all of the lines, adding an additional space between each line: PS (12) > $s = [string]::join(" ", $s) PS (13) > $s.length 22010 Now $s contains a single string containing the whole text of the file. We verified this by checking the length rather than displaying it. Next we’ll split it into an array of words. PS (14) > $words = $s.split(" `t", >> [stringsplitoptions]::RemoveEmptyEntries) >> PS (15) > $words.length 3316 So the text of the file has 2,696 words in it. We need to find out how many unique words there are. There are a couple ways of doing this. The easiest way is to use the Sort-Object cmdlet with the unique parameter. This will sort the list of words and then remove all of the duplicates. PS (16) > $uniq = $words | sort -uniq PS (17) > $uniq.count ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

641 604 This help topic contains 604 unique words. Using the Sort cmdlet is fast and simple, but it doesn’t cover everything we said we wanted to do, because it doesn’t give the frequency of use. Let’s look at another approach: using the Foreach-Object cmdlet and a hashtable.

16.3.2 Using hashtables to count unique words
In the previous example, we used the -Unique parameter to Sort-Object to generate a list of unique words. Now we’ll take advantage of the set-like behavior of hashtables to do the same thing, but in addition we will be able to count the number of occurrences of each word.

AUTHOR'S NOTE
In mathematics, a set is simply a collection of unique elements. This is how the keys work in a hashtable. Each key in a hashtable occurs exactly once. Attempting to add a key more than once will result in an error. In PowerShell, assigning a new value to an existing key simply replaces the old value associated with that key. The key itself remains unique. This turns out to be a powerful technique, because it’s a way of building index tables for collections of objects based on arbitrary property values. These index tables let us run database-like operations on object collections. See section B.9 for an example of how you can use this technique to implement a SQL-like “join” operation on two collections of objects.

Once again, we split the document into a stream of words. Each word in the stream will be used as the hashtable key, and we’ll keep the count of the words in the value. Here’s the script: PS (18) > $words | % {$h=@{}} {$h[$_] += 1} It’s not really much longer than the previous example. We’re using the % alias for ForEach-Object to keep it short. In the begin clause in ForEach-Object, we’re initializing the variable $h to hold the resulting hashtable. Then, in the process scriptblock, we increment the hashtable entry indexed by the word. We’re taking advantage of the way arithmetic works in PowerShell. If the key doesn’t exist yet, the hashtable returns $null. When $null is added to a number, it is treated as zero. This allows the expression $h[$_] += 1 to work. Initially, the hashtable member for a given key doesn’t exist. The += operator retrieves $null from the table, converts it to 0, adds one, then assigns the value back to the hashtable entry. Let’s verify that the script produces the same answer for the number of words as we found with the

Sort -Unique solution.
PS (19) > $h.psbase.keys.count 604 We have 604, the same as before.

AUTHOR'S NOTE
Notice that we used $h.psbase.keys.count. This is because there is a member in the hashtable that hides the keys property. In order to access the base keys member, we need to use the PSBase property to get at the base member on the hashtable.

Now we have a hashtable containing the unique words and the number of times each word is used. But hashtables aren’t stored in any particular order, so we need to sort it. We’ll use a scriptblock parameter ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

642 to specify the sorting criteria. We’ll tell it to sort the list of keys based on the frequency stored in the hashtable entry for that key. PS (20) > $frequency = $h.psbase.keys | sort {$h[$_]} The words in the sorted list are ordered from least frequent to most frequent. This means that

$frequency[0] contains the least frequently used word.
PS (21) > $frequency[0] avoid And the last entry in frequency contains the most commonly used word. If you remember from chapter 3, we can use negative indexing to get the last element of the list. PS (22) > $frequency[-1] the It comes as no surprise that the most frequent word is “the” and it’s used 344 times. PS (23) > $h["The"] 344 The next most frequent word is “to”, which is used 138 times. PS (24) > $h[$frequency[-2]] 138 PS (25) > $frequency[-2] to Here are the top 10 most frequently used words the about_Assignment_operators help text: PS (26) > -1..-10 | %{ $frequency[$_]+" "+$h[$frequency[$_]]} the 300 to 126 value 88 a 86 you 68 variable 64 of 55 $varA 41 For 41 following 37 PowerShell includes a cmdlet that is also useful for this kind of task: the Group-Object cmdlet. This cmdlet groups its input objects by into collections sorted by the specified property. This means that we can achieve the same type of ordering by the following: PS (27) > $grouped = $words | group | sort count Once again, we see that the most frequently used word is “the”: PS (28) > $grouped[-1] Count Name ----- ---300 the Group ----{the, the, the, the...}

And we can display the 10 most frequent words by doing: PS (29) > $grouped[-1..-10] Count ----300 126 88 86 68 64 55 41 Name ---the to value a you variable of $varA Group ----{the, the, the, the...} {to, to, to, to...} {value, value, value, value...} {a, a, a, a...} {you, You, you, you...} {variable, variable, variable... {of, of, of, of...} {$varA, $varA, $varA, $varA...}

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

643 41 For 37 following PowerShell. In this section, we saw how to split strings using the methods on the string class. We even saw how to split strings on a sequence of characters. But in the world of unstructured text, we'll quickly run into examples where simple splits are not enough. As is so often the case, regular expressions come to the rescue. In the next couple of sections, we’ll see how we can do more sophisticated string processing using the [regex] class. {For, for, For, For...} {following, following, follow...

We create a nicely formatted display courtesy of the formatting and output subsystem built into

16.3.3 Using regular expressions to manipulate text
In the previous section, we looked at basic string processing using members on the [string] class. While there’s a lot of potential with this class, there are times when you need to use more powerful tools. This is where regular expressions come in. As we discussed in chapter 4, regular expressions are a domain-specific language (DSL) for matching and manipulating text. We covered a number of examples using regular expressions with the -match and -replace operators. This time, we’re going to work with the regular expression class itself. SPLITTING STRINGS WITH REGULAR EXPRESSIONS As mentioned in chapter 3, there is a shortcut [regex] for the regular expression type. The [regex] type also has a Split() method, but it’s much more powerful because it uses a regular expression to decide where PS (1) > PS (2) > Hello there World! PS (3) > 3 to split strings instead of a single character. $s = "Hello-1-there-22-World!" [regex]::split($s,'-[0-9]+-')

[regex]::split($s,'-[0-9]+-').count

In this example, the fields are separated by a sequence of digits bound on either side by a dash. This is a pattern that couldn’t be specified with simple character-based split operations. When working with the .NET regular expression library, the [regex] class isn’t the only class that you’ll run into. We’ll see this in the next example, when we take a look at using regular expressions to tokenize a string. TOKENIZING TEXT WITH REGULAR EXPRESSIONS Tokenization, or the process of breaking a body of text into a stream of individual symbols, is a common activity in text processing. In chapter 2 we talked a lot about how the PowerShell interpreter has to tokenize a script before it can be executed and we saw this in action in chapters 14 and 15. In the next example, we’re going to look at how we might write a simple tokenizer for basic arithmetic expressions we might find in a programming language. First we need to define the valid tokens in these expressions. We want to allow numbers made up of one or more digits; any of the operators +,-,*, /; and we’ll also allow sequences of spaces. Here’s what the regular expression to match these elements looks like: PS (4) > $pat = [regex] "[0-9]+|\+|\-|\*|/| +" This is a pretty simple pattern using only the alternation operator “|” and the quantifier “+”, which matches one or more instances. Since we used the [regex] cast in the assignment, $pat contains a

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

644 regular expression object. We can use this object directly against an input string by calling its Match() operator. PS (5) > $m = $pat.match("11+2 * 35 -4") The

Match() operator returns a Match object (the full type name is System.Text.RegularExpressions.Match). We can use the Get-Member cmdlet to explore the

full set of members on this object at our leisure, but for now we’re interested in only three members. The first member is the Success property. This will be true if the pattern matched. The second interesting member is the Value member, which will contain the matched value. The final member we’re interested in is the NextMatch() method. Calling this method will step the regular expression engine to the next match in the string, and is the key to tokenizing an entire expression. We can use this method in a while loop to extract the tokens from the source string one at a time. In the example, we keep looping as long the Match object’s Success property is true. Then we display the Value property and call NextMatch() to step to the next token: PS (6) > while ($m.Success) >> { >> $m.value >> $m = $m.NextMatch() >> } >> 11 + 2 * 35 4 In the output, we see each token, one per line in the order they appeared in the original string. We now have a powerful collection of techniques for processing strings. The next step is to apply these techniques to processing files. Of course, we also need to spend some time finding, reading, writing, and copying files. In the next section, we’ll review the basic file abstractions in PowerShell and then look at file processing.

16.3.4 Searching files with the Select-String cmdlet
We've encountered the Select-String cmdlet previously but haven't looked at it in great detail. We'll fix that in this section. The Select-String cmdlet allows us to search through collections of strings or collections of files. It’s similar to the grep command on UNIX-derived systems and the findstr command on Windows. Figure 16.2 shows the parameters on this cmdlet.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

645
The cmdlet name Include all of the matches in the line in the result object Matches property (V2 only) Pattern to search for Specifies what to search – files or strings. Only one of these can parameters can be specified.

Select-String [-Pattern] If specified, searching is -InputObject done case-sensitively [-Path] [-AllMatches] Specifies the character Include lines around encoding scheme to use. [-CaseSensitive] the match in the output Only required if the correct [-Context ] (V2 only) encoding is not auto[-Encoding ] detected. (V2 only) Only return the first [-Exclude ] match in a file. [-Include ] Filter the collection of [-List] files to search using Return items that [-NotMatch] wildcards to select files weren’t matched. (V2 to include or exclude [-SimpleMatch] only) [-Quiet] Use a simple string Return true if there was at least match instead of a one match in the file or string regular expression being searched. when searching

Figure 16.2 This figure shows the Select-String cmdlet parameters. This cmdlet is a powerful tool for extracting information from unstructured text. Parameters marked 'V2 only' where introduced in PowerShell Version 2.

We might ask why this cmdlet is needed—doesn’t the base language do everything it does? The answer is yes, but searching through files is such a common operation that having a cmdlet optimized for this purpose makes sense. Let’s look at some examples. First, we’re going to search through all of the “about_*” topics in the PowerShell installation directory to see if the phrase “wildcard description” is there. PS (1) > Select-String "wildcard description" $pshome/en-US/about*.txt C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_wildcards.help .txt:42: Wildcard Description Example Match No match We see that there is exactly one match, but notice the uppercase letters in the matching string. Let’s rerun the search using the -CaseSensitive parameter. PS (2) > Select-String -Case "wildcard description" ` >> $pshome/en-US/about*.txt >> This time nothing was found. If we alter the case in the pattern to match the target string, then it works again. PS (3) > Select-String -case "Wildcard Description" ` >> $pshome/en-US/about*.txt >> C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_wildcards.help .txt:42: Wildcard Description Example Match No match Searching through files this way can sometimes produce more results than we really need. We'll see how to control what is returned next.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

646 USING THE -LIST AND -QUIET PARAMETERS Now let’s try out the -List parameter. Normally Select-String will find all matches in a file. The -

List switch limits the search to only the first match in a file:
PS (4) > select-string -list wildcard $pshome/en-US/about*.txt C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_aliases.help.txt:147: Property parameter of Format-List with a wildcard character (*) to display C:\Windows\System32\WindowsPowerShell\v1.0\enUS\about_Comment_Based_Help.help.txt:377: Accept wildcard c haracters? C:\Windows\System32\WindowsPowerShell\v1.0\enUS\about_Comparison_Operators.help.txt:90: Description: Match using the wildcard character (*). C:\Windows\System32\WindowsPowerShell\v1.0\enUS\about_Language_Keywords.help.txt:339: switch [-regex|-wildcar d|-exact][-casesensitive] ( pipeline ) C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_operators.help.txt:47: the like operators (-like, -notlike) , which find patterns using wildcard C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_parameters.help.txt:53: wildcard character (*) with the Param eter parameter to find information C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_pipelines.help.txt:272: Accept wildcard characters? t rue C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_properties.help.txt:112: more properties and their values. O r, you can use the wildcard C:\Windows\System32\WindowsPowerShell\v1.0\enUS\about_remote_troubleshooting.help.txt:140: Enable the policy and sp ecify the IPv4 and IPv6 filters. Wildcards (*) are C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_Switch.help.txt:65: switch [-regex|-wildcard|-exact][-cas esensitive] ( pipeline ) C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_wildcards.help.txt:2: about_Wildcards In the result, we see exactly one match per file. Now let’s try using the -Quiet switch. PS (5) > Select-String -Quiet wildcard $pshome/en-US/about*.txt True This switch returns $true if any of the files contained a match and $false if none of them did. We can also combine the two switches so that the cmdlet returns the first match in the set of files. PS (6) > Select-String -quiet -list wildcard $pshome/en-US/about*.txt C:\Windows\System32\WindowsPowerShell\v1.0\en-US\about_aliases.help.txt:147: Property parameter of Format-List with a wildcard character (*) to display SEARCHING A TREE OF FILES If you want to search a more complex set of files, you can pipe the output of Get-ChildItem into the cmdlet and it will search all of these files. Let’s search all of the log files in system32 subdirectory. PS (7) > Get-ChildItem -rec -filter *.log $env:windir\system32 | >> Select-String -List fail | Format-Table path >> Path ---©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

647 C:\WINDOWS\system32\CCM\Logs\ScanWrapper.LOG C:\WINDOWS\system32\CCM\Logs\UpdateScan.log C:\WINDOWS\system32\CCM\Logs\packages\RMSSP1_Client_RTW.log C:\WINDOWS\system32\CCM\Logs\packages\RMSSP1_Client_RTW_BC_In... C:\WINDOWS\system32\wbem\Logs\wbemcore.log C:\WINDOWS\system32\wbem\Logs\wbemess.log C:\WINDOWS\system32\wbem\Logs\wmiadap.log C:\WINDOWS\system32\wbem\Logs\wmiprov.log Notice that we’re only displaying the path. The output of Select-String is objects, as shown: PS (3) > Select-String wildcard $pshome/en-US/about*.txt | >> Get-Member -type property >> TypeName: Microsoft.PowerShell.Commands.MatchInfo Name ---Context Filename IgnoreCase Line LineNumber Matches Path Pattern MemberType ---------Property Property Property Property Property Property Property Property Definition ---------Microsoft.PowerShell.Commands.MatchInfo... System.String Filename {get;} System.Boolean IgnoreCase {get;set;} System.String Line {get;set;} System.Int32 LineNumber {get;set;} System.Text.RegularExpressions.Match[] ... System.String Path {get;set;} System.String Pattern {get;set;}

With these fields, there are many things we can't do by selecting specific members. For example, the file name can be used to open the file in an editor and the Line number can be used to set the line in the file to the matching item. In the next section we'll look at some of the more exotic properties on the

MatchInfo object.
SEARCHING WITH CONTEXTS In the output from Get-Member we can see that there is a context property. This property allows us to have Select-String include the lines before and after the matching line. PS (1) > Get-Help Select-String | >> Out-String -Stream | >> Select-String syntax -Context 3 >> Finds text in strings and files. > SYNTAX Select-String [-Path] [-Pattern] [-All Matches] [-CaseSensitive] [-Context ] [-Encoding ] [-Exclude ] [-Include ] [-List] ] This gets the lines we want but it also gets the lines before the target that we don't want. We can solve this by specifying two numbers to the parameter. The first number is the length of the prefix context and the second is the suffix context. So to get what we want, we just have to specify a prefix context of 0. The PS >> >> >> result looks like: (2) > Get-Help Select-String | Out-String -Stream | Select-String syntax -Context 0,3

> SYNTAX ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

648 Select-String [-Path] [-Pattern] [-All Matches] [-CaseSensitive] [-Context ] [-Encoding ] [-Exclude ] [-Include ] [-List] GETTING ALL MATCHES IN THE LINE Another property on the MatchInfo object is the Matches property. This property is used when the -

AllMatches parameter is specified to the cmdlet. It causes all matches in the line to be returned instead of just the first match. We'll use this to perform the same type of tokenization that we did with regular expressions in section 16.2.2. We'll pipe the expression string into Select-String with the -

AllMatches parameter and the same regular expression we used earlier.
PS (12) > "1 + 2 *3" | >> Select-String -AllMatches "[0-9]+|\+|\-|\*|/| +" | >> foreach { $_.Matches } | Format-Table -AutoSize >> Groups Success Captures Index Length Value ------ ------- -------- ----- ------ ----{1} True {1} 0 1 1 { } True { } 1 1 {+} True {+} 2 1 + { } True { } 3 1 {2} True {2} 4 1 2 { } True { } 5 1 {*} True {*} 6 1 * {3} True {3} 7 1 3 We used the foreach cmdlet to isolate the Matches property and then formatted the output as a table. We can see each of the extracted tokens in the Value field in the Matches object. Using this mechanism, we can effectively and efficiently process things like large log files where the output is essentially formatted as a table. So far in this chapter, we've looked at manipulating text with PowerShell operators, .NET methods and finally the Select-String cmdlet. All of this text has been unstructured text where there is no rigorously defined layout for that text. As a consequence, we’ve had to work fairly hard to extract the information we want out of this text. There are, however, large bodies of structured text, where the format is well-defined in the form of XML documents. In the next section, we’ll look at how to work with XML in PowerShell.

16.4 XML structured text processing
XML (Extensible Markup Language) is becoming more and more important in the computing world. XML is being used for everything from configuration files to log files to databases. PowerShell itself uses XML for its type and configuration files as well as for the help files. Clearly, for PowerShell to be effective, it has to be able to process XML documents effectively. Let’s take a look at how XML is used and supported in PowerShell.

AUTHOR'S NOTE
This section assumes some basic knowledge of XML markup.

We’ll look at the XML object type, as well as the mechanism that .NET provides for searching XML documents. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

649

16.4.1 Using XML as objects
PowerShell supports XML documents as a primitive data type. This means that we can access the elements of an XML document as though they were properties on an object. For example, we'll create a simple XML object. We’ll start with a string that defines a top-level node called “top”. This node contains three descendants “a”, “b”, and “c”, each of which has a value. Let’s turn this string into an object: PS (1) > $d = [xml] "onetwo3" The

[xml] cast takes the string and converts it into an XML object of type System.XML.XmlDocument. This object is then adapted by PowerShell so you can treat it like a regular object. Let’s try this out. First we’ll display the object: PS (2) > $d top --top As we expect, the object displays one top-level property corresponding to the top-level node in the document. Now let’s see what properties this node contains: PS (3) > $d.a PS (4) > $d.top a one b two c 3

There are three properties that correspond to the descendants of top. We can use conventional property notation to look at the value of an individual member: PS (5) > $d.top.a One We can then change the value of this node. It’s as simple as assigning a new value to the node. Let’s assign the string “Four” to the node “a”: PS (6) > $d.top.a = "Four" PS (7) > $d.top.a Four We can see that it’s been changed. But there is a limitation: we can only use an actual string as the node value. The XML object adapter won’t automatically convert non-string objects to strings in an assignment, so we get an error when we try it, as seen in the following: PS (8) > $d.top.a = 4 Cannot set "a" because only strings can be used as values to set XmlNode properties. At line:1 char:8 + $d.top.a $el= $d.CreateElement("d") In text, what we’ve created looks like “”. The tags are there, but they’re empty. Let’s set the element text, the “inner text”: PS (11) > $el.set_InnerText("Hello") #text ----Hello Notice that we’re using the property setter method here. This is because the XML adapter hides the basic properties on the XmlNode object. The other way to set this would be to use the PSBase member like we did with the hashtable example earlier in this chapter. PS (12) > $ne = $d.CreateElement("e") PS (13) > $ne.psbase.InnerText = "World" PS (14) > $d.top.AppendChild($ne) #text ----World Take a look at the revised object. PS (15) > $d.top a b c d e : : : : : one two 3 Hello World

We see that the document now has five members instead of the original three. But what does the string look like now? It would be great if we could simply cast the document back to a string and see what it looks like: PS (16) > [string] $d System.Xml.XmlDocument Unfortunately, as you can see, it isn’t that simple. Instead, we’ll save the document as a file and display it: PS (17) > $d.save("c:\temp\new.xml") PS (18) > type c:\temp\new.xml one two 3 Hello World The result is a nicely readable text file. Now that we know how to add children to a node, how can we add attributes? The pattern is basically the same as with elements. First we create an attribute object. PS (19) > $attr = $d.CreateAttribute("BuiltBy") Next we set the value of the text for that object. Again we use the PSBase member to bypass the adaptation layer. PS (20) > $attr.psbase.Value = "Windows PowerShell" And finally we add it to the top-level document. ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

651 PS (21) > $d.psbase.DocumentElement.SetAttributeNode($attr) #text ----Windows PowerShell Let’s look at the top node once again. PS (22) > $d.top BuiltBy a b c d e : : : : : : Windows PowerShell one two 3 Hello World

We see that the attribute has been added.

AUTHOR'S NOTE
While PowerShell’s XML support is good, there are some issues. The PowerShell formatting code has a bug, where trying to display an XML node that has multiple children with the same name causes an error to be generated by the formatter. For example, the statement [xml] $x = "12"; $x.root will result in an error. This can be disconcerting when you are trying to explore a document. By doing [xml] $x = "12" ; $x.root.item instead, you’ll be able to see the elements without error. Also, for experienced .NET XML and XPath users, there are times when the XML adapter hides properties on an XmlDocument or XmlNode object that the .NET programmer expects to find. In these scenarios, the .PSBase property is the workaround that lets you access the raw .NET object. Finally, some XPath users may get confused by PowerShell’s use of the property operator “.” to navigate an XML document. XPath uses / instead. Despite these issues, for the nonexpert user or for “quick and dirty” scenarios, the XML adapter provides significant benefit in terms of reducing the complexity of working with XML.

It’s time to save the document: PS (23) > $d.save("c:\temp\new.xml") Then retrieve the file. You can see how the attribute has been added to the top node in the document. PS (24) > type c:\temp\new.xml one two 3 Hello We constructed, edited, and saved XML documents, but we haven’t loaded an existing document yet, so that’s the next step.

©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

652

16.4.3 Loading and saving XML files.
At the end of the previous section, we saved an XML document to a file. If we read it back: PS (1) > $nd = [xml] ((Get-Content –read 10kb c:\temp\new.xml) -join "`n") Here’s what we’re doing. We use the Get-Content cmdlet to read the file; however, it comes back as a collection of strings when what we really want is one single string. To do this, we use the -join operator. Once we have the single string, we cast the whole thing into an XML document.

AUTHOR'S NOTE
Here’s a performance tip. By default, Get-Content reads one record at a time. This can be quite slow. When processing large files, you should use the -ReadCount parameter to specify a block size of -1. This will cause the entire file to be loaded and processed at once, which is much faster. Alternatively, here’s another way to load an XML document using the .NET methods: ($nd = [xml]"" ).Load("C:\temp\new.xml") Note that this does require that the full path to the file be specified.

Let’s verify that the document was read properly by dumping out the top-level node and then the child nodes. PS (2) > $nd top --top PS (3) > $nd.top BuiltBy a b c d : : : : : Windows PowerShell one two 3 Hello

All is as it should be. Even the attribute is there. While this is a simple approach and the one we’ll use most often, it’s not necessarily the most efficient approach because it requires loading the entire document into memory. For very large documents or collections of many documents, this may become a problem. In the next section, we’ll look at some alternative approaches that, while more complex, are more memory-efficient. EXAMPLE: THE FORMAT -X MLDOCUMENT FUNCTION The previous method we looked at for loading an XML file is very simple, but not very efficient. It requires that you load the file into memory, make a copy of the file while turning it into a single string, and create an XML document representing the entire file but with all of the overhead of the XML DOM format. A much more space-efficient way to process XML documents is to use the XML reader class. This class streams through the document one element at a time instead of loading the whole thing into memory. We’re going to write a function that will use the XML reader to stream through a document and output it properly indented. An XML pretty-printer, if you will. Here’s what we want the output of this function to look like when it dumps its built-in default document: PS (1) > Format-XmlDocument ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

653 one two 3 Hello Now let’s test our function on a more complex document where there are more attributes and more nesting. Listing 10.3 shows how to create this document.

Creating the text XML document
@' one two 1 2 3 Hello there world '@ > c:\temp\fancy.xml When we run the function, we see PS (2) > Format-XmlDocument c:\temp\fancy.xml one two 1 2 ©Manning Publications Co. Please post comments or corrections to the Author Online forum: http://www.manning-sandbox.com/forum.jspa?forumID=542

Licensed to Andrew M. Tearle

654 3 Hello there world which is pretty close to the original document. The code for the Format-XmlDocument function is shown in listing 16.4.

Listing 16.4 The Format-XmlDocument function function global:Format-XmlDocument ($doc="$PWD\fancy.xml") { $settings = New-Object System.Xml.XmlReaderSettings $doc = (Resolve-Path $doc).ProviderPath $reader = [xml.xmlreader]::create($doc, $settings) $indent=0 function indent ($s) { " "*$indent+$s } while ($reader.Read()) { if ($reader.NodeType -eq [Xml.XmlNodeType]::Element) { $close = $(if ($reader.IsEmptyElement) { "/>" } else { ">" }) if ($reader.HasAttributes) { $s = indent "

Similar Documents

Free Essay

Powershell for the It Administrator Part 1 Lab Manual V1.1

...PowerShell for the IT Administrator, Part 1 Student Lab Manual (v1.1) Microsoft | Services © 2012 Microsoft Corporation Microsoft Confidential ITOE Educate Conditions and Terms of Use Microsoft Confidential - For Internal Use Only This training package is proprietary and confidential, and is intended only for uses described in the training materials. Content and software is provided to you under a Non-Disclosure Agreement and cannot be distributed. Copying or disclosing all or any portion of the content and/or software included in such packages is strictly prohibited. The contents of this package are for informational and training purposes only and are provided "as is" without warranty of any kind, whether express or implied, including but not limited to the implied warranties of merchantability, fitness for a particular purpose, and non-infringement. Training package content, including URLs and other Internet Web site references, is subject to change without notice. Because Microsoft must respond to changing market conditions, the content should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication. Unless otherwise noted, the companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious, and no association with any real company, organization, product, domain name, e-mail address...

Words: 37959 - Pages: 152

Free Essay

Assessment

...Lab 2 Configuring Servers ------------------------------------------------- This lab contains the following exercises and activities: Exercise 2.1Exercise 2.2Exercise 2.3Lab Challenge | Completing Post-Installation Tasks Adding Roles and Features Converting the GUI Interface to Server Core Using the Server Core Interface | * Exercise 2.1 | Completing Post-Installation Tasks | Overview | In this exercise, you complete the tasks necessary to set up a server on which Windows Server 2012 R2 has just been installed. | Mindset | When you purchase a server from an original equipment manufacturer (OEM) with Windows Server 2012 R2 installed, the factory runs a program called Sysprep.exe that prepares the server for distribution by erasing all the user-specific information on the system. | Completion time | 20 minutes | Question1 | Why must you set the time zone to Eastern time, even if that is not where you are currently located?hjhjhjk | 28. Log on to SERVERB using the Administrator account and the password Pa$$w0rd. When Server Manager opens, click Local Server in the left pane, take a screen shot of the Properties window by pressing Alt+Prt Scr, and then paste it into your Lab 2 worksheet file in the page provided...

Words: 673 - Pages: 3

Premium Essay

Programming Languages

...Unit 1 Research Assignment 1: Exploring Programming Languages Computers don't do anything without someone telling them what to do, much like the average teenager. To make the computer do something useful, you must give it instructions in either of the following two ways: * Write a program that tells a computer what to do, step by step, much as you write out a recipe. * Buy a program that someone else has already written that tells the computer what to do. Ultimately, to get a computer to do something useful, you (or somebody else) must write a program. A program does nothing more than tell the computer how to accept some type of input, manipulate that input, and spit it back out again in some form that humans find useful. Table 1 lists some common types of programs, the types of input that they accept, and the output that they produce. Essentially, a program tells the computer how to solve a specific problem. Because the world is full of problems, the number and variety of programs that people can write for computers is practically endless. But to tell a computer how to solve one big problem, you usually must tell the computer how to solve a bunch of little problems that make up the bigger problem. If you want to make your own video game, for example, you need to solve some of the following problems: * Determine how far to move a cartoon figure (such as a car, a spaceship, or a man) on-screen as the user moves a joystick. * Detect whether the cartoon figure...

Words: 3836 - Pages: 16

Premium Essay

Microsoft Network Os Research Assignment

...assignment, I will answer the following questions about the changes made to Windows Server 2008. The questions are as follows: 1. Why does Windows Server 2008 come in different versions? What is the significance of each version? Windows Server 2008 comes in different versions so that everyone has a variety to choose from depending on their individual needs. Windows Server 2008 R2 Standard has more features than the previous versions of Windows Server. Windows Server 2008 R2 Enterprise has all of the features of the Standard edition plus high availability, cost-effective virtualization, increased scalability and enhanced security. Windows Server 2008 R2 Datacenter has large-scale virtualization, enhanced power management, and more options for mobile users. Windows Web Server 2008 R2 uses Web capabilities such as IIS 7.0, ASP.NET, and the Microsoft .NET Framework.  Windows HPC Server 2008 R2 is used for optimizing the power of high-performance computing. 2. What are the new features or enhancements made to Windows Server 2008? How is Windows Server 2008 different from Windows Server 2003? The new changes in Windows Server 2008 are virtualization, a Server Core installation, IIS 7, role-based installation, Read Only Domain Controllers, enhanced terminal services, Network Access Protection, Windows PowerShell and better security. Windows Server 2008 is different from Windows Server 2003 because it has Server Core and Hyper-V. 3. Why is 64-bit architecture an advantage? The advantage...

Words: 561 - Pages: 3

Premium Essay

Research Assignment 1 It 221

...1. Why Windows Server 2008 comes in different versions? What is the significance of each version? WS2K8 comes in several different editions. Each edition is specifically designed for different amounts of server activity. Standard and Enterprise have most of the basic needs as well as all of the new features in WS2K8 but there are a total of 10 editions of WS2K8. What one business needs may not be what another needs so that is why the software comes in a variety of versions. 2. What are the new features or enhancements made to windows Server 2008? How is Windows Server 2008 different from Windows Server 2003? Windows Server 2008 is the largest upgrade to the Windows server line ever. WS2K8 is architected to be functionally componentized. This creates a more secure and reliable system because communication between components is kept to a minimum. WS2K8 has many new features that make major upgrades to the entire server market. The greatest improvement is that WS2K8 is very secure. This alone would be reason enough to upgrade. Server Core, Server Manager, RODC and many others are just some of the key features of WS2K8 that make it superior version to its predecessor. 3. Why is 64-bit architecture an advantage? The terms 64 and 32 bit refer to a way a CPU processes information. 64 bit based versions are faster, manage memory better, and are more secure than 32 bit versions. 64 bit versions have more memory support then 32 bit versions. Programs that are...

Words: 820 - Pages: 4

Premium Essay

Architecture Changes in Windows Server 2008

...Architecture changes in Windows Server 2008 Windows Server 2008 comes in different versions to provide key functionality to support any sized business and IT challenge. Foundation is a cost-effective, entry-level technology foundation targeted at small business owners and IT generalists supporting small businesses. Standard has with built-in, enhanced Web and virtualization capabilities, it is designed to increase the reliability and flexibility of your server infrastructure while helping save time and reduce costs. Enterprise is the advanced server that provides cost-effective and reliable support for mission-critical workloads. Datacenter delivers an enterprise-class platform for deploying business-critical applications and large-scale virtualization on small and large servers. Web Server is a powerful Web application and services platform. Featuring Internet Information Services (IIS) 7.5 and designed exclusively as an Internet-facing server. (Microsoft, 2010) Top 10 New Features in Windows Server 2008 #10: The self-healing NTFS file system: Ever since the days of DOS, an error in the file system meant that a volume had to be taken offline for it to be remedied. In WS2K8, a new system service works in the background that can detect a file system error, and perform a healing process without anyone taking the server down. #9: Parallel session creation: "Prior to Server 2008, session creation was a serial operation," Russinovich reminded us. "If you've got a Terminal Server...

Words: 1157 - Pages: 5

Premium Essay

Research Assignment 1

...Research Assignment 1 Windows Server 2008 comes in different versions to accommodate features useful to different types of users. It is similar to buying a car with different features. The more features or the better, the more money it will cost. Since Microsoft created multiple versions, they were able to maximize their profits by capturing both types of customers (Large business, and smaller ones). The significance of each of the Windows Server 2008 version depends on what it being used for. Some versions are designed for small businesses, medium businesses, or for Itanium-based systems. The IA-64 version has been optimized for high-workload scenarios like database servers. Most editions of Windows Server 2008 are available in x86-64 and IA-32 versions, but Microsoft has already claimed that Windows Server 2008 is the last 32-bit operating system.1 The new features or enhancements made to Windows Server 2008 are Core OS improvements, Active Directory improvements, Policy related improvements, Disk management and file storage improvements, Protocol and cryptography improvements, and improvements due to client-side (Windows Vista) enhancements. The main difference between Windows Server 2008 and Windows Server 2003 is management and virtualization. Windows Server 2008 has the self-healing NTFS file system while Windows Server 2003 does not. With Windows Server 2003, session creation was a serial operation. With Windows Server 2008, its parallel operation has a better...

Words: 842 - Pages: 4

Premium Essay

Course Project

...Research Assignment Part 1 Part 1: 1. Why Windows Server 2008 comes in different versions? What is the significance of each version? Significance of each versions: A. Windows Server 2008 R2 (Foundation) – Is the basic starter server package. B. Windows Server 2008 R2 (Standard) – Is well built, with the average tools and flexibility. C. Windows Server 2008 R2 (Enterprise) – Specialized to meet the needs of very unique applications. D. Windows Server 2008 R2 (Datacenter) – Has a Very large capacity. E. Windows Server 2008 R2 (Web) – For internet applications only. F. Windows Server 2008 R2 (HPC Suite) – It is Customizable to get the most out of the system resources. G. Windows Server 2008 R2 (Itanium Based) – It is the most scalable.   Answers: A reason why Windows comes in different versions is that it can satisfy the customer needs. 2. What are the new features or enhancements made to Windows Server 2008? How is Windows Server 2008 different from Windows Server 2003? New features or enhancements made to Window Server 2008: * Powerful hardware * Scaling features * Reduce power consumption * Hyper –V * R2 Service pack 1 * Expand desktop deployment option with VDI The difference between Window server 2008 and Window server 2003 is that it is easier and more efficient server management. 3. Why is 64-bit architecture and advantage? Answer: 64 –bit architecture is actual design of the CPU.   Some...

Words: 796 - Pages: 4

Premium Essay

Cr Research

...Jennifer Finn ITT 221 Research Assignment #1 Why does Windows Server 2008 come in different versions? What is the significance of each version? Window Server 2008 offers different versions to meet the various needs of its consumers. There are currently five editions available including Standard, Enterprise, Datacenter, Web Server and Itanium-based Systems, each of which are detailed below. Please note: * Windows Server Standard Edition is a full network server meant for small to medium sized businesses and is available in Server Core. * Windows Server Enterprise Edition is a full network server for larger organizations. The Enterprise edition has the same features as the standard edition as well as failover clustering, Active Directory Federation, improved scalability and availability. * Windows Server Datacenter Edition is a full network for the largest organizations dealing with online transactions and data warehousing. * Windows Web Server is an operating system that is limited to hosting websites and delivering web applications. * Windows Server for Itanium-based Servers is a limited server for the Intel Itanium 2 64bit processor based Systems. What are the new features or enhancements made to Windows Server 2008? How is Windows Server 2008 different from Windows Server 2003? Windows Server 2008 comes with a Server Manager to configure and control all roles on the server. It has superior security that protects the entire network, secures remote access and...

Words: 659 - Pages: 3

Free Essay

Pt1420 Unit 6 Assignment 1

...Week 06 Assignments Textbook Reading * Chapter 4 * Chapter 5 sections 5.1, 5.2, and 5.3 (pages 196-201) Week 06 Homework From the Gaddis textbook: * Programming Exercises 2, 6 and 9, on pages 160-161 For the Programming Exercises, design a program means write the pseudocode for the program. Except for Programming Exercise 2, your design should include multiple modules, not just main(). Upload a Microsoft Word document with the pseudocode to your shared PT1420 community website or submit a paper copy to your instructor by the beginning of the Week 7 class. Programing Exercises pg. 160-161 2. Areas of rectangles Module Main() Declare width1 integer = 0 Declare length1 integer = 0 Declare width2 integer = 0 Declare length2 integer = 0 Declare area1 integer = 0 Declare area2 integer = 0 Call rectangle1 (width1, length1, area1) Call rectangle2 (width2, length2, area2) Call comparison (rectangle1, rectangle2) Display “press enter to continue” End module Module rectangle1 (value width1 as integer, value length1 as integer, value area1 as integer) Display "Enter the width of rectangle 1” Input width1 Display "Enter the length of rectangle 1" Input length1 area1 = width1 * length1 End module Module rectangle2 (value width2 as integer, value length2 as integer, value area2 as integer) Display "Enter the width of rectangle 2” Input width2 Display "Enter the length of rectangle 2" Input length2 area2 = width2 * length2 End module ...

Words: 764 - Pages: 4

Free Essay

It Management

...Overview This lab will give you with the opportunity to experience a number of different options for upgrading or migrating a private cloud from Windows Server 2012 to Windows Server 2012 R2. There are four major upgrade or migration options available, each with different properties, those options include: cross version live migration, export and import, copy cluster role wizard and in place upgrade. The ultimate goal of this lab is to take a Hyper-V cluster running with Windows Server 2012 and fully upgrade the cluster to Windows Server 2012 R2. The following document exists to facilitate upgrade in this lab environment. Hyper-V ClusterTwo Node Windows Server 2012 ClusterThree LUNs * Quorum LUN * Two CSV’sNetwork Teaming With Three NetworksManagementSubnet: 172.16.0.0/16IP: 172.16.<pod#>.<server#> i.e. 172.16.10.1DNS: 172.16.0.1; 172.16.0.2Vlan: default (untagged)Note cluster IP is .3ClusterSubnet: 192.168.1xx.0/24IP: 192.168.1<pod>.<server#> i.e. 192.168.110.1Vlan: 2922Live MigrationSubnet: 192.168.2xx.0/24IP: 192.168.2<pod>.<server#> i.e. 192.168.210.1Vlan: 2923Virtual MachinesFour virtual machines * Copy Cluster Role Wizard * Export and Import * Live Migration * In Place Upgrade | | User Names and Password Username: mvp.lab\mvp<pod#> Example mvp.lab\mvp10 Password: P@ssw0rd (zero for the ‘o’) Overview of Upgrade Options At the end of the document is the TechNet documentation that was published on TechNet...

Words: 1312 - Pages: 6

Premium Essay

It 221 Research 1

...1. Why windows Server 2008 comes in different versions? What is the significance of each version? Each Windows Server 2008 R2 edition provides key functionality to support any size business and IT challenge. Use the information below to decide which edition best meets your business needs. Windows Server 2008 R2 Datacenter Edition is optimized for your large-scale virtualization of workloads that require the highest levels of scalability, reliability, and availability to support large, mission-critical applications. With unlimited virtualization use rights and a hypervisor-based virtualization technology, Windows Server 2008 R2 Datacenter provides both flexibility and cost savings. Windows Server 2008 R2 Datacenter also supports the memory and processing needs of large-scale, business-critical workloads such as ERP, databases, server consolidations, and custom and line-of-business applications. Windows Server 2008 R2 Enterprise Edition provides you with high levels of system uptime and the scalability to support the growth of mission-critical applications. It also provides a cost-effective way to realize the benefits of virtualization. Providing uninterrupted business services to employees, vendors, and partners around the clock has become a critical business factor for global businesses. Remote employees and international customers and partners need to have continuous access to systems and data. A disruption in services can result in diminished productivity...

Words: 5150 - Pages: 21

Free Essay

Week 4 Best Answer

...Macdonald NTC/324 Feb 15, 2016 Professor Jason Kaluzny Lesson 13 Best Answer What is the key difference between groups and Organizational Units (OUs)? a. Because groups are independent from domain structure, its members may be located anywhere in the domain or outside the domain. b. You cannot apply Group Policy settings directly to group objects. c. OUs are containers, whereas groups are not containers. d. There is essentially no difference between OUs and groups. 2. An Active Directory functional level must be low enough to ensure interoperability between domain controllers running different versions of Windows Server. How does the functional level affect the AD forest? a. Higher functional level means more efficient AD communication. b. Higher functional level means few Global Catalog errors. c. Lower functional level means fewer features available. d. Lower functional level means time to upgrade the lowest servers. 3. What is the primary reason for creating different sites on an Active Directory network? a. To create geographical divisions within the Active Directory b. To provide another boundary when applying Group Policy settings (along with domains and OUs) c. To provide a layer of access control between objects in differing sites d. To control the amount of traffic passing over the relatively slow and expensive WAN links between locations 4. What is the simplest way for administrators to upgrade their Active Directory Domain Services...

Words: 1019 - Pages: 5

Free Essay

Powershell Regular Expressions

...Regular Expression Characters For a list of regular expression characters: Get-Help About_Regular_Expressions PowerShell regular expression characters: PowerShell also supports the character classes from .NET regular expressions: As well as the ,NET regular expression quantifiers: Using Regular Expressions The $Matches variable holds all the values that match the regular expression. In this example we are looking for a single (the first) “word” character: If we want to find multiple word characters we would do this: $matches only holds the word “PowerShell” because the space is not a word character so the matching stops. To find more than one word we need to put a (literal) space between our word expressions: We can do the same for numbers. Or, to match more than one numeral: To find an IP address, say in a log file: So far we have only been matching one thing at a time. Using the .NET regex object gives us a lot more flexibility. [regex] specifies that the expression “\w*” is not a string object but an object of type regex. Regex objects have a “matches” property that produces the output above. Notice that the word (or space) is contained in a “value property. We can see only the items matched, without the index, length, etc. by iterating through the matches and specifying only the value property. Note that $var and $var1 are still populated from the previous example. We have been populating strings to search through...

Words: 388 - Pages: 2

Premium Essay

Window Server 2008

...1. Why Windows server 2008 comes in different versions? What is the significance of each version? • Windows server 2008 comes in different versions for different people different Business and different likes or dislikes for each Editions. • The significance of each version is that it fits the needs for each Individual Person are Business, for example a Business with many Cluster would most likely use Windows HPC Server 2008. 2. What are the new features or enhancements made to Windows Server 2008? How is Windows Server 2008 different from Windows 2003? • Technical, security, management and administrative features new to Windows Vista such as the rewritten networking stack (native IPv6, native wireless, speed and security improvements); improved image-based installation, deployment and recovery; improved diagnostics, monitoring, event logging and reporting tools; new security features such as Bit-Locker and ASLR; improved Windows Firewall with secure default configuration; .NET Framework 3.0 technologies, specifically Windows Communication Foundation, Microsoft Message Queuing and Windows Workflow Foundation; and the core kernel, memory and file system improvements. Processors and memory devices are modeled as Plug and Play devices, to allow hot-plugging of these devices. • 2008 has self-healing NTFS file system 2003 do not, In 2003 session creation was a serial operation but with 2008 its parallel.2008 has a better Clean service shutdown.In WS2K8, that 20-second...

Words: 1129 - Pages: 5