PSCX Developer's Guide
The goal is to provide the PSCX user with a snapin that looks and feels like PowerShell cmdlets. We also want to avoid polluting the user's environment with unnecessary temp variables.
Try to limit the number of global scope variables that you create. In fact, the only global variable created by PowerShell should be $Pscx. Any exceptions should be posted for comment in the discussions group. Under $Pscx there are two hashtables:
- Session - for global session variables
- Preferences - for preference variables
Preferences are for CMDLETs that pull default values from a Preferernce variable in the absence of a parameter. Session global variables are for all the little global state variable that you need to persist between invocations of your command e.g. $Pscx.Session.Cd#ForwardStack
Note the naming convention for session objects. To help identify which command (script, function, cmdlet) the session variable is associated with, please prefix the property name with this information. For instance, ForwardStack is need by the CD function that PSCX installs. So the session variable name would be "CD#ForwardStack".
Periodically run PowerShell and examine the global variable space after running your functions and/or scripts. Make sure that your functions and scripts do not pollute the gloal scope. When this is necessary please conform to the following naming style:
Variables that don't follow naming convention
Any other "approved" global variables created from PSCX that don't follow the PSCX naming convention should be created using the Set-Variable cmdlet as shown in the following example in order to associate the variable with PSCX: Set-Variable Shell (new-object -com Shell.Application) -Scope global -Option ReadOnly -Description "PSCX variable"
In order to find all aliases created by PSCX, we give each alias a description that starts with "PSCX" e.g.: Set-Alias cvxml Convert-Xml -Option AllScope -Description "PSCX cmdlet alias"
This makes it very easy to find all the PSCX created aliases by executing:
How to be a nice PSCX cmdlet
We have a few base classes which handle most of the repeating common tasks a cmdlet can perform. The most basic class, which all commands should derive from, is called Pscx.Commands.PscxCmdlet
. From this, we have derived some higher level Cmdlets you may find useful:
- PscxPathCommandBase - base for commands that act upon paths, that may or may not contain wildcard characters. Contains Path and LiteralPath parameters.
- PscxEncodingCommandBase - derived from PscxPathCommandBase, adds an Encoding parameter. We recommend to derive from PscxInputObjectPathCommandBase, which provides much more functionality, if that makes sense for your command.
- PscxInputObjectPathCommandBase - base class for commands that can work both on files and objects. Your implementation should only register its methods using RegisterInputType<T> method, and possibly override InputSettings property.
has some useful features for working with Paths and Providers built-in. We have a specific class called PscxPathInfo
which is similiar to System.Management.Automation.PathInfo
except that it can represent all sorts of paths: literal, resolvable and even invalid paths. We use two attributes ProviderConstraintAttribute
in conjunction with each other to constrain our cmdlets to particular providers (or particular interfaces that a provider might implement, like IContentProvider), and to automatically create PscxPathInfo instances representing path strings passed to cmdlet parameters. You can view how this works by examing this sample PscxCmdlet: Export-Registry Cmdlet
You should try to use the built-in PowerShell verbs, defined in VerbsCommon, VerbsCommunications, VerbsData, VerbsDiagnostic, VerbsLifecycle, VerbsOther and VerbsSecurity. If you are not sure, create a work item or discussion thread, so we all can choose the best name.
If the non-standard verb is the only one that makes sense in that particular domain, place it in Pscx.PscxVerbs class. Similarily, any noun you use should be a constant in the Pscx.PscxNouns class.
If you are going to read or write from/to a file, you should call one of the FileHandler.ProcessRead, FileHandler.ProcessWrite or FileHandler.ProcessText
methods, and provide them with a delegate that will perform the task. These methods will ensure correct error handling.
We have a nice little structure EncodingParameter
which should be used when you want an Encoding parameter and can't derive from PscxEncodingCommandBase. See its usage in PscxEncodingCommandBase.
If you need to handle a non-terminating error, use one of the ErrorHandler.WriteXXXError methods. If there is no method that would suit your error condition, and there is the slightest chance your ErrorRecord might be reused, add a method to the IPscxErrorHandler that will create the error record and call either WriteError or ThrowTerminatingError.
If a rare situation, you might want to create the error record somewhere else than in the write method. In that case, put it in the PscxErrorRecord class, and call that method from IPscxErrorHandler.
You should not throw any exceptions (using the throw keyword), unless the error is caused by a bug. User input errors, external error conditions, etc. should be handled using the ErrorHandler.WriteXXX and ErrorHandler.ThrowXXX only.
There are a number of preference variables that are used by various PSCX cmdlets. If you need to create one of these please conform to the following naming convention: $Pscx<CmdletNameorAbbrev><DescriptiveName
For example: $PscxSmtpHostPreference
To retrieve the parameter / preference variable, use PscxCmdlet.GetPreferenceVariable
Cmdlet Unit Testing
NUnit Installation and Setup
To get started with being able to run the Pscx_UnitTests follow these steps:
- Install NUnit from $/PowerShellCX/Trunk/Tools/NUnit
- Set the Pscx_UnitTests project to be the startup project (right-click on project node in the Solution Explorer and select "Set as StartUp Project".
- Open Pscx_UnitTests Properties (right-click on project node in the Solution Explorer and select Properties)
- Select the Debug tab in the Pscx_UnitTest Properties window.
- Set the Start Action to be "Start exteral program:" and use the path to the nunit-gui.exe which is typically something like: C:\Program Files\NUnit-Net-2.0 2.2.9\bin\nunit-gui.exe
- In Start Options set the "Command line arguments to "Pscx_UnitTests.dll".
Creating new unit tests
Most new tests should derive from PscxCmdletTest which takes care of the Runspace setup (adds the snapin) and creates a pipeline for use in your tests. Right now the runspace gets created and destroyed after every test. We'll try it this way and see how the performance is. It seemed like a good way to start to minimize interaction between tests.
The project is organized roughly like so:
- /Branches - Release and private developer branches go here
- /Trunk - Most development should be on the trunk
- /Imports - External dependencies go here
- /PscxCore - Shared code that isn't Pscx specific.
- /PscxSnapin - The PSCX snapin project (duh)
- /Commands - Cmdlet source goes here
- /Clipboard - Cmdlets that target the clipboard go here
- /DirectoryServices - Cmdlets related to active directory go here
- /IO - Cmdlets that target dirs and files go here
- /Net - Network-related cmdlets go here
- /Text - Put related cmdlets together in a subdir
- /Xml - Cmdlets that operate on Xml go here
- /Providers - Provider source goes here
- /Profile- Profile/environment related scripts go here
- /PscxHelp - Project used to generate help files
- /PscxSetup - The intaller project
The directory structure reflects the namespace structure. Always put the object types your commands
produce into the same namespace as the command. The root namespace is reserved for classes
shared by all code.
File Comment Headers
// Authors: <your name>
// Description: <short description>
// Creation Date: <creation date>
// Modified Date: <modified date>: <your name>: <your changes>
Source code line length - 120
Please do not bother wrapping your source code until you hit column 120. Most of us have wide monitors these days so this shouldn't result in horizontal scrolling to see code.
Place using directives outside of the namespace, and sort them alphabetically
Place all fields at the top of the class declaration for easy reference. Always specify explicitly their accessibility e.g. private. For all non-public fields, prepend their name with an underscore for easier identification of fields. Follow the underscore with a camelCase name describing the intent of the field e.g. _paths. Avoid using public and protected fields. Use properties instead.
All safe PInvoke functions and structures should be placed in the NativeMethods.cs file. Those which require pointer manipulation should be placed in the UnsafeNativeMethods.cs. However the preferred way is to use IntPtrs, GCHandles and Marshal API when possible.
If you only have a few properties and fields then avoid using regions for fields and/or parameters. Most cmdlets are simple enough that having these areas of code in a region doesn't add much value but use your judgment. If you have a large amount of properties and/or fields then using a region can add value to the readability of the code.
Please put each custom attribute in its own brackets for easy copying and pasting.
The cmdlet help is generated using the Update-PscxHelp script in PscxHelp\Scripts directory, which merges xml files in Pscx\Help with the information obtained from assembly metadata. The following attributes are used:
Here's how the help is generated in versions 1.1 and later:
The script generates the MAML file and the about_Pscx.help.txt file. You need to checkout the files first before running the script.
How to obtain and use the TFS client
If you just want to obtain our source code, grab it from the Source Code
tab. However, if you are a new member of the development team, you'll need some bits and pieces:Obtaining the Team Explorer client
There is some great info for VSTS Source Control (e.g. Team Foundation) at the VSTSGuidance