PowerShell Script Module Design: Building Tools to Automate the Process

As I previously mentioned a little over a month ago in my blog article “PowerShell Script Module Design Philosophy“, I’m transitioning my module build process to a non-monolithic design in development and a monolithic design for production to take advantage of the best of both worlds. Be sure to read the previously referenced blog article for more details on the subject.

My goal is to write a reusable tool to retrieve the necessary information from a non-monolithic script module that’s necessary to create a monolithic version of the same module. Initially, I wrote some horrendous code to get the necessary information and finally decided that I needed to learn about the Abstract Syntax Tree (AST). Be sure to read the blog articles I’ve written about the AST over the past few weeks:

While there are multiple ways of retrieving the necessary information, the easiest is to simply use the PowerShell Abstract Syntax Tree (AST) otherwise this process could be more convoluted than trying to put Humpty Dumpty back together again.

First, I’ll create a helper function to get all of the AST types. This function is based off of some of the code I wrote in part 3 of my AST blog article series. Normally, I don’t recommend sorting your output from within a function because it slows down receiving the output, but in this case it was an easy way to make sure the returned values are unique.

Although I’ve shown the results in the previous set of results, the Get-MrAstType function will remain private in the MrModuleBuildTools module. That function is used by my Get-MrAst function. While I could have added those results to a ValidateSet, it would have been a long list of static comma separated values. I decided to use a dynamic parameter to create a dynamic validate set based on the results of Get-MrAstType.

I’ll break the Get-MrAst function down into smaller pieces and elaborate a little about each one. The following section specifies the required version of PowerShell and the function declaration.

The next part is the function is its comment-based help. Although there’s a push by some to move to Microsoft Assistance Markup Language (MAML) based help, in my opinion, the documentation needs to reside as close as possible to the code otherwise there’s a greater chance it won’t be used or updated and that it could easily become separated if someone just grabs one function instead of the entire module. My recommendation is that if you need multilingual support for your help, use MAML, otherwise use comment-based help. I know, some would argue that MAML also gives you updatable help, but with the PowerShell Gallery, it’s just as easy to release a new minor version of a module to correct bugs in its help. If you’re going to use MAML, definitely check out PlatyPS.

CmdletBinding is used to turn the function into an advanced function. The default parameter set is defined within CmdletBinding.

The function has three parameters, each of which are in a different parameter set. One or more Path(s) is accepted via parameter input or via the pipeline. Pipeline input is accepted by both value (type) and property name for this parameter. If no parameters or values are specified, it defaults to all of the PS1 and PSM1 files in the current location. One thing that you may be wondering about is that all of the parameters are set to “ValueFromRemainingArguments“. That’s a trick I picked up from Stack Overflow where static and dynamic parameters only seem to honor positional binding for parameters of the same type and this was the only way to make it bind the dynamic parameter first.

The next parameter, which is in a different parameter set, is the Code parameter. It accepts arbitrary code as its input. I originally set the Code parameter to ValidateNotNullOrEmpty instead of Mandatory. When I would retrieve the code from a function with Get-Content, it would generate an error when set to Mandatory. Although changing the parameter to ValidateNotNullOrEmpty made it run without error, the format was off for the results of the AST. The solution (from Rob Campbell on Stack Overflow) was to use the Raw parameter when retrieving the content of a function with Get-Content.

The final standard parameter is ScriptBlock. As you could have probably guessed, accepts one or more script blocks. It exists in a separate parameter set from the two parameters.

As previously mentioned, a dynamic parameter is used to create a dynamic validate set as this isn’t possible with the ValidateSet parameter validation attribute. I found two blog articles on the Hey, Scripting Scripting Guy! Blog that helped me figure out how to use parameter validation with a dynamic parameter. “Dynamic ValidateSet in a Dynamic Parameter” and “Creating a function or script with PowerShell Dynamic Parameters“.

The Begin block is simply used to assign whatever value is provided for the AstType parameter to a variable. It’s used later to narrow down the results if that particular parameter is specified.

Last, but not least, there’s the Process block. The parameter set name that’s being used is determined via a switch statement. $PSCmdlet.ParameterSetName is used instead of $PSBoundParameters because the later does not work when using pipeline input, only when parameter input is used.

One of two different .NET static methods is used depending on whether or not the Path or Code parameter set is being used. See the Microsoft documentation for the Parser Class for more information.

If the Path parameter set is used, Get-ChildItem retrieves the FullName for each item (the specified files and/or paths). Then it runs those though a foreach loop and assigns the results from all of them to a variable. The first $null variable is used for the tokens and the second one is for errors. I don’t care about those so I’m sending them directly to the bit bucket by using the $null variable. This is one scenario where if you used actual variable names for those items, you’d need to initialize them first before using them otherwise an error would be generated.

The process is similar, but simpler if the Code parameter set is used.

The process is even simpler if the ScriptBlock parameter set is used. That’s because beginning with PowerShell version 3.0, the AST is already available from a script block via a property named “AST”.

I also added a default value so the code always has an execution path. It also let’s me know that my code’s not working properly (if it reaches that point) which has been known to happen from time to time. And finally, the switch statement ends with the closing curly brace.

If the AstType parameter is specified, then the results are narrowed down to only the type specified.

The results are outputted, the Process block is closed, and the function ends.

The Get-MrAst function is shown in its entirety in the following example.

Now, let’s take a look at the default output from the Get-MrAst function.

Determine the PowerShell version and the modules (if any) specified via the requires statement in the function is simple and there’s no need to try to parse the file with a complicated regular expression.

Specifying the AstType makes it easy to retrieve the name of the function.

As are the contents of the function itself.  Notice that this allows you to retrieve the function without the requires statement being included since I’ll want to strip that information out when combining the functions in a PSM1 file.

Those should be all of the items that I’ll need to put the pieces back together.

The development version of the functions shown in this blog article can be downloaded from my ModuleBuildTools repository on GitHub.

µ

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: