Simple Talk is now part of the Redgate Community hub - find out why

Documenting Your PowerShell Binary Cmdlets

Whereas it is easy to provide inline documentation for a normal scripted
PowerShell cmdlet or function so as  to provide comprehensive help at the command-line or IDE, the same isn't true of binary cmdlets written in C#. At last, there is an open-source utility to assist with this that is being actively
maintained and updated. At last, binary cmdlets need no longer be the poor cousins of scripted cmdlets in their documentation

Contents:

You have developed a whiz-bang library of C#-based binary PowerShell cmdlets for your team/­department/­company (or perhaps the whole PowerShell-loving community) to use. And being a PowerShell power user, you know that all good cmdlets must have documentation. After all, you do not want to suffer the ire of your colleagues when they invoke Get-Help on Get-YourGreatCmdlet and they discover that there’s almost nothing there beyond a skeleton!

PowerShell script modules (i.e. those written in PowerShell itself), provide a range of opportunities for documentation from within embedded structured comments. However, help documentation for binary PowerShell modules written in C# is so much worse than for script modules. Although basic information is possible, as you’ll see shortly, it falls a long way short of best-practice for a script module. All you get is what PowerShell can derive from your DLL, which is essentially (a) the syntax of your cmdlet and (b) a few standard properties of each parameter. But just as vital are the parts you do not get, the prose that you write to explain your cmdlet and explain your parameters.

To provide anything extra, you need to provide a documentation file written in an XML dialect called Microsoft Assistance Markup Language (MAML) that sits next to your DLL. From that, PowerShell will automatically generate standard-style help text when you invoke Get-Help. The biggest catch is creating that MAML file! It is not easy-here is a sample. Not too long after PowerShell was introduced, along came the CmdletHelpEditor to help you do just that. However, unlike conventional C# where you embed doc-comments in your code-and can keep them properly up-to-date as the code evolves-this utility lets you build documentation components into a MAML file-completely separate from your code base. CmdletHelpEditor was supported up through 2011, and was then supplanted by PowerShell Cmdlet Help Editor. (Those name choices certainly made it easy to confuse the two!) But this newer utility suffered from the same major impediments: completely separate from the code, and not all that easy to use.

Then things got interesting. In 2014, Redgate Software-well-known for their database tools-was developing some PowerShell cmdlets of their own. Chris Lambrou was participating in that effort and, realizing there was no easy Sandcastle equivalent for PowerShell, spearheaded the effort to create one-and make it available to everyone as open source. This new utility, XmlDoc2CmdletDoc, generates the requisite MAML file for you using essentially standard C# doc-comments embedded directly in your code, just like documenting any other C# library. Now you can document PowerShell simply and easily, and keep it synchronized with the code.

The rest of this article gives you practical tips and guidelines on how to do just that.

Documentation: A Snapshot

To show you where we are headed, the images here give you a good overview. Presented first is generating documentation from traditional C# classes. Using documentation comments embedded in the code, Sandcastle and Sandcastle Help File Builder made it very straightforward to generate the documentation. (You can see a live example documenting my open source C# library here.)

2311-img8.jpg

A few years back I wrote an article explaining how best to document your C# libraries (Taming Sandcastle: A .NET Programmer’s Guide to Documenting Your Code). That task was made much easier because (a) .NET directly supports-and provides Intellisense-for documentation-comments, (b) Microsoft provided a great foundation for generating the documentation from the doc-comments in the form of Sandcastle, and (c) last but not least, Eric Woodruff provided Sandcastle Help File Builder, which made using Sandcastle easy.

Similarly, for scripted PowerShell cmdlets (i.e. those written in PowerShell itself), you could embed documentation comments in the code and here you do not even need to invoke a documentation generator. PowerShell does it automatically and on-the-fly when you invoke Get-Help. (You can also see a live example of this documenting my open source PowerShell library here.)

2311-img9.jpg

Now you have the analogous treatment for compiled PowerShell cmdlets (i.e. those written in C#). Using documentation comments embedded in the code, XmlDoc2CmdletDoc makes it very straightforward to generate the documentation. (The live example for this requires you to build the sample project, then run XmlDoc2CmdletDoc, and use Get-Help on the Get-Gizmo cmdlet.)

2311-imgA.jpg

Let’s explore just how to do the documentation for the compiled PowerShell cmdlets.

Getting Documentation out of your Cmdlets

You start, of course, with a Visual Studio project containing your cmdlets. This should be a project of type Class Library. Within your project in Visual Studio, you need to enable XML documentation creation (Go to ‘Project Properties‘ >> ‘build pane’ >> ‘Output’ and check the XML documentation file option) then compile your project. You will now find a corresponding XML file adjacent to your project DLL in your output directory. For example, attached to this article is a sample CmdletSamplesProject solution that generates CmdletSamples.dll and, with the above setting, CmdletSamples.XML. Note that this XML file is an intermediate file; it is not the requisite MAML file that PowerShell needs in order to generate documentation.

Here’s the code for our initial cmdlet, Get-Gizmo, that you will find in the sample project. To be a cmdlet you have to both derive from Cmdlet and decorate the class with the Cmdlet attribute, as you will see below. To give your cmdlet parameters, you need to create each as a public property and decorate each with the Parameter attribute. Here you will see two: Flavor, which is an auto-generated property, and Quantity, which has a backing field. The ProcessRecord method is where the actual work of your cmdlet gets done, but that is not germane to creating the documentation.

That cmdlet is compiled into the CmdletSamples.dll. In a PowerShell window import the DLL as a module. You can then use Get-Command to list its cmdlets. (Note that the red text is what you type.)

Before doing any documentation generation, let’s execute Get-Help and review the output. As shown below, it is quite basic; but only because it has little to go on. It knows the syntax of the cmdlet and the properties of its parameters from processing the DLL. Also, because of the HelpMessage attribute on the Flavor parameter, it can present a brief description of that parameter.

Next let’s apply the documentation generator:

In your output directory, besides the DLL and the XML files, you will now also find a CmdletSample.dll-Help.xml file that contains the help text in MAML format, which PowerShell can directly consume. So now look at the help we get from the cmdlet-you will need to re-import the module with the -Force parameter, though!

Compare this help text with the previous version without documentation generation, and you will see all the additional sections that now show up in the help.

(Note: Curiously there is one thing you lose, too: with no help file for the module, parameters document three properties-Parameter set name, Aliases, and Dynamic-that are absent when you add the help file! But those same three properties are also absent from standard/built-in cmdlets, so XmlDoc2CmdletDoc is just mirroring what the .NET framework did.)

Components of Cmdlet Help

Without further ado, here is a brief description of each of the sections of the help output for a PowerShell cmdlet.

Name

The name of the cmdlet; think of this as the title of the help page.

Source:

NounVerb, where Noun and Verb are the arguments to the Cmdlet attribute attached to the cmdlet, highlighted here:

Example:

Synopsis

A brief summary of the cmdlet, typically just a single sentence.

Source:

The synopsis paragraph in the summary attached to the cmdlet.

Example:

Syntax

This auto-generated section lays out all the possible parameters and parameter combinations that are valid for the cmdlet, using a common variant of the well-known Backus-Naur Form notation.

Source:

Derived from the parameters of your cmdlet.

Description

This is where you explain-in as much detail as appropriate-what your cmdlet does, how to use it, what to watch out for, and things to keep in mind when using it. In other words, everything your user needs to know to use your cmdlet effectively, except for the details you add to specific parameters and to specific examples.

Source:

The description paragraphs in the summary attached to the cmdlet.

Example:

Parameters

This section enumerates each of your parameters, documenting values for common properties (whether the parameter is required, what its default value is, etc.) and providing a description of each parameter, supplementing your cmdlet’s description.

Source:

The description paragraphs in the summary attached to a parameter, along with its Parameter and Alias attributes.

Example:

Inputs

This auto-generated section enumerates each type of object that may be piped to your cmdlet.

Source:

The description paragraph and parameter type for each parameter decorated with the ValueFromPipeline or ValueFromPipelineByPropertyName argument in its Parameter attribute.

Example:

Outputs

This auto-generated section enumerates each type of object that your cmdlet may return.

Source:

The argument to the OutputType attribute(s) attached to the cmdlet and the description paragraph of the referenced type.

Example:

Notes

Here you provide any ancillary information that may be relevant, just not worth cluttering up the main description with. Think of this as akin to footnotes.

Source:

A list of type alertSet in the doc-comments attached to the cmdlet. Each <item> constitutes one note. Within each <item>, the <term> is the title and the <description> is the content of the note. The <description> may be text only or it may contain one or more <para> elements.

Example:

Known Issues:

PowerShell does not layout multiple notes very well. To simulate multiple notes, it is better to use a single note (<item> element) with multiple paragraphs (<para> elements) and with no title (<term> element). (See source issue on GitHub: Multiple notes do not render as expected)

Examples

An important section but often given short shrift, this is where you show the world how to really use your cmdlet. You should include exactly what needs to be typed, describe what happens with the particular set of parameters you are using, and even show some sample output, when appropriate.

Source:

One or more <example> elements in the doc-comments attached to the cmdlet. Each example may contains one or more paragraphs (<para> elements preceding <code>) as introductory text, then the example invocation (<code> element), and one or more remarks (<para> elements following <code>).

Example:

Known Issues:

In principle, you can include introductory text as shown in the example; but in practice PowerShell does not render it particularly well. And in fact, when Chris investigated this reported issue, he noted that there were virtually no standard .NET cmdlets that even tried to include introductory paragraphs. It is better to describe your example in remarks following the <code> element. (See source issue on GitHub: No newline after an example’s introduction)

Related Links

A list of related topics, typically other cmdlet names (e.g. Get-ChildItem, New-Alias, etc.), other PowerShell help topic names (e.g. about_Providers), or ordinary URLs pointing to web pages.

Source:

Paragraphs of type link, which may optionally include a uri attribute for web pages. Convention is not to include a uri for standard PowerShell help topics, since those would be immediately accessible with another invocation of Get-Help.

Example:

Known Issues:

If you include a URI in your link, PowerShell renders it by simply juxtaposing it next to the text, so the output would appear as:

Call me particular, but that is not user-friendly! I urge you to include some textual demarcation in your uri attribute. I recommend using the standard markdown format for hyperlinks:

The link will then be rendered by Get-Help like this (and could potentially be made into a live link should you pass the help text through a post-processor that understands markdown):

Of course, this could easily be compensated for by XmlDoc2CmdletDoc so perhaps by the time you read this it may already be fixed-see issue #10: Render related web links to be more user friendly.

Parameters

So far, you learned the fundamentals of documenting your PowerShell binary cmdlets written in C#. I’ve explained the basics of what was needed: adding a synopsis paragraph and description paragraphs to each cmdlet class, and also adding description paragraphs to each parameter. I then described all the moving pieces needed to be able to thoroughly document your custom set of PowerShell cmdlets.

This section provides a further bit of sanding and polishing, if you will, to really make your documentation shine! Specifically you will learn the particulars about documenting the parameters of your cmdlets.

Parameter Description

The parameter description is text of arbitrary length describing a given parameter. Consider the Flavor parameter, repeated here:

With no help file (i.e. without using XmlDoc2CmdletDoc), this description stems from the HelpMessage property:

However, with a help file present, the description instead comes from the description paragraph within the <summary> element immediately preceding the parameter. Since the goal here is to generate just such a help file, you should utilize this <summary> element for your description.

The HelpMessage property is still useful, though. When a parameter is required but not supplied on the command-line, PowerShell prompts you to enter its value. If you are not sure what it expects for the value, you can ask for more information at that prompt. That’s where the HelpMessage appears:

Parameter Enumerated Types

When my colleagues and I at NextIT were working to rapidly integrate XmlDoc2CmdletDoc into our build process, I noticed that we had to manually document the possible values for an enum. And, if you look at standard .NET cmdlets that is how they do it as well-this is clearly manually written documentation:

I thought we could do better, and so I tweaked XmlDoc2CmdletDoc to auto-document the enumerated values. Thus, with just this code:

You would get this output – notice the Possible values line automatically inserted:

(Of course, if you need or want to explain those enum values, you will still need manually written comments, but in many cases the auto-generated text would be sufficient.)

But there’s more. Notice at the very beginning-before using XmlDoc2CmdletDoc-the Syntax section listed the possible enum values right there:

But standard .NET cmdlets do not do this!

And that’s also the way that XmlDoc2CmdletDoc worked when I found it. But I really liked the idea of listing the enum values in the Syntax section so I updated the code to do that. (Thus you will find support for both forms of auto-documenting enums merged into the latest version of XmlDoc2CmdletDoc!)

Parameter Alias

PowerShell lets you define one or more aliases both to cmdlets and to parameters of a cmdlet. The Flavor parameter, for example, defines two aliases. With the following code you could invoke Get-Gizmo -Flavor or Get-Gizmo -Aroma or Get-Gizmo -JeNeSaisQuoi.

Standard PowerShell documentation does little to document those aliases automatically. It is up to the developer who is writing the documentation comments to mention the alias as was done, for example, with the File parameter for Get-ChildItem and its alias af:

What if you knew the alias, -af, but did not know the actual parameter name, and wanted to get help? If you try Get-Help Get-ChildItem -Param af, PowerShell will simply report that it does not know of any such parameter. Fear not! XmlDoc2CmdletDoc makes this more convenient for you and your users. It essentially promotes each alias to be a first-class parameter with respect to the generated help. Excerpting the help shown earlier, you will see that both the Flavor parameter and its aliases are documented identically (with the one exception that each alias explicitly identifies itself as an alias).

You automatically get a second benefit as well. You can now ask for help on a parameter using either the actual parameter name or any of its aliases; each of these commands will yield the same help text!

Parameter Default Values

Often you want to define a default value for a given parameter. When writing a scripted cmdlet (i.e. a cmdlet written in PowerShell rather than C#), this is straightforward to do: just supply a default value in the function signature:

When writing regular C# methods, you could provide default values in a similar fashion, but not so when writing a compiled cmdlet (i.e. a cmdlet written in C#). Here, you typically create a class deriving from Cmdlet (or PSCmdlet) wherein each parameter is defined as a property, and the main processing of the cmdlet is done in the overridden ProcessRecord method (and its ilk). So the task, then, is how to provide a default value to a property in a way that the documentation generator will recognize it. It turns out to be actually quite straightforward once you know how: simply provide a backing field for a property that is initialized at compile time. You can see this done for the Quantity property in our cmdlet sample; here is the relevant portion of code:

The default value manifests in the documentation thus:

But the story does not end there! Consider the Flavor parameter again-it lacks a backing field…

… and yet the documentation yields a default value:

And if you add these other parameters, all without backing fields…

…here’s what the documentation yields (I’ve filtered the output to just show the defaults for each):

As you may have surmised, if you do not use a backing field to explicitly provide a default value then the compiler default is used: zero for an integer, false for a Boolean, empty string for a string, zero for a DateTime, and so forth. So why did the Flavor parameter (of type FlavorTypes) show a default of Chocolate? Hearken back to the enumerated FlavorType definition:

As it turns out, enumerated types map to the set of integers beginning with 0. Thus Chocolate is really equivalent to 0, but since it is an enumerated type, the documentation nicely displays its enumerated label. To expose this mechanism, force the FlavorTypes to start with something other than 0, e.g.

Then the default in the documentation would show an actual 0, because there is now no label in the enumerated type that corresponds to 0.

Final note: you cannot really know from the documentation what the default value is when it is blank: in the case of the string parameter above it is an empty string, but for the Container parameter above it is null!

Input Parameters

You are likely to be familiar with piping objects from one cmdlet to another. When you write your own cmdlets, you can make specific parameters receive pipeline input using either the ValueFromPipeline property (which accepts a value of the same type expected by the parameter), or the ValueFromPipelineByPropertyName property (which accepts a value from a property of the input object of the same name as the parameter). Here are the salient portions of the sample cmdlet showing these in use on several parameters. Note that some have documentation comments and some do not. Some of those doc-comments are of type description, and some of type inputType. Some of the parameters are built-in .NET objects but some are custom objects. I’ve also included the custom Container class definition for discussion as well.

Of course, all parameters, pipeline-able or not, are listed in the Parameters section of the documentation. But those that may receive input from the pipeline are also listed separately in the Inputs section. Here is what XmlDoc2CmdletDoc produces for those two sections, again compressed to show just the relevant portions.

Each of these five parameters is a different case. Study the code, comparing to the output, and you will discern the following:

  • If a parameter is decorated with a description paragraph, that text appears in the Parameters section. [Flavor, SomeString]
  • If a parameter lacks a description paragraph, that parameter has no descriptive text in the Parameters section. [SomeSwitch, SomeDateTime, MyContainer]
  • If a parameter is decorated with an inputType paragraph, that text appears in the Inputs section. [SomeString, SomeDateTime]
  • If a parameter lacks an inputType paragraph but has a description paragraph, that text appears in the Inputs section. [Flavor]
  • If a parameter lacks both an inputType and a description paragraph, then the description paragraph of the parameter’s type appears in the Inputs section. [MyContainer]
  • If a parameter lacks both an inputType and a description paragraph, and the parameter’s type lacks a description paragraph (either because it is a .NET type or a custom type lacking a description paragraph), then that parameter has no descriptive text in the Inputs section. [SomeSwitch]

Output Parameters

Just as the Inputs section documents those parameters that may be received via the pipeline, the Outputs section documents those parameters that may be transmitted on to the next cmdlet in the pipeline. Unique to outputs, though, the crucial point to know is that the compiler does not validate what you specify as being the actual return type! That is, the compiler validates that the argument to the OutputType attribute is a valid, known type. But it does not validate that your cmdlet is, in fact, returning objects of that type; and, if you think about it, that makes sense because a cmdlet does not have a method signature specifying its inputs and outputs. The inputs are simply the public properties, and the outputs are whatever objects are returned from the base.WriteObject() call in your cmdlet (deriving from either Cmdlet or PSCmdlet). And you are even at liberty to return objects of different types in successive calls to WriteObject. Get-ChildItem, for example returns FileInfo objects and DirectoryInfo objects in a single invocation. You will see this reflected in the Outputs section of Get-Help:

Thus, take care that your OutputType values are accurate as you maintain your code over time, and remember that OutputType exists solely for the benefit of editors (for Intellisense) and documentation generators.

As far as documentation usage, decorate the cmdlet class with the OutputType attribute, as you saw in the previous article:

But another important point here is that XmlDoc2CmdletDoc can give you enhanced output (compared to standard .NET cmdlets) if you attach doc-comments to your return typeGizmo in the example above:

Your generated Outputs section will then list the return type you specified with OutputType along with the description taken from that type itself:

That works well for your custom types. However, if your return type is a standard .NET class-which would not have the appropriate description paragraphs-then the help text will have only the type name with no description.

Summary

With the advent of XmlDoc2CmdletDoc, what used to be an arduous, tedious, and error-prone task of assembling documentation for PowerShell cmdlets has become much easier. XmlDoc2CmdletDoc brings the experience of documenting cmdlets much closer to that of both (a) documenting any other C# API, and (b) documenting scripted cmdlets (i.e. those written in PowerShell itself). In both of these cases you put the documentation comments right alongside the code, greatly reducing the maintenance hassle.

Furthermore, XmlDoc2CmdletDoc provides several enhancements to standard PowerShell documentation:

  • Each custom type in the Outputs section includes a description.
  • The Syntax section includes possible values for enumerated types*.
  • The Parameter section includes possible values for enumerated types*.
  • Aliases are documented automatically in the Parameters section*.
  • Aliases are treated as first-class parameters so you can ask for help on an alias*.
  • You can optionally use a different description for a parameter in the Inputs section as you have for the Parameter section.
  • Web links are automatically rendered in markdown format, for possible post-processing to live links. (This enhancement is pending, as noted earlier.)
  • You can require documentation of all elements by running with the -strict flag in your build process, then abort your build if the error code is non-zero.

* The starred items are tweaks to the code base that I introduced as my colleagues and I were giving XmlDoc2CmdletDoc a good workout.

This article provides all the details you need to effectively use XmlDoc2CmdletDoc. So what are you waiting for? Help your users help themselves by giving them thorough and complete documentation!

Update – April 2016

Since this article, I’ve created a wallchart putting both XmlDoc2Cmdlet and DocTreeGenerator in context, showing you how to do a complete documentation solution for your PowerShell work in both C# and PowerShell. Click here for more details.
2407-1-5728ddc3-433a-4b82-a6dc-84f5f450b

How you log in to Simple Talk has changed

We now use Redgate ID (RGID). If you already have an RGID, we’ll try to match it to your account. If not, we’ll create one for you and connect it.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue

Simple Talk now uses Redgate ID

If you already have a Redgate ID (RGID), sign in using your existing RGID credentials. If not, you can create one on the next screen.

This won’t sign you up to anything or add you to any mailing lists. You can see our full privacy policy here.

Continue