Skip to main content

Resources

Create a String in PowerShell

Problem

You want to create a variable that holds text.

Solution

Use PowerShell string variables to give you a way to store and work with text.

To define a string that supports variable expansion and escape characters in its definition, surround it with double quotes:

$myString = "Hello World"

To define a literal string (that does not support variable expansion or escape characters), surround it with single quotes:

$myString = 'Hello World'

Discussion

String literals come in two varieties: literal (nonexpanding) and expanding strings. To create a literal string, place single quotes ($myString = 'Hello World') around the text. To create an expanding string, place double quotes ($myString = "Hello World") around the text.

In a literal string, all the text between the single quotes becomes part of your string. In an expanding string, PowerShell expands variable names (such as $myString) and escape sequences (such as `n) with their values (such as the content of $myString and the newline character, respectively).

One exception to the “all text in a literal string is literal” rule comes from the quote characters themselves. In either type of string, PowerShell lets you to place two of that string’s quote characters together to add the quote character itself:

$myString = "This string includes ""double quotes"" because it combined quote characters." $myString = 'This string includes ''single quotes'' because it combined quote characters.'

This helps prevent escaping atrocities that would arise when you try to include a single quote in a singlequoted string. For example:

$myString = 'This string includes ' + "'" + 'single quotes' + "'"

This example shows how easy PowerShell makes it to create new strings by adding other strings together. This is an attractive way to build a formatted report in a script but should be used with caution.

Due to the way that the .NET Framework (and therefore PowerShell) manages strings, adding information to the end of a large string this way causes noticeable performance problems. 

Get the Current Location in Windows PowerShell

Problem

You want to determine the current location.

Solution

To determine the current location, use the GetLocation cmdlet: PS >GetLocation

Path

C:\temp PS >$currentLocation = (GetLocation).Path PS >$currentLocation C:\temp

Discussion

One problem that sometimes impacts scripts that work with the .NET Framework is that PowerShell’s concept of “current location” isn’t always the same as the PowerShell.exe process’s “current directory.” Take, for example:

PS >GetLocation

Path

C:\temp

PS >GetProcess | ExportCliXml processes.xml PS >$reader = NewObject Xml.XmlTextReader processes.xml PS >$reader.BaseURI file:///C:/Documents and Settings/Lee/processes.xml

PowerShell keeps these concepts separate because it supports multiple pipelines of execution. The processwide current directory affects the entire process, so you would risk corrupting the environment of all background tasks as you navigate around the shell if that changed the process’s current directory.

When you use filenames in most .NET methods, the best practice is to use fully qualified pathnames. The ResolvePath cmdlet makes this easy:

PS >GetLocation

Path

C:\temp

PS >GetProcess | ExportCliXml processes.xml PS >$reader = NewObject Xml.XmlTextReader (ResolvePath processes.xml) PS >$reader.BaseURI file:///C:/temp/processes.xml

If you want to access a path that doesn’t already exist, use the JoinPath in combination with the GetLocation cmdlet: PS >JoinPath (GetLocation) newfile.txt

C:\temp\newfile.txt

List Currently Running Windows PowerShell Processes

Problem

You want to see which processes are running on the system.

Solution

To retrieve the list of currently running processes, use the GetProcess cmdlet: PS >GetProcess

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

274 6 1328 3940 33 1084 alg 85 4 3816 6656 57 5.67 3460 AutoHotkey 50 2 2292 1980 14 384.25 1560 BrmfRsmg 71 3 2520 4680 35 0.42 2592 cmd

946 7 3676 6204 32 848 csrss 84 4 732 2248 22 3144 csrss 68 4 936 3364 30 0.38 3904 ctfmon

243 7 3648 9324 48 2.02 2892 Ditto (...)

Discussion

The GetProcess cmdlet retrieves information about all processes running on the system. Because these are rich .NET objects (of the type System.Diagnostics.Process), advanced filters and operations are easier than ever before.

For example, to find all processes using more than 100 MB of memory:

PS >GetProcess | WhereObject { $_.WorkingSet gt 100mb }

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

1458 29 83468 105824 273 323.80 3992 BigBloatedApp

To group processes by company:

PS >GetProcess | GroupObject Company

Count Name Group

39 {alg, csrss, csrss, dllhost...} 4 {AutoHotkey, Ditto, gnuserv, mafwTray} 1 Brother Industries, Ltd. {BrmfRsmg}

19 Microsoft Corporation {cmd, ctfmon, EXCEL, explorer...} 1 Free Software Foundation {emacs} 1 Microsoft (R) Corporation {FwcMgmt}

(...)

Or perhaps to sort by start time (with the most recent first):

PS >GetProcess | Sort Descending StartTime | SelectObject First 10

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName

1810 39 53616 33964 193 318.02 1452 iTunes 675 6 41472 50180 146 49.36 296 powershell 1240 35 48220 58860 316 167.58 4012 OUTLOOK

305 8 5736 2460 105 21.22 3384 WindowsSearch... 464 7 29704 30920 153 6.00 3680 powershell

1458 29 83468 105824 273 324.22 3992 iexplore 478 6 24620 23688 143 17.83 3548 powershell 222 8 8532 19084 144 20.69 3924 EXCEL

14 2 396 1600 15 0.06 2900 logon.scr 544 18 21336 50216 294 180.72 2660 WINWORD

These advanced tasks become incredibly simple due to the rich amount of information that PowerShell returns for each process. For more information about the GetProcess cmdlet, type GetHelp GetProcess.

Variables and Objects in Windows PowerShell

As touched on in Chapter 2, Pipelines , PowerShell makes life immensely easier by keeping information in its native form: objects. Users expend most of their effort in traditional shells just trying to resuscitate information that the shell converted from its native form to plain text. Tools have evolved that ease the burden of working with plain text, but that job is still significantly more difficult than it needs to be.

Since PowerShell builds on Microsoft’s .NET Framework, native information comes in the form of .NET objects—packages of information, and functionality closely related to that information.

Let’s say that you want to get a list of running processes on your system. In other shells, your command (such as tlist.exe or /bin/ps) generates a plaintext report of the running processes on your system. To work with that output, you send it through a bevy of text processing tools—if you are lucky enough to have them available.

PowerShell’s GetProcess cmdlet generates a list of the running processes on your system. In contrast to other shells, though, these are fullfidelity System.Diagnostics. Process objects straight out of the .NET Framework. The .NET Framework documentation describes them as objects that “... [provide] access to local and remote processes, and [enable] you to start and stop local system processes.” With those objects in hand, PowerShell makes it trivial for you to access properties of objects (such as their process name or memory usage) and to access functionality on these objects (such as stopping them, starting them, or waiting for them to exit).

Program: Add a Graphical User Interface to Your Script

While the techniques provided in the rest of this chapter are usually all you need, it is sometimes helpful to provide a graphical user interface to interact with the user.

Since PowerShell fully supports traditional executables, simple programs can usually fill this need. If creating a simple program in an environment such as Visual Studio is inconvenient, you can often use PowerShell to create these applications directly.

Example 129 demonstrates the techniques you can use to develop a Windows Forms application using PowerShell scripting alone.

Example 129. SelectGraphicalFilteredObject.ps1

############################################################################## ## ## SelectGraphicalFilteredObject.ps1 ## ## Display a Windows Form to help the user select a list of items piped in. ## Any selected items get passed along the pipeline. ## ## ie: ## ## PS >dir | SelectGraphicalFilteredObject ##

Example 129. SelectGraphicalFilteredObject.ps1 (continued)

## Directory: Microsoft.PowerShell.Core\FileSystem::C:\

##

##

## Mode
LastWriteTime Length Name

##

## d
10/7/2006
4:30 PM
Documents and Settings

## d
3/18/2007
7:56 PM
Windows

##

 

##############################################################################

$objectArray = @($input)

## Ensure that they've piped information into the script if($objectArray.Count eq 0) {

WriteError "This script requires pipeline input." return }

## Load the Windows Forms assembly [void] [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

## Create the main form $form = NewObject Windows.Forms.Form $form.Size = NewObject Drawing.Size @(600,600)

## Create the listbox to hold the items from the pipeline $listbox = NewObject Windows.Forms.CheckedListBox $listbox.CheckOnClick = $true $listbox.Dock = "Fill" $form.Text = "Select the list of objects you wish to pass down the pipeline" $listBox.Items.AddRange($objectArray)

## Create the button panel to hold the OK and Cancel buttons $buttonPanel = NewObject Windows.Forms.Panel $buttonPanel.Size = NewObject Drawing.Size @(600,30) $buttonPanel.Dock = "Bottom"

## Create the Cancel button, which will anchor to the bottom right $cancelButton = NewObject Windows.Forms.Button $cancelButton.Text = "Cancel" $cancelButton.DialogResult = "Cancel" $cancelButton.Top = $buttonPanel.Height $cancelButton.Height 5 $cancelButton.Left = $buttonPanel.Width $cancelButton.Width 10 $cancelButton.Anchor = "Right"

## Create the OK button, which will anchor to the left of Cancel $okButton = NewObject Windows.Forms.Button $okButton.Text = "Ok" $okButton.DialogResult = "Ok" $okButton.Top = $cancelButton.Top $okButton.Left = $cancelButton.Left $okButton.Width 5 $okButton.Anchor = "Right"

Example 129. SelectGraphicalFilteredObject.ps1 (continued)

## Add the buttons to the button panel $buttonPanel.Controls.Add($okButton) $buttonPanel.Controls.Add($cancelButton)

## Add the button panel and list box to the form, and also set ## the actions for the buttons $form.Controls.Add($listBox) $form.Controls.Add($buttonPanel) $form.AcceptButton = $okButton $form.CancelButton = $cancelButton $form.Add_Shown( { $form.Activate() } )

## Show the form, and wait for the response $result = $form.ShowDialog()

## If they pressed OK (or Enter,) go through all the ## checked items and send the corresponding object down the pipeline if($result eq "OK") {

foreach($index in $listBox.CheckedIndices) { $objectArray[$index] } }

Discover Registry Settings for Programs in Windows PowerShell

Problem

You want to automate the configuration of a program, but that program does not document its registry configuration settings.

Solution

To discover a registry setting for a program, use Sysinternals’ Process Monitor to observe registry access by that program. Process Monitor is available from http:// www.microsoft.com/technet/sysinternals/FileAndDisk/processmonitor.mspx.

Discussion

In an ideal world, all programs would fully support commandline administration and configuration through PowerShell cmdlets. Many programs do not, however, so the solution is to look through their documentation in the hope that they list the registry keys and properties that control their settings. While many programs document their registry configuration settings, many still do not.

Although these programs may not document their registry settings, you can usually observe their registry access activity to determine the registry paths they use. To illustrate this, we will use the Sysinternals’ Process Monitor to discover PowerShell’s execution policy configuration keys. Although PowerShell documents these keys and makes its automated configuration a breeze, it illustrates the general technique.

Tell Process Monitor to begin capturing information

Switch to the Process Monitor window, and then press CtrlE (or click the magnifying glass icon). Process Monitor now captures all registry access for the program in question.

Manually set the configuration option

Click OK, Apply, or whatever action it takes to actually complete the program’s configuration. For the PowerShell example, this means pressing Enter.

Tell Process Monitor to stop capturing information

Switch again to the Process Monitor window, and then press CtrlE (or click the magnifying glass icon). Process Monitor now no longer captures the application’s activity.

Review the capture logs for registry modification

The Process Monitor window now shows all registry keys that the application interacted with when it applied its configuration setting.

Press CtrlF (or click the binoculars icon); then search for RegSetValue. Process Monitor highlights the first modification to a registry key.

Press Enter (or doubleclick the highlighted row) to see the details about this specific registry modification. In this example, we can see that PowerShell changed the value of the ExecutionPolicy property (under HKLM:\Software\Microsoft\PowerShell\1\ ShellIds\Microsoft.PowerShell)to RemoteSigned. Press F3 to see the next entry that corresponds to a registry modification.

Automate these registry writes

Now that you know all registry writes that the application performed when it updated its settings, judgment and experimentation will help you determine which modifications actually represent this setting. Since PowerShell only performed one registry write (to a key that very obviously represents the execution policy), the choice is pretty clear in this example.

Once you’ve discovered the registry keys, properties, and values that the application uses to store its configuration data, you can use the techniques discussed in Article

PS >$key = "HKLM:\Software\Microsoft\PowerShell\1\" + >> "ShellIds\Microsoft.PowerShell" >> PS >SetItemProperty $key ExecutionPolicy AllSigned PS >GetExecutionPolicy AllSigned PS >SetItemProperty $key ExecutionPolicy RemoteSigned PS >GetExecutionPolicy RemoteSigned

Access and Manage Your Console History in Windows PowerShell

Problem

After working in the shell for awhile, you want to invoke commands from your history, view your command history, and save your command history.

Solution

To get the most recent commands from your session, use the GetHistory cmdlet:

GetHistory

To rerun a specific command from your session history, provide its Id to the InvokeHistory cmdlet: InvokeHistory Id

To increase (or limit) the number of commands stored in your session history, assign a new value to the $MaximumHistoryCount variable: $MaximumHistoryCount = Count

To save your command history to a file, pipe the output of GetHistory to the ExportCliXml cmdlet:

GetHistory | ExportCliXml Filename

To add a previously saved command history to your current session history, call the ImportCliXml cmdlet and then pipe that output to the AddHistory cmdlet:

ImportCliXml Filename | AddHistory

Discussion

Unlike the console history hotkeys the GetHistory cmdlet produces rich objects that represent information about items in your history. Each object contains that item’s ID, command line, start of execution time, and end of execution time.

Once you know the ID of a history item (as shown in the output of GetHistory), you can pass it to InvokeHistory to execute that command again.

The IDs provided by the GetHistory cmdlet differ from the IDs given by the Windows console common history hotkeys (such as F7), because their history management techniques differ.

By default, PowerShell stores only the last 64 entries of your command history. If you want to raise or lower this amount, set the $MaximumHistoryCount variable to the size you desire. To make this change permanent, set the variable in your PowerShell profile script. To clear your history, either restart the shell, or temporarily set the $MaximumHistoryCount variable to 1.

Determine Whether an Array Contains an Item

Problem

You want to determine whether an array or list contains a specific item.

Solution

To determine whether a list contains a specific item, use the –contains operator:

PS >"Hello","World" contains "Hello" True PS >"Hello","World" contains "There" False

Discussion

The –contains operator is a useful way to quickly determine whether a list contains a specific element. To search a list for items that instead match a pattern, use the –match or –like operators.

Program: Create a ZIP Archive

Discussion

When transporting or archiving files, it is useful to store those files in an archive. ZIP archives are the most common type of archive, so it would be useful to have a script to help manage them.

For many purposes, traditional commandline ZIP archive utilities may fulfill your needs. If they do not support the level of detail or interaction that you need for administrative tasks, a more programmatic alternative is attractive.

Example 177 lets you create ZIP archives simply by piping files into them. It requires that you have the SharpZipLib installed, which you can obtain from http:// www.icsharpcode.net/OpenSource/SharpZipLib/ .

Example 177. NewZipFile.ps1

############################################################################## ## ## NewZipFile.ps1 ## ## Create a Zip file from any files piped in. Requires that ## you have the SharpZipLib installed, which is available from ## http://www.icsharpcode.net/OpenSource/SharpZipLib/ ## ## ie: ## ## PS >dir *.ps1 | NewZipFile scripts.zip d:\bin\ICSharpCode.SharpZipLib.dll ## PS >"readme.txt" | NewZipFile docs.zip d:\bin\ICSharpCode.SharpZipLib.dll ## ##############################################################################

Example 177. NewZipFile.ps1 (continued)

param( $zipName = $(throw "Please specify the name of the file to create."), $libPath = $(throw "Please specify the path to ICSharpCode.SharpZipLib.dll.") )

## Load the Zip library [void] [Reflection.Assembly]::LoadFile($libPath) $namespace = "ICSharpCode.SharpZipLib.Zip.{0}"

## Create the Zip File $zipName =

$executionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($zipName) $zipFile = NewObject ($namespace f "ZipOutputStream") ([IO.File]::Create($zipName)) $zipFullName = (ResolvePath $zipName).Path

[byte[]] $buffer = NewObject byte[] 4096

## Go through each file in the input, adding it to the Zip file ## specified foreach($file in $input) {

## Skip the current file if it is the zip file itself if($file.FullName eq $zipFullName) {

continue }

## Convert the path to a relative path, if it is under the current location $replacePath = [Regex]::Escape( (GetLocation).Path + "\" ) $zipName = ([string] $file) replace $replacePath,""

## Create the zip entry, and add it to the file $zipEntry = NewObject ($namespace f "ZipEntry") $zipName $zipFile.PutNextEntry($zipEntry)

$fileStream = [IO.File]::OpenRead($file.FullName) [ICSharpCode.SharpZipLib.Core.StreamUtils]::Copy($fileStream, $zipFile, $buffer) $fileStream.Close()

}

## Close the file $zipFile.Close()

Manage an Exchange 2007 Server

Pointandclick management has long been the stereotype of Windows administration. While it has always been possible to manage portions of Windows (or other applications) through the command line, support usually comes as an afterthought. Once all the administration support has been added to the user interface, the developers of an application might quickly cobble together a COM API or commandline tool if you are lucky. If you aren’t lucky, they might decide only to publish some registry key settings, or perhaps nothing at all.

This inequality comes almost naturally from implementing a management model as an afterthought: with a fully functional user interface complete, very few application developers deem fully functional scriptable administration to be a high priority.

And then there’s Exchange 2007.

In contrast to those who cobble together a management model only after completing the user interface, the Exchange 2007 team wrote their management infrastructure first. In Exchange 2007, it’s not only a firstclass feature—it’s a way of life. Exchange 2007 includes nearly 400 cmdlets to let you manage Exchange systems. Not only is the magnitude stunning, but its breadth and depth is as well. To guarantee full coverage by way of PowerShell cmdlets, the Exchange Management Console user interface builds itself completely on top of PowerShell cmdlets.

Any command that affects the Exchange environment does so through one of the included Exchange Management cmdlets.

The benefit of this model is immense. It doesn’t matter whether you are working with users, groups, mailboxes, or anything else in Exchange: you can automate it all.

Program: Get-PageUrls

When working with HTML, it is common to require advanced regular expressions that separate the content you care about from the content you don’t. Aperfect example of this is extracting all the HTML links from a web page.

Links come in many forms, depending on how lenient you want to be. They may be wellformed according to the various HTML standards. They may use relative paths, or they may use absolute paths. They may place double quotes around the URL, or they may place single quotes around the URL. If you’re really unlucky, they may accidentally include quotes on only one side of the URL.

Example 92 demonstrates some approaches for dealing with this type of advanced parsing task. Given a web page that you’ve downloaded from the Internet, it extracts all links from the page and returns a list of the URLs in that page. It also fixes URLs that were originally written as relative URLs (for example, /file.zip) to include the server from which they originated.

Example 92. GetPageUrls.ps1

############################################################################## ## GetPageUrls.ps1 ## ## Parse all of the URLs out of a given file. ## ## Example: ## GetPageUrls microsoft.html http://www.microsoft.com ## ############################################################################## param(

## The filename to parse [string] $filename = $(throw "Please specify a filename."),

## The URL from which you downloaded the page. ## For example, http://www.microsoft.com [string] $base = $(throw "Please specify a base URL."),

## The Regular Expression pattern with which to filter ## the returned URLs [string] $pattern = ".*"

)

## Load the System.Web DLL so that we can decode URLs [void] [Reflection.Assembly]::LoadWithPartialName("System.Web")

## Defines the regular expression that will parse an URL ## out of an anchor tag. $regex = "\s*a\s*[^>]*?href\s*=\s*[`"']*([^`"'>]+)[^>]*?>"

## Parse the file for links function Main {

## Do some minimal source URL fixups, by switching backslashes to ## forward slashes $base = $base.Replace("\", "/")

if($base.IndexOf("://") lt 0)

Example 92. GetPageUrls.ps1 (continued)

{ throw "Please specify a base URL in the form of " + "http://server/path_to_file/file.html" }

## Determine the server from which the file originated. This will ## help us resolve links such as "/somefile.zip" $base = $base.Substring(0,$base.LastIndexOf("/") + 1) $baseSlash = $base.IndexOf("/", $base.IndexOf("://") + 3) $domain = $base.Substring(0, $baseSlash)

## Put all of the file content into a big string, and ## get the regular expression matches $content = [String]::Join(' ', (getcontent $filename)) $contentMatches = @(GetMatches $content $regex)

foreach($contentMatch in $contentMatches) { if(not ($contentMatch match $pattern)) { continue }

$contentMatch = $contentMatch.Replace("\", "/")

## Hrefs may look like: ## ./file ## file ## ../../../file ## /file ## url ## We'll keep all of the relative paths, as they will resolve. ## We only need to resolve the ones pointing to the root. if($contentMatch.IndexOf("://") gt 0) {

$url = $contentMatch } elseif($contentMatch[0] eq "/") {

$url = "$domain$contentMatch" } else {

$url = "$base$contentMatch" $url = $url.Replace("/./", "/") }

## Return the URL, after first removing any HTML entities [System.Web.HttpUtility]::HtmlDecode($url) } }

function GetMatches([string] $content, [string] $regex)

Example 92. GetPageUrls.ps1 (continued)

{ $returnMatches = NewObject System.Collections.ArrayList

## Match the regular expression against the content, and ## add all trimmed matches to our return list $resultingMatches = [Regex]::Matches($content, $regex, "IgnoreCase") foreach($match in $resultingMatches) {

$cleanedMatch = $match.Groups[1].Value.Trim() [void] $returnMatches.Add($cleanedMatch) }

$returnMatches }

. Main

Program: Connect-WebService

Although screen scraping (parsing the HTML of a web page) is the most common way to obtain data from the Internet, web services are becoming increasingly common. Web services provide a significant advantage over HTML parsing, as they are much less likely to break when the web designer changes minor features in a design.

The only benefit to web services isn’t their more stable interface, however. When working with web services, the .NET Framework lets you generate proxies that let you interact with the web service as easily as you would work with a regular .NET object. That is because to you, the web service user, these proxies act almost exactly the same as any other .NET object. To call a method on the web service, simply call a method on the proxy.

The primary differences you will notice when working with a web service proxy (as opposed to a regular .NET object) are the speed and Internet connectivity requirements. Depending on conditions, a

method call on a web service proxy could easily take several seconds to complete. If your computer (or the remote computer) experiences network difficulties, the call might even return a network error message (such as a timeout) instead of the information you had hoped for.

Example 93 lets you connect to a remote web service if you know the location of its service description file (WSDL). It generates the web service proxy for you, allowing you to interact with it as you would any other .NET object.

Example 93. ConnectWebService.ps1

############################################################################## ## ConnectWebService.ps1 ## ## Connect to a given web service, and create a type that allows you to ## interact with that web service. ## ## Example: ## ## $wsdl = "http://terraserver.microsoft.com/TerraService2.asmx?WSDL" ## $terraServer = ConnectWebService $wsdl ## $place = NewObject Place ## $place.City = "Redmond" ## $place.State = "WA" ## $place.Country = "USA" ## $facts = $terraserver.GetPlaceFacts($place) ## $facts.Center ############################################################################## param(

[string] $wsdlLocation = $(throw "Please specify a WSDL location"), [string] $namespace, [Switch] $requiresAuthentication)

## Create the web service cache, if it doesn't already exist if(not (TestPath Variable:\Lee.Holmes.WebServiceCache)) {

${GLOBAL:Lee.Holmes.WebServiceCache} = @{} }

## Check if there was an instance from a previous connection to ## this web service. If so, return that instead. $oldInstance = ${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation] if($oldInstance) {

$oldInstance return }

## Load the required Web Services DLL [void] [Reflection.Assembly]::LoadWithPartialName("System.Web.Services")

## Download the WSDL for the service, and create a service description from ## it. $wc = NewObject System.Net.WebClient

if($requiresAuthentication) { $wc.UseDefaultCredentials = $true }

Example 93. ConnectWebService.ps1 (continued)

$wsdlStream = $wc.OpenRead($wsdlLocation)

## Ensure that we were able to fetch the WSDL if(not (TestPath Variable:\wsdlStream)) {

return }

$serviceDescription = [Web.Services.Description.ServiceDescription]::Read($wsdlStream) $wsdlStream.Close()

## Ensure that we were able to read the WSDL into a service description if(not (TestPath Variable:\serviceDescription)) {

return }

## Import the web service into a CodeDom $serviceNamespace = NewObject System.CodeDom.CodeNamespace if($namespace) {

$serviceNamespace.Name = $namespace }

$codeCompileUnit = NewObject System.CodeDom.CodeCompileUnit $serviceDescriptionImporter = NewObject Web.Services.Description.ServiceDescriptionImporter $serviceDescriptionImporter.AddServiceDescription(

$serviceDescription, $null, $null) [void] $codeCompileUnit.Namespaces.Add($serviceNamespace) [void] $serviceDescriptionImporter.Import(

$serviceNamespace, $codeCompileUnit)

## Generate the code from that CodeDom into a string $generatedCode = NewObject Text.StringBuilder $stringWriter = NewObject IO.StringWriter $generatedCode $provider = NewObject Microsoft.CSharp.CSharpCodeProvider $provider.GenerateCodeFromCompileUnit($codeCompileUnit, $stringWriter, $null)

## Compile the source code. $references = @("System.dll", "System.Web.Services.dll", "System.Xml.dll") $compilerParameters = NewObject System.CodeDom.Compiler.CompilerParameters $compilerParameters.ReferencedAssemblies.AddRange($references) $compilerParameters.GenerateInMemory = $true

$compilerResults = $provider.CompileAssemblyFromSource($compilerParameters, $generatedCode)

## Write any errors if generated. if($compilerResults.Errors.Count gt 0)

Example 93. ConnectWebService.ps1 (continued)

{ $errorLines = "" foreach($error in $compilerResults.Errors) {

$errorLines += "`n`t" + $error.Line + ":`t" + $error.ErrorText }

WriteError $errorLines

return } ## There were no errors. Create the web service object and return it. else {

## Get the assembly that we just compiled $assembly = $compilerResults.CompiledAssembly

## Find the type that had the WebServiceBindingAttribute. ## There may be other "helper types" in this file, but they will ## not have this attribute $type = $assembly.GetTypes() |

WhereObject { $_.GetCustomAttributes( [System.Web.Services.WebServiceBindingAttribute], $false) }

if(not $type)

{ WriteError "Could not generate web service proxy." return

}

## Create an instance of the type, store it in the cache, ## and return it to the user. $instance = $assembly.CreateInstance($type)

## Many services that support authentication also require it on the ## resulting objects if($requiresAuthentication) {

if(@($instance.PsObject.Properties | where { $_.Name eq "UseDefaultCredentials" }).Count eq 1) { $instance.UseDefaultCredentials = $true } }

${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation] = $instance

$instance }

Export Command Output As a Web Page

Problem

You want to export the results of a command as a web page so that you can post it to a web server.

Solution

Use PowerShell’s ConvertToHtml cmdlet to convert command output into a web page. For example, to create a quick HTML summary of PowerShell’s commands:

PS >$filename = "c:\temp\help.html" PS > PS >$commands = GetCommand | Where { $_.CommandType ne "Alias" } PS >$summary = $commands | GetHelp | Select Name,Synopsis PS >$summary | ConvertToHtml | SetContent $filename

Discussion

When you use the ConvertToHtml cmdlet to export command output to a file, PowerShell generates an HTML table that represents the command output. In the table, it creates a row for each object that you provide. For each row, PowerShell creates columns to represent the values of your object’s properties.

The ConvertToHtml cmdlet lets you customize this table to some degree through parameters that allow you to add custom content to the head and body of the resulting page.

For more information about the ConvertToHtml cmdlet, type GetHelp ConvertToHtml.

Manage and Change the Attributes of a File

Problem

You want to update the ReadOnly, Hidden, or System attributes of a file.

Solution

Most of the time, you will want to use the familiar attrib.exe program to change the attributes of a file:

attrib +r test.txt

attrib s test.txt To set only the ReadOnly attribute, you can optionally set the IsReadOnly property on the file:

$file = GetItem test.txt $file.IsReadOnly = $true

To apply a specific set of attributes, use the Attributes property on the file:

$file = GetItem test.txt $file.Attributes = "ReadOnlyNotContentIndexed"

Directory listings show the attributes on a file, but you can also access the Mode or Attributes property directly:

PS >$file.Attributes = "ReadOnly","System","NotContentIndexed" PS >$file.Mode rs PS >$file.Attributes ReadOnly, System, NotContentIndexed

Discussion

When the GetItem or GetChildItem cmdlets retrieve a file, the resulting file has an Attributes property. This property doesn’t offer much in addition to the regular attrib.exe program, although it does make it easier to set the attributes to a specific state.

Be aware that setting the Hidden attribute on a file removes it from most default views. If you want to retrieve it after hiding it, most commands require a –Force parameter. Similarly, setting the ReadOnly

attribute on a file causes most write operations on that file to fail unless you call that command with the –Force parameter.

If you want to add an attribute to a file using the Attributes property (rather than attrib.exe for some reason), this is how you would do that:

$file = GetItem test.txt $readOnly = [IO.FileAttributes] "ReadOnly" $file.Attributes = $file.Attributes bor $readOnly

Program: List Logon or Logoff Scripts for a Windows PowerShell User

The Group Policy system in Windows stores logon and logoff scripts under the registry keys HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\r SID> \Scripts\Logon and HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\ State\r SID>\Scripts\Logoff. Each key has a subkey for each group policy object that applies. Each of those child keys has another level of keys that correspond to individual scripts that apply to the user.

This can be difficult to investigate when you don’t know the SID of the user in ques tion, so Example 241 automates the mapping of username to SID, as well as all the registry manipulation tasks required to access this information.

Example 241. GetUserLogonLogoffScript.ps1

############################################################################## ## ## GetUserLogonLogoffScript.ps1 ## ## Get the logon or logoff scripts assigned to a specific user ## ## ie: ## ## PS >GetUserLogonLogoffScript LEEDESK\LEE Logon ## ##############################################################################

param( $username = $(throw "Please specify a username"), $scriptType = $(throw "Please specify the script type") )

## Verify that they've specified a correct script type $scriptOptions = "Logon","Logoff" if($scriptOptions notcontains $scriptType) {

$error = "Cannot convert value {0} to a script type. " + "Specify one of the following values and try again. " + "The possible values are ""{1}""."

$ofs = ", " throw ($error f $scriptType, ([string] $scriptOptions)) }

## Find the SID for the username $account = NewObject System.Security.Principal.NTAccount $username $sid =

$account.Translate([System.Security.Principal.SecurityIdentifier]).Value

## Map that to their group policy scripts $registryKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\" + "Group Policy\State\$sid\Scripts"

## Go through each of the policies in the specified key foreach($policy in GetChildItem $registryKey\$scriptType) {

## For each of the scripts in that policy, get its script name ## and parameters foreach($script in GetChildItem $policy.PsPath) {

GetItemProperty $script.PsPath | Select Script,Parameters } }

Convert Numbers Between Bases in Windows PowerShell

Problem

You want to convert a number to a different base.

Solution

The PowerShell scripting language allows you to enter both decimal and hexadecimal numbers directly. It does not natively support other number bases, but its support for interaction with the .NET Framework enables conversion both to and from binary, octal, decimal, and hexadecimal.

To convert a hexadecimal number into its decimal representation, prefix the number by 0x to enter the number as hexadecimal: PS >$myErrorCode = 0xFE4A PS >$myErrorCode

65098 To convert a binary number into its decimal representation, supply a base of 2 to the [Convert]::ToInt32() method:

PS >[Convert]::ToInt32("10011010010", 2) 1234 To convert an octal number into its decimal representation, supply a base of 8 to the [Convert]::ToInt32() method:

PS >[Convert]::ToInt32("1234", 8) 668 To convert a number into its hexadecimal representation, use either the [Convert] class or PowerShell’s format operator:

PS >## Use the [Convert] class PS >[Convert]::ToString(1234, 16) 4d2

PS >## Use the formatting operator PS >"{0:X4}" f 1234 04D2

To convert a number into its binary representation, supply a base of 2 to the [Convert]::ToString() method:

PS >[Convert]::ToString(1234, 2) 10011010010 To convert a number into its octal representation, supply a base of 8 to the

[Convert]::ToString() method: PS >[Convert]::ToString(1234, 8) 2322

Discussion

It is most common to want to convert numbers between bases when you are dealing with numbers that represent binary combinations of data, such as the attributes of a file.

Simple Files in Windows PowerShell

When administering a system, you naturally spend a significant amount of time working with the files on that system. Many of the things you want to do with these files are simple: get their content, search them for a pattern, or replace text inside them.

For even these simple operations, PowerShell’s objectoriented flavor adds several unique and powerful twists.

Create Your Own PowerShell Cmdlet

Problem

You want to write your own PowerShell cmdlet.

Discussion

As mentioned previously in “Structured Commands (Cmdlets)” in A Guided Tour of Windows PowerShell, PowerShell cmdlets offer several significant advantages over traditional executable programs. From the user’s perspective, cmdlets are incredibly consistent—and their support for strongly typed objects as input makes them powerful. From the cmdlet author’s perspective, cmdlets are incredibly easy to write when compared to the amount of power they provide. Creating and exposing a new commandline parameter is as easy as creating a new public property on a class. Supporting a rich pipeline model is as easy as placing your implementation logic into one of three standard method overrides.

While a full discussion on how to implement a cmdlet is outside the scope of this book, the following steps illustrate the process behind implementing a simple cmdlet.

For more information on how to write a PowerShell cmdlet, see the MSDN topic, “How to Create a Windows PowerShell Cmdlet,” available at http://msdn2.microsoft. com/enus/library/ms714598.aspx .

Step 1: Download the Windows SDK

The Windows SDK contains samples, tools, reference assemblies, templates, documentation, and other information used when developing PowerShell cmdlets. It is available by searching for “Microsoft Windows SDK” on http://download.microsoft. com and downloading the latest Windows Vista SDK.

Step 2: Create a file to hold the cmdlet and snapin source code

Create a file called InvokeTemplateCmdletCommand.cs with the content from Example 1512 and save it on your hard drive.

Example 1512. InvokeTemplateCmdletCommand.cs

using System; using System.ComponentModel; using System.Management.Automation;

/* To build and install:

1) SetAlias csc $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\csc.exe

2) SetAlias installutil `

$env:WINDIR\Microsoft.NET\Framework\v2.0.50727\installutil.exe 3) $ref = [PsObject].Assembly.Location csc /out:TemplateSnapin.dll /t:library InvokeTemplateCmdletCommand.cs /r:$ref

4) installutil TemplateSnapin.dll

5) AddPSSnapin TemplateSnapin

To run:

PS >InvokeTemplateCmdlet

To uninstall:

installutil /u TemplateSnapin.dll

*/

namespace Template.Commands

{ [Cmdlet("Invoke", "TemplateCmdlet")] public class InvokeTemplateCmdletCommand : Cmdlet {

[Parameter(Mandatory=true, Position=0, ValueFromPipeline=true)] public string Text {

get { return text; }

Example 1512. InvokeTemplateCmdletCommand.cs (continued)

set { text = value;

} } private string text;

protected override void BeginProcessing() { WriteObject("Processing Started"); }

protected override void ProcessRecord() { WriteObject("Processing " + text); }

protected override void EndProcessing() { WriteObject("Processing Complete."); } }

[RunInstaller(true)] public class TemplateSnapin : PSSnapIn {

public TemplateSnapin()

: base() { }

///

The snapin name which is used for registration

public override string Name {

get { return "TemplateSnapin";

} } ///

Gets vendor of the snapin.

public override string Vendor {

get { return "Template Vendor";

} } ///

Gets description of the snapin.

public override string Description

Example 1512. InvokeTemplateCmdletCommand.cs (continued)

{ get {

return "This is a snapin that provides a template cmdlet."; } } } }

Step 3: Compile the snapin

APowerShell cmdlet is a simple .NET class. The DLL that contains the compiled cmdlet is called a snapin.

SetAlias csc $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\csc.exe $ref = [PsObject].Assembly.Location csc /out:TemplateSnapin.dll /t:library InvokeTemplateCmdletCommand.cs /r:$ref

Step 4: Install and register the snapin

Once you have compiled the snapin, the next step is to register it. Registering a snapin gives PowerShell the information it needs to let you use it. This command requires administrative permissions.

SetAlias installutil ` $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\installutil.exe installutil TemplateSnapin.dll

Step 5: Add the snapin to your session

Although step 4 registered the snapin, PowerShell doesn't add the commands to your active session until you call the AddPsSnapin cmdlet. AddPsSnapin TemplateSnapin

Step 6: Use the snapin

Once you've added the snapin to your session, you can call commands from that snapin as though you would call any other cmdlet.

PS >"Hello World" | InvokeTemplateCmdlet Processing Started Processing Hello World Processing Complete.

Program: Import PowerShell Users in Bulk to Active Directory

When importing several users into Active Directory, it quickly becomes tiresome to do it by hand (or even to script the addition of each user onebyone). To solve this problem, we can put all our data into a CSV, and then do a bulk import from the information in the CSV.

Example 232 supports this in a flexible way. You provide a container to hold the user accounts and a CSV that holds the account information. For each row in the CSV, the script creates a user from the data in that row. The only mandatory column is a CN column to define the common name of the user. Any other columns, if present, represent other Active Directory attributes you want to define for that user.

Example 232. ImportADUser.ps1

############################################################################## ## ## ImportAdUser.ps1 ## ## Create users in Active Directory from the content of a CSV. ## ## For example: ## $container = "LDAP://localhost:389/ou=West,ou=Sales,dc=Fabrikam,dc=COM" ## ImportADUser.ps1 $container .\users.csv ## ## In the user CSV, One column must be named "CN" for the user name.

Example 232. ImportADUser.ps1 (continued)

## All other columns represent properties in Active Directory for that user. ## ## For example: ## CN,userPrincipalName,displayName,manager ## MyerKen,Ken.Myer@fabrikam.com,Ken Myer, ## DoeJane,Jane.Doe@fabrikam.com,Jane Doe,"CN=MyerKen,OU=West,OU=Sales,DC=..." ## SmithRobin,Robin.Smith@fabrikam.com,Robin Smith,"CN=MyerKen,OU=West,OU=..." ## ##############################################################################

param( $container = $(throw "Please specify a container (such as " +

"LDAP://localhost:389/ou=West,ou=Sales,dc=Fabrikam,dc=COM)"), $csvPath = $(throw "Please specify the path to the users CSV") )

## Bind to the container $userContainer = [adsi] $container

## Ensure that the container was valid if(not $userContainer.Name) {

WriteError "Could not connect to $container" return }

## Load the CSV $users = @(ImportCsv $csvPath) if($users.Count eq 0) {

return }

## Go through each user from the CSV foreach($user in $users) {

## Pull out the name, and create that user $username = $user.CN $newUser = $userContainer.Create("User", "CN=$username")

## Go through each of the properties from the CSV, and set its value ## on the user foreach($property in $user.PsObject.Properties) {

## Skip the property if it was the CN property that sets the ## user name if($property.Name eq "CN") {

continue }

## Ensure they specified a value for the property

Example 232. ImportADUser.ps1 (continued)

if(not $property.Value) { continue }

## Set the value of the property $newUser.Put($property.Name, $property.Value) }

## Finalize the information in Active Directory $newUser.SetInfo() }

Manage Large Conditional Statements with Switches in Windows PowerShell

Problem

You want to find an easier or more compact way to represent a large if ... elseif ... else conditional statement.

Solution

Use PowerShell’s switch statement to more easily represent a large if ... elseif ... else conditional statement.

For example:

$temperature = 20 switch($temperature)

{ { $_ lt 32 } 32 { $_ le 50 } { $_ le 70 } default }
{ "Below Freezing"; break }{ "Exactly Freezing"; break }{ "Cold"; break }{ "Warm"; break }{ "Hot" }

Discussion

PowerShell’s switch statement lets you easily test its input against a large number of comparisons. The switch statement supports several options that allow you to configure how PowerShell compares the input against the conditions—such as with a wildcard, regular expression, or even arbitrary script block. Since scanning through the text in a file is such a common task, PowerShell’s switch statement supports that directly. These additions make PowerShell switch statements a great deal more powerful than those in C and C++.

Although used as a way to express large conditional statements more cleanly, a switch statement operates much like a large sequence of if statements, as opposed to a large sequence of if ... elseif ... elseif ... else statements. Given the input that you provide, PowerShell evaluates that input against each of the comparisons in the switch statement. If the comparison evaluates to true, PowerShell then executes the script block that follows it. Unless that script block contains a break statement, PowerShell continues to evaluate the following comparisons.

Repeat Operations with Loops in Windows PowerShell

Problem

You want to execute the same block of code more than once.

Solution

Use one of PowerShell’s looping statements (for, foreach, while, and do), or PowerShell’s ForeachObject cmdlet to run a command or script block more than once. For example:

for loop for($counter = 1; $counter le 10; $counter++) { "Loop number $counter" }

foreach loop foreach($file in dir) { "File length: " + $file.Length }

ForeachObject cmdlet GetChildItem | ForeachObject { "File length: " + $_.Length }

while loop $response = "" while($response ne "QUIT") { $response = ReadHost "Type something" }

do..while loop $response = "" do { $response = ReadHost "Type something" } while($response ne "QUIT")

Discussion

Although any of the looping statements can be written to be functionally equivalent to any of the others, each lends itself to certain problems.

You usually use a for loop when you need to perform an operation an exact number of times. Because using it this way is so common, it is often called a counted for loop.

You usually use a foreach loop when you have a collection of objects and want to visit each item in that collection. If you do not yet have that entire collection in memory (as in the dir collection from the foreach example above), the ForeachObject cmdlet is usually a more efficient alternative.

Unlike the foreach loop, the ForeachObject cmdlet allows you to process each element in the collection as PowerShell generates it. This is an important distinction; asking PowerShell to collect the entire output of a large command (such as GetContent hugefile.txt) in a foreach loop can easily drag down your system.

A handy shortcut to repeat an operation on the command line is: PS >1..10 | foreach { "Working" } Working

Working Working Working Working Working Working Working Working Working

The while and do..while loops are similar, in that they continue to execute the loop as long as its condition evaluates to true.A while loop checks for this before ever running your script block, while a do..while loop checks the condition after running your script block.

Add a Pause or Delay in PowerShell

Problem

You want to pause or delay your script or command.

Solution

To pause until the user presses ENTER, use the ReadHost cmdlet:

PS >ReadHost "Press ENTER" Press ENTER:

To pause until the user presses a key, use the ReadKey() method on the $host object: PS >$host.UI.RawUI.ReadKey() To pause a script for a given amount of time, use the StartSleep cmdlet: PS >StartSleep 5 PS >StartSleep Milliseconds 300

Discussion

When you want to pause your script until the user presses a key or for a set amount of time, the ReadHost and StartSleep cmdlets are the two you are most likely to use. In other situations, you may sometimes want to write a loop in your script that runs at a constant speed—such as once per minute, or 30 times per second. That is typically a difficult task, as the commands in the loop might take up a significant amount of time, or even an inconsistent amount of time.

In the past, many computer games suffered from solving this problem incorrectly. To control their game speed, game developers added commands to slow down their game. For example, after much tweaking and fiddling, the developers might realize that the game plays correctly on a typical machine if they make the computer count to one million every time it updates the screen. Unfortunately, these commands (such as counting) depend heavily on the speed of the computer. Since a fast computer can count to 1 million much more quickly than a slow computer, the game ends up running much quicker (often to the point of incomprehensibility) on faster computers!

To make your loop run at a regular speed, you can measure how long the commands in a loop take to complete, and then delay for whatever time is left, as shown in Example 41.

Example 41. Running a loop at a constant speed

$loopDelayMilliseconds = 650 while($true) {

$startTime = GetDate

## Do commands here

"Executing"

$endTime = GetDate $loopLength = ($endTime $startTime).TotalMilliseconds $timeRemaining = $loopDelayMilliseconds $loopLength

if($timeRemaining gt 0) { StartSleep Milliseconds $timeRemaining } }

For more information about the StartSleep cmdlet, type GetHelp StartSleep.

Strings and Unstructured Text in PowerShell

Creating and manipulating text has long been one of the primary tasks of scripting languages and traditional shells. In fact, Perl (the language) started as a simple (but useful) tool designed for text processing. It has grown well beyond those humble roots, but its popularity provides strong evidence of the need it fills.

In textbased shells, this strong focus continues. When most of your interaction with the system happens by manipulating the textbased output of programs, powerful text processing utilities become crucial. These text parsing tools such as awk, sed, and grep form the keystones of textbased systems management.

In PowerShell’s objectbased environment, this traditional tool chain plays a less critical role. You can accomplish most of the tasks that previously required these tools much more effectively through other PowerShell commands. However, being an objectoriented shell does not mean that PowerShell drops all support for text processing. Dealing with strings and unstructured text continues to play an important part in a system administrator’s life. Since PowerShell lets you to manage the majority of your system in its full fidelity (using cmdlets and objects,) the text processing tools can once again focus primarily on actual text processing tasks.

Program: Search the Windows Start Menu in PowerShell

When working at the command line, you might want to launch a program that is normally found only on your Start menu. While you could certainly click through the Start menu to find it, you could also search the Start menu with a script, as shown in Example 144.

Example 144. SearchStartMenu.ps1

############################################################################## ## ## SearchStartMenu.ps1 ## ## Search the Start Menu for items that match the provided text. This script ## searches both the name (as displayed on the Start Menu itself,) and the ## destination of the link. ## ## ie: ## ## PS >SearchStartMenu "Character Map" | InvokeItem ## PS >SearchStartMenu "network" | SelectFilteredObject | InvokeItem ## ##############################################################################

param( $pattern = $(throw "Please specify a string to search for.") )

## Get the locations of the start menu paths $myStartMenu = [Environment]::GetFolderPath("StartMenu") $shell = NewObject Com WScript.Shell $allStartMenu = $shell.SpecialFolders.Item("AllUsersStartMenu")

## Escape their search term, so that any regular expression ## characters don't affect the search $escapedMatch = [Regex]::Escape($pattern)

## Search for text in the link name dir $myStartMenu *.lnk rec | ? { $_.Name match "$escapedMatch" } dir $allStartMenu *.lnk rec | ? { $_.Name match "$escapedMatch" }

## Search for text in the link destination dir $myStartMenu *.lnk rec | WhereObject { $_ | SelectString "\\[^\\]*$escapedMatch\." Quiet } dir $allStartMenu *.lnk rec | WhereObject { $_ | SelectString "\\[^\\]*$escapedMatch\." Quiet }

Windows PowerShell Processes

Working with system processes is a natural aspect of system administration. It is also the source of most of the regular expression magic and kung fu that makes system administrators proud. After all, who wouldn’t boast about this Unix oneliner to stop all processes using more than 100 MB of memory:

ps el | awk '{ if ( $6 > (1024*100)) { print $3 } }' | grep v PID | xargs kill

While helpful, it also demonstrates the inherently fragile nature of pure text processing. For this command to succeed, it must:

  • Depend on the ps command to display memory usage in column 6.
  • Depend on column 6 of the ps command’s output to represent the memory usage in kilobytes.
  • Depend on column 3 of the ps command’s output to represent the process id.
  • Remove the header column from the ps command’s output.

Since PowerShell’s GetProcess cmdlet returns information as highly structured .NET objects, fragile text parsing becomes a thing of the past:

GetProcess | WhereObject { $_.WorkingSet gt 100mb } | StopProcess –WhatIf

If brevity is important, PowerShell defines aliases to make most commands easier to type:

gps | ? { $_.WS gt 100mb } | kill –WhatIf

Automate Windows PowerShell Data-Intensive Tasks

Problem

You want to invoke a simple task on large amounts of data.

Solution

If only one piece of data changes (such as a server name or user name), store the data in a text file. Use the GetContent cmdlet to retrieve the items, and then use the ForeachObject cmdlet (which has the standard aliases foreach and %) to work with each item in that list.

Example 25. Using information from a text file to automate dataintensive tasks

PS >GetContent servers.txt SERVER1 SERVER2 PS >$computers = GetContent servers.txt PS >$computers | ForeachObject { GetWmiObject Win32_OperatingSystem Computer $_ }

SystemDirectory : C:\WINDOWS\system32 Organization : BuildNumber : 2600 Version : 5.1.2600

SystemDirectory : C:\WINDOWS\system32

Example 25. Using information from a text file to automate dataintensive tasks (continued)

Organization : BuildNumber : 2600 Version : 5.1.2600

If it becomes cumbersome (or unclear) to include the actions in the ForeachObject cmdlet, you can also use the foreach

Example 26. Using the foreach scripting keyword to make a looping statement easier to read

$computers = GetContent servers.txt

foreach($computer in $computers)

{

## Get the information about the operating system from WMI

$system = GetWmiObject Win32_OperatingSystem Computer $computer

## Determine if it is running Windows XP

if($system.Version eq "5.1.2600")

{

"$computer is running Windows XP" } }

If several aspects of the data change per task (for example, both the WMI class and the computer name for computers in a large report), create a CSV file with a row for each task. Use the ImportCsv cmdlet to import that data into PowerShell, and then use properties of the resulting objects as multiple sources of related data.

Example 27. Using information from a CSV to automate dataintensive tasks

PS >GetContent WmiReport.csv ComputerName,Class LEEDESK,Win32_OperatingSystem LEEDESK,Win32_Bios PS >$data = ImportCsv WmiReport.csv PS >$data

ComputerName Class

LEEDESK Win32_OperatingSystem LEEDESK Win32_Bios

PS >$data | >> ForeachObject { GetWmiObject $_.Class Computer $_.ComputerName } >>

SystemDirectory : C:\WINDOWS\system32 Organization :

Example 27. Using information from a CSV to automate dataintensive tasks (continued)

BuildNumber : 2600 Version : 5.1.2600

SMBIOSBIOSVersion : ASUS A7N8X Deluxe ACPI BIOS Rev 1009

Manufacturer
: Phoenix Technologies, LTD

Name
: Phoenix AwardBIOS v6.00PG

SerialNumber
: xxxxxxxxxxx

Version
: Nvidia 42302e31

Discussion

One of the major benefits of PowerShell is its capability to automate repetitive tasks. Sometimes, these repetitive tasks are actionintensive (such as system maintenance through registry and file cleanup) and consist of complex sequences of commands that will always be invoked together. In those situations, you can write a script to combine these operations to save time and reduce errors.

Other times, you need only to accomplish a single task (for example, retrieving the results of a WMI query) but need to invoke that task repeatedly for a large amount of data. In those situations, PowerShell’s scripting statements, pipeline support, and data management cmdlets help automate those tasks.

One of the options given by the solution is the ImportCsv cmdlet. The ImportCsv cmdlet reads a CSV file and, for each row, automatically creates an object with prop a CSV that contains a ComputerName and Class header.

Example 28. The ImportCsv cmdlet creating objects with ComputerName and Class properties

PS >$data = ImportCsv WmiReport.csv PS >$data

ComputerName Class

LEEDESK Win32_OperatingSystem LEEDESK Win32_Bios

PS > PS >$data[0].ComputerName LEEDESK

As the solution illustrates, you can use the ForeachObject cmdlet to provide data from these objects to repetitive cmdlet calls. It does this by specifying each parameter name, followed by the data (taken from a property of the current CSV object) that applies to it.

While this is the most general solution, many cmdlet parameters can automatically retrieve their value from incoming objects if any property of that object has the same name. This can let you to omit the ForeachObject and property mapping steps altogether. Parameters that support this feature are said to support Value from pipeline by property name. The MoveItem cmdlet is one example of a cmdlet with parameters that support this, as shown by the Accept pipeline input

Example 29. Help content of the MoveItem showing a parameter that accepts value from pipeline by property name

PS >GetHelp MoveItem Full (...) PARAMETERS

path Specifies the path to the current location of the items. The default is the current directory. Wildcards are permitted.

Required? true Position? 1 Default value Accept pipeline input? true (ByValue, ByPropertyName) Accept wildcard characters? true

destination Specifies the path to the location where the items are being moved. The default is the current directory. Wildcards are permitted, but the result must specify a single location.

To rename the item being moved, specify a new name in the value of Destination.

Required? false Position? 2 Default value Accept pipeline input? true (ByPropertyName) Accept wildcard characters? True

(...)

If you purposefully name the columns in the CSV to correspond to parameters that take their value from pipeline by property name, PowerShell can do some (or all) of items in bulk.

Example 210. Using the ImportCsv cmdlet to automate a cmdlet that accepts value from pipeline by property name

PS >GetContent ItemMoves.csv Path,Destination test.txt,Test1Directory test2.txt,Test2Directory PS >dir test.txt,test2.txt | Select Name

Name

Example 210. Using the ImportCsv cmdlet to automate a cmdlet that accepts value from pipeline by property name (continued)

test.txt test2.txt

PS >ImportCsv ItemMoves.csv | MoveItem PS >dir Test1Directory | Select Name

Name

test.txt

PS >dir Test2Directory | Select Name

Name

test2.txt