Skip to main content

Windows

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