- Part 1: How to Document your PowerShell Library
- Part 2: Using C# to Create PowerShell Cmdlets: The Basics
- Part 3: Documenting Your PowerShell Binary Cmdlets
- Part 4: Unified Approach to Generating Documentation for PowerShell Cmdlets
Contents:
- Documentation: A Snapshot
- Getting Documentation out of your Cmdlets
- Components of Cmdlet Help
- Parameters
- Summary
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.)
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.)
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.)
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/// <summary> /// <para type="synopsis">This is the cmdlet synopsis.</para> /// <para type="description">This is part of the longer cmdlet description.</para> /// <para type="description">Also part of the longer cmdlet description.</para> /// </summary> [Cmdlet(VerbsCommon.Get, "Gizmo")] public class GetGizmoCmdlet : Cmdlet { /// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(Position = 0, Mandatory = true, ValueFromPipeline=true, HelpMessage = "Enter the flavor name, e.g. Marshmallow")] [Alias("Aroma","JeNeSaisQuoi")] public FlavorTypes Flavor { get; set; } /// <summary> /// <para type="description">The quantity of the gizmo.</para> /// </summary> [Parameter()] public int Quantity { get { return _quantity; } set { _quantity = value; } } private int _quantity = 42; protected override void ProcessRecord() { base.ProcessRecord(); } } public enum FlavorTypes { Chocolate, Vanilla, Marshmallow } |
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.)
1 2 3 4 5 6 |
PS> Import-Module .\CmdletSamples\bin\Debug\CmdletSamples.dll PS> Get-Command -Module CmdletSamples CommandType Name ModuleName ----------- ---- ---------- Cmdlet Get-Gizmo CmdletSamples |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
PS> Get-Help Get-Gizmo -full NAME Get-Gizmo SYNTAX Get-Gizmo [-Flavor] <FlavorTypes> {Chocolate | Vanilla | Marshmallow} [-Quantity <int>] [<CommonParameters>] PARAMETERS -Flavor <FlavorTypes> Enter the flavor name, e.g. Marshmallow Required? true Position? 0 Accept pipeline input? true (ByValue) Parameter set name (All) Aliases Aroma, JeNeSaisQuoi Dynamic? false -Quantity <int> Required? false Position? Named Accept pipeline input? false Parameter set name (All) Aliases None Dynamic? false <CommonParameters> This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). INPUTS System.String OUTPUTS System.Object ALIASES None |
Next let’s apply the documentation generator:
1 |
PS> C:\lib\XmlDoc2CmdletDoc.exe .\CmdletSamples\bin\Debug\CmdletSamples.dll |
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
PS> Import-Module .\CmdletSamples\bin\Debug\CmdletSamples.dll -force PS> Get-Help Get-Gizmo -full NAME Get-Gizmo SYNOPSIS This is the cmdlet synopsis. SYNTAX Get-Gizmo [-Flavor] <FlavorTypes> {Chocolate | Vanilla | Marshmallow} [-Quantity <int>] [<CommonParameters>] DESCRIPTION This is part of the longer cmdlet description. Also part of the longer cmdlet description. PARAMETERS -Flavor <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? false -Aroma <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow This is an alias of the Flavor parameter. Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? false -JeNeSaisQuoi <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow This is an alias of the Flavor parameter. Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? false -Quantity <int> The quantity of the gizmo. Required? false Position? named Default value 42 Accept pipeline input? false Accept wildcard characters? false <CommonParameters> This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer and OutVariable. For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). INPUTS CmdletSamples.FlavorTypes The flavor for the gizmo. OUTPUTS RELATED LINKS |
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:
Noun–Verb, where Noun and Verb are the arguments to the Cmdlet attribute attached to the cmdlet, highlighted here:
Example:
1 2 3 |
[Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
Synopsis
A brief summary of the cmdlet, typically just a single sentence.
Source:
The synopsis paragraph in the summary attached to the cmdlet.
Example:
1 2 3 4 5 6 7 8 |
/// <summary> /// <para type="synopsis">This is the cmdlet synopsis.</para> /// <para type="description">This is part of the longer cmdlet description.</para> /// <para type="description">Also part of the longer cmdlet description.</para> /// </summary> [Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
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:
1 2 3 4 5 6 7 8 |
/// <summary> /// <para type="synopsis">This is the cmdlet synopsis.</para> /// <para type="description">This is part of the longer cmdlet description.</para> /// <para type="description">Also part of the longer cmdlet description.</para> /// </summary> [Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
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:
1 2 3 4 5 6 7 8 9 |
/// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(Position = 0, Mandatory = true, ValueFromPipeline=true, HelpMessage = "Enter the flavor name, e.g. Marshmallow")] [Alias("Aroma","JeNeSaisQuoi")] public FlavorTypes Flavor { get; set; } |
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:
1 2 3 4 5 6 7 8 9 |
/// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(Position = 0, Mandatory = true, ValueFromPipeline=true, HelpMessage = "Enter the flavor name, e.g. Marshmallow")] [Alias("Aroma","JeNeSaisQuoi")] public FlavorTypes Flavor { get; set; } |
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:
1 2 3 |
[Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/// <list type="alertSet"> /// <item> /// <term>First Note</term> /// <description> /// This is the description for the first note. /// </description> /// </item> /// <item> /// <term>Second Note</term> /// <description> /// <para>This is part of the description for the second note.</para> /// <para>This is also part of the description for the second note.</para> /// </description> /// </item> /// </list> |
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:
1 2 3 4 5 6 7 |
/// <example> /// <para>This is part of the first example's introduction.</para> /// <para>This is also part of the first example's introduction.</para> /// <code>New-Thingy | Write-Host</code> /// <para>This is part of the first example's remarks.</para> /// <para>This is also part of the first example's remarks.</para> /// </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:
1 2 3 4 5 6 7 8 9 10 |
/// <summary> /// <para type="synopsis">This is the cmdlet synopsis.</para> /// <para type="description">This is part of the longer cmdlet description.</para> /// <para type="description">Also part of the longer cmdlet description.</para> /// </summary> /// <para type="link" uri="http://tempuri.org">My Web Page</para> /// <para type="link">about_Gizmos</para> [Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
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:
1 |
My Web Page http://tempuri.org |
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:
1 |
/// <para type="link" uri="(http://tempuri.org)">[My Web Page]</para> |
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):
1 |
[My Web Page](http://tempuri.org) |
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:
1 2 3 4 5 6 7 8 9 |
/// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(Position = 0, Mandatory = true, ValueFromPipeline=true, HelpMessage = "Enter the flavor name, e.g. Marshmallow")] [Alias("Aroma","JeNeSaisQuoi")] public FlavorTypes Flavor { get; set; } |
With no help file (i.e. without using XmlDoc2CmdletDoc
), this description stems from the HelpMessage property:
1 2 |
-Flavor <FlavorTypes> Enter the flavor name, e.g. Marshmallow |
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.
1 2 |
-Flavor <FlavorTypes> The flavor for the gizmo. |
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:
1 2 3 4 5 6 7 8 |
PS> Get-Gizmo cmdlet Get-Gizmo at command pipeline position 1 Supply values for the following parameters: (Type !? for Help.) Flavor: !? Enter the flavor name, e.g. Marshmallow Flavor: |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
PS> Get-Help Add-Computer -Param Options -Options <JoinOptions> Sets advanced options for the Add-Computer join operation. Enter one or more values in a comma-separated string. Valid values are: -- AccountCreate: Creates a domain account. The Add-Computer cmdlet automatically creates a domain account when it adds a computer to a domain. This option is included for completeness. -- Win9XUpgrade: Indicates that the join operation is part of a Windows operating system upgrade. -- UnsecuredJoin: Performs an unsecured join. To request an unsecured join, use the Unsecure parameter or this option. |
I thought we could do better, and so I tweaked XmlDoc2CmdletDoc
to auto-document the enumerated values. Thus, with just this code:
1 2 3 4 5 |
/// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(Position = 0, Mandatory = true, ...)] public FlavorTypes Flavor { get; set; } |
You would get this output – notice the Possible values line automatically inserted:
1 2 3 4 5 6 7 8 9 10 11 |
PARAMETERS -Flavor <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? False |
(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:
1 2 |
SYNTAX Get-Gizmo [-Flavor] <FlavorTypes> {Chocolate | Vanilla | Marshmallow} [-Quantity <int>] [<CommonParameters>] |
But standard .NET cmdlets do not do this!
1 2 3 4 5 |
PS> Get-Help Add-Computer SYNTAX Add-Computer [-DomainName] <String> [-ComputerName <String[]>] [-Force [<SwitchParameter>]] [-LocalCredential <PSCredential>] [-NewName <String>] [-Options <JoinOptions>] [-OUPath <String>] . . . |
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
.
1 2 |
[Alias("Aroma","JeNeSaisQuoi")] public FlavorTypes Flavor { get; set; } |
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
:
1 2 3 4 5 6 7 8 9 10 11 |
PS> Get-Help Get-ChildItem -Param file -File [<SwitchParameter>] Gets files. To get only files, use the File parameter and omit the Directory parameter. To exclude files, use the Directory parameter and omit the File parameter, or use the Attributes parameter. To get files, use the File parameter, its "af" alias, or the File value of the Attributes parameter. |
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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
-Flavor <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? false -Aroma <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow This is an alias of the Flavor parameter. Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? false -JeNeSaisQuoi <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow This is an alias of the Flavor parameter. Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? False |
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!
1 2 3 |
PS> Get-Help Get-Gizmo -Param Flavor PS> Get-Help Get-Gizmo -Param Aroma PS> Get-Help Get-Gizmo -Param JeNeSaisQuoi |
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:
1 2 3 |
function Get-Value($count = 42) { Write-Output $count } |
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:
1 2 3 4 5 |
public int Quantity { get { return _quantity; } set { _quantity = value; } } private int _quantity = 42; |
The default value manifests in the documentation thus:
1 2 3 4 5 6 7 8 |
-Quantity <int> The quantity of the gizmo. Required? false Position? named Default value 42 Accept pipeline input? false Accept wildcard characters? False |
But the story does not end there! Consider the Flavor parameter again-it lacks a backing field…
1 |
public FlavorTypes Flavor { get; set; } |
… and yet the documentation yields a default value:
1 2 3 4 5 6 7 8 9 10 |
-Flavor <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow Required? true Position? 0 Default value Chocolate Accept pipeline input? true (ByValue) Accept wildcard characters? False |
And if you add these other parameters, all without backing fields…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[Parameter()] public int SomeInteger { get; set; } [Parameter()] public SwitchParameter SomeSwitch { get; set; } [Parameter()] public string SomeString { get; set; } [Parameter()] public DateTime SomeDateTime { get; set; } [Parameter(ValueFromPipelineByPropertyName = true)] public Container MyContainer { get; set; } |
…here’s what the documentation yields (I’ve filtered the output to just show the defaults for each):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-SomeInteger <int> Default value 0 -SomeSwitch <SwitchParameter> Default value False -SomeString <string> Default value -SomeDateTime <DateTime> Default value 1/1/0001 12:00:00 AM -MyContainer <Container> Default value |
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:
1 2 3 4 5 6 |
public enum FlavorTypes { Chocolate, Vanilla, Marshmallow } |
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.
1 2 3 4 5 6 |
public enum FlavorTypes { Chocolate = 5, Vanilla, Marshmallow } |
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.
1 2 |
-Flavor <FlavorTypes> Default value 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class GetGizmoCmdlet : Cmdlet { /// <summary> /// <para type="description">The flavor for the gizmo.</para> /// </summary> [Parameter(ValueFromPipeline=true] public FlavorTypes Flavor { get; set; } [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter SomeSwitch { get; set; } /// <summary> /// <para type="inputType">Input description for parameter.</para> /// <para type="description">Main description for parameter.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public string SomeString { get; set; } /// <summary> /// <para type="inputType">Description for input only.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public DateTime SomeDateTime { get; set; } [Parameter(ValueFromPipelineByPropertyName = true)] public Container MyContainer { get; set; } } /// <summary> /// <para type="description">Container object.</para> /// </summary> public class Container { } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
PARAMETERS -Flavor <FlavorTypes> The flavor for the gizmo. Possible values: Chocolate, Vanilla, Marshmallow -SomeSwitch <SwitchParameter> -SomeString <string> Main description for parameter. -SomeDateTime <DateTime> -MyContainer <Container> INPUTS CmdletSamples.FlavorTypes The flavor for the gizmo. System.Management.Automation.SwitchParameter System.String Input description for parameter. System.DateTime Description for input only. CmdletSamples.Container Container object. |
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 theParameters
section. [Flavor
,SomeString
] - If a parameter lacks a
description
paragraph, that parameter has no descriptive text in theParameters
section. [SomeSwitch
,SomeDateTime
,MyContainer
] - If a parameter is decorated with an
inputType
paragraph, that text appears in theInputs
section. [SomeString
,SomeDateTime
] - If a parameter lacks an
inputType
paragraph but has adescription
Inputs
section. [Flavor
] - If a parameter lacks both an
inputType
and adescription
paragraph, then thedescription
paragraph of the parameter’s type appears in the Inputs section. [MyContainer
] - If a parameter lacks both an
inputType
and adescription
paragraph, and the parameter’s type lacks adescription
paragraph (either because it is a .NET type or a custom type lacking adescription
paragraph), then that parameter has no descriptive text in theInputs
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
:
1 2 |
OUTPUTS System.IO.DirectoryInfo, System.IO.FileInfo, System.String |
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:
1 2 3 |
[Cmdlet(VerbsCommon.Get, "Gizmo")] [OutputType(typeof(Gizmo))] public class GetGizmoCmdlet : Cmdlet |
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 type–Gizmo
in the example above:
1 2 3 4 |
/// <summary> /// <para type="description">Description of the Gizmo class.</para> /// </summary> public class Gizmo { . . . } |
Your generated Outputs
section will then list the return type you specified with OutputType
along with the description taken from that type itself:
1 2 3 4 |
OUTPUTS CmdletSamples.Gizmo Description of the Gizmo class. |
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 theParameter
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.
Load comments