Cmdlets

Extend Windows PowerShell With Custom Commands

Jim Truher

Code download available at:PowerShell2007_12.exe(154 KB)

This article discusses:

  • Windows PowerShell SDK
  • Using the System.IO.IsolatedStorage namespace
  • Overriding default cmdlet behavior
  • Installing and using custom cmdlets
This article uses the following technologies:
Windows PowerShell, .NET Framework, Isolated Storage

Contents

An Overview of Cmdlets
Cmdlet Attribute
Cmdlet Class Definition
Parameter Definition
Common Parameters
ShouldProcess Parameters
Parameter Sets
Method Overrides
Emitting Results
Reporting Error Conditions
Troubleshooting Messages
Cmdlet Groups
Creating a Snap-In
Formatting
Installing and Loading a PSSnapIn
Putting It All Together

As you've probably already discovered, Windows PowerShellTM is a powerful and flexible tool. You may not know, however, that you can extend Windows PowerShell by writing your own cmdlets. In this article, I will show you how to create your own cmdlets by writing three custom cmdlets that allow you to interact with IsolatedStorage.

I chose IsolatedStorage for these samples because I have not seen any other cmdlets related to IsolatedStorage and I thought its functionality would be useful, offering a place to keep data that won't conflict with other applications.

In a nutshell, the System.IO.IsolatedStorage namespace lets you create and use isolated stores. You can read and write data that less-trusted code cannot access, preventing sensitive information from being exposed. Basically, data is only available to the current user or the assembly in which the code exists (it can also be isolated by domain).

When using IsolatedStorage in these examples, I will be saving a key/value pair as strings. IsolatedStorage can store any type of data you need, but I'm sticking to strings for the purposes of this article. Remember that this article is really about cmdlets—IsolatedStorage merely provides a sample for me to work with. Also keep in mind I'm merely providing a starting point here. When you're ready to go much deeper into creating your own custom cmdlets, be sure to check out the Windows PowerShell SDK.

An Overview of Cmdlets

When Microsoft created Windows PowerShell, it was designed to make it easy to create other command-line tools that offer the same consistency and reliability as the tools that shipped as a part of Windows PowerShell. This is in large part because the shell has a single parser for all cmdlets—this model allows the dev team to be sure that all argument parsing, error handling, and so on is done similarly for everything that a user might do.

As a result, there are some pretty significant differences between a Windows PowerShell cmdlet and commands in other standalone shell environments. For instance, a cmdlet is an instance of a Microsoft® .NET Framework class; it is not a standalone executable. Cmdlets generally output objects rather than text and should not format their output. A cmdlet processes its input objects from an object pipeline rather than from a stream of text. A cmdlet should not parse its own arguments and it should not specify a presentation for errors. Finally, cmdlets are record-oriented and generally process a single object at a time.

Cmdlets have a specific structure; they must be attributed in a particular way and they must be derived from a specific base class. If a particular cmdlet supports parameters, those parameters must also be attributed in a specific way, and the cmdlet must provide implementations of some specific methods. If you're wondering what all this means, don't worry. I'll explain each of these requirements in more detail.

Cmdlet Attribute

To declare a .NET class as a cmdlet, you attribute the class with the CmdletAttribute attribute (which is the only required attribute for any cmdlet). When you specify the CmdletAttribute attribute, you must specify a verb and noun name pair, which will be used as the name of the cmdlet. This should describe what the cmdlet does and what sort of resource the cmdlet works with.

Note that the CmdletAttribute attribute itself is a .NET class. The properties of the class correspond to the parameters available when using the cmdlet.

The noun portion of the cmdlet name allows you to differentiate your custom cmdlet from other cmdlets. The noun part specifies the resources upon which the cmdlet acts. Ideally, the noun used in cmdlet naming should be very specific; if you've got a generic term, you should use that term as a suffix to your cmdlet. For example, if you create a get-server cmdlet for your SQL database, you should use "New-SQLDatabase" rather than "New-Database". The combination of specific noun and verb names makes it easy for the user to discover cmdlets quickly and anticipate functionality. Remember that these should be words that people will recognize, so you shouldn't use reserved punctuation (slashes, brackets, and so on) or wildcard characters in your cmdlet names.

Since I am creating cmdlets that work with Windows® IsolatedStorage, I'll use that as the basis for my noun. It may be a bit long, but with cmdlet names, the more specific, the better.

There are some pretty strong guidelines regarding what verbs should be used for verb names. Remember that consistency is important in Windows PowerShell, so be sure you use the appropriate verbs. By using one of the predefined verb names, you will improve consistency between your custom cmdlets, the included cmdlets, and cmdlets created by other users. For example, to retrieve data you use Get, rather than Retrieve or Acquire. Common verbs used in Windows PowerShell include: Add, Clear, Copy, Get, Join, Lock, Move, New, Remove, Rename, Select, Set, Split, and Unlock. You can tell what each is used for just from its name. In this article I'll create three cmdlets: one to set the data contents of the IsolatedStorage, one to retrieve the contents, and one to remove the IsolatedStorage file. Thus, I will create three cmdlets: Set-IsolatedStorageData, Get-IsolatedStorageData, and Remove-IsolatedStorageFile.

Cmdlet Class Definition

Now I need to create the code that will implement my cmdlets, starting with my Set-IsolatedStorageData cmdlet. First I declare the cmdlet class:

[Cmdlet(VerbsCommon.Set , "IsolatedStorage", SupportsShouldProcess=true)] public class SetIsolatedStorageCommand : PSCmdlet

Notice that I'm using Pascal casing and that the name of the class includes the verb and noun name for the cmdlet. Strictly speaking, this isn't necessary, but it makes my code much more readable.

The cmdlet attribution indicates that I'm using a common verb, in this case Set. If possible, you should always use common verbs when creating cmdlets. Setting SupportsShouldProcess to True indicates that the cmdlet supports calls to the ShouldProcess method, which provides the cmdlet the opportunity to prompt the user for verification before an action that changes the system is performed. If this attribute isn't present or is set to False (which is the default value), it would indicate that the cmdlet does not support calls to the ShouldProcess method.

All cmdlets that change resources outside of Windows PowerShell should set the SupportsShouldProcess property to true when declaring the CmdletAttribute attribute. This allows the cmdlet to call the ShouldProcess method before performing its action. If the ShouldProcess call returns false, the action will not be taken. (For more information about the confirmation requests generated by ShouldProcess call, see the MSDN® documentation at msdn2.microsoft.com/ bb204629.) The Confirm and WhatIf cmdlet parameters are available only for cmdlets that support ShouldProcess calls.

The last bit declares the class and uses PSCmdlet as the base class, which means that I'll get all the extended behaviors associated with a Windows PowerShell cmdlet.

Windows PowerShell supports cmdlets that are derived from two different base classes: PSCmdlet and Cmdlet. A cmdlet derived from PSCmdlet gives you access to the Windows PowerShell runtime. This enables calls to other scripts, and allows access to the Windows PowerShell providers for working with session state. PSCmdlet also provides access to the Windows PowerShell logging features, though this comes at the price of being a bit bigger and leaves you dependent upon the Windows PowerShell runtime.

Cmdlets derived from the Cmdlet class offer only the fewest dependencies on the Windows PowerShell runtime. This has some benefits—these cmdlets are a bit smaller since they have less functionality, and you're less likely to encounter problems due to changes in Windows PowerShell over time. In addition, these cmdlets can be easily included in other applications without the Windows PowerShell runtime.

If your plan is to create a cmdlet that will always be part of the Windows PowerShell environment, you should use PSCmdlet as your base class. However, if you think that your code will be used in more than just Windows PowerShell, you should use Cmdlet as a base class.

For my examples, I am deriving from PSCmdlet. When you are creating your cmdlet, you will need to reference System.Management.Automation.dll, which can be tricky to find since it's only in the Global Assembly Cache (GAC), but I've got a trick to help with that, which I'll share in just a moment.

Parameter Definition

Next I need to consider the parameters for my cmdlet. Parameters allow the user to provide input to the cmdlet.

Cmdlet parameter names should be consistent across the cmdlet design. The Windows PowerShell SDK has detailed suggestions on parameter names and how you should use them in your cmdlet to help ensure consistency with other cmdlets you may come across.

To declare parameters for a cmdlet, you must first define the properties that represent the parameters. To inform the Windows PowerShell runtime that a property is a cmdlet parameter, you add a ParameterAttribute attribute to the property definition.

Parameters must be explicitly marked as public; ones that are not marked as public default to internal and are not found by the Windows PowerShell runtime. This can lead to some confusion when you're trying to figure out why your cmdlet doesn't have the parameters you think it should have.

In my sample, I know that all my cmdlets will need a name that designates the actual name of the IsolatedStorage file. So here is the parameter declaration—a Name parameter:

private string _name = "PowerShellIsolatedStore"; /// <summary>name of store</summary> [Parameter] public string Name { get { return _name; } set { _name = value; } }

When you create a parameter, you should choose whether it is positional or named. With a positional parameter you don't need to provide the parameter name—just the value:

PS> cd c:\windows

By setting Position=num as part of the attribute, you designate what position is used for that parameter. If a parameter is not positional, you leave off the Position attribute and use the parameter name from the command line to provide a value.

The documentation recommends that you make frequently used parameters positional whenever possible. The only problem with this guidance is that if you have many parameters, it can be a bit much to remember. Of course, even when a parameter is positional, the parameter name can still be used from the command line.

Cmdlet parameters can be defined as mandatory, meaning that they must have a value assigned before the Windows PowerShell runtime will invoke the cmdlet. Alternatively, they can be defined as optional. By default, all parameters are defined as optional. To define an optional parameter, then, simply omit the Mandatory property in the attribute declaration. Since I'll be storing a key and a value in the isolated storage, I need to create parameters so I can gather those values (see Figure 1).

Figure 1 Parameters Needed to Gather Key and Value

private string _key = null; [Parameter( Mandatory=true, Position=1, ValueFromPipelineByPropertyName=true )] public string Key { get { return _key; } set { _key = value; } } private string _value = null; /// <summary>the value to store</summary> [Parameter( Mandatory=true, Position=2, ValueFromPipelineByPropertyName=true )] public string Value { get { return _value; } set { _value = value; } }

Cmdlet parameters can also have aliases. To tell Windows PowerShell that a parameter has an alias, you add an AliasAttribute attribute to the property definition. The basic syntax for declaring the attribute is [Alias("alias")]. In my example, I create an alias called Filename that applies to the Name parameter, like so:

private string _name = "PowerShellIsolatedStore"; /// <summary>name of store</summary> [Alias("Filename")] [Parameter] public string Name { get { return _name; } set { _name = value; } }

Common Parameters

Windows PowerShell reserves a few parameter names, referred to as Common parameters, which you can't use: WhatIf, Confirm, Verbose, Debug, ErrorAction, ErrorVariable, OutVariable, and OutBuffer. In addition, the following aliases for these parameter names are reserved: vb, db, ea, ev, ov, and ob.

ShouldProcess Parameters

Another group of parameters, the ShouldProcess parameters, are present only when the cmdlet specifies the SupportsShouldProcess keyword in its CmdletAttribute attribute.

When your cmdlet supports ShouldProcess, you have access to the following parameters at runtime: Confirm and WhatIf. Confirm specifies whether user confirmation is required before a cmdlet performs an action that modifies the system. True indicates that confirmation is required; false indicates that it's not required.

WhatIf specifies whether a cmdlet should inform the user what changes would have been made if the action were performed but without the action occurring. True indicates that the user is informed without the action occurring, False indicates that the action should occur.

When SupportsShouldProcess is specified, cmdlets that attempt to declare these parameters will fail when trying to register the cmdlet. You shouldn't define these parameters in your cmdlet directly. Instead, include SupportsShouldProcess when you use the [Cmdlet(...)] attribute.

Parameter Sets

Windows PowerShell uses the concept of parameter sets. This enables you to write a single cmdlet that exposes different sets of parameters to the user and returns different information based on the parameters specified by the user. For example, the Get-EventLog cmdlet (built into Windows PowerShell) returns different information when the user specifies the List or LogName parameter. When LogName is specified, the cmdlet returns information about the events in a given event log. However, when List is specified, the cmdlet returns information about the log files themselves (not the event information they contain). In this case, List and LogName identify two different parameter sets.

When multiple parameter sets are defined, the cmdlet can indicate which parameter set to use if Windows PowerShell doesn't have enough information to make that determination. The parameter set that is used in this case is referred to as the default parameter set, and is specified using the DefaultParameterSet keyword of the CmdletAttribute declaration. Note that I'm not using parameter sets in my sample cmdlets.

Method Overrides

The Cmdlet class provides virtual methods, shown in Figure 2, that can be used to process records. One or more of these methods must be overridden by all derived cmdlet classes.

Figure 2 Virtual Methods

Method Purpose
BeginProcessing Provides optional one-time, preprocessing functionality for the cmdlet.
ProcessRecord Provides record-by-record processing functionality for the cmdlet. It may be called any number of times or not at all, depending on the input of the cmdlet.
EndProcessing Provides optional one-time, post-processing functionality for the cmdlet.
StopProcessing Stops processing when the user stops the cmdlet asynchronously, such as by entering the key combination Ctrl+C.

Since my cmdlets deal with files, I'll use the BeginProcessing method to implement the code used to open the IsolatedStorage files, as shown in Figure 3. There are a couple of things worth noting. First, I'm opening the files in create mode, which is a system change, so I should wrap that code in a ShouldProcess block. That means I will have an opportunity to tell the user what I'm going to do before I actually do it. (This is done when the user uses either Confirm or WhatIf arguments.) Also notice that I am going to catch exceptions and wrap them with ThrowTerminatingError. In this sample, if anything goes wrong, it shouldn't proceed because the file open will have failed. It's usually bad form to catch all exceptions, but since I'm wrapping them in ThrowTerminatingError, I'll be able to provide a bit more information in case of failure.

Figure 3 Using the BeginProcessing Method

protected override void BeginProcessing() { try { if ( ShouldProcess( Name )) { WriteVerbose("Opening Isolated Storage: " + Name); isoStore = this.GetMyStore(); fs = new IsolatedStorageFileStream( Name, FileMode.OpenOrCreate|FileMode.Append, FileAccess.Write, isoStore ); sw = new StreamWriter(fs); WriteDebug("Stream encoding: " + sw.Encoding); } } catch ( Exception e ) { this.closeStreams(); ThrowTerminatingError( new ErrorRecord( e, "OpenIsolatedStorage", ErrorCategory.NotSpecified, Name ) ); } }

In my implementation, I'll use ProcessRecord to do the work of creating new entries for data in isolated storage (see Figure 4). Note the use of the try/catch statements, in order to add more information in case an error occurs. In this case I'm using WriteError instead of ThrowTerminatingError; this is because I don't need to stop the pipeline in case of a bad write.

Figure 4 Using ProcessRecord in Set-IsolatedStorageData

protected override void ProcessRecord() { try { // Remember ShouldProcess may not have opened the file if(sw != null ) { WriteVerbose("Setting " + Key + " = " + Value); sw.WriteLine(Key + "=" + Value); } } catch ( Exception e ) { WriteError( new ErrorRecord( e, "SetIsolatedStorageValue", ErrorCategory.NotSpecified, Name ) ); } }

Since I opened the isolated storage from within BeginProcessing, I'll use the EndProcessing method to close the files. I'm not reading or writing to the isolated storage file in Remove-IsolatedStorageFile; therefore, I'll use EndProcessing to remove the file in that cmdlet. Here's what the EndProcessing code looks like for the Get-IsolatedStorageData and Set-IsolatedStorageData cmdlets:

protected override void EndProcessing() { if (sw != null ) { sw.Close(); } if (fs != null ) { fs.Close(); } if (isoStore != null ) { isoStore.Close(); } }

The code in Remove-IsolatedStorageFile is a bit trickier. In this cmdlet, I delete the file itself using the appropriate methods from the IsolatedStorage object:

if(ShouldProcess("Remove Isolated Storage")) { WriteVerbose("Deleting Isolated Storage: " + Name); isoStore = this.GetMyStore(); isoStore.DeleteFile(Name); }

Notice that I'm using ShouldProcess again. Since I'm making a change to the system, I need to notify the user of what I'm about to do, should they want this information.

Emitting Results

Windows PowerShell is all about results, but you have to find a balance in the way you provide those results. The balance involves making sure you return as much information as you can without causing too much impact on the performance (such as by using too much memory, taking too long to execute, and so on). Since I'll be saving text to a file, I could just return the text all by itself. While this would be OK, I want to demonstrate something better, so I will provide the key and the value.

When I was first learning about IsolatedStorage, I noticed that it's pretty tough to find the actual file being used for the storage, so I want to include that information in my results. This will make the results more useful. By returning the key, value, and the path to the data, my object looks like this:

public class IsolatedStorageData { public string Key; // The Key public string Value; // The Value public string FullName; // The path to the storage }

I also have to consider what sort of string the object's ToString method should return. By default, most .NET objects return just the name of the type—this isn't very useful. So I'll have the ToString method return the Value rather than the name of the type.

In order to get the value of the actual filename for the FullName member, I need to do a little reflection because this information isn't surfaced as part of the IsolatedStorage information. So the implementation of the object that I'll use for the results will look like what's shown in Figure 5.

Figure 5 Object Used for the Results

public class IsolatedStorageData { public string Key; // The Key public string Value; // The Value public string FullName; // The path to the storage public override string ToString() { return Value; } public IsolatedStorageData( string _key, string _value, IsolatedStorageFileStream _fs ) { Key = _key; Value = _value; FullName = _fs.GetType() . GetField("m_FullPath", BindingFlags.Instance|BindingFlags.NonPublic ) . GetValue(_fs).ToString(); } }

Again, remember that in order to provide consistency across all cmdlets, Windows PowerShell cmdlets return objects into a pipeline rather than text to a stream. WriteObject is what the cmdlet uses to emit the results. After retrieving the data from the IsolatedStorage file, I convert that to an instance of an IsolatedStorageData type and use WriteObject to emit those results into the pipeline.

Reporting Error Conditions

When running code, there will be times when things just don't do what you want and some sort of error occurs. Windows PowerShell has some fairly sophisticated behaviors for these circumstances and its error-reporting capabilities allow for very fine-grained control over what happens.

Sometimes when an error occurs, it's not catastrophic. For example, if you want to remove thousands of files in a directory, failing to remove one or two of those files won't invalidate all the other file deletions. These are non-terminating errors—that is, it's still an error, but it's not an error that will result in you having to stop whatever you're doing. You can continue to remove the files that are removable.

However, there are operations that you can't recover from. Suppose you need to create a temporary file to hold on to some data that you'll use later. If you can't create and use the temporary file, there's no point in proceeding with the rest of the operation because the data you'll need won't be available. This qualifies as a TerminatingError. In Windows PowerShell, two different cmdlet methods—WriteError and ThrowTerminatingError—allow you to make this distinction.

WriteError is used whenever there's some sort of exceptional circumstance in the execution of your cmdlet that isn't fatal to the overall operation of the cmdlet. The method takes as an argument an instance of an ErrorRecord, which allows you to include more than just the exception (the cause of the error).

You really shouldn't throw an exception in a cmdlet. Instead, ThrowTerminatingError allows you to stop the execution of the pipeline and provide much more information than you could with an exception.

You'll notice in the code for my cmdlets that I use ThrowTerminatingError in BeginProcessing but then use WriteError in the ProcessRecord methods. This is because if I can't get access to the IsolatedStorage, there's not much I'll be able to do with regard to the operation of the cmdlet in general. However, if I encounter an error while reading the file, I may be able to recover and continue.

Troubleshooting Messages

Windows PowerShell provides a number of ways to communicate with the user. There are three methods you can use to notify the user when you want to communicate something that isn't a result or an error. You should know that when you use these methods, you can't redirect the messages that will be sent, but you can suppress them by setting some preferences in the shell. These methods communicate directly with the hosting application, in this case PowerShell.exe, and in turn write to the console window. Preference variables offer a variety of behaviors, from not writing anything to asking whether the message should be written before continuing.

I use these in my sample cmdlets a little more than you might because I want to show how they can help. If you have complicated code that needs more than just error or results, be sure to use these available methods. The last thing you want to do is use something like System.Console.WriteLine from a cmdlet. First, it's pretty bad practice and second, you shouldn't depend on the hosting application since the console may not even be present.

You should use WriteVerbose if you have extra out-of-band information you want to pass to the user. This isn't for developer messages; it's for letting your user know what's going on under the covers. The interface is pretty simple. Here's an example from my Remove-IsolatedStorageFile, which uses WriteVerbose to output that it's about to remove the IsolatedStorage file:

if(ShouldProcess("Remove Isolated Storage")) { WriteVerbose("Deleting Isolated Storage: " + Name); isoStore = this.GetMyStore(); isoStore.DeleteFile(Name); }

You use WriteDebug when you want to communicate with developers, giving them a chance to troubleshoot errant behavior in your cmdlet. Typically, you would attach a debugger to the system, but you can't always do that in the field. WriteDebug will help in these situations.

WriteWarning is yet another way to communicate with the user, providing information about the execution of your cmdlet. (I haven't used this method in my sample cmdlets.) A good example of using WriteWarning is if you want to change the behavior of a cmdlet over time. You can use WriteWarning, for instance, if you were to deprecate a parameter and wanted to tell the user to stop using it in favor of a new parameter.

Cmdlet Groups

All three of my cmdlets pertain to IsolatedStorage, so the nouns all have the same root: IsolatedStorage. Two of the cmdlets are for working with the actual data (Set-IsolatedStorageData and Get-IsolatedStorageData) while the other removes the file (Remove-IsolatedStorageFile).

Since all of these cmdlets have a parameter in common (the name of the actual file that will be used), I don't have to implement the same parameter in all of the cmdlets. Instead, I can create a base class, IsolatedStorageBase, that derives from PSCmdlet, and the cmdlets will derive from IsolatedStorageBase (see Figure 6). This is a good way to ensure that you are consistent from cmdlet to cmdlet.

Figure 6 IsolatedStorageBase Base Class

public class IsolatedStorageBase : PSCmdlet { [Parameter] public string Name ... } [Cmdlet(VerbsCommon.Set, "IsolatedStorageData",SupportsShouldProcess = true)] public class SetIsolatedStorageDataCommand: IsolatedStorageBase { ... } [Cmdlet(VerbsCommon.Get, "IsolatedStorageData")] public class GetIsolatedStorageDataCommand: IsolatedStorageBase { ... } [Cmdlet(VerbsCommon.Remove,"IsolatedStorageFile",SupportsShouldProcess = true)] public class RemoveIsolatedStorageFileCommand: IsolatedStorageBase { ... }

Creating a Snap-In

In order to use these new cmdlets, you need to add them to the Windows PowerShell environment. Windows PowerShell has the ability to dynamically add cmdlets to the session via a snap-in. To avoid potential confusion with MMC snap-ins, a Windows PowerShell snap-in is called a PSSnapIn.

To create a PSSnapIn, you need to write a bit of code that will perform two tasks. First, it provides identification for your snap-in so it can be distinguished from other snap-ins installed on your system. Second, it provides information for properly installing the snap-in and for creating the appropriate registry entries to allow Windows PowerShell to find the assembly.

There are two types of Windows PowerShell snap-ins in the System.Management.Automation namespace: PSSnapIn and CustomPSSnapIn. You should use PSSnapIn when you want to register all the cmdlets and providers automatically in one assembly. CustomPSSnapIn should be used when you want to register a subset of the cmdlets and providers in one assembly or when you want to register cmdlets and providers that are in different assemblies. The code for my snap-in is shown in Figure 7. It's quite straightforward. I simply override the appropriate members and then I'm pretty much finished.

Figure 7 Snap-In for My Custom Cmdlets

// This class defines the properties of a snapin [RunInstaller(true)] public class IsolatedStorageCmdlets : PSSnapIn { /// <summary>Creates an instance of DemoSnapin class.</summary> public IsolatedStorageCmdlets() : base() { } ///<summary>The snap-in name that is used for registration</summary> public override string Name { get { return "IsolatedStorageCmdlets"; } } /// <summary>Gets vendor of the snap-in.</summary> public override string Vendor { get { return "James W. Truher"; } } /// <summary>Gets description of the snap-in. </summary> public override string Description { get { return "Isolated Storage Cmdlets"; } } /// <summary>The format file for the snap-in. </summary> private string[] _formats = { "IsolatedStorage.Format.ps1xml" }; public override string[] Formats { get { return _formats ; } } }

Formatting

Notice in Figure 7 that there is a value for the Formats member of this snap-in. This allows you to create formatting directives for the objects that the cmdlets emit. In this case, I have a simple object, but I don't want to present all the members by default—I want to be sure that I print only the information that is useful, which in this case is the key and the value.

The creation of these format files could fill an article by itself, and the topic is beyond the scope of this article. Figure 8 provides sample code that creates a table format with the two columns I'm interested in: the key and value of the IsolatedStorageData object. For the purposes of this example, you can just create a file named IsolatedStorage.Format.ps1xml in the same directory as the assembly and when the snap-in is loaded, you shouldn't see any errors.

Figure 8 Sample Format File

<Configuration> <ViewDefinitions> <View> <Name>IsolatedStorage</Name> <ViewSelectedBy> <TypeName>IsolatedStorageSnapin.IsolatedStorageData</TypeName> </ViewSelectedBy> <TableControl> <TableHeaders> <TableColumnHeader> <Label>Key</Label> <Width>12</Width> </TableColumnHeader> <TableColumnHeader /> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>Key</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Value</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions> </Configuration>

Installing and Loading a PSSnapIn

Installing a snap-in is extremely easy. Just run Installutil.exe with the path to your assembly. When this utility runs, it creates some registry entries under HKLM\SOFTWARE\Microsoft\PowerShell\1\PowerShellSnapins\<snapinname>. When Windows PowerShell loads a snap-in, these entries are used to load the assembly and find the various configuration files. It is also worth mentioning that Installutil.exe is the recommended installation method during development only, as the utility's actions don't properly register their uninstallation dependencies. For installation in production environments, you should configure the registry entries directly.

Loading the snap-in is also easy. The main cmdlets you will use are Add-PSSnapIn, Remove-PSSnapIn, and Get-PSSnapIn. Not surprisingly, add-PSSnapIn adds one or more Windows PowerShell snap-ins to the current session. Remove-PSSnapIn removes snap-ins from the current session. And Get-PSSnapIn retrieves the Windows PowerShell snap-ins on the computer.

Note that Remove-PSSnapIn doesn't actually unload the assembly. It just removes the cmdlets and providers from the lists that Windows PowerShell uses to find cmdlets and access providers.

Putting It All Together

Once I do everything I've just described, I am ready to start using my new custom cmdlets. Of course, I am doing everything from the command line. First, I must compile the cmdlet and snap-in code. Remember when I said I'll need to compile this code with a reference to System.Management.Automation.dll? This DLL can be found both in the SDK and in the GAC, but if you don't have the SDK installed, don't worry about it. This little script can easily create the Snap-In assembly. The first thing that I need to do is create an alias for the C# compiler, once that is established I find the location of the System.Management.Automation.dll and compile the assembly:

New-Alias csc "${V2Framework}\csc.exe" $SMADLL = [PSObject].Assembly.Location csc /target:library IsolatedStorageSnapin.cs /r:$SMADLL

Now that I have compiled my assembly, I can use Installutil.exe to register my new snap-in, as shown in Figure 9. After the installation has been completed successfully, I can find the new PSSnapIn:

Figure 9 Registering the Snap-In

Figure 9** Registering the Snap-In **(Click the image for a larger view)

PS> get-PSSnapIn -reg iso* Name : IsolatedStorageCmdlets PSVersion : 1.0 Description : Isolated Storage Cmdlets

Then I add the snap-in to my session:

PS> add-PSSnapIn IsolatedStorageCmdlets

And now I can use the cmdlets, as shown in Figure 10!

Figure 10 Using the Cmdlets

Figure 10** Using the Cmdlets **(Click the image for a larger view)

Jim Truher is currently a Program Manager in Microsoft Research Incubation working on the Response Point product. Before moving to Microsoft Research, James was a Program Manager on the Windows PowerShell team and created the Windows PowerShell scripting language with Bruce Payette.