PowerShell Script Module Design: Don’t Use Asterisks (*) in your Module Manifest

Using asterisks (*) in your module manifest is a bad idea no matter how you look at it. First, your module will be slower because it will have to figure out what to export. More importantly, if you use a "#Requires -Modules" statement in your functions and they're in separate PS1 files, all of the specified module's commands will show as being part of your module.

I'll pick up where I left off in one of my previous blog articles PowerShell Script Module Design: Plaster Template for Creating Modules. I've created what I'm calling a development version of a script module using the Plaster template mentioned in that previously referenced blog article.

I still have the same PowerShell session open which has the following information stored in a hash table.

 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')
13}

The following example shows where asterisks are defined by default in a PowerShell module manifest that's created with New-ModuleManifest.

1Get-Content -Path "$ModulePath\$($plasterParams.Name).psd1" | Select-String -SimpleMatch '*'

no-asterisks1a.png

First, let's store the path to the script module in a variable to make this process easier.

1if ($plasterParams.Git) {
2    $ModulePath = "$($plasterParams.DestinationPath)\$($plasterParams.GitRepoName)\$($plasterParams.Name)"
3}
4else {
5    $ModulePath = "$($plasterParams.DestinationPath)\$($plasterParams.Name)"
6}

no-asterisks3a.png

Trying to set the values to an empty string or array results in an error.

1Update-ModuleManifest -Path "$ModulePath\$($plasterParams.Name).psd1" -FunctionsToExport '' -AliasesToExport '' -VariablesToExport '' -CmdletsToExport ''
2Update-ModuleManifest -Path "$ModulePath\$($plasterParams.Name).psd1" -FunctionsToExport @() -AliasesToExport @() -VariablesToExport @() -CmdletsToExport @()

no-asterisks5a.png

Placing an empty array inside of quotes seems to work fine.

1if (Test-Path -Path $ModulePath -PathType Container) {
2    Update-ModuleManifest -Path "$ModulePath\$($plasterParams.Name).psd1" -FunctionsToExport '@()' -AliasesToExport '@()' -VariablesToExport '@()' -CmdletsToExport '@()'
3}
4else {
5    Write-Warning -Message "'$ModulePath' path does not exist or is not valid!"
6}

no-asterisks4a.png

All of the ones with asterisks are now gone, but strangely enough the quotes themselves also end up as part of the value, although this seems to achieve the desired result.

1Get-Content -Path "$ModulePath\$($plasterParams.Name).psd1" | Select-String -SimpleMatch '*'
2Get-Content -Path "$ModulePath\$($plasterParams.Name).psd1" | Select-String -SimpleMatch '@()'

no-asterisks6a.png

I wrote a function a while back (Get-MrFunctionsToExport) that's part of my MrToolkit module that I'll use to update the FunctionsToExport section of the manifest. That function gets the list of a module's functions based on the file names of the individual PS1 files and it just so happens that I name my files the same as the function within them. There's a better way to do this and I'll be updating that function along with moving it to this new MrModuleBuildTools module, but at this point I have a chicken and egg problem (Which one came first?), because I'm building my module build tools that will be used to build my modules.

1Get-MrFunctionsToExport -Path $ModulePath\public\ -Simple

no-asterisks7a.png

Use the function shown in the previous example to update the FunctionsToExport section in the module manifest.

1Update-ModuleManifest -Path "$ModulePath\$($plasterParams.Name).psd1" -FunctionsToExport (Get-MrFunctionsToExport -Path $ModulePath\public -Simple)

no-asterisks8a.png

That did indeed update the FunctionsToExport section.

1Get-Content -Path "$ModulePath\$($plasterParams.Name).psd1"

no-asterisks9a.png

Be sure to read the comments listed in the module manifest.

Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.

One thing I've never quite understood about Microsoft products is they tell you not to set something a certain way and then they set it that way by default or they say the optimum setting is "X" and then they set that setting to "Y" by default. Why can't they just set it to the optimum setting by default? and if it shouldn't be set a certain way, then why set it that way by default?

I also have a function to test to make sure the functions listed in FunctionsToExport match the functions that I want exported (which will also be updated and moved).

1Test-MrFunctionsToExport -ManifestPath $ModulePath\$($plasterParams.Name).psd1

no-asterisks10a.png

Beware of Update-ModuleManifest because it does things you don't ask it to. In other words, always test anytime you make updates to the manifest because it updates things which can break your modules. It's like a mechanic who tightens one bolt and loosens three more or maybe like playing Russian roulette since it only breaks things sometimes depending on what else you're updating.

The functions listed in FunctionsToExport should only be the ones that you want made publicly available. Don't add private functions to FunctionsToExport. If you're using separate PS1 files and dot-sourcing them from the PSM1 file, then you do need to dot-source both the public and private functions. Here's what my PSM1 template file looks like that's part of my Plaster template.

1#Dot source all functions in all ps1 files located in the module's public and private folder
2Get-ChildItem -Path $PSScriptRoot\public\*.ps1, $PSScriptRoot\private\*.ps1 -Exclude *.tests.ps1, *profile.ps1 |
3ForEach-Object {
4    . $_.FullName
5}

I exclude tests and profiles just in case any of them happen to be saved in one of the folders. Tests shouldn't be in either one as they'll live in their own folder. Once upon a time, I had a problem where my profile was reloading because of re-importing a module which almost drove me crazy until I realized what was happening.

µ