Chapter 3. Variables and Objects

Introduction

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

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

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

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

Display the Properties of an Item as a List

Problem

You have an item (for example, an error record, directory item, or .NET object), and you want to display detailed information about that object in a list format.

Solution

To display detailed information about an item, pass that item to the Format-List cmdlet. For example, to display an error in list format, type the following commands:

$currentError = $error[0]
$currentError | Format-List -Force

Discussion

Many commands by default display a summarized view of their output in a table format, for example, the Get-Process cmdlet:

PS > Get-Process PowerShell

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    920      10    43808      48424   183     4.69   1928 powershell
    149       6    18228       8660   146     0.48   1940 powershell
    431      11    33308      19072   172            2816 powershell

In most cases, the output actually contains a great deal more information. You can use the Format-List cmdlet to view it:

PS > Get-Process PowerShell | Format-List *


__NounName                 : Process
Name                       : powershell
Handles                    : 443
VM                         : 192176128
WS                         : 52363264
PM                         : 47308800
NPM                        : 9996
Path                       : C:\WINDOWS\system32\WindowsPowerShell\v1.0\power
                             shell.exe
Company                    : Microsoft Corporation
CPU                        : 4.921875
FileVersion                : 6.0.6002.18139 (vistasp2_gdr_win7ip_winman(wmbla
                             ).090902-1426)
ProductVersion             : 6.0.6002.18139
Description                : Windows PowerShell
(...)

The Format-List cmdlet is one of the four PowerShell formatting cmdlets. These cmdlets are Format-Table, Format-List, Format-Wide, and Format-Custom. The Format-List cmdlet takes input and displays information about that input as a list.

By default, PowerShell takes the list of properties to display from the *.format.ps1xml files in PowerShell’s installation directory. In many situations, you’ll only get a small set of the properties:

PS > Get-Process PowerShell | Format-List

Id      : 2816
Handles : 431
CPU     :
Name    : powershell

Id      : 5244
Handles : 665
CPU     : 10.296875
Name    : powershell

To display all properties of the item, type Format-List *. If you type Format-List * but still do not get a list of the item’s properties, then the item is defined in the *.format.ps1xml files, but does not define anything to be displayed for the list command. In that case, type Format-List -Force.

One common stumbling block in PowerShell’s formatting cmdlets comes from putting them in the middle of a script or pipeline:

PS > Get-Process PowerShell | Format-List | Sort Name
out-lineoutput : The object of type "Microsoft.PowerShell.Commands.Internal.
Format.FormatEntryData" is not valid or not in the correct sequence. This is
likely caused by a user-specified "format-*" command which is conflicting with
the default formatting.

Internally, PowerShell’s formatting commands generate a new type of object: Microsoft.PowerShell.Commands.Internal.Format.*. When these objects make it to the end of the pipeline, PowerShell automatically sends them to an output cmdlet: by default, Out-Default. These Out-* cmdlets assume that the objects arrive in a certain order, so doing anything with the output of the formatting commands causes an error in the output system.

To resolve this problem, try to avoid calling the formatting cmdlets in the middle of a script or pipeline. When you do this, the output of your script no longer lends itself to the object-based manipulation so synonymous with PowerShell.

If you want to use the formatted output directly, send the output through the Out-String cmdlet as described in Program: Search Formatted Output for a Pattern.

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

Display the Properties of an Item as a Table

Problem

You have a set of items (for example, error records, directory items, or .NET objects), and you want to display summary information about them in a table format.

Solution

To display summary information about a set of items, pass those items to the Format-Table cmdlet. This is the default type of formatting for sets of items in PowerShell and provides several useful features.

To use PowerShell’s default formatting, pipe the output of a cmdlet (such as the Get-Process cmdlet) to the Format-Table cmdlet:

Get-Process | Format-Table

To display specific properties (such as Name and WorkingSet) in the table formatting, supply those property names as parameters to the Format-Table cmdlet:

Get-Process | Format-Table Name,WS

To instruct PowerShell to format the table in the most readable manner, supply the -Auto flag to the Format-Table cmdlet. PowerShell defines WS as an alias of the WorkingSet property for processes:

Get-Process | Format-Table Name,WS -Auto

To define a custom column definition (such as a process’s WorkingSet in megabytes), supply a custom formatting expression to the Format-Table cmdlet:

$fields = "Name",@{
    Label = "WS (MB)"; Expression = {$_.WS / 1mb}; Align = "Right"} 
Get-Process | Format-Table $fields -Auto

Discussion

The Format-Table cmdlet is one of the four PowerShell formatting cmdlets. These cmdlets are Format-Table, Format-List, Format-Wide, and Format-Custom. The Format-Table cmdlet takes input and displays information about that input as a table. By default, PowerShell takes the list of properties to display from the *.format.ps1xml files in PowerShell’s installation directory. You can display all properties of the items if you type Format-Table *, although this is rarely a useful view.

The -Auto parameter to Format-Table is a helpful way to automatically format the table in the most readable way possible. It does come at a cost, however. To figure out the best table layout, PowerShell needs to examine each item in the incoming set of items. For small sets of items, this doesn’t make much difference, but for large sets (such as a recursive directory listing) it does. Without the -Auto parameter, the Format-Table cmdlet can display items as soon as it receives them. With the -Auto flag, the cmdlet displays results only after it receives all the input.

Perhaps the most interesting feature of the Format-Table cmdlet is illustrated by the last example: the ability to define completely custom table columns. You define a custom table column similarly to the way that you define a custom column list. Rather than specify an existing property of the items, you provide a hashtable. That hashtable includes up to three keys: the column’s label, a formatting expression, and alignment. The Format-Table cmdlet shows the label as the column header and uses your expression to generate data for that column. The label must be a string, the expression must be a script block, and the alignment must be either "Left", "Center", or "Right". In the expression script block, the $_ (or $PSItem) variable represents the current item being formatted.

Note

The Select-Object cmdlet supports a similar hashtable to add calculated properties, but uses Name (rather than Label) as the key to identify the property. After realizing how confusing this was, version 2 of PowerShell updated both cmdlets to accept both Name and Label.

The expression shown in the last example takes the working set of the current item and divides it by 1 megabyte (1 MB).

One common stumbling block in PowerShell’s formatting cmdlets comes from putting them in the middle of a script or pipeline:

PS > Get-Process | Format-Table | Sort Name
out-lineoutput : The object of type "Microsoft.PowerShell.Commands.Internal.
Format.FormatEntryData" is not valid or not in the correct sequence. This is 
likely caused by a user-specified "format-*" command which is conflicting with
the default formatting.

Internally, PowerShell’s formatting commands generate a new type of object: Microsoft.PowerShell.Commands.Internal.Format.*. When these objects make it to the end of the pipeline, PowerShell then automatically sends them to an output cmdlet: by default, Out-Default. These Out-* cmdlets assume that the objects arrive in a certain order, so doing anything with the output of the formatting commands causes an error in the output system.

To resolve this problem, try to avoid calling the formatting cmdlets in the middle of a script or pipeline. When you do this, the output of your script no longer lends itself to the object-based manipulation so synonymous with PowerShell.

If you want to use the formatted output directly, send the output through the Out-String cmdlet as described in Program: Search Formatted Output for a Pattern.

For more information about the Format-Table cmdlet, type Get-Help Format-Table. For more information about hashtables, see Create a Hashtable or Associative Array. For more information about script blocks, see Write a Script Block.

Store Information in Variables

Problem

You want to store the output of a pipeline or command for later use or to work with it in more detail.

Solution

To store output for later use, store the output of the command in a variable. You can access this information later, or even pass it down the pipeline as though it were the output of the original command:

PS > $result = 2 + 2
PS > $result
4

PS > $output = ipconfig
PS > $output | Select-String "Default Gateway" | Select -First 1

   Default Gateway . . . . . . . . . : 192.168.11.1

PS > $processes = Get-Process
PS > $processes.Count
85
PS > $processes | Where-Object { $_.ID -eq 0 }

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)    Id ProcessName
-------  ------    -----      ----- -----   -----     -- -----------
      0       0        0         16     0              0 Idle

Discussion

Variables in PowerShell (and all other scripting and programming languages) let you store the output of something so that you can use it later. A variable name starts with a dollar sign ($) and can be followed by nearly any character. A small set of characters have special meaning to PowerShell, so PowerShell provides a way to make variable names that include even these.

For more information about the syntax and types of PowerShell variables, see Variables.

You can store the result of any pipeline or command in a variable to use it later. If that command generates simple data (such as a number or string), then the variable contains simple data. If the command generates rich data (such as the objects that represent system processes from the Get-Process cmdlet), then the variable contains that list of rich data. If the command (such as a traditional executable) generates plain text (such as the output of traditional executable), then the variable contains plain text.

Note

If you’ve stored a large amount of data into a variable but no longer need that data, assign a new value (such as $null) to that variable. That will allow PowerShell to release the memory it was using to store that data.

In addition to variables that you create, PowerShell automatically defines several variables that represent things such as the location of your profile file, the process ID of PowerShell, and more. For a full list of these automatic variables, type Get-Help about_automatic_variables.

See Also

Variables

Access Environment Variables

Problem

You want to use an environment variable (such as the system path or the current user’s name) in your script or interactive session.

Solution

PowerShell offers several ways to access environment variables.

To list all environment variables, list the children of the env drive:

Get-ChildItem env:

To get an environment variable using a more concise syntax, precede its name with $env:

$env:variablename

(For example, $env:username.)

To get an environment variable using its provider path, supply env: or Environment:: to the Get-ChildItem cmdlet:

Get-ChildItem env:variablename
Get-ChildItem Environment::variablename

Discussion

PowerShell provides access to environment variables through its environment provider. Providers let you work with data stores (such as the registry, environment variables, and aliases) much as you would access the filesystem.

By default, PowerShell creates a drive (called env) that works with the environment provider to let you access environment variables. The environment provider lets you access items in the env: drive as you would any other drive: dir env:\variablename or dir env:variablename. If you want to access the provider directly (rather than go through its drive), you can also type dir Environment::variablename.

However, the most common (and easiest) way to work with environment variables is by typing $env:variablename. This works with any provider but is most typically used with environment variables.

This is because the environment provider shares something in common with several other providers—namely, support for the *-Content set of core cmdlets (see Example 3-1).

Example 3-1. Working with content on different providers

PS > "hello world" > test
PS > Get-Content test
hello world
PS > Get-Content c:test
hello world
PS > Get-Content variable:ErrorActionPreference
Continue
PS > Get-Content function:more
param([string[]]$paths)
$OutputEncoding = [System.Console]::OutputEncoding

if($paths)
{
    foreach ($file in $paths)
    {
        Get-Content $file | more.com
    }
}
else
{
    $input | more.com
}
PS > Get-Content env:systemroot
C:\WINDOWS

For providers that support the content cmdlets, PowerShell lets you interact with this content through a special variable syntax (see Example 3-2).

Example 3-2. Using PowerShell’s special variable syntax to access content

PS > $function:more
param([string[]]$paths); if(($paths -ne $null) -and ($paths.length -ne 0)) { ...
       Get-Content $local:file | Out-Host -p } } else { $input | Out-Host ...
PS > $variable:ErrorActionPreference
Continue
PS > $c:test
hello world
PS > $env:systemroot
C:\WINDOWS

This variable syntax for content management lets you both get and set content:

PS > $function:more = { $input | less.exe }
PS > $function:more
$input | less.exe

Now, when it comes to accessing complex provider paths using this method, you’ll quickly run into naming issues (even if the underlying file exists):

PS > $c:\temp\test.txt
Unexpected token '\temp\test.txt' in expression or statement.
At line:1 char:17
+ $c:\temp\test.txt <<<<

The solution to that lies in PowerShell’s escaping support for complex variable names. To define a complex variable name, enclose it in braces:

PS > ${1234123!@#$!@#$12$!@#$@!} = "Crazy Variable!"
PS > ${1234123!@#$!@#$12$!@#$@!}
Crazy Variable!
PS > dir variable:\1*

Name                              Value
----                              -----
1234123!@#$!@#$12$!@#$@!          Crazy Variable!

The following is the content equivalent (assuming that the file exists):

PS > ${c:\temp\test.txt}
hello world

Since environment variable names do not contain special characters, this Get-Content variable syntax is the best (and easiest) way to access environment variables.

For more information about working with PowerShell variables, see Variables. For more information about working with environment variables, type Get-Help About_Environment_Variable.

See Also

Variables

Program: Retain Changes to Environment Variables Set by a Batch File

When a batch file modifies an environment variable, cmd.exe retains this change even after the script exits. This often causes problems, as one batch file can accidentally pollute the environment of another. That said, batch file authors sometimes intentionally change the global environment to customize the path and other aspects of the environment to suit a specific task.

However, environment variables are private details of a process and disappear when that process exits. This makes the environment customization scripts mentioned earlier stop working when you run them from PowerShell—just as they fail to work when you run them from another cmd.exe (for example, cmd.exe /c MyEnvironmentCustomizer.cmd).

The script in Example 3-3 lets you run batch files that modify the environment and retain their changes even after cmd.exe exits. It accomplishes this by storing the environment variables in a text file once the batch file completes, and then setting all those environment variables again in your PowerShell session.

To run this script, type Invoke-CmdScript Scriptname.cmd or Invoke-CmdScript Scriptname.bat—whichever extension the batch files uses.

Note

If this is the first time you’ve run a script in PowerShell, you will need to configure your Execution Policy. For more information about selecting an execution policy, see Enable Scripting Through an Execution Policy.

Notice that this script uses the full names for cmdlets: Get-Content, Foreach-Object, Set-Content, and Remove-Item. This makes the script readable and is ideal for scripts that somebody else will read. It is by no means required, though. For quick scripts and interactive use, shorter aliases (such as gc, %, sc, and ri) can make you more productive.

Example 3-3. Invoke-CmdScript.ps1

##############################################################################
##
## Invoke-CmdScript
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Invoke the specified batch file (and parameters), but also propagate any
environment variable changes back to the PowerShell environment that
called it.

.EXAMPLE

PS > type foo-that-sets-the-FOO-env-variable.cmd
@set FOO=%*
echo FOO set to %FOO%.

PS > $env:FOO
PS > Invoke-CmdScript "foo-that-sets-the-FOO-env-variable.cmd" Test

C:\Temp>echo FOO set to Test.
FOO set to Test.

PS > $env:FOO
Test

#>

param(
    ## The path to the script to run
    [Parameter(Mandatory = $true)]
    [string] $Path,

    ## The arguments to the script
    [string] $ArgumentList
)

Set-StrictMode -Version 3

$tempFile = [IO.Path]::GetTempFileName()

## Store the output of cmd.exe.  We also ask cmd.exe to output
## the environment table after the batch file completes
cmd /c " `"$Path`" $argumentList && set > `"$tempFile`" "

## Go through the environment variables in the temp file.
## For each of them, set the variable in our local environment.
Get-Content $tempFile | Foreach-Object {
    if($_ -match "^(.*?)=(.*)$")
    {
        Set-Content "env:\$($matches[1])" $matches[2]
    }
}

Remove-Item $tempFile

For more information about running scripts, see Run Programs, Scripts, and Existing Tools.

Control Access and Scope of Variables and Other Items

Problem

You want to control how you define (or interact with) the visibility of variables, aliases, functions, and drives.

Solution

PowerShell offers several ways to access variables.

To create a variable with a specific scope, supply that scope before the variable name:

$SCOPE:variable = value

To access a variable at a specific scope, supply that scope before the variable name:

$SCOPE:variable

To create a variable that remains even after the script exits, create it in the GLOBAL scope:

$GLOBAL:variable = value

To change a scriptwide variable from within a function, supply SCRIPT as its scope name:

$SCRIPT:variable = value

Discussion

PowerShell controls access to variables, functions, aliases, and drives through a mechanism known as scoping. The scope of an item is another term for its visibility. You are always in a scope (called the current or local scope), but some actions change what that means.

When your code enters a nested prompt, script, function, or script block, PowerShell creates a new scope. That scope then becomes the local scope. When it does this, PowerShell remembers the relationship between your old scope and your new scope. From the view of the new scope, the old scope is called the parent scope. From the view of the old scope, the new scope is called a child scope. Child scopes get access to all the variables in the parent scope, but changing those variables in the child scope doesn’t change the version in the parent scope.

Note

Trying to change a scriptwide variable from a function is often a “gotcha” because a function is a new scope. As mentioned previously, changing something in a child scope (the function) doesn’t affect the parent scope (the script). The rest of this discussion describes ways to change the value for the entire script.

When your code exits a nested prompt, script, function, or script block, the opposite happens. PowerShell removes the old scope, then changes the local scope to be the scope that originally created it—the parent of that old scope.

Some scopes are so common that PowerShell gives them special names:

Global

The outermost scope. Items in the global scope are visible from all other scopes.

Script

The scope that represents the current script. Items in the script scope are visible from all other scopes in the script.

Local

The current scope.

When you define the scope of an item, PowerShell supports two additional scope names that act more like options: Private and AllScope. When you define an item to have a Private scope, PowerShell does not make that item directly available to child scopes. PowerShell does not hide it from child scopes, though, as child scopes can still use the -Scope parameter of the Get-Variable cmdlet to get variables from parent scopes. When you specify the AllScope option for an item (through one of the *-Variable, *-Alias, or *-Drive cmdlets), child scopes that change the item also affect the value in parent scopes.

With this background, PowerShell provides several ways for you to control access and scope of variables and other items.

Variables

To define a variable at a specific scope (or access a variable at a specific scope), use its scope name in the variable reference. For example:

$SCRIPT:myVariable = value

As illustrated in Variables, the *-Variable set of cmdlets also lets you specify scope names through their -Scope parameter.

Functions

To define a function at a specific scope (or access a function at a specific scope), use its scope name when creating the function. For example:

function GLOBAL:MyFunction { ... }
GLOBAL:MyFunction args

Aliases and drives

To define an alias or drive at a specific scope, use the Option parameter of the *-Alias and *-Drive cmdlets. To access an alias or drive at a specific scope, use the Scope parameter of the *-Alias and *-Drive cmdlets.

For more information about scopes, type Get-Help About-Scope.

See Also

Variables

Program: Create a Dynamic Variable

When working with variables and commands, some concepts feel too minor to deserve an entire new command or function, but the readability of your script suffers without them.

A few examples where this becomes evident are date math (yesterday becomes (Get-Date).AddDays(-1)) and deeply nested variables (windowTitle becomes $host.UI.RawUI.WindowTitle).

Note

There are innovative solutions on the Internet that use PowerShell’s debugging facilities to create a breakpoint that changes a variable’s value whenever you attempt to read from it. While unique, this solution causes PowerShell to think that any scripts that rely on the variable are in debugging mode. This, unfortunately, prevents PowerShell from enabling some important performance optimizations in those scripts.

Although we could write our own extensions to make these easier to access, Get-Yesterday, Get-WindowTitle, and Set-WindowTitle feel too insignificant to deserve their own commands.

PowerShell lets you define your own types of variables by extending its PSVariable class, but that functionality is largely designed for developer scenarios, and not for scripting scenarios. Example 3-4 resolves this quandary by creating a new variable type (DynamicVariable) that supports dynamic script actions when you get or set the variable’s value.

Example 3-4. New-DynamicVariable.ps1

##############################################################################
##
## New-DynamicVariable
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Creates a variable that supports scripted actions for its getter and setter

.EXAMPLE

PS > .\New-DynamicVariable GLOBAL:WindowTitle `
     -Getter { $host.UI.RawUI.WindowTitle } `
     -Setter { $host.UI.RawUI.WindowTitle = $args[0] }

PS > $windowTitle
Administrator: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
PS > $windowTitle = "Test"
PS > $windowTitle
Test

#>

param(
    ## The name for the dynamic variable
    [Parameter(Mandatory = $true)]
    $Name,

    ## The script block to invoke when getting the value of the variable
    [Parameter(Mandatory = $true)]
    [ScriptBlock] $Getter,

    ## The script block to invoke when setting the value of the variable
    [ScriptBlock] $Setter
)

Set-StrictMode -Version 3

Add-Type @"
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;

namespace Lee.Holmes
{
    public class DynamicVariable : PSVariable
    {
        public DynamicVariable(
            string name,
            ScriptBlock scriptGetter,
            ScriptBlock scriptSetter)
                : base(name, null, ScopedItemOptions.AllScope)
        {
            getter = scriptGetter;
            setter = scriptSetter;
        }
        private ScriptBlock getter;
        private ScriptBlock setter;

        public override object Value
        {
            get
            {
                if(getter != null)
                {
                    Collection<PSObject> results = getter.Invoke();
                    if(results.Count == 1)
                    {
                        return results[0];
                    }
                    else
                    {
                        PSObject[] returnResults =
                            new PSObject[results.Count];
                        results.CopyTo(returnResults, 0);
                        return returnResults;
                    }
                }
                else { return null; }
            }
            set
            {
                if(setter != null) { setter.Invoke(value); }
            }
        }
    }
}
"@

## If we've already defined the variable, remove it.
if(Test-Path variable:\$name)
{
    Remove-Item variable:\$name -Force
}

## Set the new variable, along with its getter and setter.
$executioncontext.SessionState.PSVariable.Set(
    (New-Object Lee.Holmes.DynamicVariable $name,$getter,$setter))

Work with .NET Objects

Problem

You want to use and interact with one of the features that makes PowerShell so powerful: its intrinsic support for .NET objects.

Solution

PowerShell offers ways to access methods (both static and instance) and properties.

To call a static method on a class, place the type name in square brackets, and then separate the class name from the method name with two colons:

[ClassName]::MethodName(parameter list)

To call a method on an object, place a dot between the variable that represents that object and the method name:

$objectReference.MethodName(parameter list)

To access a static property on a class, place the type name in square brackets, and then separate the class name from the property name with two colons:

[ClassName]::PropertyName

To access a property on an object, place a dot between the variable that represents that object and the property name:

$objectReference.PropertyName

Discussion

One feature that gives PowerShell its incredible reach into both system administration and application development is its capability to leverage Microsoft’s enormous and broad .NET Framework. The .NET Framework is a large collection of classes. Each class embodies a specific concept and groups closely related functionality and information. Working with the .NET Framework is one aspect of PowerShell that introduces a revolution to the world of management shells.

An example of a class from the .NET Framework is System.Diagnostics.Process—the grouping of functionality that “provides access to local and remote processes, and enables you to start and stop local system processes.”

Note

The terms type and class are often used interchangeably.

Classes contain methods (which let you perform operations) and properties (which let you access information).

For example, the Get-Process cmdlet generates System.Diagnostics.Process objects, not a plain-text report like traditional shells. Managing these processes becomes incredibly easy, as they contain a rich mix of information (properties) and operations (methods). You no longer have to parse a stream of text for the ID of a process; you can just ask the object directly!

PS > $process = Get-Process Notepad
PS > $process.Id
3872

Static methods

          [ClassName]::MethodName(parameter list)

Some methods apply only to the concept the class represents. For example, retrieving all running processes on a system relates to the general concept of processes instead of a specific process. Methods that apply to the class/type as a whole are called static methods.

For example:

PS > [System.Diagnostics.Process]::GetProcessById(0)

This specific task is better handled by the Get-Process cmdlet, but it demonstrates PowerShell’s capability to call methods on .NET classes. It calls the static GetProcessById method on the System.Diagnostics.Process class to get the process with the ID of 0. This generates the following output:

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
      0      0     0    16     0         0 Idle

Instance methods

          $objectReference.MethodName(parameter list)

Some methods relate only to specific, tangible realizations (called instances) of a class. An example of this would be stopping a process actually running on the system, as opposed to the general concept of processes. If $objectReference refers to a specific System.Diagnostics.Process (as output by the Get-Process cmdlet, for example), you may call methods to start it, stop it, or wait for it to exit. Methods that act on instances of a class are called instance methods.

Note

The term object is often used interchangeably with the term instance.

For example:

PS > $process = Get-Process Notepad
PS > $process.WaitForExit()

stores the Notepad process into the $process variable. It then calls the WaitForExit() instance method on that specific process to pause PowerShell until the process exits. To learn about the different sets of parameters (overloads) that a given method supports, type that method name without any parameters:

PS > $now = Get-Date
PS > $now.ToString

OverloadDefinitions
-------------------
string ToString()
string ToString(string format)
string ToString(System.IFormatProvider provider)
string ToString(string format, System.IFormatProvider provider)
string IFormattable.ToString(string format, System.IFormatProvider formatProvider)
string IConvertible.ToString(System.IFormatProvider provider)

For both static methods and instance methods, you may sometimes run into situations where PowerShell either generates an error or fails to invoke the method you expected. In this case, review the output of the Trace-Command cmdlet, with MemberResolution as the trace type (see Example 3-5).

Example 3-5. Investigating PowerShell’s method resolution

PS > Trace-Command MemberResolution -PsHost {
    [System.Diagnostics.Process]::GetProcessById(0) }

DEBUG: MemberResolution Information: 0 : cache hit, Calling Method: static
 System.Diagnostics.Process GetProcessById(int processId)
DEBUG: MemberResolution Information: 0 : Method argument conversion.
DEBUG: MemberResolution Information: 0 :     Converting parameter "0" to
"System.Int32".
DEBUG: MemberResolution Information: 0 : Checking for possible references.

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
      0       0        0         12     0               0 Idle

If you are adapting a C# example from the Internet and PowerShell can’t find a method used in the example, the method may have been added through a relatively rare technique called explicit interface implementation. If this is the case, you can cast the object to that interface before calling the method:

$sourceObject = 123
$result = ([IConvertible] $sourceObject).ToUint16($null)

Static properties

[ClassName]::PropertyName

or:

[ClassName]::PropertyName = value

Like static methods, some properties relate only to information about the concept that the class represents. For example, the System.DateTime class “represents an instant in time, typically expressed as a date and time of day.” It provides a Now static property that returns the current time:

PS > [System.DateTime]::Now
Saturday, June 2, 2010 4:57:20 PM

This specific task is better handled by the Get-Date cmdlet, but it demonstrates PowerShell’s capability to access properties on .NET objects.

Although they are relatively rare, some types let you set the value of some static properties as well: for example, the [System.Environment]::CurrentDirectory property. This property represents the process’s current directory—which represents PowerShell’s startup directory, as opposed to the path you see in your prompt.

Instance properties

$objectReference.PropertyName

or:

$objectReference.PropertyName = value

Like instance methods, some properties relate only to specific, tangible realizations (called instances) of a class. An example of this would be the day of an actual instant in time, as opposed to the general concept of dates and times. If $objectReference refers to a specific System.DateTime (as output by the Get-Date cmdlet or [System.DateTime]::Now, for example), you may want to retrieve its day of week, day, or month. Properties that return information about instances of a class are called instance properties.

For example:

PS > $today = Get-Date
PS > $today.DayOfWeek
Saturday

This example stores the current date in the $today variable. It then calls the DayOfWeek instance property to retrieve the day of the week for that specific date.

With this knowledge, the next questions are: “How do I learn about the functionality available in the .NET Framework?” and “How do I learn what an object does?”

For an answer to the first question, see Appendix F for a hand-picked list of the classes in the .NET Framework most useful to system administrators. For an answer to the second, see Learn About Types and Objects and Get Detailed Documentation About Types and Objects.

Create an Instance of a .NET Object

Problem

You want to create an instance of a .NET object to interact with its methods and properties.

Solution

Use the New-Object cmdlet to create an instance of an object.

To create an instance of an object using its default constructor, use the New-Object cmdlet with the class name as its only parameter:

PS > $generator = New-Object System.Random
PS > $generator.NextDouble()
0.853699042859347

To create an instance of an object that takes parameters for its constructor, supply those parameters to the New-Object cmdlet. In some instances, the class may exist in a separate library not loaded in PowerShell by default, such as the System.Windows.Forms assembly. In that case, you must first load the assembly that contains the class:

Add-Type -Assembly System.Windows.Forms
$image = New-Object System.Drawing.Bitmap source.gif
$image.Save("source_converted.jpg", "JPEG")

To create an object and use it at the same time (without saving it for later), wrap the call to New-Object in parentheses:

PS > (New-Object Net.WebClient).DownloadString("http://live.com")

Discussion

Many cmdlets (such as Get-Process and Get-ChildItem) generate live .NET objects that represent tangible processes, files, and directories. However, PowerShell supports much more of the .NET Framework than just the objects that its cmdlets produce. These additional areas of the .NET Framework supply a huge amount of functionality that you can use in your scripts and general system administration tasks.

Note

To create an instance of a generic object, see Example 3-6.

When it comes to using most of these classes, the first step is often to create an instance of the class, store that instance in a variable, and then work with the methods and properties on that instance. To create an instance of a class, you use the New-Object cmdlet. The first parameter to the New-Object cmdlet is the type name, and the second parameter is the list of arguments to the constructor, if it takes any. The New-Object cmdlet supports PowerShell’s type shortcuts, so you never have to use the fully qualified type name. For more information about type shortcuts, see Type Shortcuts.

A common pattern when working with .NET objects is to create them, set a few properties, and then use them. The -Property parameter of the New-Object cmdlet lets you combine these steps:

$startInfo = New-Object Diagnostics.ProcessStartInfo -Property @{
    'Filename' = "powershell.exe";
    'WorkingDirectory' = $pshome;
    'Verb' = "RunAs"
}
[Diagnostics.Process]::Start($startInfo)

Or even more simply through PowerShell’s built-in type conversion:

$startInfo = [Diagnostics.ProcessStartInfo] @{
    'Filename' = "powershell.exe";
    'WorkingDirectory' = $pshome;
    'Verb' = "RunAs"
}

When calling the New-Object cmdlet directly, you might encounter difficulty when trying to specify a parameter that itself is a list. Assuming $byte is an array of bytes:

PS > $memoryStream = New-Object System.IO.MemoryStream $bytes
New-Object : Cannot find an overload for ".ctor" and the argument count: "11".
At line:1 char:27
+ $memoryStream = New-Object <<<< System.IO.MemoryStream $bytes

To solve this, provide an array that contains an array:

PS > $parameters = ,$bytes
PS > $memoryStream = New-Object System.IO.MemoryStream $parameters

or:

PS > $memoryStream = New-Object System.IO.MemoryStream @(,$bytes)

Load types from another assembly

PowerShell makes most common types available by default. However, many are available only after you load the library (called the assembly) that defines them. The MSDN documentation for a class includes the assembly that defines it. For more information about loading types from another assembly, please see Access a .NET SDK Library.

For a hand-picked list of the classes in the .NET Framework most useful to system administrators, see Appendix F. To learn more about the functionality that a class supports, see Learn About Types and Objects.

For more information about the New-Object cmdlet, type Get-Help New-Object. For more information about the Add-Type cmdlet, type Get-Help Add-Type.

Create Instances of Generic Objects

When you work with the .NET Framework, you’ll often run across classes that have the primary responsibility of managing other objects. For example, the System.Collections.ArrayList class lets you manage a dynamic list of objects. You can add objects to an ArrayList, remove objects from it, sort the objects inside, and more. These objects can be any type of object: String objects, integers, DateTime objects, and many others. However, working with classes that support arbitrary objects can sometimes be a little awkward. One example is type safety. If you accidentally add a String to a list of integers, you might not find out until your program fails.

Although the issue becomes largely moot when you’re working only inside PowerShell, a more common complaint in strongly typed languages (such as C#) is that you have to remind the environment (through explicit casts) about the type of your object when you work with it again:

// This is C# code
System.Collections.ArrayList list =
    new System.Collections.ArrayList();
list.Add("Hello World");

string result = (String) list[0];

To address these problems, the .NET Framework includes a feature called generic types: classes that support arbitrary types of objects but let you specify which type of object. In this case, a collection of strings:

// This is C# code
System.Collections.ObjectModel.Collection<String> list =
    new System.Collections.ObjectModel.Collection<String>();
list.Add("Hello World");

string result = list[0];

PowerShell version 2 and on support generic parameters by placing them between square brackets, as demonstrated in Example 3-6. If you are using PowerShell version 1, see New-GenericObject included in the book’s sample downloads.

Example 3-6. Creating a generic object

PS > $coll = New-Object System.Collections.ObjectModel.Collection[Int]
PS > $coll.Add(15)
PS > $coll.Add("Test")
Cannot convert argument "0", with value: "Test", for "Add" to type "System
.Int32": "Cannot convert value "Test" to type "System.Int32". Error: "Input
string was not in a correct format.""
At line:1 char:10
+ $coll.Add <<<< ("Test")
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

For a generic type that takes two or more parameters, provide a comma-separated list of types, enclosed in quotes (see Example 3-7).

Example 3-7. Creating a multiparameter generic object

PS > $map = New-Object "System.Collections.Generic.Dictionary[String,Int]"
PS > $map.Add("Test", 15)
PS > $map.Add("Test2", "Hello")
Cannot convert argument "1", with value: "Hello", for "Add" to type "System
.Int32": "Cannot convert value "Hello" to type "System.Int32". Error: 
"Input string was not in a correct format.""
At line:1 char:9
+ $map.Add <<<< ("Test2", "Hello")
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

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 operator:

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

To reduce typing for static methods of multiple types in a namespace, use the -f 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 third-party SDK) is that it quickly becomes tiresome to specify their fully qualified type names. For example, many useful collection classes in the .NET Framework 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 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 = New-Object ($namespace -f "ArrayList")
$queue = New-Object ($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 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()

For more information about PowerShell’s format operator, see Place Formatted Information in a String.

Use a COM Object

Problem

You want to create a COM object to interact with its methods and properties.

Solution

Use the New-Object cmdlet (with the -ComObject parameter) to create a COM object from its ProgID. You can then interact with the methods and properties of the COM object as you would any other object in PowerShell.

$object = New-Object -ComObject ProgId

For example:

PS > $sapi = New-Object -Com Sapi.SpVoice
PS > $sapi.Speak("Hello World")

Discussion

Historically, many applications have exposed their scripting and administration interfaces as COM objects. While .NET APIs (and PowerShell cmdlets) are by far the most common, interacting with COM objects is still a routine administrative task.

As with classes in the .NET Framework, it is difficult to know what COM objects you can use to help you accomplish your system administration tasks. For a hand-picked list of the COM objects most useful to system administrators, see Appendix H.

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

See Also

Appendix H

Learn About Types and Objects

Problem

You have an instance of an object and want to know what methods and properties it supports.

Solution

The most common way to explore the methods and properties supported by an object is through the Get-Member cmdlet.

To get the instance members of an object you’ve stored in the $object variable, pipe it to the Get-Member cmdlet:

$object | Get-Member
Get-Member -InputObject $object

To get the static members of an object you’ve stored in the $object variable, supply the -Static flag to the Get-Member cmdlet:

$object | Get-Member -Static
Get-Member -Static -InputObject $object

To get the static members of a specific type, pipe that type to the Get-Member cmdlet, and also specify the -Static flag:

[Type] | Get-Member -Static
Get-Member -InputObject [Type]

To get members of the specified member type (for example, Method or Property) from an object you have stored in the $object variable, supply that member type to the -MemberType parameter:

$object | Get-Member -MemberType MemberType
Get-Member -MemberType MemberType -InputObject $object

Discussion

The Get-Member cmdlet is one of the three commands you will use most commonly as you explore Windows PowerShell. The other two commands are Get-Command and Get-Help.

Note

To interactively explore an object’s methods and properties, see Program: Interactively View and Explore Objects.

If you pass the Get-Member cmdlet a collection of objects (such as an Array or ArrayList) through the pipeline, PowerShell extracts each item from the collection and then passes them to the Get-Member cmdlet one by one. The Get-Member cmdlet then returns the members of each unique type that it receives. Although helpful the vast majority of the time, this sometimes causes difficulty when you want to learn about the members or properties of the collection class itself.

If you want to see the properties of a collection (as opposed to the elements it contains), provide the collection to the -InputObject parameter instead. Alternatively, you can wrap the collection in an array (using PowerShell’s unary comma operator) so that the collection class remains when the Get-Member cmdlet unravels the outer array:

PS > $files = Get-ChildItem
PS > ,$files | Get-Member

   TypeName: System.Object[]

Name               MemberType     Definition
----               ----------     ----------
Count              AliasProperty  Count = Length
Address            Method         System.Object& Address(Int32 )
(...)

For another way to learn detailed information about types and objects, see Get Detailed Documentation About Types and Objects.

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

Get Detailed Documentation About Types and Objects

Problem

You have a type of object and want to know detailed information about the methods and properties it supports.

Solution

The documentation for the .NET Framework [available here] is the best way to get detailed documentation about the methods and properties supported by an object. That exploration generally comes in two stages:

  1. Find the type of the object.

    To determine the type of an object, you can either use the type name shown by the Get-Member cmdlet (as described in Learn About Types and Objects) or call the GetType() method of an object (if you have an instance of it):

    PS > $date = Get-Date
    PS > $date.GetType().ToString()
    System.DateTime
  2. Enter that type name into the search box here.

Discussion

When the Get-Member cmdlet does not provide the information you need, the MSDN documentation for a type is a great alternative. It provides much more detailed information than the help offered by the Get-Member cmdlet—usually including detailed descriptions, related information, and even code samples. MSDN documentation focuses on developers using these types through a language such as C#, though, so you may find interpreting the information for use in PowerShell to be a little difficult at first.

Typically, the documentation for a class first starts with a general overview, and then provides a hyperlink to the members of the class—the list of methods and properties it supports.

Note

To get to the documentation for the members quickly, search for them more explicitly by adding the term “members” to your MSDN search term: “typename members.”

Documentation for the members of a class lists the class’s methods and properties, as does the output of the Get-Member cmdlet. The S icon represents static methods and properties. Click the member name for more information about that method or property.

Public constructors

This section lists the constructors of the type. You use a constructor when you create the type through the New-Object cmdlet. When you click on a constructor, the documentation provides all the different ways that you can create that object, including the parameter list that you will use with the New-Object cmdlet.

Public fields/public properties

This section lists the names of the fields and properties of an object. The S icon represents a static field or property. When you click on a field or property, the documentation also provides the type returned by this field or property.

For example, you might see the following in the definition for System.DateTime.Now:

C#
public static DateTime Now { get; }

Public means that the Now property is public—that you can access it from PowerShell. Static means that the property is static (as described in Work with .NET Objects). DateTime means that the property returns a DateTime object when you call it. get; means that you can get information from this property but cannot set the information. Many properties support a set; as well (such as the IsReadOnly property on System.IO.FileInfo), which means that you can change its value.

Public methods

This section lists the names of the methods of an object. The S icon represents a static method. When you click on a method, the documentation provides all the different ways that you can call that method, including the parameter list that you will use to call that method in PowerShell.

For example, you might see the following in the definition for System.DateTime.AddDays():

C#
public DateTime AddDays (
    double value
)

Public means that the AddDays method is public—that you can access it from PowerShell. DateTime means that the method returns a DateTime object when you call it. The text double value means that this method requires a parameter (of type double). In this case, that parameter determines the number of days to add to the DateTime object on which you call the method.

Add Custom Methods and Properties to Objects

Problem

You have an object and want to add your own custom properties or methods (members) to that object.

Solution

Use the Add-Member cmdlet to add custom members to an object.

Discussion

The Add-Member cmdlet is extremely useful in helping you add custom members to individual objects. For example, imagine that you want to create a report from the files in the current directory, and that report should include each file’s owner. The Owner property is not standard on the objects that Get-ChildItem produces, but you could write a small script to add them, as shown in Example 3-8.

Example 3-8. A script that adds custom properties to its output of file objects

##############################################################################
##
## Get-OwnerReport
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Gets a list of files in the current directory, but with their owner added
to the resulting objects.

.EXAMPLE

PS > Get-OwnerReport | Format-Table Name,LastWriteTime,Owner
Retrieves all files in the current directory, and displays the
Name, LastWriteTime, and Owner

#>

Set-StrictMode -Version 3

$files = Get-ChildItem
foreach($file in $files)
{
    $owner = (Get-Acl $file).Owner
    $file | Add-Member NoteProperty Owner $owner
    $file
}

For more information about running scripts, see Run Programs, Scripts, and Existing Tools.

The most common type of information to add to an object is static information in a NoteProperty. Add-Member even uses this as the default if you omit it:

PS > $item = Get-Item C:\
PS > $item | Add-Member VolumeName "Operating System"
PS > $item.VolumeName
Operating System

In addition to note properties, the Add-Member cmdlet supports several other property and method types, including AliasProperty, ScriptProperty, CodeProperty, CodeMethod, and ScriptMethod. For a more detailed description of these other property types, see Working with the .NET Framework, as well as the help documentation for the Add-Member cmdlet.

Note

To create entirely new objects (instead of adding information to existing ones), see Create and Initialize Custom Objects.

Although the Add-Member cmdlet lets you customize specific objects, it does not let you customize all objects of that type. For information on how to do that, see Add Custom Methods and Properties to Types.

Calculated properties

Calculated properties are another useful way to add information to output objects. If your script or command uses a Format-Table or Select-Object command to generate its output, you can create additional properties by providing an expression that generates their value. For example:

Get-ChildItem |
  Select-Object Name,
      @{Name="Size (MB)"; Expression={ "{0,8:0.00}" -f ($_.Length / 1MB) } }

In this command, we get the list of files in the directory. We use the Select-Object command to retrieve its name and a calculated property called Size (MB). This calculated property returns the size of the file in megabytes, rather than the default (bytes).

Note

The Format-Table cmdlet supports a similar hashtable to add calculated properties, but uses Label (rather than Name) as the key to identify the property. To eliminate the confusion this produced, version 2 of PowerShell updated the two cmdlets to accept both Name and Label.

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

For more information about adding calculated properties, type Get-Help Select-Object or Get-Help Format-Table.

Create and Initialize Custom Objects

Problem

You want to return structured results from a command so that users can easily sort, group, and filter them.

Solution

Use the [PSCustomObject] type cast to a new PSCustomObject, supplying a hashtable with the custom information as its value, as shown in Example 3-9.

Example 3-9. Creating a custom object

$output = [PSCustomObject] @{
    'User' = 'DOMAIN\User';
    'Quota' = 100MB;
    'ReportDate' = Get-Date;
}

If you want to create a custom object with associated functionality, place the functionality in a module, and load that module with the -AsCustomObject parameter:

$obj = Import-Module PlottingObject -AsCustomObject
$obj.Move(10,10)

$obj.Points = SineWave
while($true) { $obj.Rotate(10); $obj.Draw(); Sleep -m 20 }

Discussion

When your script outputs information to the user, always prefer richly structured data over hand-formatted reports. By emitting custom objects, you give the end user as much control over your script’s output as PowerShell gives you over the output of its own commands.

Despite the power afforded by the output of custom objects, user-written scripts have frequently continued to generate plain-text output. This can be partly blamed on PowerShell’s previously cumbersome support for the creation and initialization of custom objects, as shown in Example 3-10.

Example 3-10. Creating a custom object in PowerShell version 1

$output = New-Object PsObject
Add-Member -InputObject $output NoteProperty User 'DOMAIN\user'
Add-Member -InputObject $output NoteProperty Quota 100MB
Add-Member -InputObject $output NoteProperty ReportDate (Get-Date)

$output

In PowerShell version 1, creating a custom object required creating a new object (of the type PsObject), and then calling the Add-Member cmdlet multiple times to add the desired properties. PowerShell version 2 made this immensely easier by adding the -Property parameter to the New-Object cmdlet, which applied to the PSObject type as well. PowerShell version 3 made this as simple as possible by directly supporting the [PSCustomObject] type cast.

While creating a PSCustomObject makes it easy to create data-centric objects (often called property bags), it does not let you add functionality to those objects. When you need functionality as well, the next step is to create a module and import that module with the -AsCustomObject parameter (see Example 3-11). Any variables exported by that module become properties on the resulting object, and any functions exported by that module become methods on the resulting object.

Note

An important point about importing a module as a custom object is that variables defined in that custom object are shared by all versions of that object. If you import the module again as a custom object (but store the result in another variable), the two objects will share their internal state.

Example 3-11. Creating a module designed to be used as a custom object

##############################################################################
##
## PlottingObject.psm1
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Demonstrates a module designed to be imported as a custom object

.EXAMPLE

Remove-Module PlottingObject
function SineWave { -15..15 | % { ,($_,(10 * [Math]::Sin($_ / 3))) } }
function Box { -5..5 | % { ($_,-5),($_,5),(-5,$_),(5,$_) } }

$obj = Import-Module PlottingObject -AsCustomObject
$obj.Move(10,10)

$obj.Points = SineWave
while($true) { $obj.Rotate(10); $obj.Draw(); Sleep -m 20 }

$obj.Points = Box
while($true) { $obj.Rotate(10); $obj.Draw(); Sleep -m 20 }

#>

## Declare some internal variables
$SCRIPT:x = 0
$SCRIPT:y = 0
$SCRIPT:angle = 0
$SCRIPT:xScale = -50,50
$SCRIPT:yScale = -50,50

## And a variable that we will later export
$SCRIPT:Points = @()
Export-ModuleMember -Variable Points

## A function to rotate the points by a certain amount
function Rotate($angle)
{
    $SCRIPT:angle += $angle
}
Export-ModuleMember -Function Rotate

## A function to move the points by a certain amount
function Move($xDelta, $yDelta)
{
    $SCRIPT:x += $xDelta
    $SCRIPT:y += $yDelta
}
Export-ModuleMember -Function Move

## A function to draw the given points
function Draw
{
    $degToRad = 180 * [Math]::Pi
    Clear-Host

    ## Draw the origin
    PutPixel 0 0 +

    ## Go through each of the supplied points,
    ## move them the amount specified, and then rotate them
    ## by the angle specified
    foreach($point in $points)
    {
        $pointX,$pointY = $point
        $pointX = $pointX + $SCRIPT:x
        $pointY = $pointY + $SCRIPT:y

        $newX = $pointX * [Math]::Cos($SCRIPT:angle / $degToRad ) -
            $pointY * [Math]::Sin($SCRIPT:angle / $degToRad )
        $newY = $pointY * [Math]::Cos($SCRIPT:angle / $degToRad ) +
            $pointX * [Math]::Sin($SCRIPT:angle / $degToRad )

        PutPixel $newX $newY O
    }

    [Console]::WriteLine()
}
Export-ModuleMember -Function Draw

## A helper function to draw a pixel on the screen
function PutPixel($x, $y, $character)
{
    $scaledX = ($x - $xScale[0]) / ($xScale[1] - $xScale[0])
    $scaledX *= [Console]::WindowWidth

    $scaledY = (($y * 4 / 3) - $yScale[0]) / ($yScale[1] - $yScale[0])
    $scaledY *= [Console]::WindowHeight

    try
    {
        [Console]::SetCursorPosition($scaledX,
            [Console]::WindowHeight - $scaledY)
        [Console]::Write($character)
    }
    catch
    {
        ## Take no action on error. We probably just rotated a point
        ## out of the screen boundary.
    }
}

For more information about creating modules, see Package Common Commands in a Module.

If neither of these options suits your requirements (or if you need to create an object that can be consumed by other .NET libraries), use the Add-Type cmdlet. For more information about this approach, see Define or Extend a .NET Class.

Add Custom Methods and Properties to Types

Problem

You want to add your own custom properties or methods to all objects of a certain type.

Solution

Use the Update-TypeData cmdlet to add custom members to all objects of a type.

Update-TypeData -TypeName AddressRecord `
    -MemberType AliasProperty -Membername Cell -Value Phone

Alternatively, use custom type extension files.

Discussion

Although the Add-Member cmdlet is extremely useful in helping you add custom members to individual objects, it requires that you add the members to each object that you want to interact with. It does not let you automatically add them to all objects of that type. For that purpose, PowerShell supports another mechanism—custom type extensions.

The simplest and most common way to add members to all instances of a type is through the Update-TypeData cmdlet. This cmdlet supports aliases, notes, script methods, and more:

$r = [PSCustomObject] @{
    Name = "Lee";
    Phone = "555-1212";
    SSN = "123-12-1212"
}
$r.PSTypeNames.Add("AddressRecord")
Update-TypeData -TypeName AddressRecord `
    -MemberType AliasProperty -Membername Cell -Value Phone

Custom type extensions let you easily add your own features to any type exposed by the system. If you write code (for example, a script or function) that primarily interacts with a single type of object, then that code might be better suited as an extension to the type instead.

For example, imagine a script that returns the free disk space on a given drive. That might be helpful as a script, but instead you might find it easier to make PowerShell’s PSDrive objects themselves tell you how much free space they have left.

In addition to the Update-TypeData approach, PowerShell supports type extensions through XML-based type extension files. Since type extension files are XML files, make sure that your customizations properly encode the characters that have special meaning in XML files, such as <, >, and &.

For more information about the features supported by these formatting XML files, type Get-Help about_format.ps1xml.

Getting started

If you haven’t done so already, the first step in creating a type extension file is to create an empty one. The best location for this is probably in the same directory as your custom profile, with the filename Types.Custom.ps1xml, as shown in Example 3-12.

Example 3-12. Sample Types.Custom.ps1xml file

<?xml version="1.0" encoding="utf-8" ?>
<Types>
</Types>

Next, add a few lines to your PowerShell profile so that PowerShell loads your type extensions during startup:

$typeFile = (Join-Path (Split-Path $profile) "Types.Custom.ps1xml")
Update-TypeData -PrependPath $typeFile

By default, PowerShell loads several type extensions from the Types.ps1xml file in PowerShell’s installation directory. The Update-TypeData cmdlet tells PowerShell to also look in your Types.Custom.ps1xml file for extensions. The -PrependPath parameter makes PowerShell favor your extensions over the built-in ones in case of conflict.

Once you have a custom types file to work with, adding functionality becomes relatively straightforward. As a theme, these examples do exactly what we alluded to earlier: add functionality to PowerShell’s PSDrive type.

Note

PowerShell version 2 does this automatically. Type Get-PSDrive to see the result.

To support this, you need to extend your custom types file so that it defines additions to the System.Management.Automation.PSDriveInfo type, shown in Example 3-13. System.Management.Automation.PSDriveInfo is the type that the Get-PSDrive cmdlet generates.

Example 3-13. A template for changes to a custom types file

<?xml version="1.0" encoding="utf-8" ?>
<Types>
  <Type>
    <Name>System.Management.Automation.PSDriveInfo</Name>
    <Members>
        add members such as <ScriptProperty> here
    <Members>
  </Type>
</Types>

Add a ScriptProperty

A ScriptProperty lets you add properties (that get and set information) to types, using PowerShell script as the extension language. It consists of three child elements: the Name of the property, the getter of the property (via the GetScriptBlock child), and the setter of the property (via the SetScriptBlock child).

In both the GetScriptBlock and SetScriptBlock sections, the $this variable refers to the current object being extended. In the SetScriptBlock section, the $args[0] variable represents the value that the user supplied as the righthand side of the assignment.

Example 3-14 adds an AvailableFreeSpace ScriptProperty to PSDriveInfo, and should be placed within the members section of the template given in Example 3-13. When you access the property, it returns the amount of free space remaining on the drive. When you set the property, it outputs what changes you must make to obtain that amount of free space.

Example 3-14. A ScriptProperty for the PSDriveInfo type

<ScriptProperty>
  <Name>AvailableFreeSpace</Name>
  <GetScriptBlock>
    ## Ensure that this is a FileSystem drive
    if($this.Provider.ImplementingType -eq
       [Microsoft.PowerShell.Commands.FileSystemProvider])
    {
       ## Also ensure that it is a local drive
       $driveRoot = $this.Root
       $fileZone = [System.Security.Policy.Zone]::CreateFromUrl(`
                 $driveRoot).SecurityZone
       if($fileZone -eq "MyComputer")
       {
          $drive = New-Object System.IO.DriveInfo $driveRoot
          $drive.AvailableFreeSpace
       }
    }
  </GetScriptBlock>
  <SetScriptBlock>
   ## Get the available free space
   $availableFreeSpace = $this.AvailableFreeSpace

   ## Find out the difference between what is available, and what they
   ## asked for.
   $spaceDifference = (([long] $args[0]) - $availableFreeSpace) / 1MB

   ## If they want more free space than they have, give that message
   if($spaceDifference -gt 0)
   {
       $message = "To obtain $args bytes of free space, " +
          " free $spaceDifference megabytes."
       Write-Host $message
    }
   ## If they want less free space than they have, give that message
   else
   {
       $spaceDifference = $spaceDifference * -1
       $message = "To obtain $args bytes of free space, " +
           " use up $spaceDifference more megabytes."
        Write-Host $message
    }
  </SetScriptBlock>
</ScriptProperty>

Add an AliasProperty

An AliasProperty gives an alternative name (alias) for a property. The referenced property does not need to exist when PowerShell processes your type extension file, since you (or another script) might later add the property through mechanisms such as the Add-Member cmdlet.

Example 3-15 adds a Free AliasProperty to PSDriveInfo, and it should also be placed within the members section of the template given in Example 3-13. When you access the property, it returns the value of the AvailableFreeSpace property. When you set the property, it sets the value of the AvailableFreeSpace property.

Example 3-15. An AliasProperty for the PSDriveInfo type

<AliasProperty>
  <Name>Free</Name>
  <ReferencedMemberName>AvailableFreeSpace</ReferencedMemberName>
</AliasProperty>

Add a ScriptMethod

A ScriptMethod lets you define an action on an object, using PowerShell script as the extension language. It consists of two child elements: the Name of the property and the Script.

In the script element, the $this variable refers to the current object you are extending. Like a standalone script, the $args variable represents the arguments to the method. Unlike standalone scripts, ScriptMethods do not support the param statement for parameters.

Example 3-16 adds a Remove ScriptMethod to PSDriveInfo. Like the other additions, place these customizations within the members section of the template given in Example 3-13. When you call this method with no arguments, the method simulates removing the drive (through the -WhatIf option to Remove-PSDrive). If you call this method with $true as the first argument, it actually removes the drive from the PowerShell session.

Example 3-16. A ScriptMethod for the PSDriveInfo type

<ScriptMethod>
  <Name>Remove</Name>
  <Script>
    $force = [bool] $args[0]
    ## Remove the drive if they use $true as the first parameter
    if($force)
    {
       $this | Remove-PSDrive
    }
    ## Otherwise, simulate the drive removal
    else
    {
       $this | Remove-PSDrive -WhatIf
    }
  </Script>
</ScriptMethod>

Add other extension points

PowerShell supports several additional features in the types extension file, including CodeProperty, NoteProperty, CodeMethod, and MemberSet. Although not generally useful to end users, developers of PowerShell providers and cmdlets will find these features helpful. For more information about these additional features, see the Windows PowerShell SDK or the MSDN documentation.

Define Custom Formatting for a Type

Problem

You want to emit custom objects from a script and have them formatted in a specific way.

Solution

Use a custom format extension file to define the formatting for that type, followed by a call to the Update-FormatData cmdlet to load them into your session:

$formatFile = Join-Path (Split-Path $profile) "Format.Custom.Ps1Xml"
Update-FormatData -PrependPath $typesFile

If a file-based approach is not an option, use the Formats property of the [Runspace]::DefaultRunspace.InitialSessionState type to add new formatting definitions for the custom type.

Discussion

When PowerShell commands produce output, this output comes in the form of richly structured objects rather than basic streams of text. These richly structured objects stop being of any use once they make it to the screen, though, so PowerShell guides them through one last stage before showing them on screen: formatting and output.

The formatting and output system is based on the concept of views. Views can take several forms: table views, list views, complex views, and more. The most common view type is a table view. This is the form you see when you use Format-Table in a command, or when an object has four or fewer properties.

As with the custom type extensions described in Add Custom Methods and Properties to Types, PowerShell supports both file-based and in-memory updates of type formatting definitions.

The simplest and most common way to define formatting for a type is through the Update-FormatData cmdlet, as shown in the Solution. The Update-FormatData cmdlet takes paths to Format.ps1xml files as input. There are many examples of formatting definitions in the PowerShell installation directory that you can use. To create your own formatting customizations, use these files as a source of examples, but do not modify them directly. Instead, create a new file and use the Update-FormatData cmdlet to load your customizations.

For more information about the features supported by these formatting XML files, type Get-Help about_format.ps1xml.

In addition to file-based formatting, PowerShell makes it possible (although not easy) to create formatting definitions from scratch. Example 3-17 provides a script to simplify this process.

Example 3-17. Add-FormatData.ps1

##############################################################################
##
## Add-FormatData
##
## From Windows PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Adds a table formatting definition for the specified type name.

.EXAMPLE

PS > $r = [PSCustomObject] @{
    Name = "Lee";
    Phone = "555-1212";
    SSN = "123-12-1212"
}
PS > $r.PSTypeNames.Add("AddressRecord")
PS > Add-FormatData -TypeName AddressRecord -TableColumns Name, Phone
PS > $r

Name Phone
---- -----
Lee  555-1212

#>

param(
    ## The type name (or PSTypeName) that the table definition should
    ## apply to.
    $TypeName,

    ## The columns to be displayed by default
    [string[]] $TableColumns
)

Set-StrictMode -Version 3

## Define the columns within a table control row
$rowDefinition = New-Object Management.Automation.TableControlRow

## Create left-aligned columns for each provided column name
foreach($column in $TableColumns)
{
    $rowDefinition.Columns.Add(
            (New-Object Management.Automation.TableControlColumn "Left", 
            (New-Object Management.Automation.DisplayEntry $column,"Property")))
}

$tableControl = New-Object Management.Automation.TableControl
$tableControl.Rows.Add($rowDefinition)

## And then assign the table control to a new format view,
## which we then add to an extended type definition. Define this view for the
## supplied custom type name.
$formatViewDefinition = 
    New-Object Management.Automation.FormatViewDefinition "TableView",$tableControl
$extendedTypeDefinition = 
    New-Object Management.Automation.ExtendedTypeDefinition $TypeName
$extendedTypeDefinition.FormatViewDefinition.Add($formatViewDefinition)

## Add the definition to the session, and refresh the format data
[Runspace]::DefaultRunspace.InitialSessionState.Formats.Add($extendedTypeDefinition)
Update-FormatData