It’s been a while since I’ve blogged anything useful due to all my writing efforts going to the book that I’ve been working on. However, I’ve recently wrapped up my last chapter for the book so I figured what better time to start focusing on my blog again.

The first thing I wanted to cover was something that I should have documented back in May of 2010 when I released my SharePoint 2010 cmdlets and it is something that I just finished referencing in my last book chapter: creating help files for custom cmdlets.

In the book I emphasize that creating these help files is not a trivial task and there are virtually no good tools available to help you with the effort. When I created my SharePoint 2007 cmdlets, I had first tried using the Cmdlet Help Editor v1.0 that the Windows PowerShell team released back in May of 2007; I quickly found that this wasn’t very practical for long term use for numerous reasons, particularly when looking at SharePoint cmdlets where I don’t have a snap-in that I can point the tool to. But the biggest issue I had with using this tool, or any other tool that required me to add descriptions manually, was that I just couldn’t keep up with the changes. I was making new cmdlets and extending existing ones so fast that it was taking me longer to update the help file than it was to create/update the cmdlet – that was just unacceptable to me.

As you may know, I’m all about automation; so I figured, how hard could it possibly be to automate the creation of the help file? All I needed to do was to decorate my cmdlets with some metadata that I could then use with a little reflection to spit out the required XML. I could then generate this XML on the Post Build event of my assembly so that the help file would be automatically updated just prior to the SharePoint Solution Package (WSP) being generated. So, back during the beta days of SharePoint 2010, while migrating my 2007 STSADM extensions to cmdlets, I decided to go ahead and implement my idea. I released it along with my SharePoint 2010 source code with little fanfare (not very many people are generating PowerShell cmdlets and even fewer are creating help files for them). However, with my book having a reference to this code I figured I would write a short post about it and, along the way, break the code out of the SharePoint project and into it’s own project, thereby making it useful for anyone who wishes to dynamically develop Windows PowerShell help files for their cmdlets.

Downloads You can download the .NET Assembly (Lapointe.PowerShell.MamlGenerator.dll) alone or the source code to the assembly from my git page.

So how does it work? It’s actually quite simple; I’ve created a series of custom Attribute classes which you can use to decorate your cmdlet class. I then use reflection to interrogate the Assembly for any classes that are based on PSCmdlet; for each class I look for these attributes. The rest is just simple XML generation. The Attributes are defined in the following table:

CmdletDescriptionAttribute: This Attribute is assigned to the class and contains the main, verbose description, as well as the synopsis for the cmdlet (or shorter description). You can only have one CmdletDescriptionAttribute assigned to the class.[CmdletDescription("Delete a list from a web site.")]
ExampleAttribute: This Attribute is assigned to the class and contains any example code and corresponding descriptions. You may assign multiple instances of the ExampleAttribute to the class (one for each example).`[Example(Code = “PS C:\> Get-SPList "http://server_name/lists/mylist"
RelatedCmdletsAttribute: This Attribute is assigned to the class and contains the listing of related cmdlets. The related cmdlets can be added using the cmdlet’s type or any applicable string representation of the cmdlet (appropriate for related cmdlets that are external to the current project). You can only have one RelatedCmdletsAttribute assigned to the class.[RelatedCmdlets(typeof(SPCmdletGetList), ExternalCmdlets = new[] {"Get-SPWeb"})]
SupportsWildcardsAttribute: This Attribute is assigned to the class and indicates whether the cmdlet supports wildcards. This Attribute is a marker only and contains no properties. You can only have one SupportsWildcardsAttribute assigned to the class.[SupportsWildcards]

These are all the custom Attributes that I’m using; the rest are are native to PowerShell and can be found in the System.Management.Automation namespace. Primarily I’m using CmdletAttribute and ParameterAttribute.

The example is taken from my SharePoint 2010 cmdlets and demonstrates how most of these Attributes are used:

 1using System.Collections.Generic;
 2using Microsoft.SharePoint;
 3using Microsoft.SharePoint.PowerShell;
 4using System.Management.Automation;
 5using Lapointe.SharePoint2010.Automation.Cmdlets.PipeBindObjects;
 6using Lapointe.PowerShell.MamlGenerator.Attributes;
 8namespace Lapointe.SharePoint2010.Automation.Cmdlets.Lists
10    [Cmdlet(VerbsCommon.Get, "SPList", SupportsShouldProcess = false),
11    SPCmdlet(RequireLocalFarmExist = true)]
12    [CmdletDescription("Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.")]
13    [RelatedCmdlets(typeof(SPCmdletDeleteList), typeof(SPCmdletCopyList), typeof(SPCmdletCopyListSecurity),
14        typeof(SPCmdletExportListSecurity), ExternalCmdlets = new[] {"Get-SPWeb", "Start-SPAssignment", "Stop-SPAssignment"})]
15    [Example(Code = "PS C:\\> $list = Get-SPList \"http://server_name/lists/mylist\"",
16        Remarks = "This example retrieves the list at http://server_name/lists/mylist.")]
17    public class SPCmdletGetList : SPGetCmdletBaseCustom<SPList>
18    {
19        #region Parameters
21        [Parameter(Mandatory = false,
22            ValueFromPipeline = true,
23            Position = 0,
24            ParameterSetName = "AllListsInIdentity")]
25        public SPListPipeBind Identity { get; set; }
27        [Parameter(Mandatory = false,
28            ValueFromPipeline = false,
29            ParameterSetName = "AllListsByType")]
30        public SPBaseType ListType { get; set; }
32        [Parameter(Mandatory = false,
33            ValueFromPipeline = true,
34            HelpMessage = "Specifies the URL or GUID of the Web containing the list to be retrieved.\r\n\r\nThe type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid name of Microsoft SharePoint Foundation 2010 Web site (for example, MySPSite1); or an instance of a valid SPWeb object.")]
35        public SPWebPipeBind Web { get; set; }
37        #endregion
39        protected override IEnumerable<SPList> RetrieveDataObjects()
40        {
41            List<SPList> lists = new List<SPList>();
42            SPWeb web = null;
43            if (this.Web != null)
44                web = this.Web.Read();
46            if (Identity == null && ParameterSetName != "AllListsByType")
47            {
48                foreach (SPList list in web.Lists)
49                    lists.Add(list);
50            }
51            else if (Identity == null && ParameterSetName == "AllListsByType")
52            {
53                foreach (SPList list in web.GetListsOfType(ListType))
54                    lists.Add(list);
55            }
56            else
57            {
58                SPList list = this.Identity.Read(web);
59                if (list != null)
60                    lists.Add(list);
61            }
63            AssignmentCollection.Add(web);
64            foreach (SPList list1 in lists)
65            {
66                AssignmentCollection.Add(list1.ParentWeb);
67                AssignmentCollection.Add(list1.ParentWeb.Site);
68            }
70            return lists;
71        }
72    }

With the cmdlet properly decorated I can make quick changes to my code and update the help documentation as I update the cmdlet. I don’t have to remember to go to another class file and remember this extremely cryptic MAML based XML format. This makes me considerably more efficient and it keeps my help files more up to date and inline with the actual cmdlet (I wish Microsoft would do something like this as much of the help for the SharePoint 2010 cmdlets is inaccurate, incomplete, or missing entirely).

The following shows a snippet of the help file that gets generated based on the attributes defined (I also use the Assembly’s CopyrightAttribute and DescriptionAttribute to add the copyright details that you see in the XML below):

  1<command:command xmlns:maml="" xmlns:dev="" xmlns:command="">
  2  <command:details>
  3    <command:name>Get-SPList</command:name>
  4    <maml:description>
  5      <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
  6    </maml:description>
  7    <maml:copyright>
  8      <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
  9      <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
 10      <maml:para>  &gt;</maml:para>
 11      <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
 12      <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
 13    </maml:copyright>
 14    <command:verb>Get</command:verb>
 15    <command:noun>SPList</command:noun>
 16    <dev:version></dev:version>
 17  </command:details>
 18  <maml:description>
 19    <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
 20    <maml:para />
 21    <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
 22    <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
 23    <maml:para>  &gt;</maml:para>
 24    <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
 25    <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
 26  </maml:description>
 27  <command:syntax>
 28    <command:syntaxItem>
 29      <maml:name>Get-SPList</maml:name>
 30      <command:parameter required="false" position="1">
 31        <maml:name>Identity</maml:name>
 32        <command:parameterValue required="true">SPListPipeBind</command:parameterValue>
 33      </command:parameter>
 34      <command:parameter required="false" position="named">
 35        <maml:name>Web</maml:name>
 36        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
 37      </command:parameter>
 38      <command:parameter required="false" position="named">
 39        <maml:name>AssignmentCollection</maml:name>
 40        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
 41      </command:parameter>
 42    </command:syntaxItem>
 43    <command:syntaxItem>
 44      <maml:name>Get-SPList</maml:name>
 45      <command:parameter required="false" position="named">
 46        <maml:name>ListType</maml:name>
 47        <command:parameterValue required="true">GenericList | DocumentLibrary | Unused | DiscussionBoard | Survey | Issue | UnspecifiedBaseType</command:parameterValue>
 48      </command:parameter>
 49      <command:parameter required="false" position="named">
 50        <maml:name>Web</maml:name>
 51        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
 52      </command:parameter>
 53      <command:parameter required="false" position="named">
 54        <maml:name>AssignmentCollection</maml:name>
 55        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
 56      </command:parameter>
 57    </command:syntaxItem>
 58  </command:syntax>
 59  <command:parameters>
 60    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="1" variableLength="false">
 61      <maml:name>Identity</maml:name>
 62      <maml:description>
 63        <maml:para />
 64      </maml:description>
 65      <command:parameterValue required="false" variableLength="false">SPListPipeBind</command:parameterValue>
 66      <dev:type>
 67        <maml:name>SPListPipeBind</maml:name>
 68        <maml:uri />
 69        <maml:description>
 70          <maml:para />
 71        </maml:description>
 72      </dev:type>
 73    </command:parameter>
 74    <command:parameter required="false" globbing="false" pipelineInput="false" position="named" variableLength="false">
 75      <maml:name>ListType</maml:name>
 76      <maml:description>
 77        <maml:para />
 78      </maml:description>
 79      <command:parameterValue required="false" variableLength="false">SPBaseType</command:parameterValue>
 80      <dev:type>
 81        <maml:name>SPBaseType</maml:name>
 82        <maml:uri />
 83        <maml:description>
 84          <maml:para />
 85        </maml:description>
 86      </dev:type>
 87    </command:parameter>
 88    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
 89      <maml:name>Web</maml:name>
 90      <maml:description>
 91        <maml:para>Specifies the URL or GUID of the Web containing the list to be retrieved.</maml:para>
 92        <maml:para />
 93        <maml:para>The type must be a valid GUID, in the form 12345678-90ab-cdef-1234-567890bcdefgh; a valid name of Microsoft SharePoint Foundation 2010 Web site (for example, MySPSite1); or an instance of a valid SPWeb object.</maml:para>
 94      </maml:description>
 95      <command:parameterValue required="false" variableLength="false">SPWebPipeBind</command:parameterValue>
 96      <dev:type>
 97        <maml:name>SPWebPipeBind</maml:name>
 98        <maml:uri />
 99        <maml:description>
100          <maml:para />
101        </maml:description>
102      </dev:type>
103    </command:parameter>
104    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
105      <maml:name>AssignmentCollection</maml:name>
106      <maml:description>
107        <maml:para>Manages objects for the purpose of proper disposal. Use of objects, such as SPWeb or SPSite, can use large amounts of memory and use of these objects in Windows PowerShell scripts requires proper memory management. Using the SPAssignment object, you can assign objects to a variable and dispose of the objects after they are needed to free up memory. When SPWeb, SPSite, or SPSiteAdministration objects are used, the objects are automatically disposed of if an assignment collection or the Global parameter is not used.</maml:para>
108        <maml:para />
109        <maml:para>When the Global parameter is used, all objects are contained in the global store. If objects are not immediately used, or disposed of by using the Stop-SPAssignment command, an out-of-memory scenario can occur.</maml:para>
110      </maml:description>
111      <command:parameterValue required="false" variableLength="false">SPAssignmentCollection</command:parameterValue>
112      <dev:type>
113        <maml:name>SPAssignmentCollection</maml:name>
114        <maml:uri />
115        <maml:description>
116          <maml:para />
117        </maml:description>
118      </dev:type>
119    </command:parameter>
120  </command:parameters>
121  <command:inputTypes>
122    <command:inputType>
123      <dev:type>
124        <maml:name />
125        <maml:uri />
126        <maml:description>
127          <maml:para />
128        </maml:description>
129      </dev:type>
130    </command:inputType>
131  </command:inputTypes>
132  <command:returnValues>
133    <command:returnValue>
134      <dev:type>
135        <maml:name />
136        <maml:uri />
137        <maml:description>
138          <maml:para />
139        </maml:description>
140      </dev:type>
141    </command:returnValue>
142  </command:returnValues>
143  <command:terminatingErrors />
144  <command:nonTerminatingErrors />
145  <maml:alertSet>
146    <maml:title />
147    <maml:alert>
148      <maml:para>For more information, type "Get-Help Get-SPList -detailed". For technical information, type "Get-Help Get-SPList -full".</maml:para>
149    </maml:alert>
150  </maml:alertSet>
151  <command:examples>
152    <command:example>
153      <maml:title>------------------EXAMPLE------------------</maml:title>
154      <dev:code>PS C:\&gt; $list = Get-SPList "http://server_name/lists/mylist"</dev:code>
155      <dev:remarks>
156        <maml:para>This example retrieves the list at http://server_name/lists/mylist.</maml:para>
157      </dev:remarks>
158    </command:example>
159  </command:examples>
160  <maml:relatedLinks>
161    <maml:navigationLink>
162      <maml:linkText>Remove-SPList</maml:linkText>
163      <maml:uri />
164    </maml:navigationLink>
165    <maml:navigationLink>
166      <maml:linkText>Copy-SPList</maml:linkText>
167      <maml:uri />
168    </maml:navigationLink>
169    <maml:navigationLink>
170      <maml:linkText>Copy-SPListSecurity</maml:linkText>
171      <maml:uri />
172    </maml:navigationLink>
173    <maml:navigationLink>
174      <maml:linkText>Export-SPListSecurity</maml:linkText>
175      <maml:uri />
176    </maml:navigationLink>
177    <maml:navigationLink>
178      <maml:linkText>Get-SPWeb</maml:linkText>
179      <maml:uri />
180    </maml:navigationLink>
181    <maml:navigationLink>
182      <maml:linkText>Start-SPAssignment</maml:linkText>
183      <maml:uri />
184    </maml:navigationLink>
185    <maml:navigationLink>
186      <maml:linkText>Stop-SPAssignment</maml:linkText>
187      <maml:uri />
188    </maml:navigationLink>
189  </maml:relatedLinks>

As you can see, there is a lot of XML that you would have to manually generate (with over 50 cmdlets at the time of writing this and close to a hundred additional planned, you can hopefully understand why I wouldn’t want to generate this manually).

So how do you call out to the assembly to generate the help file? Pretty easily! In the Lapointe.PowerShell.MamlGenerator Assembly there is a static class called CmdletHelpGenerator. This class has three static methods that you can call based on how you want to pass in the cmdlet’s Assembly information:

1public static void GenerateHelp(string outputPath, bool oneFile);
2public static void GenerateHelp(string inputFile, string outputPath, bool oneFile);
3public static void GenerateHelp(Assembly asm, string outputPath, bool oneFile);

If you want to run this code from PowerShell you can do so using the following syntax:

1$asm = "W:\Lapointe.SharePoint2010.Automation.dll"
2$cmdletAsm = "W:\Lapointe.PowerShell.MamlGenerator.dll"
3$targetPath = "W:\MyProject\POWERSHELL\Help"
4[System.Reflection.Assembly]::LoadFrom($asm) | Out-Null
5[Lapointe.PowerShell.MamlGenerator.CmdletHelpGenerator]::GenerateHelp($asm, $targetPath, $true)

And again, this works with any cmdlet development, not just SharePoint cmdlet development. Hopefully all you ambitious PowerShell developers out there who appreciate the need to provide your IT Administrators with proper help documentation will find this code as useful as I have.

Good luck and happy PowerShelling!