PowerShell Script Module Design: Plaster Template for Creating Modules

I recently began updating my PowerShell script module build process. Updating my Plaster template was one of the first things I needed to do. If you haven't already read my blog article about Using Plaster to create a PowerShell Script Module template, I'd recommend beginning there as this blog article assumes you already have a basic understanding of how to use Plaster.

All of the information from my previous Plaster template is still there with the exception of the required PowerShell version as I plan to obtain that information and update it a different way.

The first addition to my template that you'll notice is the option to create both public and private folders within the module folder. By default, both of them are created.

1<parameter name="Folders" type="multichoice" default="0,1" prompt="Select folders to create">
2    <choice label="&amp;public" value="public" help="Adds a public folder to module root"/>
3    <choice label="&amp;private" value="private" help="Adds a private folder to module root"/>

The code shown in the previous example has a bug in it. See the update at the bottom of this blog article for the corrected code.

When I create a module (for development), I want to create a root folder as a Git repo and then the module's root folder as a subfolder within that folder. I've added this ability to my Plaster template along with the option of not having the top level Git folder.

1<parameter name="Git" type="choice" default="0" prompt="Create Git top level folder?">
2     <choice label="&amp;Yes" value="Yes" help="Create a top level folder for Git with module folder nested in it."/>
3     <choice label="&amp;No" value="No" help="No Git top level folder. Module folder will be the root."/>

If the option to create the Git folder is selected (which is the default), then a conditional parameter asks for the name for the Git repo.

1<parameter condition="$PLASTER_PARAM_Git -eq 'Yes'" name="GitRepoName" type="text" prompt="Git repo name for this module? " default="${PLASTER_PARAM_Name}"/>

I ran into a problem with Plaster where paths for conditional parameters were validated even when the condition evaluated to false. As of this writing, Plaster version 1.1.3 is the newest version in the PowerShell Gallery and has this problem. Version 1.1.4 is available from GitHub and this problem is resolved in it. See this issue on GitHub for more details about the problem.

The final parameter is a multi-selection one and it's also conditional based on whether or not creating a Git folder is selected.

1<parameter condition="$PLASTER_PARAM_Git -eq 'Yes'" name="Options" type="multichoice" prompt="Select one or more of the following options:" default="0,1,2,3" store="text">
2     <choice label="Add &amp;MIT License" help="Adds MIT LICENSE file" value="License"/>
3     <choice label="Add &amp;Readme.md file" help="Adds Readme.md file" value="Readme"/>
4     <choice label="Add &amp;Git .gitignore file" help="Adds a .gitignore file." value="GitIgnore"/>
5     <choice label="Add G&amp;it .gitattributes file" help="Adds a .gitattributes file." value="GitAttributes"/>
6     <choice label="&amp;None" help="No options specified." value="None"/>

The default selection adds an MIT license, a Readme.md file, and both .gitignore and .gitattributes files.

While those parameters are nice, they don't do anything except ask questions. They need to be wired up to accomplish the desired tasks. The content section is where that happens.

 2    <message>Creating folder structure</message>
 3    <file condition="$PLASTER_PARAM_Folders -contains 'public' -and $PLASTER_PARAM_Git -eq 'Yes'" source="" destination="${PLASTER_PARAM_GitRepoName}\${PLASTER_PARAM_Name}\public"/>
 4    <file condition="$PLASTER_PARAM_Folders -contains 'private' -and $PLASTER_PARAM_Git -eq 'Yes'" source="" destination="${PLASTER_PARAM_GitRepoName}\${PLASTER_PARAM_Name}\private"/>
 5    <file condition="$PLASTER_PARAM_Folders -contains 'public' -and $PLASTER_PARAM_Git -eq 'No'" source="" destination="${PLASTER_PARAM_Name}\public"/>
 6    <file condition="$PLASTER_PARAM_Folders -contains 'private' -and $PLASTER_PARAM_Git -eq 'No'" source="" destination="${PLASTER_PARAM_Name}\private"/>
 7    <message>Deploying common files</message>
 8    <file condition="$PLASTER_PARAM_Git -eq 'Yes'" source="module.psm1" destination="${PLASTER_PARAM_GitRepoName}\${PLASTER_PARAM_Name}\${PLASTER_PARAM_Name}.psm1"/>
 9    <file condition="$PLASTER_PARAM_Git -eq 'No'" source="module.psm1" destination="${PLASTER_PARAM_Name}\${PLASTER_PARAM_Name}.psm1"/>
10    <message>Creating Module Manifest</message>
11    <newModuleManifest condition="$PLASTER_PARAM_Git -eq 'Yes'" destination="${PLASTER_PARAM_GitRepoName}\${PLASTER_PARAM_Name}\${PLASTER_PARAM_Name}.psd1" moduleVersion="$PLASTER_PARAM_Version" rootModule="${PLASTER_PARAM_Name}.psm1" author="$PLASTER_PARAM_Author" companyName="$PLASTER_PARAM_CompanyName" description="$PLASTER_PARAM_Description" encoding="UTF8-NoBOM"/>
12    <newModuleManifest condition="$PLASTER_PARAM_Git -eq 'No'" destination="${PLASTER_PARAM_Name}\${PLASTER_PARAM_Name}.psd1" moduleVersion="$PLASTER_PARAM_Version" rootModule="${PLASTER_PARAM_Name}.psm1" author="$PLASTER_PARAM_Author" companyName="$PLASTER_PARAM_CompanyName" description="$PLASTER_PARAM_Description" encoding="UTF8-NoBOM"/>
13    <message condition="$PLASTER_PARAM_Options -notcontains 'None'">Deploying optional components</message>
14    <templateFile condition="$PLASTER_PARAM_Options -contains 'License' -and $PLASTER_PARAM_Options -notcontains 'None'" source="LICENSE" destination="${PLASTER_PARAM_GitRepoName}\LICENSE"/>
15    <templateFile condition="$PLASTER_PARAM_Options -contains 'Readme' -and $PLASTER_PARAM_Options -notcontains 'None'" source="xReadme.md" destination="${PLASTER_PARAM_GitRepoName}\Readme.md"/>
16    <file condition="$PLASTER_PARAM_Options -contains 'GitIgnore' -and $PLASTER_PARAM_Options -notcontains 'None'" source=".gitignore" destination="${PLASTER_PARAM_GitRepoName}\.gitignore"/>
17    <file condition="$PLASTER_PARAM_Options -contains 'GitAttributes' -and $PLASTER_PARAM_Options -notcontains 'None'" source=".gitattributes" destination="${PLASTER_PARAM_GitRepoName}\.gitattributes"/>

I'll use this template to create a new module named MrModuleBuildTools where I'll be placing the tools that I'll be creating to build PowerShell script modules.

 1$plasterParams = @{
 2    TemplatePath      = 'U:\GitHub\Plaster\Template'
 3    DestinationPath   = 'U:\GitHub'
 4    Name              = 'MrModuleBuildTools'
 5    Description       = 'PowerShell Script Module Building Toolkit'
 6    Version           = '1.0.0'
 7    Author            = 'Mike F. Robbins'
 8    CompanyName       = 'mikefrobbins.com'
 9    Folders           = 'public', 'private'
10    Git               = 'Yes'
11    GitRepoName       = 'ModuleBuildTools'
12    Options           = ('License', 'Readme', 'GitIgnore', 'GitAttributes')
14If (-not(Test-Path -Path $plasterParams.DestinationPath -PathType Container)) {
15    New-Item -Path $plasterParams.DestinationPath -ItemType Directory | Out-Null
17Invoke-Plaster @plasterParams


At this point, I created a repo on GitHub for my ModuleBuildTools. I created it as a private repo until I'm ready to make it public.


One thing I haven't figured out is if there's a way to create the repo on GitHub from the command line?

Finally, I initialize the local folder as a Git repo, add all of the files that exist so far, commit them to the local repo, add the repo on GitHub as a remote, and push the changes to the repo on GitHub.


The basic structure for the development version of my MrModuleBuildTools PowerShell script module is now complete.


The xReadme.md template file contains the following content.

1# <%=$PLASTER_PARAM_GitRepoName%>

You might be wondering why the template file isn't called Readme.md without the leading "x"? I didn't want its content to show up on GitHub because it's only meaningful to Plaster. Those variables are automatically replaced when the following line of the template is executed and the leading "x" is removed from the destination file during this process.

1<templateFile condition="$PLASTER_PARAM_Options -contains 'Readme' -and $PLASTER_PARAM_Options -notcontains 'None'" source="xReadme.md" destination="${PLASTER_PARAM_GitRepoName}\Readme.md"/>

This means the first line is replace by whatever is specified as the GitRepoName and the third line is replaced by the Description.

The information in the MIT license is similar so that the current year is always specified along with the name of the module author.

1Copyright (c) <%=(Get-Date).Year%> <%=$PLASTER_PARAM_Author %>

My complete Plaster template can be found in my Plaster repository on GitHub. I used SAPIEN PrimalXML to format the XML in my template.

Be sure to keep an eye on this blog site as I'll be adding tools to the MrModuleBuildTools module and blogging about them over the next few weeks.

Update - August 31, 2018

One of the huge benefits of open-sourcing your code and sharing it online is free code reviews! Tommy Maynard contacted me about a bug in the Public/Private folder creation process when my template is used manually with Invoke-Plaster. Which would your like? Select P or P as shown in the following example.


The letter for the hotkey is the character immediately after &amp so the solution is to simply shift it one (or more) place(s) to the right.

1<parameter name="Folders" type="multichoice" default="0,1" prompt="Select folders to create">
2    <choice label="&amp;Public" value="public" help="Adds a public folder to module root"/>
3    <choice label="P&amp;rivate" value="private" help="Adds a private folder to module root"/>


Thanks to Tommy for pointing out this bug! I've also made the corrections to the template in my Plaster repo on GitHub.