Chapter 8. Utility Tasks

Introduction

When scripting or just using the interactive shell, a handful of needs arise that are simple but useful: measuring commands, getting random numbers, and more.

Get the System Date and Time

Problem

You want to get the system date.

Solution

To get the system date, run the command Get-Date.

Discussion

The Get-Date command generates rich object-based output, so you can use its result for many date-related tasks. For example, to determine the current day of the week:

PS > $date = Get-Date
PS > $date.DayOfWeek
Sunday

For more information about the Get-Date cmdlet, type Get-Help Get-Date.

For more information about working with classes from the .NET Framework, see the section called “Work with .NET Objects”.

Measure the Duration of a Command

Problem

You want to know how long a command takes to execute.

Solution

To measure the duration of a command, use the Measure-Command cmdlet:

PS > Measure-Command { Start-Sleep -Milliseconds 337 }

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 0
Milliseconds      : 339
Ticks             : 3392297
TotalDays         : 3.92626967592593E-06
TotalHours        : 9.42304722222222E-05
TotalMinutes      : 0.00565382833333333
TotalSeconds      : 0.3392297
TotalMilliseconds : 339.2297

Discussion

In interactive use, it is common to want to measure the duration of a command. An example of this might be running a performance benchmark on an application you've developed. The Measure-Command cmdlet makes this easy to do. Because the command generates rich object-based output, you can use its output for many date-related tasks. See the section called “Work with .NET Objects” for more information.

If the accuracy of a command measurement is important, general system activity can easily influence the timing of the result. To improve accuracy, a common techique is to repeat the measurement many times, ignore the outliers (the top and bottom 10 percent), and then average the remaining results. Example 8.1, “Measure-CommandPerformance.ps1” implements this technique.

Example 8.1. Measure-CommandPerformance.ps1


<#

.SYNOPSIS
Measures the average time of a command, accounting for natural variability by
automatically ignoring the top and bottom ten percent.

.EXAMPLE
PS > .\Measure-CommandPerformance.ps1 { Start-Sleep -m 300 }

Count    : 30
Average  : 312.10155
(...)

#>

param(
    [Scriptblock] $command,

    [int] $iterations = 30)

Set-StrictMode -Version Latest

$buffer = [int] ($iterations * 0.1)
$totalIterations = $iterations + (2 * $buffer)

$results = 1..$totalIterations | Foreach-Object { Measure-Command $command }

$middleResults = $results | Sort TotalMilliseconds |
    Select -Skip $buffer -First $iterations

$middleResults | Measure-Object -Average TotalMilliseconds

      

2 comments

  1. Johannes Rössel Posted 16 days and 19 hours ago

    I'd suggest starting parameter names with a capital letter. This makes tab-completing the parameter names comparable with built-in cmdlets. Since PowerShell doesn't do this capitalization on its own the result would then be

    PS Home:\> Measure-CommandPerformance -c<TAB>
    PS Home:\> Measure-CommandPerformance -command
    

    instead of

    PS Home:\> Measure-CommandPerformance.ps1 -Command
    

    This is probably very minor, but it'd be a nice touch.

  2. Lee Holmes Posted 14 days and 18 hours ago

    Thanks, I definitely agree. I need to do a review of the included scripts for consistency, and this is one goal.

Add a comment


For more information about the Measure-Command cmdlet, type Get-Help Measure-Command.

Read and Write from the Windows Clipboard

Problem

You want to interact with the Windows clipboard.

Solution

Use the Get-Clipboard and Set-Clipboard scripts.

Example 8.2. Get-Clipboard.ps1


<#

.SYNOPSIS

Retrieve the text contents of the Windows Clipboard.

.EXAMPLE

PS >Get-Clipboard
Hello World

#>

PowerShell -NoProfile -STA -Command {
    Add-Type -Assembly PresentationCore
    [Windows.Clipboard]::GetText()
}
      

Example 8.3. Set-Clipboard.ps1


<#

.SYNOPSIS

Sends the given input to the Windows clipboard.

.EXAMPLE

PS >dir | Set-Clipboard

This example sends the view of a directory listing to the clipboard

.EXAMPLE

PS >Set-Clipboard "Hello World"

This example sets the clipboard to the string, "Hello World".

#>

param(
    [Parameter(ValueFromPipeline = $true)]
    [object[]] $inputObject
)

begin
{
    $objectsToProcess = @()
}

process
{
    $objectsToProcess += $inputObject
}

end
{
    $objectsToProcess | PowerShell -NoProfile -STA -Command {
        Add-Type -Assembly PresentationCore

        $clipText = ($input | Out-String -Stream) -join "`r`n"

        [Windows.Clipboard]::SetText($clipText)
    }
}
      

Discussion

While Windows includes a command-line utility (clip.exe) to place text in the Windows clipboard, it doesn't support direct input (i.e.: clip.exe "Hello World"), and doesn't have a corresponding utility to retrieve the contents to the Windows clipboard.

The Set-Clipboard and Get-Clipboard scripts given in the solution resolve both of these issues.

Both rely on the System.Windows.Clipboard class, which has a special requirement that it be run from an application in Single Threaded Apartment (STA) mode. To support that, the scripts launch a new instance of PowerShell in this mode. For more information about interacting with this type of class, see the section called “Interact With UI Frameworks and STA Objects”.

1 comment

  1. Johannes Rössel Posted 16 days and 19 hours ago

    Maybe this should add a note that System.Windows.Clipboard is part of .NET 3 which is not required for PowerShell 2. In fact, Windows XP users with only .NET 2 won't be able to use this solution and probably might wonder why.

    Alternatively the two scripts could be provided with System.Windows.Forms.Clipboard, though I don't know whether the code would be similarly short or not. Proper clipboard support usually is quite a lot code, as far as I have seen.

Add a comment

For more information about working with classes from the .NET Framework, see the section called “Work with .NET Objects”.

Generate a Random Number or Object

Problem

You want to generate a random number, or pick a random element from a set of objects.

Solution

Call the Get-Random cmdlet to generate a random positive integer.

Get-Random

Use the -Minimum and -Maximum parameters to generate a number between Minimum and up to (but not including) Maximum.

Get-Random -Minimum 1 -Maximum 21

Use the -InputObject parameter (or simple pipeline input) to pick a random element from a list.

PS > $suits = "Hearts","Clubs","Spades","Diamonds"
PS > $faces = (2..10)+"A","J","Q","K"
PS > $cards = foreach($suit in $suits) { foreach($face in $faces) { "$face of $suit" } }
PS > $cards | Get-Random
A of Spades
PS > $cards | Get-Random
2 of Clubs

Discussion

The Get-Random cmdlet solves the problems usually associated with picking random numbers or random elements from a collection: scaling and seeding.

Most random number generators only generate numbers between 0 and 1. If you need a number from a different range, you have to go through a separate scaling step to map those numbers to the appropriate range. While not terribly difficult, it's a usability hurdle that requires more than trivial knowledge to do properly.

1 comment

  1. Johannes Rössel Posted 16 days and 18 hours ago

    Actually, most PRNGs generate integer values, only very few directly yield floating-point numbers. Many random-number generation frameworks break generating numbers down to a bit stream that is then transformed the numbers as needed. Creating a double value from that is trivial then: Just grab 53 bits and put them into the mantissa.

    Scaling such a number to get integer results gives rise to aliasing artifacts, though, which may result in an uneven distribution.

Add a comment

Ensuring that the random number generator picks good random numbers is a different problem entirely. All general-purpose random number generators use mathematical equations to generate their values. They make new values by incorporating the number they generated just before that—a feedback process that guarantees evenly-distributed sequences of numbers. Maintaining this internal state is critical, as restarting from a specific point will always generate the same number—not very random at all! You lose this internal state every time you create a new random number generator.

1 comment

  1. Johannes Rössel Posted 16 days and 19 hours ago

    “just before that - a feedback process” → “just before that—a feedback process”

    “the same number - not very random at all!” → “the same number—not very random at all!”

    (em dashes)

Add a comment

To create their first value, generators need a random number seed. While you can supply a seed directly (through the -SetSeed parameter) for testing purposes, it is usually derived from the system time.

Unless you re-use the same random number generator, this last point usually leads to the downfall of realistically random numbers. When you generate them quickly, you create new random number generators that are likely to have the same seed:

PS > 1..10 | Foreach-Object { (New-Object System.Random).Next(1, 21) }
20
7
7
15
15
11
11
18
18
18

The Get-Random cmdlet saves you from this issue by internally maintaining a random number generator and its state:

PS > 1..10 | Foreach-Object { Get-Random -Min 1 -Max 21 }
20
18
7
12
16
10
9
13
16
14

For more information about working with classes from the .NET Framework, see the section called “Work with .NET Objects”.

1 comment

  1. David "Makovec" Moravec Posted 14 days and 21 hours ago

    I would say that Count is another very useful parametr of Get-Random. So for example last code can be also in this form:

    1..20 | Get-Random -Count 10

    or another example:

    Get-Process | Get-Random -Count 5

Add a comment

Program: Search the Windows Start Menu

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 8.4, “Search-StartMenu.ps1”.

Example 8.4. Search-StartMenu.ps1


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

$myStartMenu = [Environment]::GetFolderPath("StartMenu")
$shell = New-Object -Com WScript.Shell
$allStartMenu = $shell.SpecialFolders.Item("AllUsersStartMenu")

$escapedMatch = [Regex]::Escape($pattern)

dir $myStartMenu *.lnk -rec | ? { $_.Name -match "$escapedMatch" }
dir $allStartMenu *.lnk -rec | ? { $_.Name -match "$escapedMatch" }

dir $myStartMenu *.lnk -rec | 
    Where-Object { $_ | Select-String "\\[^\\]*$escapedMatch\." -Quiet }
dir $allStartMenu *.lnk -rec | 
    Where-Object { $_ | Select-String "\\[^\\]*$escapedMatch\." -Quiet }

      

For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

Program: Show Colorized Script Content

Discussion

When viewing or demonstrating scripts, syntax highlighting makes the information immensely easier to read. Viewing the scripts in the PowerShell Integrated Scripting Environment (ISE) is the most natural (and powerful) option, but you might want to view them in the console as well.

In addition to basic syntax highlighting, other useful features during script review are line numbers, and highlighting ranges of lines. Range highlighting is especially useful when discussing portions of a script in a larger context.

Example 8.5, “Show-ColorizedContent.ps1” enables all of these scenarios by providing syntax highlighting of scripts in a console session.

Figure 8.1. Sample colorized content

Sample colorized content

1 comment

  1. David "Makovec" Moravec Posted 14 days and 20 hours ago

    Parameter HighlightRange is with capital 'H' but in script is with lower 'h'

Add a comment

In addition to having utility all on its own, Show-ColorizedContent.ps1 demonstrates how to use PowerShell's Tokenizer API, as introduced in the section called “Parse and Interpret PowerShell Scripts”. While many of the techniques are specific to syntax highlighting in a PoweShell console, many more apply to all forms of script manipulation.

Example 8.5. Show-ColorizedContent.ps1


<#

.SYNOPSIS
Displays syntax highlighting, line numbering, and range highlighting for
PowerShell scripts.

.EXAMPLE
PS >Show-ColorizedContent Invoke-MyScript.ps1

001 | function Write-Greeting
002 | {
003 |     param($greeting)
004 |     Write-Host "$greeting World"
005 | }
006 |
007 | Write-Greeting "Hello"

.EXAMPLE
PS >Show-ColorizedContent Invoke-MyScript.ps1 -highlightRange (1..3+7)

001 > function Write-Greeting
002 > {
003 >     param($greeting)
004 |     Write-Host "$greeting World"
005 | }
006 |
007 > Write-Greeting "Hello"  

#>

#requires -version 2.0

param(
    $filename = $(throw "Please specify a filename."),
    $highlightRange = @(),
    [Switch] $excludeLineNumbers)

$replacementColours = @{ 
    'Attribute' = 'DarkCyan'
    'Command' = 'Blue'
    'CommandArgument' = 'Magenta'
    'CommandParameter' = 'DarkBlue'
    'Comment' = 'DarkGreen'
    'GroupEnd' = 'Black'
    'GroupStart' = 'Black'
    'Keyword' = 'DarkBlue'
    'LineContinuation' = 'Black'
    'LoopLabel' = 'DarkBlue'
    'Member' = 'Black'
    'NewLine' = 'Black'
    'Number' = 'Magenta'
    'Operator' = 'DarkGray'
    'Position' = 'Black'
    'StatementSeparator' = 'Black'
    'String' = 'DarkRed'
    'Type' = 'DarkCyan'
    'Unknown' = 'Black'
    'Variable' = 'Red'
}

$highlightColor = "Red"
$highlightCharacter = ">"
$highlightWidth = 6
if($excludeLineNumbers) { $highlightWidth = 0 }

$file = (Resolve-Path $filename).Path
$content = [IO.File]::ReadAllText($file)
$parsed = [System.Management.Automation.PsParser]::Tokenize(
    $content, [ref] $null) | Sort StartLine,StartColumn

function WriteFormattedLine($formatString, [int] $line)
{
    if($excludeLineNumbers) { return }
    
    $hColor = "DarkGray"
    $separator = "|"
    
    if($highlightRange -contains $line)
    {
        $hColor = $highlightColor
        $separator = $highlightCharacter
    }
    
    $text = $formatString -f $line,$separator
    Write-Host -NoNewLine -Fore $hColor -Back White $text
}

function CompleteLine($column)
{
    $lineRemaining = $host.UI.RawUI.WindowSize.Width - 
        $column - $highlightWidth + 1
    
    if($lineRemaining -lt 0)
    {
        $lineRemaining += $host.UI.RawUI.WindowSize.Width
    }
    
    Write-Host -NoNewLine -Back White (" " * $lineRemaining)
}


Write-Host
WriteFormattedLine "{0:D3} {1} " 1

$column = 1
foreach($token in $parsed)
{
    $color = "Gray"

    $color = $replacementColours[[string]$token.Type]
    if(-not $color) { $color = "Gray" }

    if(($token.Type -eq "NewLine") -or ($token.Type -eq "LineContinuation"))
    {
        CompleteLine $column
        WriteFormattedLine "{0:D3} {1} " ($token.StartLine + 1)
        $column = 1
    }
    else
    {
        if($column -lt $token.StartColumn)
        {
            $text = " " * ($token.StartColumn - $column)
            Write-Host -Back White -NoNewLine $text
            $column = $token.StartColumn
        }

        $tokenEnd = $token.Start + $token.Length - 1

        if(
            (($token.Type -eq "String") -or
             ($token.Type -eq "Comment")) -and
            ($token.EndLine -gt $token.StartLine))
        {
            $lineCounter = $token.StartLine
            
            $stringLines = $(
                -join $content[$token.Start..$tokenEnd] -split "`n")
            
            foreach($stringLine in $stringLines)
            {
                $stringLine = $stringLine.Trim()
                
                if($lineCounter -gt $token.StartLine)
                {
                    CompleteLine $column
                    WriteFormattedLine "{0:D3} {1} " $lineCounter
                    $column = 1
                }
                
                Write-Host -NoNewLine -Fore $color -Back White $stringLine
                $column += $stringLine.Length
                $lineCounter++
            }
        }
        else
        {
            $text = (-join $content[$token.Start..$tokenEnd])
            Write-Host -NoNewLine -Fore $color -Back White $text
        }

        $column = $token.EndColumn
    }
}

CompleteLine $column
Write-Host 

For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

You must sign in or register before commenting
*
*
*
*
*

Atom Icon Comments on this page or Comments on the whole book.