Creating PowerShell Help Files Dynamically

Posted on Posted in PowerShell Cmdlets, SharePoint 2010

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 downloads page. I thought about providing a separate download for the source but I’m lazy and really didn’t want to have to manage two different download packages; so, if you wish to download the source, just download the SharePoint 2010 source code and you’ll find the Lapointe.PowerShell.MamlGenerator project.

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:

Name Example

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\" | Remove-SPList -BackupDirectory \"c:\\backups\\mylist\"", 
    Remarks = "This example deletes the list mylist and creates a backup of the list in the c:\\backups\\mylist folder.")]

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:

using System.Collections.Generic;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using System.Management.Automation;
using Lapointe.SharePoint2010.Automation.Cmdlets.PipeBindObjects;
using Lapointe.PowerShell.MamlGenerator.Attributes;

namespace Lapointe.SharePoint2010.Automation.Cmdlets.Lists
{
    [Cmdlet(VerbsCommon.Get, "SPList", SupportsShouldProcess = false),
    SPCmdlet(RequireLocalFarmExist = true)]
    [CmdletDescription("Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.")]
    [RelatedCmdlets(typeof(SPCmdletDeleteList), typeof(SPCmdletCopyList), typeof(SPCmdletCopyListSecurity),
        typeof(SPCmdletExportListSecurity), ExternalCmdlets = new[] {"Get-SPWeb", "Start-SPAssignment", "Stop-SPAssignment"})]
    [Example(Code = "PS C:\\> $list = Get-SPList \"http://server_name/lists/mylist\"",
        Remarks = "This example retrieves the list at http://server_name/lists/mylist.")]
    public class SPCmdletGetList : SPGetCmdletBaseCustom<SPList>
    {
        #region Parameters

        [Parameter(Mandatory = false,
            ValueFromPipeline = true,
            Position = 0,
            ParameterSetName = "AllListsInIdentity")]
        public SPListPipeBind Identity { get; set; }

        [Parameter(Mandatory = false,
            ValueFromPipeline = false,
            ParameterSetName = "AllListsByType")]
        public SPBaseType ListType { get; set; }

        [Parameter(Mandatory = false,
            ValueFromPipeline = true,
            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.")]
        public SPWebPipeBind Web { get; set; }

        #endregion

        protected override IEnumerable<SPList> RetrieveDataObjects()
        {
            List<SPList> lists = new List<SPList>();
            SPWeb web = null;
            if (this.Web != null)
                web = this.Web.Read();

            if (Identity == null && ParameterSetName != "AllListsByType")
            {
                foreach (SPList list in web.Lists)
                    lists.Add(list);
            }
            else if (Identity == null && ParameterSetName == "AllListsByType")
            {
                foreach (SPList list in web.GetListsOfType(ListType))
                    lists.Add(list);
            }
            else
            {
                SPList list = this.Identity.Read(web);
                if (list != null)
                    lists.Add(list);
            }

            AssignmentCollection.Add(web);
            foreach (SPList list1 in lists)
            {
                AssignmentCollection.Add(list1.ParentWeb);
                AssignmentCollection.Add(list1.ParentWeb.Site);
            }

            return lists;
        }
    }
}

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):

<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/1" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10">
  <command:details>
    <command:name>Get-SPList</command:name>
    <maml:description>
      <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
    </maml:description>
    <maml:copyright>
      <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
      <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
      <maml:para>  &gt; http://blog.falchionconsulting.com/</maml:para>
      <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
      <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
    </maml:copyright>
    <command:verb>Get</command:verb>
    <command:noun>SPList</command:noun>
    <dev:version>1.0.0.0</dev:version>
  </command:details>
  <maml:description>
    <maml:para>Retrieve an SPList object by name or type. Use the AssignmentCollection parameter to ensure parent objects are properly disposed.</maml:para>
    <maml:para />
    <maml:para>Copyright 2010 Falchion Consulting, LLC</maml:para>
    <maml:para>  &gt; For more information on this cmdlet and others:</maml:para>
    <maml:para>  &gt; http://blog.falchionconsulting.com/</maml:para>
    <maml:para>  &gt; Use of this cmdlet is at your own risk.</maml:para>
    <maml:para>  &gt; Gary Lapointe assumes no liability.</maml:para>
  </maml:description>
  <command:syntax>
    <command:syntaxItem>
      <maml:name>Get-SPList</maml:name>
      <command:parameter required="false" position="1">
        <maml:name>Identity</maml:name>
        <command:parameterValue required="true">SPListPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>Web</maml:name>
        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>AssignmentCollection</maml:name>
        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
      </command:parameter>
    </command:syntaxItem>
    <command:syntaxItem>
      <maml:name>Get-SPList</maml:name>
      <command:parameter required="false" position="named">
        <maml:name>ListType</maml:name>
        <command:parameterValue required="true">GenericList | DocumentLibrary | Unused | DiscussionBoard | Survey | Issue | UnspecifiedBaseType</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>Web</maml:name>
        <command:parameterValue required="true">SPWebPipeBind</command:parameterValue>
      </command:parameter>
      <command:parameter required="false" position="named">
        <maml:name>AssignmentCollection</maml:name>
        <command:parameterValue required="true">SPAssignmentCollection</command:parameterValue>
      </command:parameter>
    </command:syntaxItem>
  </command:syntax>
  <command:parameters>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="1" variableLength="false">
      <maml:name>Identity</maml:name>
      <maml:description>
        <maml:para />
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPListPipeBind</command:parameterValue>
      <dev:type>
        <maml:name>SPListPipeBind</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="false" position="named" variableLength="false">
      <maml:name>ListType</maml:name>
      <maml:description>
        <maml:para />
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPBaseType</command:parameterValue>
      <dev:type>
        <maml:name>SPBaseType</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
      <maml:name>Web</maml:name>
      <maml:description>
        <maml:para>Specifies the URL or GUID of the Web containing the list to be retrieved.</maml:para>
        <maml:para />
        <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>
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPWebPipeBind</command:parameterValue>
      <dev:type>
        <maml:name>SPWebPipeBind</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
    <command:parameter required="false" globbing="false" pipelineInput="true (ByValue)" position="named" variableLength="false">
      <maml:name>AssignmentCollection</maml:name>
      <maml:description>
        <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>
        <maml:para />
        <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>
      </maml:description>
      <command:parameterValue required="false" variableLength="false">SPAssignmentCollection</command:parameterValue>
      <dev:type>
        <maml:name>SPAssignmentCollection</maml:name>
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:parameter>
  </command:parameters>
  <command:inputTypes>
    <command:inputType>
      <dev:type>
        <maml:name />
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:inputType>
  </command:inputTypes>
  <command:returnValues>
    <command:returnValue>
      <dev:type>
        <maml:name />
        <maml:uri />
        <maml:description>
          <maml:para />
        </maml:description>
      </dev:type>
    </command:returnValue>
  </command:returnValues>
  <command:terminatingErrors />
  <command:nonTerminatingErrors />
  <maml:alertSet>
    <maml:title />
    <maml:alert>
      <maml:para>For more information, type "Get-Help Get-SPList -detailed". For technical information, type "Get-Help Get-SPList -full".</maml:para>
    </maml:alert>
  </maml:alertSet>
  <command:examples>
    <command:example>
      <maml:title>------------------EXAMPLE------------------</maml:title>
      <dev:code>PS C:\&gt; $list = Get-SPList "http://server_name/lists/mylist"</dev:code>
      <dev:remarks>
        <maml:para>This example retrieves the list at http://server_name/lists/mylist.</maml:para>
      </dev:remarks>
    </command:example>
  </command:examples>
  <maml:relatedLinks>
    <maml:navigationLink>
      <maml:linkText>Remove-SPList</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Copy-SPList</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Copy-SPListSecurity</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Export-SPListSecurity</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Get-SPWeb</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Start-SPAssignment</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
    <maml:navigationLink>
      <maml:linkText>Stop-SPAssignment</maml:linkText>
      <maml:uri />
    </maml:navigationLink>
  </maml:relatedLinks>
</command:command>

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:

public static void GenerateHelp(string outputPath, bool oneFile);
public static void GenerateHelp(string inputFile, string outputPath, bool oneFile);
public 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:

$asm = "W:\Lapointe.SharePoint2010.Automation.dll"
$cmdletAsm = "W:\Lapointe.PowerShell.MamlGenerator.dll"
$targetPath = "W:\MyProject\POWERSHELL\Help"
[System.Reflection.Assembly]::LoadFrom($asm) | Out-Null
[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!

9 thoughts on “Creating PowerShell Help Files Dynamically

  1. Good tool! Saves a lot of work.
    A few things though:
    1. The example have switched the variables. $cmdletAsm should realy point to the asembly you would like to extract the attributes from.
    2. Seems like output file logic inserts a “Cmdlets” in the path.
    File.WriteAllText(Path.Combine(outputPath, string.Format(“{0}.Cmdlets.dll-help.xml”, asm.GetName().Name)), sb.ToString());
    /Mårten

    1. 1. Yup – minor typo – fixed it in the post – thanks for the heads-up
      2. Yeah, I need to remove that – it’s a remnant from how it was when it was baked directly into my cmdlet code (I wanted the “.Cmdlets.” in the file name). I’ll remove that from the code as soon as I get a chance.
      Thanks again for the feedback!

  2. Gary:

    It didn’t take too long for the tedium of doing it by hand to get to me, so I’m going to give this a shot.

    I’m also looking for one other thing I don’t think I’ve seen elsewhere – a way to build (even by hand if I have to) an overview for a group of cmdlets – so that someone who doesn’t know the names of the individual cmdlets can at least find out what they are by asking for help on a more obvious name.

    TIA
    Josh

  3. I found your MAML Help generator tool, attempted to use it, but am getting an error running as follows:

    PS C:\Users\Administrator> $genasm = “”
    PS C:\Users\Administrator> $cmdletasm = “”
    PS C:\Users\Administrator> $targetPath = “”
    PS C:\Users\Administrator> [System.Reflection.Assembly]::LoadFrom($genasm) | Out-Null
    PS C:\Users\Administrator> [Lapointe.PowerShell.MamlGenerator.CmdletHelpGenerator]::GenerateHelp($cmdletAsm, $targetPath, $true)
    Exception calling “GenerateHelp” with “3” argument(s): “The given key was not present in the dictionary.”
    At line:1 char:1
    + [Lapointe.PowerShell.MamlGenerator.CmdletHelpGenerator]::GenerateHelp($cmdletAsm …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : KeyNotFoundException

    I’ve annotated my cmdlets with minimal / empty attributes like:

    [CmdletDescription(“”)]
    [Example(Code = “”)]
    [RelatedCmdlets(typeof(InstallPerfCountersCmdlet))]

    and added the using statement so that Visual Studio builds my Solution with no errors

    What does the error message mean and is there anything else I should have done / I’ve missed
    Thanks for any help

    1. Hard to say without seeing the stack trace. After you get the error type “$error[0] | select *” (without the quotes) and you’ll see where the error was. Alternatively (and probably a lot easier as the stack trace may not help), you can download the source code from my site and attach the debugger to the console where you made the call – you should be able to easily see what’s going on from there.

  4. My previous post has resulted in the locations for:
    $genasm
    $cmdletasm
    $targetPath
    being set to the empty string

    I have valid values for each:
    $genasm points to the Lapointe.PowerShell.MamlGenerator.dll
    $cmdletasm points to the cmdlet dll for which I want to generate MAML help
    $targetPath points to the location where the help file is to be generated

Leave a Reply

Your email address will not be published. Required fields are marked *

CAPTCHA

*