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.
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#.
You want to automate a program or system task through its COM automation interface.
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
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.
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.
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”.
You want to access system performance counter information from PowerShell.
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
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.8710351576814If 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.
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#.
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() 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

Click into the definition, and we see the C# signature:
Figure 17.2. 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.
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”.
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”.
You want to define a new .NET class, or extend an existing one.
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");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.
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 WorldFor 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”.
You want to write a portion of your script in C# (or another .NET language.)
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)
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
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”.
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).
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()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.
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.comThe 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”.
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.
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.
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.");
}
}
} 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”.
Once you have compiled the module, the final step is to load it.
Import-Module .\TemplateBinaryModule.dll
Once you've added the module to your session, you can call commands from that module as you would call any other cmdlet.
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 )
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”.
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.
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.
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.
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();
}
}
}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
4 comments
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.
That's a good suggestion, but I don't think I can do it justice while still staying on topic.
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?
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