Skip to main content

Resources

Convert a String to Upper/Lowercase in Windows PowerShell

Problem

You want to convert a string to uppercase or lowercase.

Solution

Use the ToUpper() and ToLower() methods of the string to convert it to uppercase and lowercase, respectively. To convert a string to uppercase, use the ToUpper() method:

PS >"Hello World".ToUpper()

HELLO WORLD To convert a string to lowercase, use the ToLower() method:

PS >"Hello World".ToLower()

hello world

Discussion

Since PowerShell strings are fully featured .NET objects, they support many stringoriented operations directly. The ToUpper() and ToLower() methods are two examples of the many features that the String class supports.

Neither PowerShell nor the methods of the .NET String class directly support capitalizing only the first letter of a word. If you want to capitalize only the first character of a word or sentence, try the following

commands:

PS >$text = "hello" PS >$newText = $text.Substring(0,1).ToUpper() + >> $text.Substring(1) >> $newText >> Hello

One thing to keep in mind as you convert a string to uppercase or lowercase is your motivation for doing it. One of the most common reasons is for comparing strings, as shown in Example 54.

Example 54. Using the ToUpper() method to normalize strings

## $text comes from the user, and contains the value "quit" if($text.ToUpper() eq "QUIT") { ... }

Unfortunately, explicitly changing the capitalization of strings fails in subtle ways when your script runs in different cultures. Many cultures follow different capitalization and comparison rules than you may be used to. For example, the Turkish language includes two types of the letter “I”: one with a dot, and one without. The uppercase version of the lowercase letter “i” corresponds to the version of the capital I with a dot, not the capital I used in QUIT. Those capitalization rules cause the string comparison code in Example 54 to fail in the Turkish culture.

To compare some input against a hardcoded string in a caseinsensitive manner, the better solution is to use PowerShell’s –eq operator without changing any of the casing yourself. The –eq operator is caseinsensitive and cultureneutral by default:

PS >$text1 = "Hello" PS >$text2 = "HELLO" PS >$text1 –eq $text2 True

Use .NET to Perform Advanced WMI Tasks

Problem

You want to work with advanced features of WMI, but PowerShell’s access (through the [Wmi], [WmiClass], and [WmiSearcher] accelerators) does not directly support them.

Solution

To interact with advanced features through their .NET interface, use the PsBase property of the resulting objects.

Advanced instance features

To get WMI instances related to a given instance, call the GetRelated() method:

$instance = [Wmi] 'Win32_Service.Name="winmgmt"' $instance.PsBase.GetRelated()

To enable security privileges for a command that requires them (such as changing the system time), set the EnablePrivileges property to $true:

$system = GetWmiObject Win32_OperatingSystem $system.PsBase.Scope.Options.EnablePrivileges = $true $system.SetDateTime($class.ConvertFromDateTime("01/01/2007"))

Advanced class features

To retrieve the WMI properties and qualifiers of a class, access the PsBase. Properties property: $class = [WmiClass] "Win32_Service" $class.PsBase.Properties

Advanced query feature

To configure connection options, such as Packet Privacy and Authentication, set the options on the Scope property:

$credential = GetCredential $query = [WmiSearcher] "SELECT * FROM IISWebServerSetting" $query.Scope.Path = "\\REMOTE_COMPUTER\Root\MicrosoftIISV2" $query.Scope.Options.Username = $credential.Username $query.Scope.Options.Password = $credential.GetNetworkCredential().Password $query.Scope.Options.Authentication = "PacketPrivacy" $query.get() | SelectObject AnonymousUserName

Discussion

The [Wmi], [WmiClass], and [WmiSearcher] type shortcuts return instances of .NET System.Management.ManagementObject, System.Management.ManagementClass, and System.Management.ManagementObjectSearcher classes, respectively.

As might be expected, the .NET Framework provides comprehensive support for WMI queries, with PowerShell providing an easiertouse interface to that support. If you need to step outside the support offered directly by PowerShell, these classes in the .NET Framework provide an advanced outlet.

Access Services on a Windows PowerShell Remote Machine

Problem

You want to list or manage services on a remote machine.

Solution

To retrieve the services from a remote machine, use the [System.ServiceProcess. ServiceController]::GetServices() method from the .NET Framework.

PS >[void] ([Reflection.Assembly]::LoadWithPartialName("System.ServiceProcess"))

PS >[System.ServiceProcess.ServiceController]::GetServices("LEEDESK")

Status
Name
DisplayName

Running
ADAM_Test
Test

Stopped
Alerter
Alerter

Running
ALG
Application Layer Gateway Service

Stopped
AppMgmt
Application Management

Stopped
aspnet_state
ASP.NET State Service

Running
AudioSrv
Windows Audio

Running
BITS
Background Intelligent Transfer Ser...

Running
Browser
Computer Browser

Stopped
CiSvc
Indexing Service

To control one, use the WhereObject cmdlet to retrieve that one specifically and then call the methods on the object that manage it:

[void] ([Reflection.Assembly]::LoadWithPartialName("System.ServiceProcess"))

$service = [System.ServiceProcess.ServiceController]::GetServices("LEEDESK") | WhereObject { $_.Name eq "Themes" }

$service.Stop() $service.WaitForStatus("Stopped") StartSleep 2 $service.Start()

Discussion

If you have administrator privileges on a remote machine, the [System. ServiceProcess.ServiceController]::GetServices() method from the .NET Framework lets you control services on that machine.

When doing this, note that both of the examples from the solution require that you first load the assembly that contains the .NET classes that manage services. The *Service cmdlets load this DLL automati

cally.

Reduce Typing for Long Class Names

Problem

You want to reduce the amount of redundant information in your script when you interact with classes that have long type names.

Solution

To reduce typing for static methods, store the type name in a variable:

$math = [System.Math] $math::Min(1,10) $math::Max(1,10)

To reduce typing for multiple objects in a namespace, use the f (format) operator:

$namespace = "System.Collections.{0}" $arrayList = NewObject ($namespace f "ArrayList") $queue = NewObject ($namespace f "Queue")

To reduce typing for static methods of multiple types in a namespace, use the f (format) operator along with a cast:

$namespace = "System.Diagnostics.{0}" ([Type] ($namespace f "EventLog"))::GetEventLogs() ([Type] ($namespace f "Process"))::GetCurrentProcess()

Discussion

One thing you will notice when working with some .NET classes (or classes from a thirdparty SDK), is that it quickly becomes tiresome to specify their fully qualified type names. For example, many useful collection classes in the .NET Framework all start with "System.Collections". This is called the namespace of that class. Most programming languages solve this problem with a using directive that lets you to specify a list of namespaces for that language to search when you type a plain class name such as "ArrayList". PowerShell lacks a using directive, but there are several options to get the benefits of one.

If you are repeatedly working with static methods on a specific type, you can store that type in a variable to reduce typing as shown in the solution:

$math = [System.Math] $math::Min(1,10) $math::Max(1,10)

If you are creating instances of different classes from a namespace, you can store the namespace in a variable and then use the PowerShell f (format) operator to specify the unique class name:

$namespace = "System.Collections.{0}" $arrayList = NewObject ($namespace f "ArrayList") $queue = NewObject ($namespace f "Queue")

If you are working with static methods from several types in a namespace, you can store the namespace in a variable, use the f (format) operator to specify the unique class name, and then finally cast that into a type:

$namespace = "System.Diagnostics.{0}" ([Type] ($namespace f "EventLog"))::GetEventLogs() ([Type] ($namespace f "Process"))::GetCurrentProcess()

Program: Analyze a Script’s Performance Profile

When you write scripts that heavily interact with the user, you may sometimes feel that your script could benefit from better performance.

When tackling performance problems, the first rule is to measure the problem. Unless you can guide your optimization efforts with hard performance data, you are almost certainly directing your efforts to the wrong spots. Random cute performance improvements will quickly turn your code into an unreadable mess, often with no appreciable performance gain! Lowlevel optimization has its place, but it should always be guided by hard data that supports it.

The way to obtain hard performance data is from a profiler. PowerShell doesn’t ship with a script profiler, but Example 135 uses PowerShell features to implement one.

Example 135. GetScriptPerformanceProfile.ps1

################################################################################ ## ## GetScriptPerformanceProfile.ps1 ## ## Computes the performance characteristics of a script, based on the transcript ## of it running at trace level 1. ## ## To profile a script:

##
1) Turn on script tracing in the window that will run the script:

##
SetPsDebug trace 1

##
2) Turn on the transcript for the window that will run the script:

##
StartTranscript

##
(Note the filename that PowerShell provides as the logging destination.)

##
3) Type in the script name, but don't actually start it.

##
4) Open another PowerShell window, and navigate to the directory holding

##
this script.
Type in 'GetScriptPerformanceProfile
',

##
replacing
with the path given in step 2.
Don't

##
press yet.

##
5) Switch to the profiled script window, and start the script.

##
Switch to the window containing this script, and press

##
6) Wait until your profiled script exits, or has run long enough to be

##
representative of its work.
To be statistically accurate, your script

##
should run for at least ten seconds.

##
7) Switch to the window running this script, and press a key.

##
8) Switch to the window holding your profiled script, and type:

##
StopTranscript

##
9) Delete the transcript.

##

## Note: You can profile regions of code (ie: functions) rather than just lines ## by placing the following call at the start of the region: ## writedebug "ENTER " ## and the following call and the end of the region: ## writedebug "EXIT" ## This is implemented to account exclusively for the time spent in that ## region, and does not include time spent in regions contained within the ## region. For example, if FunctionA calls FunctionB, and you've surrounded ## each by region markers, the statistics for FunctionA will not include the ## statistics for FunctionB. ## ################################################################################

Example 135. GetScriptPerformanceProfile.ps1 (continued)

param($logFilePath = $(throw "Please specify a path to the transcript log file."))

function Main

{ ## Run the actual profiling of the script. $uniqueLines gets ## the mapping of line number to actual script content. ## $samples gets a hashtable mapping line number to the number of times ## we observed the script running that line. $uniqueLines = @{} $samples = GetSamples $uniqueLines

"Breakdown by line:" ""

## Create a new hash table that flips the $samples hashtable ## one that maps the number of times sampled to the line sampled. ## Also, figure out how many samples we got altogether. $counts = @{} $totalSamples = 0; foreach($item in $samples.Keys) {

$counts[$samples[$item]] = $item $totalSamples += $samples[$item] }

## Go through the flipped hashtable, in descending order of number of ## samples. As we do so, output the number of samples as a percentage of ## the total samples. This gives us the percentage of the time our script ## spent executing that line. foreach($count in ($counts.Keys | SortObject Descending)) {

$line = $counts[$count] $percentage = "{0:#0}" f ($count * 100 / $totalSamples) "{0,3}%: Line {1,4} {2}" f $percentage,$line,

$uniqueLines[$line] }

## Go through the transcript log to figure out which lines are part of any ## marked regions. This returns a hashtable that maps region names to ## the lines they contain. "" "Breakdown by marked regions:" "" $functionMembers = GenerateFunctionMembers

## For each region name, cycle through the lines in the region. As we ## cycle through the lines, sum up the time spent on those lines and output ## the total. foreach($key in $functionMembers.Keys) {

$totalTime = 0 foreach($line in $functionMembers[$key])

Example 135. GetScriptPerformanceProfile.ps1 (continued)

{ $totalTime += ($samples[$line] * 100 / $totalSamples) }

$percentage = "{0:#0}" f $totalTime "{0,3}%: {1}" f $percentage,$key } }

## Run the actual profiling of the script. $uniqueLines gets ## the mapping of line number to actual script content. ## Return a hashtable mapping line number to the number of times ## we observed the script running that line. function GetSamples($uniqueLines) {

## Open the log file. We use the .Net file I/O, so that we keep monitoring ## just the end of the file. Otherwise, we would make our timing inaccurate ## as we scan the entire length of the file every time. $logStream = [System.IO.File]::Open($logFilePath, "Open", "Read", "ReadWrite") $logReader = NewObject System.IO.StreamReader $logStream

$random = NewObject Random $samples = @{}

$lastCounted = $null

## Gather statistics until the user presses a key. while(not $host.UI.RawUI.KeyAvailable) {

## We sleep a slightly random amount of time. If we sleep a constant ## amount of time, we run the very real risk of improperly sampling ## scripts that exhibit periodic behaviour. $sleepTime = [int] ($random.NextDouble() * 100.0) StartSleep Milliseconds $sleepTime

## Get any content produced by the transcript since our last poll. ## From that poll, extract the last DEBUG statement (which is the last ## line executed.) $rest = $logReader.ReadToEnd() $lastEntryIndex = $rest.LastIndexOf("DEBUG: ")

## If we didn't get a new line, then the script is still working on the ## last line that we captured. if($lastEntryIndex lt 0) {

if($lastCounted) { $samples[$lastCounted] ++ } continue; }

## Extract the debug line. $lastEntryFinish = $rest.IndexOf("\n", $lastEntryIndex) if($lastEntryFinish eq 1) { $lastEntryFinish = $rest.length }

Example 135. GetScriptPerformanceProfile.ps1 (continued)

$scriptLine = $rest.Substring(

$lastEntryIndex, ($lastEntryFinish $lastEntryIndex)).Trim() if($scriptLine match 'DEBUG:[ \t]*([09]*)\+(.*)') {

## Pull out the line number from the line $last = $matches[1]

$lastCounted = $last $samples[$last] ++

## Pull out the actual script line that matches the line number $uniqueLines[$last] = $matches[2] }

## Discard anything that's buffered during this poll, and start waiting ## again $logReader.DiscardBufferedData()

}

## Clean up $logStream.Close() $logReader.Close()

$samples }

## Go through the transcript log to figure out which lines are part of any ## marked regions. This returns a hashtable that maps region names to ## the lines they contain. function GenerateFunctionMembers {

## Create a stack that represents the callstack. That way, if a marked ## region contains another marked region, we attribute the statistics ## appropriately. $callstack = NewObject System.Collections.Stack $currentFunction = "Unmarked" $callstack.Push($currentFunction)

$functionMembers = @{}

## Go through each line in the transcript file, from the beginning foreach($line in (GetContent $logFilePath)) {

## Check if we're entering a monitor block ## If so, store that we're in that function, and push it onto ## the callstack. if($line match 'writedebug "ENTER (.*)"') {

$currentFunction = $matches[1] $callstack.Push($currentFunction) }

Example 135. GetScriptPerformanceProfile.ps1 (continued)

## Check if we're exiting a monitor block ## If so, clear the "current function" from the callstack, ## and store the new "current function" onto the callstack. elseif($line match 'writedebug "EXIT"') {

[void] $callstack.Pop()

$currentFunction = $callstack.Peek() } ## Otherwise, this is just a line with some code. ## Add the line number as a member of the "current function" else {

if($line match 'DEBUG:[ \t]*([09]*)\+')

{ ## Create the arraylist if it's not initialized if(not $functionMembers[$currentFunction]) {

$functionMembers[$currentFunction] = NewObject System.Collections.ArrayList }

## Add the current line to the ArrayList if(not $functionMembers[$currentFunction].Contains($matches[1])) {

[void] $functionMembers[$currentFunction].Add($matches[1]) } } } }

$functionMembers }

. Main

Get the Newest Entries from an Event Log

Problem

You want to retrieve the most recent entries from an event log.

Solution

To retrieve the most recent entries from an event log, use the –Newest parameter of the GetEventLog cmdlet, as shown in Example 201.

Example 201. Retrieving the 10 newest entries from the System event log

PS >GetEventLog System Newest 10 | FormatTable Index,Source,Message A

Index Source Message

2922 Service Control Manager The Background Intelligent Transfer Servi... 2921 Service Control Manager The Background Intelligent Transfer Servi... 2920 Service Control Manager The Logical Disk Manager Administrative S... 2919 Service Control Manager The Logical Disk Manager Administrative S... 2918 Service Control Manager The Logical Disk Manager Administrative S...

2917 TermServDevices
Driver Microsoft XPS Document Writer requ...

2916 Print
Printer Microsoft Office Document Image W...

2915 Print
Printer Microsoft Office Document Image W...

2914 Print
Printer Microsoft Office Document Image W...

2913 TermServDevices
Driver Microsoft Shared Fax Driver requir...

Discussion

The –Newest parameter of the GetEventLog cmdlet retrieves the most recent entries from an event log that you specify.

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

Windows PowerShell Configure Debug, Verbose, and Progress Output

Problem

You want to manage the detailed debug, verbose, and progress output generated by cmdlets and scripts.

Solution

To enable debug output for scripts and cmdlets that generate it:

$debugPreference = "Continue" StartDebugCommand

To enable verbose mode for a cmdlet that checks for the Verbose parameter:

CopyItem c:\temp\*.txt c:\temp\backup\ Verbose

To disable progress output from a script or cmdlet that generates it:

$progressPreference = "SilentlyContinue" GetProgress.ps1

Discussion

In addition to error output many scripts and cmdlets generate several other types of output. This includes:

Debug output

Helps you diagnose problems that may arise and can provide a view into the inner workings of a command. You can use the WriteDebug cmdlet to produce this type of output in a script or the WriteDebug( ) method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host.PrivateData.Debug* color configuration variables.

Verbose output

Helps you monitor the actions of commands at a finer level than the default. You can use the WriteVerbose cmdlet to produce this type of output in a script or the WriteVerbose( ) method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host. PrivateData.Verbose* color configuration variables.

Progress output

Helps you monitor the status of longrunning commands. You can use the WriteProgress cmdlet to produce this type of output in a script or the WriteProgress( ) method to produce this type of output in a cmdlet. PowerShell displays this output in yellow, unless you customize it through the $host. PrivateData.Progress* color configuration variables.

Some cmdlets generate verbose and debug output only if you specify the Verbose and Debug parameters, respectively.

To configure the debug, verbose, and progress output of a script or cmdlet, modify the $debugPreference, $verbosePreference, and $progressPreference shell variables. These variables can accept the following values:

SilentlyContinue

Do not display this output.

Stop

Treat this output as an error.

Continue

Display this output.

Inquire

Display a continuation prompt for this output.

Windows PowerShell User Interaction

While most scripts are designed to run automatically, you will frequently find it useful to have your scripts interact with the user.

The best way to get input from your user is through the arguments and parameters to your script or function. This lets your users to run your script without having to be there as it runs!

If your script greatly benefits from (or requires) an interactive experience, PowerShell offers a range of possibilities. This might be simply waiting for a keypress, prompting for input, or displaying a richer choicebased prompt.

User input isn’t the only aspect of interaction though. In addition to its input facilities, PowerShell supports output as well—from displaying simple text strings to much more detailed progress reporting and interaction with UI frameworks.

Add a Site to an Internet Explorer Security Zone

Problem

You want to add a site to a specific Internet Explorer security zone.

Solution

To create the registry keys and properties required to add a site to a specific security zone, use the NewItem and NewItemProperty cmdlets. Example 183 adds www. example.com to the list of sites trusted by Internet Explorer.

Example 183. Adding www.example.com to the list of trusted sites in Internet Explorer

SetLocation "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings" SetLocation ZoneMap\Domains NewItem example.com SetLocation example.com NewItem www SetLocation www NewItemProperty . Name http Value 2 Type DWORD

The Internet Explorer zone identifiers are:

  1. My Computer
  2. Local intranet
  3. Trusted sites
  4. Internet
  5. Restricted sites

When Internet Explorer is configured in its Enhanced Security Configuration mode, you must also update entries under the EscDomains key.

Once a machine has enabled Internet Explorer’s Enhanced Security Configuration, those settings persist even after removing Enhanced Security Configuration. The following commands allow your machine

to trust UNC paths again:

SetLocation "HKCU:\Software\Microsoft\Windows\" SetLocation "CurrentVersion" SetLocation "Internet Settings" SetItemProperty ZoneMap UNCAsIntranet Type DWORD 1 SetItemProperty ZoneMap IntranetName Type DWORD 1

To remove the zone mapping for a specific domain, use the RemoveItem cmdlet: PS >GetChildItem

Hive: Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Mi crosoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains

SKC
VC Name
Property

1

0 example.com
{}

PS >RemoveItem Recurse example.com PS >GetChildItem PS >

For more information about using the Internet Explorer registry entries to configure security zones, see the Microsoft KB article “Description of Internet Explorer Security Zones Registry Entries” at http://support.microsoft.com/kb/182569. For more information about managing Internet Explorer’s Enhanced Security Configuration, search for it on http://technet.microsoft.com.

Manage Outlook Web Access

Problem

You want to get and modify Outlook Web Access settings from the Exchange Management Shell.

Solution

To retrieve information about an Outlook Web Access virtual directory, use the GetOwaVirtualDirectory cmdlet:

$owa = GetOwaVirtualDirectory "owa (Default Web Site)"

To modify information about that virtual directory, use the SetOwaVirtualDirectory cmdlet. This example prevents users from changing their passwords through Outlook Web Access:

$owa | SetOwaVirtualDirectory –ChangePasswordEnabled:$false

Discussion

For more information about the GetOwaVirtualDirectory cmdlet, type GetHelp GetOwaVirtualDirectory. For more information about the SetOwaVirtualDirectory cmdlet, type GetHelp SetOwaVirtualDirectory.

Manage an Operations Manager 2007 Server

Like Exchange 2007, System Center Operations Manager 2007 (previously known as Microsoft Operations Manager) broadly adopts PowerShell as a technique for automated management. Operations Manager 2007 takes a dualpronged approach to this. It includes a set of more than 70 PowerShell cmdlets, as well as a PowerShell provider that lets you scope commands to (and navigate) management groups.

The most common management tasks in Operations Manager 2007 fall largely into one of several categories: managing agents, management packs, rules, tasks, alerts, and maintenance windows.

Get Windows PowerShell Help on a Command

Problem

You want to learn about how a specific command works and how to use it.

Solution

The command that provides help and usage information about a command is called GetHelp. It supports several different views of the help information, depending on your needs.

To get the summary of help information for a specific command, provide the command’s name as an argument to the GetHelp cmdlet. This primarily includes its synopsis, syntax, and detailed description:

GetHelp CommandName

or

CommandName ? To get the detailed help information for a specific command, supply the –Detailed flag to the GetHelp cmdlet. In addition to the summary view, this also includes its parameter descriptions and examples:

GetHelp CommandName Detailed To get the full help information for a specific command, supply the –Full flag to the GetHelp cmdlet. In addition to the detailed view, this also includes its full parameter descriptions and additional notes:

GetHelp CommandName Full To get only the examples for a specific command, supply the –Examples flag to the GetHelp cmdlet: GetHelp CommandName Examples

Discussion

The GetHelp cmdlet is the primary way to interact with the help system in PowerShell. Like the GetCommand cmdlet, the GetHelp cmdlet supports wildcards. If you want to list all commands that match a certain pattern (for example, *process*), you can simply type GetHelp *process*.

To generate a list of all cmdlets along with a brief synopsis, run the following command:

GetHelp * | SelectObject Name,Synopsis | FormatTable Auto

If the pattern matches only a single command, PowerShell displays the help for that command.

The GetHelp cmdlet is one of the three commands you will use most commonly as you explore Windows PowerShell. The other two commands are GetCommand and GetMember.

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

Access Arguments of a Script, Function, or Script Block in Windows PowerShell

Problem

You want to access the arguments provided to a script, function, or script block.

Solution

To access arguments by name, use a param statement:

param($firstNamedArgument, [int] $secondNamedArgument = 0)

"First named argument is: $firstNamedArgument" "Second named argument is: $secondNamedArgument"

To access unnamed arguments by position, use the $args array:

"First positional argument is: " + $args[0] "Second positional argument is: " + $args[1]

You can use these techniques in exactly the same way with scripts, functions, and script blocks, as illustrated by Example 106.

Example 106. Working with arguments in scripts, functions, and script blocks

############################################################################## ## GetArguments.ps1 ## ## Use commandline arguments ############################################################################## param($firstNamedArgument, [int] $secondNamedArgument = 0)

## Display the arguments by name "First named argument is: $firstNamedArgument" "Second named argument is: $secondNamedArgument"

function GetArgumentsFunction { ## We could use a param statement here, as well ## param($firstNamedArgument, [int] $secondNamedArgument = 0)

## Display the arguments by position "First positional function argument is: " + $args[0] "Second positional function argument is: " + $args[1] }

GetArgumentsFunction One Two

$scriptBlock = { param($firstNamedArgument, [int] $secondNamedArgument = 0)

## We could use $args here, as well "First named scriptblock argument is: $firstNamedArgument" "Second named scriptblock argument is: $secondNamedArgument"

}

& $scriptBlock First One Second 4.5

Example 106 produces the following output:

PS >GetArguments First 2 First named argument is: First Second named argument is: 2 First positional function argument is: One Second positional function argument is: Two First named scriptblock argument is: One Second named scriptblock argument is: 4

Discussion

Although PowerShell supports both the param keyword and the $args array, you will most commonly want to use the param keyword to define and access script, function, and script block parameters.

In most languages, the most common reason to access parameters through an $argsstyle array is to determine the name of the currently running script. For information about how to do this in PowerShell.

When you use the param keyword to define your parameters, PowerShell provides your script or function with many useful features that allow users to work with your script much like they work with cmdlets:

  • Users need only to specify enough of the parameter name to disambiguate it from other parameters.
  • Users can understand the meaning of your parameters much more clearly.
  • You can specify the type of your parameters, which PowerShell uses to convert input if required.
  • You can specify default values for your parameters.

The $args array is sometimes helpful, however, as a way to deal with all arguments at once. For example:

function Reverse

{ $argsEnd = $args.Length 1 $args[$argsEnd..0]

}

produces

PS >Reverse 1 2 3 4 4 3 2 1

Program: Get the MD5 or SHA1 Hash of a File

Discussion

File hashes provide a useful way to check for damage or modification to a file. Adigital hash acts like the fingerprint of a file and detects even minor modifications. If the content of a file changes, then so does its hash. Many online download services provide the hash of a file on that file’s download page so that you can determine whether the transfer somehow corrupts the file.

There are three common ways to generate the hash of a file: MD5, SHA1, SHA256. The two most common are MD5, followed by SHA1. While popular, these hash types can be trusted to detect only accidental file modification. They can be fooled if somebody wants to tamper with the file without changing its hash. The SHA256 algorithm can be used to protect against even intentional file tampering.

Example 173 lets you determine the hash of a file (or of multiple files if provided by the pipeline).

Example 173. GetFileHash.ps1

############################################################################## ## ## GetFileHash.ps1 ## ## Get the hash of an input file. ## ## ie: ## ## PS >GetFileHash myFile.txt ## PS >dir | GetFileHash ## PS >GetFileHash myFile.txt Hash SHA1 ## ##############################################################################

param( $path, $hashAlgorithm = "MD5" )

## Create the hash object that calculates the hash of our file. If they ## provide an invalid hash algorithm, provide an error message. if($hashAlgorithm eq "MD5") {

$hasher = [System.Security.Cryptography.MD5]::Create() } elseif($hashAlgorithm eq "SHA1") {

$hasher = [System.Security.Cryptography.SHA1]::Create() } elseif($hashAlgorithm eq "SHA256") {

$hasher = [System.Security.Cryptography.SHA256]::Create() } else {

$errorMessage = "Hash algorithm $hashAlgorithm is not valid. Valid " +

"algorithms are MD5, SHA1, and SHA256." WriteError $errorMessage return

}

Example 173. GetFileHash.ps1 (continued)

## Create an array to hold the list of files $files = @()

## If they specified the file name as a parameter, add that to the list ## of files to process if($path) {

$files += $path } ## Otherwise, take the files that they piped in to the script. ## For each input file, put its full name into the file list else {

$files += @($input | ForeachObject { $_.FullName }) }

## Go through each of the items in the list of input files foreach($file in $files) {

## Convert it to a fullyqualified path $filename = (ResolvePath $file ErrorAction SilentlyContinue).Path

## If the path does not exist (or is not a file,) just continue if((not $filename) or (not (TestPath $filename Type Leaf))) {

continue }

## Use the ComputeHash method from the hash object to calculate ## the hash $inputStream = NewObject IO.StreamReader $filename $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) $inputStream.Close()

## Convert the result to hexadecimal $builder = NewObject System.Text.StringBuilder $hashBytes | ForeachObject { [void] $builder.Append($_.ToString("X2")) }

## Return a custom object with the important details from the ## hashing $output = NewObject PsObject $output | AddMember NoteProperty Path ([IO.Path]::GetFileName($file)) $output | AddMember NoteProperty HashAlgorithm $hashAlgorithm $output | AddMember NoteProperty HashValue ([string] $builder.ToString()) $output

}

Retrieve Printer Information

Problem

You want to get information about printers on the current system.

Solution

To retrieve information about printers attached to the system, use the Win32_Printer WMI class:

PS >GetWmiObject Win32_Printer | SelectObject Name,PrinterStatus

Name
PrinterStatus

Microsoft Office Document Image Wr...
3

Microsoft Office Document Image Wr...
3

CutePDF Writer
3

Brother DCP1000
3

To retrieve information about a specific printer, apply a filter based on its name:

PS >$device = GetWmiObject Win32_Printer Filter "Name='Brother DCP1000'" PS >$device | FormatList *

Status : Unknown Name : Brother DCP1000 Attributes : 588 Availability : AvailableJobSheets : AveragePagesPerMinute : 0 Capabilities : {4, 2, 5} CapabilityDescriptions : {Copies, Color, Collate} Caption : Brother DCP1000 (...)

To retrieve specific properties, access as you would access properties on other PowerShell objects:

PS >$device.VerticalResolution 600 PS >$device.HorizontalResolution 600

Discussion

The example in the solution uses the Win32_Printer WMI class to retrieve information about installed printers on the computer. While the Win32_Printer class gives access to most commonly used information, WMI supports several other printerrelated classes: Win32_TCPIPPrinterPort, Win32_PrinterDriver, CIM_Printer, Win32_PrinterConfiguration, Win32_PrinterSetting, Win32_PrinterController, Win32_PrinterShare, and Win32_PrinterDriverDll.

Structured Files in Windows PowerShell

In the world of textonly system administration, managing structured files is often a pain. For example, working with (or editing) an XML file means either loading it into an editor to modify by hand, or writing a custom tool that can do that for you. Even worse, it may mean modifying the file as though it were plain text while hoping to not break the structure of the XML itself.

In that same world, working with a file in CSV format means going through the file yourself, splitting each line by the commas in it. It’s a seemingly great approach, until you find yourself faced with anything but the simplest of data.

Structure and structured files don’t come only from other programs, either. When writing scripts, one common goal is to save structured data so that you can use it later. In most scripting (and programming) languages, this requires that you design a data structure to hold that data, design a way to store and retrieve it from disk, and bring it back to a usable form when you want to work with it again.

Fortunately, working with XML, CSVs, and even your own structured files becomes much easier with PowerShell at your side.

Securely Handle Sensitive Information in Windows PowerShell

Problem

You want to request sensitive information from the user, but want to do this as securely as possible.

Solution

To securely handle sensitive information, store it in a SecureString whenever possible. The ReadHost cmdlet (with the –AsSecureString parameter) lets you prompt the user for (and handle) sensitive information by returning the user’s response as a SecureString:

PS >$secureInput = ReadHost AsSecureString "Enter your private key" Enter your private key: ******************* PS >$secureInput System.Security.SecureString

Discussion

When you use any string in the .NET Framework (and therefore PowerShell), it retains that string so that it can efficiently reuse it later. Unlike most .NET data, unused strings persist even after you finish using them. When this data is in memory, there is always the chance that it could get captured in a crash dump, or swapped to disk in a paging operation. Because some data (such as passwords and other confidential information) may be sensitive, the .NET Framework includes the SecureString class—a container for text data that the framework encrypts when it stores it in memory. Code that needs to interact with the plaintext data inside a SecureString does so as securely as possible.

When a cmdlet author asks you for sensitive data (for example, an encryption key), the best practice is to designate that parameter as a SecureString to help keep your information confidential. You can provide the parameter with a SecureString variable as input, or the host prompts you for the SecureString if you do not provide one. PowerShell also supports two cmdlets (ConvertToSecureString and ConvertFromSecureString) that allow you to securely persist this data to disk.

Credentials are a common source of sensitive information.

By default, the SecureString cmdlets use Windows’ data protection API when they convert your SecureString to and from its text representation. The key it uses to encrypt your data is based on your Windows logon credentials, so only you can decrypt the data that you’ve encrypted. If you want the exported data to work on another system or separate user account, you can use the cmdlet options that let you provide an explicit key. PowerShell treats this sensitive data as an opaque blob—and so should you.

However, there are many instances when you may want to automatically provide the SecureString input to a cmdlet rather than have the host prompt you for it. In these situations, the ideal solution is to use the ConvertToSecureString cmdlet to import a previously exported SecureString from disk. This retains the confidentiality of your data and still lets you automate the input.

If the data is highly dynamic (for example, coming from a CSV), then the ConvertToSecureString cmdlet supports an –AsPlainText parameter:

$secureString = ConvertToSecureString "Kinda Secret" AsPlainText –Force

Since you’ve already provided plaintext input in this case, placing this data in a SecureString no longer provides a security benefit. To prevent a false sense of security, the cmdlet requires the Force parameter to convert plaintext data into a SecureString.

Once you have data in a SecureString, you may want to access its plaintext representation. PowerShell doest’t provide a direct way to do this, as that defeats the purpose of a SecureString. If you still want to convert a SecureString to plain text, you have two options:

1. Use the GetNetworkCredential() method of the PsCredential class

$secureString = ReadHost AsSecureString $temporaryCredential = NewObject ` System.Management.Automation.PsCredential "TempUser",$secureString $unsecureString = $temporaryCredential.GetNetworkCredential().Password

2. Use the .NET Framework’s Marshal class

$secureString = ReadHost AsSecureString $unsecureString = [Runtime.InteropServices.Marshal]::PtrToStringAuto( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString))

Modify PowerShell Properties of a Security or Distribution Group

Problem

You want to modify properties of a specific security or distribution group.

Solution

To modify a security or distribution group, use the [adsi] type shortcut to bind to the group in Active Directory, and then call the Put() method to modify properties. Finally, call the SetInfo() method to apply the changes.

$group = [adsi] "LDAP://localhost:389/cn=Management,ou=West,ou=Sales,dc=Fabrikam,dc=COM"

PS >$group.Put("Description", "Managers in the Sales West Organization") PS >$group.SetInfo()

Discussion

The solution retrieves the Management group from the Sales West OU. It then sets the description to Managers in the Sales West Organization, and then applies those changes to Active Directory.

Replace Text in a String in Windows PowerShell

Problem

You want to replace a portion of a string with another string.

Solution

PowerShell provides several options to help you replace text in a string with other text.

Use the Replace() method on the string itself to perform simple replacements:

PS >"Hello World".Replace("World", "PowerShell")

Hello PowerShell Use PowerShell’s regular expression –replace operator to perform more advanced regular expression replacements:

PS >"Hello World" replace '(.*) (.*)','$2 $1' World Hello

Discussion

The Replace() method and the –replace operator both provide useful ways to replace text in a string. The Replace() method is the quickest but also the most constrained. It replaces every occurrence of the exact string you specify with the exact replacement string that you provide. The –replace operator provides much more flexibility, since its arguments are regular expressions that can match and replace complex patterns.

The regular expressions that you use with the –replace operator often contain characters that PowerShell normally interprets as variable names or escape characters. To prevent PowerShell from interpreting

these characters, use a nonexpanding string (single quotes) as shown by the solution.

Program: Search for WMI Classes

Along with WMI’s huge scope comes a related problem: finding the WMI class that accomplishes your task. If you want to dig a little deeper, though, Example 152 lets you search for WMI classes by name, description, property name, or property description.

Example 152. SearchWmiNamespace.ps1

############################################################################## ## ## SearchWmiNamespace.ps1 ##

Example 152. SearchWmiNamespace.ps1 (continued)

## Search the WMI classes installed on the system for the provided match text. ## ## ie: ## ## PS >SearchWmiNamespace Registry ## PS >SearchWmiNamespace Process ClassName,PropertyName ## PS >SearchWmiNamespace CPU Detailed ## ##############################################################################

param( [string] $pattern = $(throw "Please specify a search pattern."), [switch] $detailed, [switch] $full,

## Supports any or all of the following match options: ## ClassName, ClassDescription, PropertyName, PropertyDescription [string[]] $matchOptions = ("ClassName","ClassDescription")

)

## Helper function to create a new object that represents ## a Wmi match from this script function NewWmiMatch {

param( $matchType, $className, $propertyName, $line )

$wmiMatch = NewObject PsObject $wmiMatch | AddMember NoteProperty MatchType $matchType $wmiMatch | AddMember NoteProperty ClassName $className $wmiMatch | AddMember NoteProperty PropertyName $propertyName $wmiMatch | AddMember NoteProperty Line $line

$wmiMatch }

## If they've specified the detailed or full options, update ## the match options to provide them an appropriate amount of detail if($detailed) {

$matchOptions = "ClassName","ClassDescription","PropertyName" }

if($full) { $matchOptions = "ClassName","ClassDescription","PropertyName","PropertyDescription" }

## Verify that they specified only valid match options foreach($matchOption in $matchOptions) {

$fullMatchOptions =

Example 152. SearchWmiNamespace.ps1 (continued)

"ClassName","ClassDescription","PropertyName","PropertyDescription"

if($fullMatchOptions notcontains $matchOption) {

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

$ofs = ", " throw ($error f $matchOption, ([string] $fullMatchOptions)) } }

## Go through all of the available classes on the computer foreach($class in GetWmiObject List) {

## Provide explicit get options, so that we get back descriptions ## as well $managementOptions = NewObject System.Management.ObjectGetOptions $managementOptions.UseAmendedQualifiers = $true $managementClass =

NewObject Management.ManagementClass $class.Name,$managementOptions

## If they want us to match on class names, check if their text ## matches the class name if($matchOptions contains "ClassName") {

if($managementClass.Name match $pattern) { NewWmiMatch "ClassName" ` $managementClass.Name $null $managementClass.__PATH } }

## If they want us to match on class descriptions, check if their text ## matches the class description if($matchOptions contains "ClassDescription") {

$description = $managementClass.PsBase.Qualifiers |

foreach { if($_.Name eq "Description") { $_.Value } } if($description match $pattern) {

NewWmiMatch "ClassDescription" ` $managementClass.Name $null $description } }

## Go through the properties of the class foreach($property in $managementClass.PsBase.Properties) {

Example 152. SearchWmiNamespace.ps1 (continued)

## If they want us to match on property names, check if their text ## matches the property name if($matchOptions contains "PropertyName") {

if($property.Name match $pattern) { NewWmiMatch "PropertyName" ` $managementClass.Name $property.Name $property.Name } }

## If they want us to match on property descriptions, check if ## their text matches the property name if($matchOptions contains "PropertyDescription") {

$propertyDescription = $property.Qualifiers |

foreach { if($_.Name eq "Description") { $_.Value } } if($propertyDescription match $pattern) {

NewWmiMatch "PropertyDescription" ` $managementClass.Name $property.Name $propertyDescription } } } }

Manage a Running PowerShell Service

Problem

You want to manage a running service.

Solution

To stop a service, use the StopService cmdlet:

PS >StopService AudioSrv WhatIf What if: Performing operation "StopService" on Target "Windows Audio (Audi oSrv)".

Likewise, use the SuspendService, RestartService, and ResumeService cmdlets to suspend, restart, and resume services, respectively.

For other tasks (such as setting the startup mode), use the GetWmiObject cmdlet:

$service = GetWmiObject Win32_Service |

WhereObject { $_.Name eq "AudioSrv" } $service.ChangeStartMode("Manual") $service.ChangeStartMode("Automatic")

Discussion

The StopService cmdlet lets you stop a service either by name or display name.

Notice that the solution uses the –WhatIf flag on the StopService cmdlet. This parameter lets you see what would happen if you were to run the command but doesn’t actually perform the action.

For more information about the StopService cmdlet, type GetHelp StopService.If you want to suspend, restart, or resume a service, see the SuspendService, RestartService, and ResumeService cmdlets, respectively.