Chapter 17. Extend the Reach of Windows PowerShell

Introduction

The PowerShell environment is phenomenally comprehensive. It provides a great surface of cmdlets to help you manage your system, a great scripting language to let you automate those tasks, and direct access to all the utilities and tools you already know.

4 comments

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

    I don't know whether the scope of the book is appropriate (or this section is even the correct one for this), but I think a small example for the Windows 7 Troubleshooting might be nice.

  2. Lee Holmes Posted 14 days and 13 hours ago

    That's a good suggestion, but I don't think I can do it justice while still staying on topic.

  3. Jose Santiago Oyervides Posted 6 days and 22 hours ago

    I think you could include a small script to show how to run a query against SQL Server, something like this:

    [void][reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnection.ConnectionString = "Server=localhost;Database=adventureworks;Integrated Security=True" $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $SqlCmd.CommandText = "select top 5 * from person.address" $SqlCmd.Connection = $SqlConnection $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $SqlAdapter.SelectCommand = $SqlCmd $DataSet = New-Object "System.Data.DataSet" "Table1" [void]$resultado = $SqlAdapter.fill($DataSet) $DataSet.Tables | Select-Object -ExpandProperty rows

    What do you think?

  4. Lee Holmes Posted 1 day and 21 hours ago

    Jose: Excellent idea. It was part of the first edition, and is still part of the second :) http://powershell.labs.oreilly.com/ch17.html#program_query_a_sql_data_source

Add a comment

The cmdlets, scripting language, and preexisting tools are just part of what makes PowerShell so comprehensive, however. In addition to these features, PowerShell provides access to a handful of technologies that drastically increase its capabilities: the .NET Framework, Windows Management Instrumentation (WMI), COM automation objects, native Windows API calls, and more.

Not only does PowerShell give you access to these technologies, but it also gives you access to them in a consistent way. The techniques you use to interact with properties and methods of PowerShell objects are the same techniques that you use to interact with properties and methods of .NET objects. In turn, those are the same techniques that you use to work with WMI and COM objects, too.

Working with these techniques and technologies provides another huge benefit—knowledge that easily transfers to working in .NET programming languages such as C#.

Automate Programs Using COM Scripting Interfaces

Problem

You want to automate a program or system task through its COM automation interface.

Solution

To instantiate and work with COM objects, use the New-Object cmdlet's –ComObject parameter.

$shell = New-Object -ComObject "Shell.Application"
$shell.Windows() | Format-Table LocationName,LocationUrl

Discussion

Like WMI, COM automation interfaces have long been a standard tool for scripting and system administration. When an application exposes management or automation tasks, COM objects are the second most common interface (right after custom command-line tools).

PowerShell exposes COM objects like it exposes most other management objects in the system. Once you have access to a COM object, you work with its properties and methods in the same way that you work with methods and properties of other objects in PowerShell.

Note

Some COM objects require a special interaction mode called Single Threaded Apartment (STA) to work correctly. For information about how to interact with components that require STA interaction, see the section called “Interact With UI Frameworks and STA Objects”.

In addition to automation tasks, many COM objects exist entirely to improve the scripting experience in languages such as VBScript. One example of this is working with files, or sorting an array.

Most of these COM objects become obsolete in PowerShell, as PowerShell often provides better alternatives to them! In many cases, PowerShell's cmdlets, scripting language, or access to the .NET Framework provide the same or similar functionality to a COM object that you might be used to.

For more information about working with COM objects, see the section called “Use a COM Object”. For a list of the most useful COM objects, see Appendix H, Selected COM Objects and Their Uses.

Program: Query a SQL Data Source

It is often helpful to perform ad hoc queries and commands against a data source such as a SQL server, Access database, or even an Excel spreadsheet. This is especially true when you want to take data from one system and put it in another, or when you want to bring the data into your PowerShell environment for detailed interactive manipulation or processing.

Although you can directly access each of these data sources in PowerShell (through its support of the .NET Framework), each data source requires a unique and hard to remember syntax. Example 17.1, “Invoke-SqlCommand.ps1” makes working with these SQL-based data sources both consistent and powerful.

Example 17.1. Invoke-SqlCommand.ps1

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

param(
    [string] $dataSource = ".\SQLEXPRESS",
    [string] $database = "Northwind",      
    [string[]] $sqlCommand = $(throw "Please specify a query."),
    [int] $timeout = 60,
    [System.Management.Automation.PsCredential] $credential
  )


$authentication = "Integrated Security=SSPI;"

if($credential)
{
    $plainCred = $credential.GetNetworkCredential()
    $authentication = 
        ("uid={0};pwd={1};" -f $plainCred.Username,$plainCred.Password)
}

$connectionString = "Provider=sqloledb; " +
                    "Data Source=$dataSource; " +
                    "Initial Catalog=$database; " +
                    "$authentication; "

if($dataSource -match '\.xls$|\.mdb$')
{
    $connectionString = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=$dataSource; "

    if($dataSource -match '\.xls$')
    {
        $connectionString += 'Extended Properties="Excel 8.0;"; '

        if($sqlCommand -notmatch '\[.+\$\]')
        {
            $error = 'Sheet names should be surrounded by square brackets, and ' +
                       'have a dollar sign at the end: [Sheet1$]'
            Write-Error $error
            return
        }
    }
}

$connection = New-Object System.Data.OleDb.OleDbConnection $connectionString
$connection.Open()

foreach($commandString in $sqlCommand)
{
    $command = New-Object System.Data.OleDb.OleDbCommand $commandString,$connection
    $command.CommandTimeout = $timeout

    $adapter = New-Object System.Data.OleDb.OleDbDataAdapter $command
    $dataset = New-Object System.Data.DataSet
    [void] $adapter.Fill($dataSet)
    
    $dataSet.Tables | Select-Object -Expand Rows
}
$connection.Close()


      

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

Access Windows Performance Counters

Problem

You want to access system performance counter information from PowerShell.

Solution

To retrieve information about a specific performance counter, use the Get-Counter cmdlet, as shown in Example 17.2, “Accessing performance counter data through the Get-Counter cmdlet”.

Example 17.2. Accessing performance counter data through the Get-Counter cmdlet

PS > $counter = Get-Counter "\System\System Up Time"
PS > $uptime = $counter.CounterSamples[0].CookedValue
PS > New-TimeSpan -Seconds $uptime


Days              : 8
Hours             : 1
Minutes           : 38
Seconds           : 58
Milliseconds      : 0
Ticks             : 6971380000000
TotalDays         : 8.06872685185185
TotalHours        : 193.649444444444
TotalMinutes      : 11618.9666666667
TotalSeconds      : 697138
TotalMilliseconds : 697138000

Alternatively, WMI's Win32_Perf* set of classes support many of the most common performance counters:

Get-WmiObject Win32_PerfFormattedData_Tcpip_NetworkInterface

Discussion

The Get-Counter provides handy access to all of Windows' performance counters. With no parameters, it gives a helpful summary of system activity:

PS > Get-Counter -Continuous

Timestamp                 CounterSamples
---------                 --------------
1/9/2010 7:26:49 PM       \\...\network interface(ethernet adapt
                          er)\bytes total/sec :
                          102739.3921377

                          \\...\processor(_total)\% processor ti
                          me :
                          35.6164383561644

                          \\...\memory\% committed bytes in use
                          :
                          29.4531607006855

                          \\...\memory\cache faults/sec :
                          98.1952324093294

                          \\...\physicaldisk(_total)\% disk time
                           :
                          144.227945205479

                          \\...\physicaldisk(_total)\current dis
                          k queue length :
                          0
(...)

When you supply a path to a specific counter, the Get-Counter cmdlet retrieves only the samples for that path. The -Computer parameter lets you target a specific remote computer, if desired:

PS > $computer = $ENV:Computername
PS > Get-Counter "\\$computer\processor(_total)\% processor time"

Timestamp                 CounterSamples
---------                 --------------
1/9/2010 7:31:58 PM       \\...\processor(_total)\% processor time :
                          15.8710351576814

If you don't know the path to the performance counter you want, you can use the -ListSet parameter to search for a counter or set of counters. To see all counter sets, use * as the parameter value:

PS > Get-Counter -List * | Format-List CounterSetName,Description


CounterSetName : TBS counters
Description    : Performance counters for the TPM Base Services component.

CounterSetName : WSMan Quota Statistics
Description    : Displays quota usage and violation information for WS-Man
                 agement processes.

CounterSetName : Netlogon
Description    : Counters for measuring the performance of Netlogon.

(...)

If you want to find a specific counter, use the Where-Object cmdlet to compare against the Description or Paths property:

Get-Counter -ListSet * | Where-Object { $_.Description -match "garbage" }
Get-Counter -ListSet * | Where-Object { $_.Paths -match "Gen 2 heap" }

CounterSetName     : .NET CLR Memory
MachineName        : .
CounterSetType     : MultiInstance
Description        : Counters for CLR Garbage Collected heap.
Paths              : {\.NET CLR Memory(*)\# Gen 0 Collections, \.NET CLR M
                     emory(*)\# Gen 1 Collections, \.NET CLR Memory(*)\# G
                     en 2 Collections, \.NET CLR Memory(*)\Promoted Memory
                      from Gen 0...}
PathsWithInstances : {\.NET CLR Memory(_Global_)\# Gen 0 Collections, \.NE
                     T CLR Memory(powershell)\# Gen 0 Collections, \.NET C
                     LR Memory(powershell_ise)\# Gen 0 Collections, \.NET
                     CLR Memory(PresentationFontCache)\# Gen 0 Collections
                     ...}
Counter            : {\.NET CLR Memory(*)\# Gen 0 Collections, \.NET CLR M
                     emory(*)\# Gen 1 Collections, \.NET CLR Memory(*)\# G
                     en 2 Collections, \.NET CLR Memory(*)\Promoted Memory
                      from Gen 0...}

Once you've retrieved a set of counters, you can use the Export-Counter cmdlet to save them in a format supported by other tools, such as the .BLG files supported by the Windows Performance Monitor application.

If you already have a set of performance counters saved in a .BLG file or .TSV file that were exported from Windows Performance Monitor, you can use the Import-Counter cmdlet to work with those samples in PowerShell.

Access Windows API Functions

Problem

You want to access functions from the Windows API, as you would access them through a Platform Invoke (P/Invoke) in a .NET language such as C#.

Solution

Obtain (or create) the signature of the Windows API function, and then pass that to the -MemberDefinition parameter of the Add-Type cmdlet. Store the output object in a variable, and then use the method on that variable to invoke the Windows API function.

Example 17.3. Get-PrivateProfileString.ps1

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

<#

.SYNOPSIS

Retrieves an element from a standard .INI file

.EXAMPLE

PS >Get-PrivateProfileString c:\windows\system32\tcpmon.ini `
    "<Generic Network Card>" Name
Generic Network Card

#>

param(
    $Path,
    
    $Category,
    
    $Key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru

$builder = New-Object System.Text.StringBuilder 1024
$null = $type::GetPrivateProfileString($category,
    $key, "", $builder, $builder.Capacity, $path)
   
$builder.ToString()      

Discussion

You can access many simple Windows APIs using the script given in the section called “Program: Invoke Simple Windows API Calls”. This approach is difficult for more complex APIs, however.

In PowerShell version one, it was possible to access these APIs in one of two ways: by generating a dynamic assembly on the fly (you wouldn't really do this for one-off calls, but the section called “Program: Invoke Simple Windows API Calls” uses this technique), or by looking up the P/Invoke definition for that API call, and compiling the C# to access it.

These are both good approaches, but PowerShell version two introduces the Add-Type cmdlet to make this much easier.

Add-Type offers four basic modes of operation:

PS > Get-Command Add-Type | Select -Expand ParameterSets | Select Name

Name
----
FromSource
FromMember
FromPath
FromAssemblyName

These are:

  • FromSource: Compile some C# (or other language) code that completely defines a type. This is useful when you want to define an entire class, its methods, namespace, etc. You supply the actual code as the value to the –TypeDefinition parameter, usually through a variable. For more information about this technique, see the section called “Define or Extend a .NET Class”.

  • FromPath: Compile from a file on disk, or load the types from an assembly at that location. For more information about this technique, see the section called “Access a .NET SDK Library”.

  • FromAssemblyName: Load an assembly from the .NET Global Assembly Cache (GAC) by its shorter name. This is not the same as the [Reflection.Assembly]::LoadWithPartialName method, since that method introduces your script to many subtle breaking changes. Instead, PowerShell maintains a large mapping table that converts the shorter name you type a strongly-named assembly reference. For more information about this technique, see the section called “Access a .NET SDK Library”.

  • FromMember: Generates a type out of a member definition (or set of them.) For example, if you specify only a method definition, PowerShell automatically generates the wrapper class for you. This parameter set is explicitly designed to easily support P/Invoke calls.

Now, how do you use the FromMember parameter set to call a Windows API? The solution shows the end-result of this process, but let's take it step-by-step. First, imagine that you want to access sections of an INI file.

PowerShell doesn't have a native way to manage INI files, and neither does the .NET Framework. However, the Windows API does, through a call to a function called GetPrivateProfileString. The .NET framework lets you access Windows functions through a technique called P/Invoke (Platform Invocation Services.) Most calls boil down to a simple "P/Invoke definition," which usually takes a lot of trial and error. However, a great community has grown around these definitions, resulting in an enormous resource called P/Invoke .NET: http://www.pinvoke.net/. The .NET Framework team also supports a tool called the P/Invoke Interop Assistant that also generates these definitions, but we won't consider that for now.

First, we'll create a script, Get-PrivateProfileString.ps1. It's a template for now:

## Get-PrivateProfileString.ps1
param(
    $Path,
    $Category,
    $Key)

$null 

To start fleshing this out, we visit P/Invoke .NET and search for GetPrivateProfileString:

Figure 17.1. Visiting P/Invoke .NET

Visiting P/Invoke .NET


Click into the definition, and we see the C# signature:

Figure 17.2. The Windows API signature for GetPrivateProfileString

The Windows API signature for GetPrivateProfileString


Next, we copy that signature as a here-string into our script. Notice that we've added public to the declaration. The signatures on PInvoke.NET assume that you'll call the method from within the C# class that defines it. We'll be calling it from scripts (which are outside of the C# class that defines it), so we need to change its visibility.

## Get-PrivateProfileString.ps1
param(
    $Path,
    $Category,
    $Key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$null 

Now, we add the call to Add-Type. This signature becomes the building block for a new class, so we only need to give it a name. To prevent its name from colliding with other classes with the same name, we also put it in a namespace. The name of our script is a good choice:

## Get-PrivateProfileString.ps1
param(
    $Path,
    $Category,
    $Key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -PassThru

$null 

When we try to run this script, though, we get an error:

The type or namespace name 'StringBuilder' could not be found (are you missing a
using directive or an assembly reference?)
c:\Temp\obozeqo1.0.cs(12) :    string lpDefault,
c:\Temp\obozeqo1.0.cs(13) : >>>    StringBuilder lpReturnedString,
c:\Temp\obozeqo1.0.cs(14) :    uint nSize,

Indeed we are. The StringBuilder class is defined in the System.Text namespace, which requires a using directive to be placed at the top of the program by the class definition. Since we're letting PowerShell define the type for us, we can either rename it to System.Text.StringBuilder, or add a –UsingNamespace parameter to have PowerShell add the using statement for us.

Note

PowerShell adds references to the System and System.Runtime.InteropServices namespaces by default.

Let's do the latter:

## Get-PrivateProfileString.ps1
param(
    $Path,
    $Category,
    $Key)

$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
    string lpAppName,
    string lpKeyName,
    string lpDefault,
    StringBuilder lpReturnedString,
    uint nSize,
    string lpFileName);
'@

$type = Add-Type -MemberDefinition $signature `
    -Name Win32Utils -Namespace GetPrivateProfileString `
    -Using System.Text -PassThru

$null 

Now, we can plug in all of the necessary parameters. The GetPrivateProfileString function puts its output in a StringBuilder, so we'll have to feed it one, and return its contents. This gives us the script shown in Example 17.3, “Get-PrivateProfileString.ps1”.

PS > Get-PrivateProfileString c:\windows\system32\tcpmon.ini `
   "<Generic Network Card>" Name
Generic Network Card

So now we have it. With just a few lines of code, we've defined and invoked a Win32 API call.

For more information about working with classes from the .NET Framework, see the section called “Run Programs, Scripts, and Existing Tools”.

Program: Invoke Simple Windows API Calls

There are times when neither PowerShell's cmdlets nor scripting language directly support a feature you need. In most of those situations, PowerShell's direct support for the .NET Framework provides another avenue to let you accomplish your task. In some cases, though, even the .NET Framework does not support a feature you need to resolve a problem, and the only way to resolve your problem is to access the core Windows APIs.

For complex API calls (ones that take highly structured data), the solution is to use the Add-Type cmdlet (or write a PowerShell cmdlet) that builds on the P/Invoke (Platform Invoke) support in the .NET Framework. The P/Invoke support in the .NET Framework is designed to let you access core Windows APIs directly.

Although it is possible to determine these P/Invoke definitions yourself, it is usually easiest to build on the work of others. If you want to know how to call a specific Windows API from a .NET language, the http://pinvoke.net web site is the best place to start.

If the API you need to access is straightforward (one that takes and returns only simple data types), however, Example 17.4, “Invoke-WindowsApi.ps1” can do most of the work for you.

For an example of this script in action, see the section called “Program: Create a Filesystem Hard Link”.

Example 17.4. Invoke-WindowsApi.ps1

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

<#

.SYNOPSIS

Invoke a native Windows API call that takes and returns simple data types.


.EXAMPLE

$parameterTypes = [string], [string], [IntPtr]
$parameters = [string] $filename, [string] $existingFilename, [IntPtr]::Zero

$result = Invoke-WindowsApi "kernel32" ([bool]) "CreateHardLink" `
    $parameterTypes $parameters

#>
param(
    [string] $dllName, 
    [Type] $returnType, 
    [string] $methodName,
    [Type[]] $parameterTypes,
    [Object[]] $parameters
    )

$domain = [AppDomain]::CurrentDomain
$name = New-Object Reflection.AssemblyName 'PInvokeAssembly'
$assembly = $domain.DefineDynamicAssembly($name, 'Run')
$module = $assembly.DefineDynamicModule('PInvokeModule')
$type = $module.DefineType('PInvokeType', "Public,BeforeFieldInit")

$inputParameters = @()
$refParameters = @()

for($counter = 1; $counter -le $parameterTypes.Length; $counter++)
{
    if($parameterTypes[$counter - 1] -eq [Ref])
    {
        $refParameters += $counter

        $parameterTypes[$counter - 1] = 
            $parameters[$counter - 1].Value.GetType().MakeByRefType()
        $inputParameters += $parameters[$counter - 1].Value
    }
    else
    {
        $inputParameters += $parameters[$counter - 1]
    }
}

$method = $type.DefineMethod($methodName, 'Public,HideBySig,Static,PinvokeImpl', 
    $returnType, $parameterTypes)
foreach($refParameter in $refParameters)
{
    [void] $method.DefineParameter($refParameter, "Out", $null)
}

$ctor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([string])
$attr = New-Object Reflection.Emit.CustomAttributeBuilder $ctor, $dllName
$method.SetCustomAttribute($attr)

$realType = $type.CreateType()

$realType.InvokeMember($methodName, 'Public,Static,InvokeMethod', $null, $null, 
    $inputParameters)

foreach($refParameter in $refParameters)
{
    $parameters[$refParameter - 1].Value = $inputParameters[$refParameter - 1]
}
      

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

Define or Extend a .NET Class

Problem

You want to define a new .NET class, or extend an existing one.

Solution

Use the -TypeDefinition parameter of the Add-Type class.

Example 17.5. Invoke-AddTypeTypeDefinition.ps1

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

<#

.SYNOPSIS

Demonstrates the use of the -TypeDefinition parameter of the Add-Type
cmdlet.

#>

$newType = @'
using System;

namespace PowerShellCookbook
{
    public class AddTypeTypeDefinitionDemo
    {
        public string SayHello(string name)
        {
            string result = String.Format("Hello {0}", name);
            return result;
        }
    }
}

'@

Add-Type -TypeDefinition $newType

$greeter = New-Object PowerShellCookbook.AddTypeTypeDefinitionDemo
$greeter.SayHello("World");

Discussion

The Add-Type cmdlet is one of the major new additions to the glue-like nature of PowerShell version two, and offers several unique ways to interact deeply with the .NET Framework. One of its major modes of operation comes from the -TypeDefinition parameter, which lets you define entirely new .NET classes. In addition to the example given in the solution, the section called “Program: Create a Dynamic Variable” demonstrates an effective use of this technique.

Once you call the Add-Type cmdlet, PowerShell compiles the source code you provide into a real .NET class. This action is equivalent to defining the class in a traditional development environment, such as Visual Studio, and is just as powerful.

Note

The thought of compiling source code as part of the execution of your script may concern you due to its performance impact. Fortunately, PowerShell saves your objects when it compiles them. If you call the Add-Type cmdlet a second time with the same source code and in the same session, PowerShell re-uses the result of the first call. If you want to change the behavior of a type you've already loaded, exit your session and create it again.

PowerShell assumes C# as the default language for source code supplied to the -TypeDefinition parameter. In addition to C#, the Add-Type cmdlet also supports C# version 3 (LINQ, the var keyword, etc), Visual Basic, and JScript. In addition, it also supports languages (such as F#) that implement the .NET-standard CodeProvider requirements.

If the code you want to compile already exists in a file, you don't have to specify it in-line. Instead, you can provide its path to the -Path parameter. This parameter automatically detects the extension of the file, and compiles using the appropriate language as needed.

In addition to supporting input from a file, you might also want to store the output into a file—such as a cmdlet DLL, or console application. The Add-Type cmdlet makes this possible through the -OutputAssembly parameter. For example, adding a cmdlet on the fly:

PS > $cmdlet = @'
>> using System.Management.Automation;
>>
>> namespace PowerShellCookbook
>> {
>>     [Cmdlet("Invoke", "NewCmdlet")]
>>     public class InvokeNewCmdletCommand : Cmdlet
>>     {
>>         [Parameter(Mandatory = true)]
>>         public string Name
>>         {
>>             get { return _name; }
>>             set { _name = value; }
>>         }
>>         private string _name;
>>
>>
>>         protected override void BeginProcessing()
>>         {
>>             WriteObject("Hello " + _name);
>>         }
>>     }
>> }
>>
>> '@
>>
PS > Add-Type -TypeDefinition $cmdlet -OutputAssembly MyNewModule.dll
PS > Import-Module .\MyNewModule.dll
PS > Invoke-NewCmdlet

cmdlet Invoke-NewCmdlet at command pipeline position 1
Supply values for the following parameters:
Name: World
Hello World

For advanced scenarios, you might want to customize how PowerShell compiles your source code: embedding resources, changing the warning options, and more. For this, use the -CompilerParameters parameter.

For an example of using the Add-Type cmdlet to generate inline C#, see the section called “Add Inline C# to your PowerShell Script”.

Add Inline C# to your PowerShell Script

Problem

You want to write a portion of your script in C# (or another .NET language.)

Solution

Use the -MemberDefinition parameter of the Add-Type class.

Example 17.6. Invoke-Inline.ps1

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

<#
.SYNOPSIS

Demonstrates the Add-Type cmdlet to invoke in-line C#

#>

$inlineType = Add-Type -Name InvokeInline_Inline -PassThru -MemberDefinition @'
    public static int RightShift(int original, int places)
    {
        return original >> places;
    }
'@

$inlineType::RightShift(1024, 3)
      


Discussion

One of the natural languages to explore after learning PowerShell is C#. It uses many of the same programming techniques as PowerShell and uses the same classes and methods in the .NET Framework as PowerShell does, too. In addition, C# sometimes offers language features or performance benefits not available through PowerShell.

Rather than having to move to C# completely for these situations, Example 17.6, “Invoke-Inline.ps1” demonstrates how you can use the Add-Type cmdlet to write and invoke C# directly in your script.

Once you call the Add-Type cmdlet, PowerShell compiles the source code you provide into a real .NET class. This action is equivalent to defining the class in a traditional development environment, such as Visual Studio, and gives you equivalent functionality. When you use the -MemberDefinition parameter, PowerShell adds the surrounding source code required to create a complete .NET class.

By default, PowerShell places your resulting type in the Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes namespace. If you use the -PassThru parameter (and define your method as static), you don't need to pay much attention to the name or namespace of the generated type. However, if you do not define your method as static, you will need to use the New-Object cmdlet to create a new instance of the object before using it. In this case, you will need to use the full name of the resulting type when creating it. For example:

New-Object Microsoft.PowerShell.Commands.AddType.AutoGeneratedTypes.InvokeInline_Inline

Note

The thought of compiling source code as part of the execution of your script may concern you due to its performance impact. Fortunately, PowerShell saves your objects when it compiles them. If you call the Add-Type cmdlet a second time with the same source code and in the same session, PowerShell re-uses the result of the first call. If you want to change the behavior of a type you've already loaded, exit your session and create it again.

PowerShell assumes C# as the default language of code supplied to the -MemberDefinition parameter. In addition to C#, it also supports C# version 3 (LINQ, the var keyword, etc), Visual Basic, and JScript. In addition, it also supports languages (such as F#) that implement the .NET-standard CodeProvider requirements.

For an example of the -MemberDefinition parameter being used as part of a larger script, see the section called “Access Windows API Functions”. For an example of using the Add-Type cmdlet to create entire types, see the section called “Define or Extend a .NET Class”.

Access a .NET SDK Library

Problem

You want to access the functionality exposed by a .NET DLL, but that DLL is packaged as part of a developer-oriented Software Development Kit (SDK).

Solution

To create objects contained in a DLL, use the -Path parameter of the Add-Type cmdlet to load the DLL, and the New-Object cmdlet to create objects contained in it. Example 17.7, “Interacting with classes from the SharpZipLib SDK DLL” illustrates this technique.

Example 17.7. Interacting with classes from the SharpZipLib SDK DLL

Add-Type -Path d:\bin\ICSharpCode.SharpZipLib.dll
$namespace = "ICSharpCode.SharpZipLib.Zip.{0}"

$zipName = Join-Path (Get-Location) "PowerShell_TDG_Scripts.zip"
$zipFile = New-Object ($namespace -f "ZipOutputStream") ([IO.File]::Create($zipName))

foreach($file in dir *.ps1)
{

   $zipEntry = New-Object ($namespace -f "ZipEntry") $file.Name
   $zipFile.PutNextEntry($zipEntry) }

$zipFile.Close()

Discussion

While C# and VB.Net developers are usually the consumers of SDKs created for the .NET Framework, PowerShell lets you access the SDK features just as easily. To do this, use the -Path parameter of the Add-Type cmdlet to load the SDK assembly, and then work with the classes from that assembly as you would work with other classes in the .NET Framework.

Note

Although PowerShell lets you access developer-oriented SDKs easily, it can't change the fact that these SDKs are developer-oriented. SDKs and programming interfaces are rarely designed with the administrator in mind, so be prepared to work with programming models that require multiple steps to accomplish your task.

To load any of the typical assemblies included in the .NET Framework, use the -Assembly parameter of the Add-Type cmdlet:

PS > Add-Type -Assembly System.Web

Like most PowerShell cmdlets, the Add-Type cmdlet supports wildcards to make long assembly names easier to type.

PS > Add-Type -Assembly system.win*.forms

If the wildcard matches more than one assembly, Add-Type generates an error.

The .NET Framework offers a similar feature through the LoadWithPartialName method of the System.Reflection.Assembly class:

Example 17.8. Loading an assembly by its partial name

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

GAC    Version        Location
---    -------        --------
True   v2.0.50727     C:\WINDOWS\assembly\GAC_32\(...)\System.Web.dll

PS > [Web.HttpUtility]::UrlEncode("http://www.bing.com")
http%3a%2f%2fwww.bing.com

The difference between the two is that the LoadWithPartialName method is unsuitable for scripts that you want to share with others or use in a production environment. It loads the most current version of the assembly, which may not be the same as the version you used to develop your script. If that assembly changes between versions, your script will no longer work. The Add-Type command, on the other hand, internally maps the short assembly names to the fully-qualified assembly names contained in a typical installation of the .NET Framework versions 2.0 and 3.5.

One thing you will notice when working with classes from an SDK is that it quickly becomes tiresome to specify their fully qualified type names. For example, zip-related classes from the SharpZipLib all start with ICSharpCode.SharpZipLib.Zip. This is called the namespace of that class. Most programming languages solve this problem with a using statement that lets you specify a list of namespaces for that language to search when you type a plain class name such as ZipEntry. PowerShell lacks a using statement, but the solution demonstrates one of several ways to get the benefits of one.

For more information on how to manage these long class names, see the section called “Reduce Typing for Long Class Names”.

Prepackaged SDKs aren't the only DLLs you can load this way, either. An SDK library is simply a DLL that somebody wrote, compiled, packaged, and released. If you are comfortable with any of the .NET languages, you can also create your own DLL, compile it, and use it exactly the same way. To see an example of this approach, see the section called “Define or Extend a .NET Class”.

For more information about working with classes from the .NET Framework, see the section called “Create an Instance of a .NET Object”.

Create Your Own PowerShell Cmdlet

Problem

You want to write your own PowerShell cmdlet.

Discussion

As mentioned in the section called “Structured Commands (Cmdlets)”, PowerShell cmdlets offer several significant advantages over traditional executable programs. From the user's perspective, cmdlets are incredibly consistent. Their support for strongly typed objects as input makes them incredibly powerful, too. From the cmdlet author's perspective, cmdlets are incredibly easy to write when compared to the amount of power they provide. Creating and exposing a new command-line parameter is as easy as creating a new public property on a class. Supporting a rich pipeline model is as easy as placing your implementation logic into one of three standard method overrides.

While a full discussion on how to implement a cmdlet is outside the scope of this book, the following steps illustrate the process behind implementing a simple cmdlet. While implementation typically happens in a fully-featured development environment (such as Visual Studio), Example 17.9, “InvokeTemplateCmdletCommand.cs” demonstrates how to compile a cmdlet simply through the csc.exe command-line compiler.

For more information on how to write a PowerShell cmdlet, see the MSDN topic, "How to Create a Windows PowerShell Cmdlet," available at http://msdn.microsoft.com/en-us/library/ms714598.aspx.

Step 1: Download the PowerShell SDK

The PowerShell SDK contains samples, reference assemblies, documentation, and other information used when developing PowerShell cmdlets. It is available by searching for "PowerShell 2.0 SDK" on http://download.microsoft.com and downloading the latest PowerShell SDK.

Step 2: Create a file to hold the cmdlet source code

Create a file called InvokeTemplateCmdletCommand.cs with the content from Example 17.9, “InvokeTemplateCmdletCommand.cs” and save it on your hard drive.

Example 17.9. InvokeTemplateCmdletCommand.cs

using System;
using System.ComponentModel;
using System.Management.Automation;

/*
To build and install:

1) Set-Alias csc $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\csc.exe
2) $ref = [PsObject].Assembly.Location
3) csc /out:TemplateBinaryModule.dll /t:library InvokeTemplateCmdletCommand.cs /r:$ref
4) Import-Module .\TemplateBinaryModule.dll

To run:

PS >Invoke-TemplateCmdlet
*/

namespace Template.Commands
{
    [Cmdlet("Invoke", "TemplateCmdlet")]
    public class InvokeTemplateCmdletCommand : Cmdlet
    {
        [Parameter(Mandatory=true, Position=0, ValueFromPipeline=true)]
        public string Text
        {
            get
            {
                return text;
            }
            set
            {
                text = value;
            }
        }
        private string text;

        protected override void BeginProcessing()
        {
            WriteObject("Processing Started");
        }

        protected override void ProcessRecord()
        {
            WriteObject("Processing " + text);
        }

        protected override void EndProcessing()
        {
            WriteObject("Processing Complete.");
        }
    }
} 

Step 3: Compile the DLL

A PowerShell cmdlet is a simple .NET class. The DLL that contains one or more compiled cmdlets is called a binary module.

Set-Alias csc $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\csc.exe
$ref = [PsObject].Assembly.Location
csc /out:TemplateBinaryModule.dll /t:library InvokeTemplateCmdletCommand.cs /r:$ref

For more information about binary modules, see the section called “Extend Your Shell with Additional Commands”.

If you don't want to use csc.exe to compile the DLL, you can also use PowerShell's built-in Add-Type cmdlet. For more information about this approach, see the section called “Define or Extend a .NET Class”.

Step 4: Load the module

Once you have compiled the module, the final step is to load it.

Import-Module .\TemplateBinaryModule.dll

Step 6: Use the module

Once you've added the module to your session, you can call commands from that module as you would call any other cmdlet.

1 comment

  1. AndrewTearle Posted 16 days and 7 hours ago

    Once you've added the module to your session, you can call commands from that module as though you would call any other cmdlet.

    ( as you would call any other cmdlet ? perhaps )

Add a comment

PS > "Hello World" | Invoke-TemplateCmdlet
Processing Started
Processing Hello World
Processing Complete.

In addition to binary modules, PowerShell supports almost all of the functionality of cmdlets through Advanced Functions. If you want to create functions with the power of cmdlets and the ease of scripting, see the section called “Provide -WhatIf, -Confirm, and Other Cmdlet Features”.

Add PowerShell Scripting to Your Own Program

Problem

You want to provide your users with an easy way to automate your program, but don't want to write a scripting language on your own.

Discussion

One of the fascinating aspects of PowerShell is how easily it lets you add many of its capabilities to your own program. This is because PowerShell is, at its core, a powerful engine that any application can use. The PowerShell console application is in fact just a text-based interface to this engine.

While a full discussion of the PowerShell hosting model is outside the scope of this book, the following example illustrates the techniques behind exposing features of your application for your users to script.

To frame Example 17.10, “RulesWizardExample.cs”, imagine an email application that lets you run rules when it receives an email. While you will want to design a standard interface that allows users to create simple rules, you will also want to provide a way for users to write incredibly complex rules. Rather than design a scripting language yourself, you can simply use PowerShell's scripting language. In the following example, we provide user-written scripts with a variable called $message that represents the current message and then runs their commands.

PS > Get-Content VerifyCategoryRule.ps1
if($message.Body -match "book")
{
    [Console]::WriteLine("This is a message about the book.")
}
else
{
    [Console]::WriteLine("This is an unknown message.")
}
PS > .\RulesWizardExample.exe (Resolve-Path VerifyCategoryRule.ps1)
This is a message about the book.

For more information on how to host PowerShell in your own application, see the MSDN topic, "How to Create a Windows PowerShell Hosting Application," available at http://msdn.microsoft.com/en-us/library/ms714661.aspx.

Step 1: Download the PowerShell SDK

The PowerShell SDK contains samples, reference assemblies, documentation, and other information used when developing PowerShell cmdlets. It is available by searching for "PowerShell 2.0 SDK" on http://download.microsoft.com and downloading the latest PowerShell SDK.

Step 2: Create a file to hold the hosting source code

Create a file called RulesWizardExample.cs with the content from Example 17.10, “RulesWizardExample.cs”, and save it on your hard drive.

Example 17.10. RulesWizardExample.cs

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace Template
{

    // Define a simple class that represents a mail message
    public class MailMessage
    {
        public MailMessage(string to, string from, string body)
        {
            this.To = to;
            this.From = from;
            this.Body = body;
        }

        public String To;
        public String From;
        public String Body;
    }

    public class RulesWizardExample
    {
        public static void Main(string[] args)
        {
            // Ensure that they've provided some script text
            if(args.Length == 0)
            {
                Console.WriteLine("Usage:");
                Console.WriteLine(" RulesWizardExample <script text>");
                return;
            }

            // Create an example message to pass to our rules wizard
            MailMessage mailMessage =
                        new MailMessage(
                            "guide_feedback@LeeHolmes.com",
                            "guide_reader@example.com",
                            "This is a message about your book.");

            // Create a runspace, which is the environment for
            // running commands
            Runspace runspace = RunspaceFactory.CreateRunspace();
            runspace.Open();

            // Create a variable, called "$message" in the Runspace, and populate
            // it with a reference to the current message in our application.
            // Pipeline commands can interact with this object like any other
            // .Net object.
            runspace.SessionStateProxy.SetVariable("message", mailMessage);

            // Create a pipeline, and populate it with the script given in the
            // first command line argument.
            Pipeline pipeline = runspace.CreatePipeline(args[0]);

            // Invoke (execute) the pipeline, and close the runspace.
            pipeline.Invoke();
            runspace.Close();
        }
    }
}

Step 3: Compile and run the example

Although the example itself provides very little functionality, it demonstrates the core concepts behind adding PowerShell scripting to your own program.

Set-Alias csc $env:WINDIR\Microsoft.NET\Framework\v2.0.50727\csc.exe
$dll = [PsObject].Assembly.Location
Csc RulesWizardExample.cs /reference:$dll
RulesWizardExample.exe <script commands to run>

For example,

PS > .\RulesWizardExample.exe '[Console]::WriteLine($message.From)'
guide_reader@example.com
You must sign in or register before commenting
*
*
*
*
*

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