Chapter 13. User Interaction

Introduction

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

Note

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 choice-based 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.

Read a Line of User Input

Problem

You want to use input from the user in your script.

Solution

To obtain user input, use the Read-Host cmdlet:

PS > $directory = Read-Host "Enter a directory name"
Enter a directory name: C:\MyDirectory
PS > $directory
C:\MyDirectory

Discussion

The Read-Host cmdlet reads a single line of input from the user. If the input contains sensitive data, the cmdlet supports an –AsSecureString parameter to read this input as a SecureString.

If the user input represents a date, time, or number, be aware that most cultures represent these data types differently. For more information about writing culturally aware scripts, see the section called “Write Culture-Aware Scripts”.

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

Read a Key of User Input

Problem

You want your script to get a single keypress from the user.

Solution

For most purposes, use the [Console]::ReadKey() method to read a key:

PS > $key = [Console]::ReadKey($true)
PS > $key

                 KeyChar                   Key              Modifiers
                 -------                   ---              ---------
                       h                     H                    Alt

For highly interactive use (for example, when you care about key down and key up), use:

PS > $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
PS > $key

    VirtualKeyCode          Character    ControlKeyState      KeyDown
    --------------          ---------    ---------------      -------
                16                    ...ssed, NumLockOn         True

PS > $key.ControlKeyState
ShiftPressed, NumLockOn

Discussion

For most purposes, the [Console]::ReadKey() is the best way to get a keystroke from a user, as it accepts simple keypresses—as well as more complex keypresses that might include the Ctrl, Alt, and Shift keys. We pass the $true parameter to tell the method to not display the character on the screen, and only to return it to us.

The following function emulates the DOS pause command:

function Pause
{
    Write-Host -NoNewLine "Press any key to continue . . . "
    [Console]::ReadKey($true) | Out-Null
    Write-Host
}

If you need to capture individual key down and key up events (including those of the Ctrl, Alt, and Shift keys), use the $host.UI.RawUI.ReadKey() method.

Program: Display a Menu to the User

It is often useful to read input from the user but restrict it to a list of choices that you specify. The following script lets you access PowerShell's prompting functionality in a manner that is friendlier than what PowerShell exposes by default. It returns a number that represents the position of their choice from the list of options you provide.

PowerShell's prompting requires that you include an accelerator key (the & before a letter in the option description) to define the keypress that represents that option. Since you don't always control the list of options (for example, a list of possible directories), Example 13.1, “Read-HostWithPrompt.ps1” automatically generates sensible accelerator characters for any descriptions that lack them.

Example 13.1. Read-HostWithPrompt.ps1

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

param(
    $caption = $null,
    $message = $null,
    $option = $(throw "Please specify some options."),
    $helpText = $null,
    $default = 0
    )

$choices = New-GenericObject Collections.ObjectModel.Collection Management.Automation.Host.ChoiceDescription

for($counter = 0; $counter -lt $option.Length; $counter++)
{
    $choice = New-Object Management.Automation.Host.ChoiceDescription $option[$counter]
    if($helpText -and $helpText[$counter])
    {
        $choice.HelpMessage = $helpText[$counter]
    }

    $choices.Add($choice)
}

$host.UI.PromptForChoice($caption, $message, $choices, $default)

      

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

Display Messages and Output to the User

Problem

You want to display messages and other information to the user.

Solution

To ensure that the output actually reaches the screen, call the Write-Host (or Out-Host) cmdlet:

PS > function Get-DirectorySize
>> {
>>     $size = (Get-ChildItem | Measure-Object -Sum Length).Sum
>>     Write-Host ("Directory size: {0:N0} bytes" -f $size)
>> }
>>
PS > Get-DirectorySize
Directory size: 46,581 bytes
PS > $size = Get-DirectorySize
Directory size: 46,581 bytes

If you want a message to help you (or the user) diagnose and debug your script, use the Write-Debug cmdlet. If you want a message to provide detailed trace-type output, use the Write-Verbose cmdlet, as shown in Example 13.2, “A function that provides debug and verbose output”.

Example 13.2. A function that provides debug and verbose output

PS > function Get-DirectorySize
>> {
>>     Write-Debug "Current Directory: $(Get-Location)"
>>
>>     Write-Verbose "Getting size"
>>     $size = (Get-ChildItem | Measure-Object -Sum Length).Sum
>>     Write-Verbose "Got size: $size"
>>
>>     Write-Host ("Directory size: {0:N0} bytes" -f $size)
>> }
>>
PS > $DebugPreference = "Continue"
PS > Get-DirectorySize
DEBUG: Current Directory: D:\lee\OReilly\Scripts\Programs
Directory size: 46,581 bytes
PS > $DebugPreference = "SilentlyContinue"
PS > $VerbosePreference = "Continue"
PS > Get-DirectorySize
VERBOSE: Getting size
VERBOSE: Got size: 46581
Directory size: 46,581 bytes
PS > $VerbosePreference = "SilentlyContinue"

Discussion

Most scripts that you write will output richly structured data, such as the actual count of bytes in a directory. That way, other scripts can use the output of that script as a building block for their functionality.

When you do want to provide output specifically to the user, use the Write-Host, Write-Debug, and Write-Verbose cmdlets.

However, be aware that this type of output bypasses normal file redirection, and is therefore difficult for the user to capture. In the case of the Write-Host cmdlet, use it only when your script already generates other structured data that the user would want to capture in a file or variable.

Most script authors eventually run into the problem illustrated by Example 13.3, “An error message caused by formatting statements” when their script tries to output formatted data to the user.

Example 13.3. An error message caused by formatting statements

PS > ## Get the list of items in a directory, sorted by length
PS > function Get-ChildItemSortedByLength($path = (Get-Location))
>> {
>>     Get-ChildItem $path | Format-Table | Sort Length
>> }
>>
PS > Get-ChildItemSortedByLength
out-lineoutput : Object of type "Microsoft.PowerShell.Commands.Internal.Fo
rmat.FormatEntryData" is not legal or not in the correct sequence. This is
likely caused by a user-specified "format-*" command which is conflicting
with the default formatting.

This happens because the Format-* cmdlets actually generate formatting information for the Out-Host cmdlet to consume. The Out-Host cmdlet (which PowerShell adds automatically to the end of your pipelines) then uses this information to generate formatted output. To resolve this problem, always ensure that formatting commands are the last commands in your pipeline, as shown in Example 13.4, “A function that does not generate formatting errors”.

Example 13.4. A function that does not generate formatting errors

PS > ## Get the list of items in a directory, sorted by length
PS > function Get-ChildItemSortedByLength($path = (Get-Location))
>> {
>>     ## Problematic version
>>     ## Get-ChildItem $path | Format-Table | Sort Length
>>
>>     ## Fixed version
>>     Get-ChildItem $path | Sort Length | Format-Table
>> }
>>
PS > Get-ChildItemSortedByLength

(...)

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         3/11/2007   3:21 PM         59 LibraryProperties.ps1
-a---          3/6/2007  10:27 AM        150 Get-Tomorrow.ps1
-a---          3/4/2007   3:10 PM        194 ConvertFrom-FahrenheitWithout
                                             Function.ps1
-a---          3/4/2007   4:40 PM        257 LibraryTemperature.ps1
-a---          3/4/2007   4:57 PM        281 ConvertFrom-FahrenheitWithLib
                                             rary.ps1
-a---          3/4/2007   3:14 PM        337 ConvertFrom-FahrenheitWithFunc
                                             tion.ps1
(...)

When it comes to producing output for the user, a common reason is to provide progress messages. PowerShell actually supports this in a much richer way, through its Write-Progress cmdlet. For more information about the Write-Progress cmdlet, see the section called “Provide Progress Updates on Long-Running Tasks”.

Provide Progress Updates on Long-Running Tasks

Problem

You want to display status information to the user for long-running tasks.

Solution

To provide status updates, use the Write-Progress cmdlet as shown in Example 13.5, “Using the Write-Progress cmdlet to display status updates”.

Example 13.5. Using the Write-Progress cmdlet to display status updates


$activity = "A long running operation"

$status = "Initializing"
for($counter = 0; $counter -lt 100; $counter++)
{
    $currentOperation = "Initializing item $counter"
    Write-Progress $activity $status -PercentComplete $counter `
                   -CurrentOperation $currentOperation
    Start-Sleep -m 20
}

$status = "Running"
for($counter = 0; $counter -lt 100; $counter++)
{
    $currentOperation = "Running task $counter"
    Write-Progress $activity $status -PercentComplete $counter `
                   -CurrentOperation $currentOperation
    Start-Sleep -m 20
}
      

Discussion

The Write-Progress cmdlet provides a way for you to provide structured status information to the users of your script for long-running operations (see Figure 13.1, “Example output from a long-running operation”).

Like the other detailed information channels (Write-Debug, Write-Verbose, and the other Write-* cmdlets), PowerShell lets users control how much of this information they see.

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

Figure 13.1. Example output from a long-running operation

Example output from a long-running operation

Write Culture-Aware Scripts

Problem

You want to ensure that your script works well on computers around the world.

1 comment

  1. AndrewTearle Posted 16 days and 9 hours ago

    You want to ensure that your script works well on computers around the world. ( Perhaps ? )

Add a comment

Solution

To write culture-aware scripts, keep the following guidelines in mind as you develop your scripts:

Discussion

Writing culture-aware programs has long been isolated to the world of professional software developers. It's not that users of simple programs and scripts can't benefit from culture awareness though. It has just frequently been too difficult for non-professional programmers to follow the best practices. PowerShell makes this much easier than traditional programming languages however.

As your script travels between different cultures, several things change.

Date, time, and number formats

Most cultures have unique date, time, and number formats. To ensure that your script works in all cultures, PowerShell first ensures that its language primitives remain consistent no matter where your script runs. Even if your script runs on a machine in France (which uses a comma for its decimal separator), you can always rely on the statement $myDouble = 3.5 to create a number halfway between three and four. Likewise, you can always count on the statement $christmas = [DateTime]"12/ 25/2007" to create a date that represents Christmas in 2007—even in cultures that write dates in the order of day, month, year.

2 comments

  1. Johannes Rössel Posted 16 days and 5 hours ago
    $christmas = [DateTime]"12/ 25/2007"
    

    should probably lose the extra space in there. It doesn't affect parsing, but usually there isn't a space there (I think).

  2. Johannes Rössel Posted 16 days and 5 hours ago

    Additionally, depending on how many readers your book gets in the US vs. other countries, I'd rather mention the option to use the ISO 8601 date/time format for specifying dates in source code.

    At least in my experience in most European countries the US date format with mm/dd/yyyy is rather confusing and looking at this map the number of countries that use m/d/y order is vanishingly small.

    Using ISO 8601 has the benefit that the format is widely known, especially among programmers, system administrators and other technical people. The following works fine, regardless of the locale:

    PS Home:\> [datetime]"2007-12-25"
    
    Dienstag, 25. Dezember 2007 00:00:00
    

    This format also allows for easy specification of a time zone or explicit UTC time:

    PS Home:\> [datetime]"2007-12-25T23:34:16+4"
    
    Dienstag, 25. Dezember 2007 20:34:16
    
    PS Home:\> [datetime]"2007-12-25T23:34:16Z"
    
    Mittwoch, 26. Dezember 2007 00:34:16
    

Add a comment

Culturally aware programs always display dates, times, and numbers using the preferences of that culture. This doesn't break scripts as they travel between cultures and is an important aspect of writing culture-aware scripts. PowerShell handles this for you, as it uses the current culture's preferences whenever it displays data.

Note

If your script asks the user for a date, time, or number, make sure that you respect the format of the user's culture's when you do so. To convert user input to a specific type of data, use the [DateTime]::Parse() method.

$userInput = Read-Host "Please enter a date"
$enteredDate = [DateTime]::Parse($userInput)

So, to ensure that your script remains culture-aware with respect to dates, times, and number formats, simply use PowerShell's language primitives when you define them in your script. When you read them from the user, use Parse() methods when you convert them from strings.

Complexity of user input and file content

English is a rare language in that its alphabet is so simple. This leads to all kinds of programming tricks that treat user input and file content as arrays of bytes or simple plain-text (ASCII) characters. In most international languages, these tricks fail. In fact, many international symbols take up two characters' worth of data in the string that contains them.

2 comments

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

    In fact, many international symbols take up two characters' worth of data in the string that contains them.

    I'd explicitly say “bytes” there instead of “characters” since the concept of a character it very vague. Common and naïve interpretation usually means a grapheme. Saying “bytes” side-steps the problem of having either vaguely inaccurate terminology or having to explain every aspect of Unicode and a writing system.

  2. Lee Holmes Posted 14 days and 17 hours ago

    Thanks. I agree that's a difficult concept. I think talking about characters instead of bytes here keeps the content at the right level for people that don't deal with bytes all that often, though.

Add a comment

PowerShell uses the standard Unicode character set for all string-based operations: reading input from the user, displaying output to the user, sending data through the pipeline, and working with files.

Note

Although PowerShell fully supports Unicode, the powershell.exe command-line host does not output some characters correctly, due to limitations in the Windows console system. Graphical PowerShell hosts (such as the Integrated Scripting Environment and the many third-party PowerShell IDEs) are not affected by these limitations however.

1 comment

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

    What's interesting to note, although I have no idea how or where this fits (if at all), is that when running native commands that have Unicode output PowerShell apparently fails to capture the output correctly in some cases.

    I once created a file named ::.txt (that is, two times U+FF1A Fullwidth Colon). When set to Raster Fonts the console automatically converts this file name when it appears in a file listing to a normal ASCII colon, creating the impression that you can have a : character in a file name. Now, it all works nicely when using a TrueType font for the console, except that you can't see the character properly but it's at least there as Unicode. That's a (hopefully) well-known problem with the font choice in console windows.

    I n cmd this also affects how program output is handled. When iterating over files with

    for %x in (*) do ...
    

    it all works fine even with Raster fonts, since variables are Unicode, while the following

    for /f "delims=" %x in ('dir /b') do ...
    

    breaks down with Raster fonts, since by running dir its output is parsed and the output is no longer Unicode.

    I just tried out something similar in PowerShell and while the Unicode-ness of strings and file names is preserved there it has interesting results when coupled with external programs. Naïvely I'd expect the two commands

    PS Home:\Stuff\test3> dir | select -expand name
    PS Home:\Stuff\test3> cmd /c "dir /b"
    

    to yield the exact same values. However, while it appears to be so on the command line:

    PS Home:\Stuff\test3> gci | select -expand name
    ::.txt
    PS Home:\Stuff\test3> cmd /c "dir /b"
    ::.txt
    

    it breaks down when writing the result into a file, for (to me) unknown reasons:

    PS Home:\Stuff\test3> gci *.txt | select -expand name | Out-File foo
    PS Home:\Stuff\test3> (cmd /c "dir /b *.txt") | Out-File bar
    PS Home:\Stuff\test3> Compare-Object (gc foo) (gc bar)
    
    InputObject                                                 SideIndicator
    -----------                                                 -------------
    ::.txt                                                      =>
    ::.txt                                                      <=
    

    If this is a bug, then it probably should go to Connect, but if this is documented behavior somewhere I think it should be mentioned. This happens regardless of whether I run the commands in the normal console PowerShell host or in the ISE. However, the call cmd /c "dir /b *.txt" yields Unicode output in the console host, while it returns ASCII (or something close enough) in the ISE. Very strange.

Add a comment

If you use PowerShell's standard features when working with user input, you do not have to worry about its complexity. If you want to work with individual characters or words in the input, though, you will need to take special precautions. The System.Globalization.StringInfo class lets you do this in a culturally aware way. For more information about working with the StringInfo class, see http://msdn.microsoft.com/en-us/library/7h9tk6x8.aspx.

1 comment

  1. Johannes Rössel Posted 15 days and 23 hours ago

    The URL could be shortened to http://msdn.microsoft.com/library/7h9tk6x8 which still is quite much to type but there probably isn't a shorter, readable (or typable) variant, I presume.

Add a comment

So, to ensure that your script remains culturally aware with respect to user input, simply use PowerShell's support for string operations whenever possible.

Capitalization rules

A common requirement in scripts is to compare user input against some predefined text (such as a menu selection). You normally want this comparison to be case insensitive, so that "QUIT" and "qUiT" mean the same thing.

A traditional way to accomplish this is to convert the user input to uppercase or lowercase:

## $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 run in different cultures, as many cultures have different capitalization and comparison rules. 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. That example causes the above string comparison to fail on a Turkish system.

To compare some input against a hard-coded string in a case-insensitive manner, the better solution is to use PowerShell's –eq operator without changing any of the casing yourself. The –eq operator is case-insensitive and culture-neutral by default:

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

1 comment

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

    There probably should be a space between the variable $text1 and the -eq operator. While not strictly necessary having whitespace around operators (especially operators that use letters instead of symbols) greatly enhances readability :-)

Add a comment

So, to ensure that your script remains culturally aware with respect to capitalization rules, simply use PowerShell's case-insensitive comparison operators whenever possible.

1 comment

  1. AndrewTearle Posted 16 days and 9 hours ago

    when-ever possible. ( whenever ? )

Add a comment

Sorting rules

Sorting rules frequently change between cultures. For example, compare English and Danish with the script given in "Program: Invoke a Script Block with Alternate Culture Settings."

PS > Use-Culture en-US { "Apple","Æble" | Sort-Object }
Æble
Apple
PS > Use-Culture da-DK { "Apple","Æble" | Sort-Object }
Apple
Æble

To ensure that your script remains culturally aware with respect to sorting rules, assume that output is sorted correctly after you sort it—but don't depend on the actual order of sorted output.

1 comment

  1. AndrewTearle Posted 16 days and 9 hours ago

    To ensure that your script remains culturally aware with respect to sorting rules, assume that output is sorted correctly after you sort it — but don't depend on the actual order of sorted output. ( A little obscure ? )

Add a comment

Other guidelines

For other resources on these factors for writing culturally aware programs, see http://msdn.microsoft.com/en-us/library/h6270d0z.aspx and http://www.microsoft.com/globaldev/getwr/steps/wrguide.mspx.

See Also

Support Other Languages in Script Output

Problem

You are displaying text messages to the user, and want to support international languages.

Solution

Use the Import-LocalizedData cmdlet, as shown inExample 13.6, “Importing culture-specific strings for a script or module”.

Example 13.6. Importing culture-specific strings for a script or module

## Create some default messages for English cultures, and
$messages = DATA {
    @{
        Greeting = "Hello, {0}"
        Goodbye = "So long."
    }
}

Import-LocalizedData messages -ErrorAction SilentlyContinue

$messages.Greeting -f "World"
$messages.Goodbye      

Discussion

The Import-LocalizedData cmdlet lets you easily write scripts that display different messages for different languages.

The core of this localization support comes from the concept of a message table: a simple mapping of message IDs (such as a "Greeting" or "Goodbye" message) to the actual message it represents. Instead of directly outputting a string to the user, you instead retrieve the string from the message table and output that. Localization of your script comes from replacing the message table with one that contains messages appropriate for the current language.

PowerShell uses standard hashtables to define message tables. Keys and values in the hashtable represent message IDs their corresponding strings, respectively.

Note

The solution defines the default message table within a DATA section. As with loading messages from .psd1 files, this places PowerShell in a data-centric subset of the full PowerShell language. While not required, it is a useful practice for both error detection and consistency.

After defining a default message table in your script, the next step is to create localized versions and place them in language-specific directories alongside your script. The real magic of the Import-LocalizedData cmdlet comes from the intelligence it applies when loading the appropriate message file.

As a background, the standard way to refer to a culture (for localization purposes) is an identifier that combines the culture and region. For example, German as spoken in Germany is defined by the identifier de-DE. English as spoken in the United States is defined by the identifier en-US, while English as spoken in Canada is defined by the identifier en-CA. Most languages are spoken in many regions.

When you call the Import-LocalizedData cmdlet, PowerShell goes to the same directory as your script, and first tries to load your messages from a directory with a name that matches the full name of the current culture (for example, en-CA or en-GB.) If that fails, it falls back to the region-neutral directory (such as en, or de) and on to the other fallback languages defined by the operating system.

To make your efforts available to the broadest set of languages, place your localized messages in the most general directory that applies. For example, place French messages (first) in the "fr" directory so that all French-speaking regions can benefit. If you want to customize your messages to a specific region after that, place them in a region-specific directory.

Rather than define these message tables in script files (like your main script), place them in .psd1 files that have the same name as your script. For example, Example 13.6, “Importing culture-specific strings for a script or module” places its localized messages in Import-LocalizedData.psd1. PowerShell's psd1 files represent a data-centric subset of the full PowerShell language, and are ideally suited for localization. In the .psd1 file, define a hashtable—but do not store it in a variable like you do for the default message table:

Example 13.7. A localized .psd1 file that defines a message table

@{
    Greeting = "Guten Tag, {0}"
    Goodbye = "Auf Wiedersehen."
}      

If you already use a set of tools to help you manage the software localization process, they may not understand the PowerShell .psd1 file format. Another standard message format is simple name-value mapping, so PowerShell supports that through the ConvertFrom-StringData cmdlet:

ConvertFrom-StringData @'
Greeting = Guten Tag, {0}
Goodbye = Auf Wiedersehen
'@

1 comment

  1. Johannes Rössel Posted 15 days and 22 hours ago

    The example should use ConvertFrom-StringData like described in the text above, instead of Import-StringData (which doesn't exist).

Add a comment

Notice that the Greeting message in Example 13.6, “Importing culture-specific strings for a script or module” uses {0}-style placeholders (and PowerShell's string formatting operator) to output strings with replacable text. This technique is vastly preferred over string concatenation (i.e.: $messages.GreetingBeforeName + " World " + $messages.GreetingAftername) because it gives additional flexibility during localization of languages with different sentence structure.

To test your script under different languages, you can use the section called “Program: Invoke a Script Block with Alternate Culture Settings”.

PS > Use-Culture de-DE { Invoke-LocalizedScript }
Guten Tag, World
Auf Wiedersehen.

For more information about script internationalization, type Get-Help about_Script_Internationalization.

Program: Invoke a Script Block with Alternate Culture Settings

Given PowerShell's diverse user community, scripts that you share will often be run on a system set to a language other than English. To ensure that your script runs properly in other languages, it is helpful to give it a test run in that culture. Example 13.8, “Use-Culture.ps1” lets you run the script block you provide in a culture of your choosing.

Example 13.8. Use-Culture.ps1

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

param(
    [System.Globalization.CultureInfo] $culture = 
        $(throw "Please specify a culture"),
    [ScriptBlock] $script = $(throw "Please specify a scriptblock")
    )

function Set-Culture([System.Globalization.CultureInfo] $culture)
{
    [System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture
    [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
}

$oldCulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture
   
trap { Set-Culture $oldCulture }

Set-Culture $culture

& $script

Set-Culture $oldCulture

      

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

Access Features of the Host's User Interface

Problem

You want to interact with features in the user interface of the hosting application, but PowerShell doesn't directly provide cmdlets for them.

Solution

To access features of the host's user interface, use the $host.UI.RawUI variable:

$host.UI.RawUI.WindowTitle = (Get-Location)

Discussion

PowerShell itself consists of two main components. The first is an engine that interprets commands, executes pipelines, and performs other similar actions. The second is the hosting application—the way that users interact with the PowerShell engine.

The default shell, PowerShell.exe, is a user interface based on the traditional Windows console. The graphical Integrated Scripting Environment hosts PowerShell in a graphical user interface. In fact, PowerShell makes it relatively simple for developers to build their own hosting applications, or even to embed the PowerShell engine features into their own application.

You (and your scripts) can always depend on the functionality available through the $host.UI variable, as that functionality remains the same for all hosts. Example 13.9, “Functionality available through the $host.UI property” shows the features available to you in all hosts.

Example 13.9. Functionality available through the $host.UI property

PS > $host.UI | Get-Member | Select Name,MemberType | Format-Table -Auto

Name                   MemberType
----                   ----------
(...)
Prompt                     Method
PromptForChoice            Method
PromptForCredential        Method
ReadLine                   Method
ReadLineAsSecureString     Method
Write                      Method
WriteDebugLine             Method
WriteErrorLine             Method
WriteLine                  Method
WriteProgress              Method
WriteVerboseLine           Method
WriteWarningLine           Method
RawUI                    Property

If you (or your scripts) want to interact with portions of the user interface specific to the current host, PowerShell provides that access through the $host.UI.RawUI variable. Example 13.10, “Functionality available through the default console host” shows the features available to you in the PowerShell console host.

Example 13.10. Functionality available through the default console host

PS > $host.UI.RawUI | Get-Member |
>>     Select Name,MemberType | Format-Table -Auto
>>

Name                      MemberType
----                      ----------
(...)
FlushInputBuffer              Method
GetBufferContents             Method
GetHashCode                   Method
GetType                       Method
LengthInBufferCells           Method
NewBufferCellArray            Method
ReadKey                       Method
ScrollBufferContents          Method
SetBufferContents             Method
BackgroundColor             Property
BufferSize                  Property
CursorPosition              Property
CursorSize                  Property
ForegroundColor             Property
KeyAvailable                Property
MaxPhysicalWindowSize       Property
MaxWindowSize               Property
WindowPosition              Property
WindowSize                  Property
WindowTitle                 Property

If you rely on the host-specific features from $host.UI.RawUI, be aware that your script will require modifications (perhaps major) before it will run properly on other hosts.

Program: Add a Graphical User Interface to Your Script

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

1 comment

  1. Johannes Rössel Posted 8 days and 17 hours ago

    Maybe mentioning WPK somewhere might be an option. Doesn't need to be a full example (even though those are very helpful) but I found it (after a short learning time) to be much easier to create a quick GUI application with WPK than by writing down Windows Forms code that is normally generated by the compiler for me.

Add a comment

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

In addition to creating Windows Forms applications through PowerShell scripts, two community projects (PowerBoots and WPK) let you easily create rich WPF interfaces for your PowerShell scripts. For more information, search the internet for PowerShell PowerBoots and PowerShell WPK.

Example 13.11, “Select-GraphicalFilteredObject.ps1” demonstrates the techniques you can use to develop a Windows Forms application using PowerShell scripting alone.

Example 13.11. Select-GraphicalFilteredObject.ps1

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

$objectArray = @($input)

if($objectArray.Count -eq 0)
{
    Write-Error "This script requires pipeline input."
    return
}

Add-Type -Assembly System.Windows.Forms

$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(600,600)

$listbox = New-Object Windows.Forms.CheckedListBox
$listbox.CheckOnClick = $true
$listbox.Dock = "Fill"
$form.Text = "Select the list of objects you wish to pass down the pipeline"
$listBox.Items.AddRange($objectArray)

$buttonPanel = New-Object Windows.Forms.Panel
$buttonPanel.Size = New-Object Drawing.Size @(600,30)
$buttonPanel.Dock = "Bottom"

$cancelButton = New-Object Windows.Forms.Button
$cancelButton.Text = "Cancel"
$cancelButton.DialogResult = "Cancel"
$cancelButton.Top = $buttonPanel.Height - $cancelButton.Height - 5
$cancelButton.Left = $buttonPanel.Width - $cancelButton.Width - 10
$cancelButton.Anchor = "Right"

$okButton = New-Object Windows.Forms.Button
$okButton.Text = "Ok"
$okButton.DialogResult = "Ok"
$okButton.Top = $cancelButton.Top
$okButton.Left = $cancelButton.Left - $okButton.Width - 5
$okButton.Anchor = "Right"

$buttonPanel.Controls.Add($okButton)
$buttonPanel.Controls.Add($cancelButton)

$form.Controls.Add($listBox)
$form.Controls.Add($buttonPanel)
$form.AcceptButton = $okButton
$form.CancelButton = $cancelButton
$form.Add_Shown( { $form.Activate() } )

$result = $form.ShowDialog()

if($result -eq "OK")
{
    foreach($index in $listBox.CheckedIndices)
    {
        $objectArray[$index]
    }
}

      

3 comments

  1. Johannes Rössel Posted 15 days and 22 hours ago

    This script could now use Add-Type -Assembly System.Windows.Forms instead of the old (v1 style) [Reflection.Assembly]::LoadWithPartialName call.

  2. Mike Martino Posted 5 days and 4 hours ago

    A good example of how to run this script is to do: 1..10 | Select-GraphicalFilteredObject.ps1.

    Maybe that was supposed to be obvious, but it took me a while to figure that out.

  3. Lee Holmes Posted 1 day and 16 hours ago

    Sorry, there's an example in the script in the comments - unfortunately, those are being eaten by the HTML publishing system at the moment :)

Add a comment


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

Interact With UI Frameworks and STA Objects

Problem

You want to interact with a user interface framework or other object that requires that the current thread be in Single Threaded Apartment (STA) mode:

PS > Add-Type -Assembly PresentationCore
PS > [Windows.Clipboard]::SetText("Hello World")
Exception calling "SetText" with "1" argument(s): "Current thread must be
set to single thread apartment (STA) mode before OLE calls can be made."
At line:1 char:29
+ [Windows.Clipboard]::SetText <<<< ("Hello World")
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationExcep
   tion
    + FullyQualifiedErrorId : DotNetMethodException

Solution

Launch PowerShell with the -STA switch. If you do this as part of a script or helper command, also use the -NoProfile switch to avoid the performance impact and side effects of loading the user's profile:

1 comment

  1. AndrewTearle Posted 16 days and 9 hours ago

    -NoProfile swtich <

Add a comment

PS > PowerShell -NoProfile -STA -Command {
>>     Add-Type -Assembly PresentationCore
>>     [Windows.Clipboard]::SetText("Hello World")
>> }
>>
PS > PowerShell -NoProfile -STA -Command {
>>     Add-Type -Assembly PresentationCore
>>     [Windows.Clipboard]::GetText()
>> }
>>
Hello World

Discussion

Threading modes define an agreement between an application and how it interacts with some of its objects. Most objects in the .NET Framework (and thus, PowerShell and nearly everything it interacts with) ignore the threading mode and are not impacted by it.

Many user interface frameworks (such as WPF and WinForms) do require a specific threading mode, though, called Single Threaded Apartment. PowerShell uses a threading mode called Multi-Threaded Apartment (MTA) by default, so you'll sometimes run into an error similar to that in the problem

1 comment

  1. AndrewTearle Posted 16 days and 9 hours ago

    Aparment (MTA) by default, ( Apartment )

Add a comment

If you frequently find yourself needing to use STA mode, you can simply modify the PowerShell link on your start menu to always load PowerShell with the -STA parameter. It is incredibly rare for a component to requre MTA mode rather than STA mode, and it has no performance impact.

Note

You might wonder why we didn't just change PowerShell's default if it was so safe. While it is very rare for a component to run into additional issues in STA mode, some advanced threading situations cease to work. Since they worked in version one of PowerShell, changing the default would have introduced compatibility issues.

If your entire script requires STA mode, you have two primary options: detect the current threading mode, or relaunch yourself under STA mode.

To detect the current threading mode, you can access the $host.Runspace.ApartmentState variable. If its value is "STA", the current threading mode is STA.

If your script has simple parameter requirements, you may be able to relaunch yourself automatically:

Example 13.12. A script that relaunches itself in STA mode

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

param(
    $parameter1,
    $parameter2
)

"Current threading mode: " + $host.Runspace.ApartmentState
"Parameter1 is: $parameter1"
"Parameter2 is: $parameter2"

if($host.Runspace.ApartmentState -ne "STA")
{
    "Relaunching"
    $file = $myInvocation.MyCommand.Path
    powershell -NoProfile -Sta -File $file $parameter1 $parameter2
    return
}

"After relaunch - current threading mode: " + $host.Runspace.ApartmentState      

When you run this script, you get the following output:

PS > .\Invoke-ScriptThatRequiresSta.ps1 Test1 Test2
Current threading mode: Unknown
Parameter1 is: Test1
Parameter2 is: Test2
Relaunching
Current threading mode: STA
Parameter1 is: Test1
Parameter2 is: Test2
After relaunch - current threading mode: STA
You must sign in or register before commenting
*
*
*
*
*

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