Windows PowerShell Scripting One Line at a Time

Don Jones

In past columns, I’ve stressed the fact that Windows PowerShell is a shell. It’s meant to be used interactively—not unlike the cmd.exe (or command prompt) shell you’re probably already familiar with. Still, Windows PowerShell does support a scripting language—one that’s more robust than the batch language of cmd.exe. And it

is as powerful as, if not more powerful than, languages like VBScript. However, the fact that Windows PowerShell™ is an interactive shell makes the scripting remarkably easy to learn. In fact, you can develop scripts interactively within the shell, making it possible to write scripts one line at a time, immediately seeing the results of your efforts.

This iterative scripting technique also makes debugging easier. Since you see the results of your script right away, you can quickly revise it when things don't go as expected.

In this column, I'll walk you through an example of interactive scripting in Windows PowerShell. I will create a script that reads service names from a text file and sets each service's startup mode to be Disabled.

What I want you to take away from this walkthrough is the concept of building Windows PowerShell scripts in small pieces, rather than trying to tackle an entire script in one big chunk. You can take whatever administrative task you need to accomplish and break it down into its component pieces and then figure out how to make each piece work independently. Windows PowerShell provides excellent ways to tie those pieces together, as you'll see. And you'll find that by working on small pieces at a time, you'll have an easier time developing the final script.

Reading Names from a File

Figuring out how to read text files in Windows PowerShell can be frustrating. If I run Help *file*, all I get is help for the Out-File cmdlet, which sends text to a file, rather than reading from it. No help at all! However, Windows PowerShell cmdlet names do follow a kind of logic I can use to my advantage. When Windows PowerShell retrieves something, the cmdlet name usually starts with Get. So I run Help Get* to display those cmdlets, and then scroll through the list and see Get-Content. Looks promising! So I run Help Get-Content to see more about what it does (see Figure 1), and it looks like it'll meet my needs.

Figure 1 Running Help Get-Content for More Information

Figure 1 Running Help Get-Content for More Information (Click the image for a larger view)

Windows PowerShell treats nearly everything as an object, and text files are no exception. A text file is technically a collection of lines, with each line in the file acting as a sort of independent object. So, if I've created a text file named C:\services.txt, and filled it with service names (placing each name on its own line within the file), Windows PowerShell can read the names individually using the Get-Content cmdlet. Since the idea of this walkthrough is to show how scripts can be developed interactively, I'll start by just running Get-Content, giving it the name of my text file, and seeing what happens:

PS C:\> get-content c:\services.txt
messenger
alerter
PS C:\>

As expected, Windows PowerShell reads the file and displays the name. Of course, simply displaying the names isn't really what I want, but now I know that Get-Content works the way I expect it to work.

Changing a Service

The next thing I want to do is change a service's startup mode. Again, I start by trying to find the right cmdlet. So I run Help *Service*. This returns a short list, and the Set-Service cmdlet looks like the only one that will fit my needs. I want to test this cmdlet to make sure I understand how it works before I try incorporating it into a script. Running Help Set-Service shows me how the cmdlet should work, and a quick test confirms it:

PS C:\> set-service messenger -startuptype
    disabled
PS C:\>

Combining the Parts

Now I need to combine the ability to read service names from a file with the Set-Service cmdlet, and that's where the powerful pipeline capabilities of Windows PowerShell come into play. With the pipeline, the output from one cmdlet can be passed as the input to a second cmdlet. The pipeline passes entire objects. In the event that a collection of objects is put into the pipeline, each object is passed through the pipeline individually. That means the output of Get-Content-which, remember, is a collection of objects-can be piped to Set-Service. Because Get-Content is passing a collection, each object-or line of text-in the collection is piped to Set-Service individually. The result is that Set-Service is executed once for each line in my text file. The command looks like this:

PS C:\> get-content c:\services.txt | 
 set-service -startuptype disabled
PS C:\>

Here's what happens:

  1. The Get-Content cmdlet executes, reading the entire file. Each line in the file is treated as a unique object, and together they are a collection of objects.
  2. The collection of objects is piped to Set-Service.
  3. The pipeline executes the Set-Service cmdlet once for each input object. For each execution, the input object is passed to Set-Service as the first parameter of the cmdlet, which is the service name.
  4. Set-Service executes, using the input object for its first parameter and whatever other parameters are specified, the -startuptype parameter, in this case.

Interestingly, I've actually accomplished my task at this point-and I haven't even written a script yet. This same action would be difficult to achieve in the Cmd.exe shell, and it would take a dozen lines of code in VBScript. But Windows PowerShell handles all of this in one line. Still, I'm not entirely done. As you can see, my command isn't providing a lot of status output or feedback. Apart from the lack of errors, it's difficult to see if anything actually happened. Now that I've mastered the functionality needed to accomplish my task, I can start making it better-looking by getting into actual scripting.

Windows PowerShell Prompt Here by Michael Murgolo

One of the most popular Microsoft® PowerToys for Windows® (and one of my favorites) is the Open Command Window Here tool. Available as part of the Microsoft PowerToys for Windows XP or in the Windows Server® 2003 Resource Kit Tools, Open Command Window Here lets you right-click on a folder or drive in Windows Explorer to open a command window that points to the selected folder.

As I was learning Windows PowerShell, I found myself wishing it had the same functionality. So I grabbed Open Command Window Here’s setup .inf file, cmdhere.inf, from the Windows Server 2003 Resource Kit Tools and modified it to create a Windows PowerShell Prompt Here context menu. This .inf file is included with the original blog post upon which this sidebar is based (available at leeholmes.com/blog/PowerShellPromptHerePowerToy.aspx). To install the tool, just right-click on the .inf file and select Install.

When creating the Windows PowerShell version of this tool, I discovered that the original had a bug—if Open Command Window Here was uninstalled, it would leave behind a dead context menu entry. As a result, I have provided an updated version of cmdhere.inf, also available from the original blog post.

Both of these PowerToys take advantage of the fact that these context menu entries are configured in the registry under keys associated with the Directory and Drive object types. This is done in the same way that context menu actions are associated with file types. For example, when you right-click on a .txt file in Windows Explorer, you get several actions (such as Open, Print, and Edit) at the top of the list. To understand how these items are configured, let’s look at the HKEY_CLASSES_ROOT registry hive.

If you open the Registry Editor and expand the HKEY_CLASSES_ROOT branch, you will see keys named for file types such as .doc, .txt, and the like. If you click on the .txt key, you’ll see that the (Default) value is txtfile (see Figure A). This is the object type associated with a .txt file. If you scroll down and expand the txtfile key, and then expand the shell key under that, you will see keys named for some of the context menu entries for a .txt file. (You will not see all of them because there are other methods for creating context menus.) Under each of these is a command key. The (Default) value under the command key is the command line that Windows executes when you select that context menu item.

The tools for both the cmd prompt and Windows PowerShell prompt use this technique to configure the Open Command Window Here and Windows PowerShell Prompt Here context menu entries. There are no file types associated with drives and directories, but there are Drive and Directory keys under HKEY_CLASSES_ROOT associated with those object.

Figure A Context Menu Entries Are Configured in the Registry

Figure A Context Menu Entries Are Configured in the Registry (Click the image for a larger view)

Michael Murgolo is a Senior Infrastructure Consultant for Microsoft Consulting Services. He is focused in the areas of operating systems, deployment, network services, Active Directory, systems management, automation, and patch management.

Scripting Interactively

One thing I need to do is have multiple cmdlets execute for each service that's listed in C:\services.txt. That way, I can output the service name and any other information I like, so that I can track the script's progress.

Before, I used the pipeline to pass objects from one cmdlet to another. This time, however, I'm going to use a more script-like technique called a Foreach construct (which I introduced in last month's column). The Foreach construct can accept a collection of objects, and it will execute multiple cmdlets for each object in the collection. I designate a variable that represents the current object each time through the loop. For example, the construct might start like this:

foreach ($service in get-content c:\services.txt)

I'm still executing that same Get-Content cmdlet to retrieve the contents of the text file. This time, I'm asking the Foreach construct to loop through the collection of objects returned by Get-Content. The loop executes one time for each object, and the current object is placed in the variable $service. Now I just need to specify the code that I want executed within the loop. I'll start by attempting to duplicate my original one-line command (this will minimize complexity and ensure I don't lose any functionality):

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> }
>>
PS C:\>

There's some interesting stuff happening here. Notice that I ended the Foreach construct's first line with an opening curly brace. Everything within the opening and closing curly brace is considered to be inside the Foreach loop and will execute once for each object in the input collection. After I typed { and pressed Enter, notice that Windows PowerShell changed its prompt to >> (see Figure 2). This indicates that it knows I've begun a construct of some kind, and that it's waiting for me to finish. I then typed my Set-Service cmdlet. This time, I used $service as the first parameter, since $service represents the current service name read from my text file. On the next line, I conclude the construct with a closing curly brace. After pressing Enter twice, Windows PowerShell immediately executes my code.

Figure 2 Windows Powershell Knows a Construct Has Been Started

Figure 2 Windows Powershell Knows a Construct Has Been Started (Click the image for a larger view)

That's right, immediately. What I've typed looks like a script, but Windows PowerShell is actually executing it live, and it isn't stored in a text file anywhere. Now I'll retype the entire thing, adding a line of code to output the current service name:

PS C:\> foreach ($service in get-content c:\services.txt) {
>> set-service $service -startuptype disabled
>> "Disabling $service"
>> }
>>
Disabling messenger
Disabling alerter
PS C:\>

Notice that I'm asking it to display something, and I've enclosed that something inside double quotation marks. The quotes tell Windows PowerShell that this is a string of text, not another command. However, when you use double quotes-as opposed to single quotes-Windows PowerShell scans the text string for any variables. If it finds any, it substitutes the variable's actual value for the variable's name. Thus, when it executes this code, you can see that the current service name is being displayed.

This Still Isn't a Script!

Up to now, I've been using Windows PowerShell interactively, which is a great way to immediately see results from what I'm writing. However, at some point retyping these lines of code will become tedious. This is why Windows PowerShell can also run scripts. In fact, Windows PowerShell scripts follow the most literal definition of the word script: Windows PowerShell basically just reads in the script text file and "types" each line it finds, exactly as if you were typing the lines manually. That means everything I've done to this point can be pasted into a script file. So, I'll use Notepad to create a file named disableservices.ps1, and paste the following into it:

foreach ($service in get-content c:\services.txt) {
 set-service $service -startuptype disabled
 "Disabling $service"
 }

I've placed this file in a folder named C:\test. Now I'll try to run it from within Windows PowerShell:

PS C:\test> disableservices
'disableservices' is not recognized as a cmdlet, function, operable program, or
<script file.
At line:1 char:15
+ disableservices <<<<
PS C:\test>

Whoops. What went wrong? Windows PowerShell is focused on the C:\test folder, but it didn't find my script. Why? Due to security constraints, Windows PowerShell is designed not to run any script from the current folder, thereby preventing any script from hijacking an operating system command. For example, I can't create a script named dir.ps1 and have it override the normal dir command. If I need to run a script from the current folder, I have to specify a relative path:

PS C:\test> ./disableservices
The file C:\test\disableservices.ps1 cannot be loaded. 
The execution of scripts is disabled on this system. 
Please see "get-help about_signing" for more details.
At line:1 char:17
+ ./disableservices <<<<
PS C:\test>

Now what? It's still not working. I've gotten the path right, but Windows PowerShell says it can't run scripts. That's because, by default, Windows PowerShell is not able to run scripts. Again, this is a security precaution designed to prevent the problems we've all had with VBScript. Malicious scripts can't execute under Windows PowerShell by default because no scripts can execute by default! In order to run my script, I need to explicitly change the execution policy:

PS C:\test> set-executionpolicy remotesigned

The RemoteSigned execution policy allows unsigned scripts to execute from the local computer-downloaded scripts still have to be signed in order to run. A better policy is AllSigned, which only executes scripts that have been digitally signed with a certificate issued by a trusted publisher. However, I don't have a certificate handy so I'm unable to sign my scripts, making RemoteSigned a rather good choice for this situation. Now I'll try running my script again:

PS C:\test> ./disableservices
Disabling messenger
Disabling alerter
PS C:\test>

I want to point out that the RemoteSigned execution policy we're using isn't a great choice; it's merely a good one. But there is a much better solution. It would be far more secure to obtain a code-signing certificate, use the Windows PowerShell Set-AuthenticodeSignature cmdlet to sign my scripts, and set the execution policy to the much safer AllSigned policy.

There's also another policy, Unrestricted, which you definitely should avoid. This policy allows all scripts-even malicious scripts from remote locations-to run unfettered on your computer, which can put you in a very dangerous position. Consequently, I don't recommend using the Unrestricted policy for any reason.

Immediate Results

The interactive scripting capabilities in Windows PowerShell enable you to quickly prototype scripts or even just small chunks of scripts. You get immediate results, so you can easily tweak your scripts to get exactly the results you want. When you're finished, you can move your code into a .ps1 file, making it permanent and easily accessible in the future. And remember: ideally, you should digitally sign those .ps1 files so that you can leave Windows PowerShell set to the AllSigned execution policy, the safest policy that allows script execution.

Don Jones is the lead Scripting Guru for SAPIEN Technologies and coauthor of Windows PowerShell: TFM (see www.SAPIENPress.com). Contact Don at don@sapien.com.

© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.