Sharing PowerShell code internally with module development
It’s time for a blog about PowerShell. How exciting! In this first part, you are going to learn to create an awesome developer experience for newcomers to the project, how to scaffold your module and to perform static code analysis for the scripts.
From Monad to PowerShell
PowerShell has been there since the dawn of time. Not that dawn of time; in fact, PowerShell wasn’t even called PowerShell when it was first launched. It was originally called Monad when it was launched to the public. Microsoft later renamed it, and many versions of PowerShell have been launched since then, even becoming open-source from PowerShell 6.0 core.
When you first started out learning PowerShell, you probably typed in a bunch of cmdlets directly from the shell itself to perform an action. Later you would have learned that it also has an Integrated Scripting Environment, the so called PowerShell ISE. You would start by adding some error handling around the cmdlets and save it as .ps1 format, so you could run it later. Maybe you already shared those scripts with your end users by saving your scripts to a .psm1 format. In that way, they could directly load it into their shell by using the Import-Module cmdlet. Well, that’s where this blog series is going to be all about.
Let’s get started
But first, a small warning. You have read that there was quite a jump from cmdlets to a .psm1 module file. If this sounds unfamiliar to you, it might be hard to grasp the concepts that you’ll see in this blog. Nevertheless, for the eager learner it will definitely provide great insights and inspiration for the future PowerShell code that you will produce. If that’s the case, then let’s get started!
Optimizing developer experience for PowerShell module development with Visual Studio Code
Most IT pros use the PowerShell ISE to build their PowerShell scripts. By now, Microsoft dropped almost development for PowerShell ISE and recommends users switch to Visual Studio Code (also known as VS Code), since it’s a much more powerful, customizable and flexible free editor. This blog will use VS Code as its editor with useful extensions to customize your development experience when you write your PowerShell scripts. If you already have VS Code installed, great! If not, grab your free VS Code from the Microsoft site.
VS Code has a rich extensibility model that lets you install extensions to support your development workflow. One of those extensions, and you might already have guessed it, is the PowerShell extension. Some of the major features of this extension are:
- Syntax highlighting
- Code snippets
- IntelliSense
To install the extension, you can type the following in any command-line shell:
code --install-extension ms-vscode.powershell
If you’ve installed the extension and created a .ps1, you may have noticed that these features became available to you.
Did you say indentation?
Now that you have the PowerShell extension installed, let’s look at indentation. VS Code helps you quite a bit with indentation, and if you like, you can specify the spaces or tabs to use in your editor settings. Still, it might sometimes be difficult to see when you’re writing your script. One of the extensions that can help you out, is the indent-rainbow extension. Let’s see how it looks like by installing the extension:
code --install-extension oderwat.indent-rainbow
Now press Ctrl+Shift+P and type in Preferences: Open Workspace Settings (JSON) and paste in the following JSON code:
"indentRainbow.colors": [
"rgba(255,255,64,0.3)",
"rgba(127,255,127,0.3)",
"rgba(255,127,255,0.3)",
"rgba(79,236,236,0.3)"
]
Let’s see both differences with and without.


It might be a bit extreme with the coloring, but hey! It does the trick, not only for you, but maybe also for less-attentive newcomers, right?!
Charging Git version control with GitLens
When you are storing your precious scripts in Git version control, you can charge up your experience and see who changed what in a glance with the GitLens extension. Again, open the command-line shell, install the extension and see what happens in a working Git repository.
code --install-extension eamodio.gitlens
Let’s see the differences for this extension when it is enabled or disabled.


On the extension page, you can find many more features that GitLens provides.
Visualizing icons and To-Dos
Lastly, VSCode-icons and Todo Tree are both visualizing extensions to give a boost to the visualizing experience when working in VS Code. Run the following commands in the shell to install both extensions:
code --install-extension vscode-icons-team.vscode-icons
code --install-extension gruntfuggly.todo-tree
You can probably immediately see that the icons are changed when you have some file or folders available in your editor. The Todo Tree gives you an easy experience when you are writing code that might not be finished, and you added the famous TODO comment to directly go to the specific line. It even highlights when you are inside the file itself.

Now of course, you have installed those extensions on your own laptop or computer, but what about your newcomers? When you are working in a repository, it is possible to configure recommended extensions by opening the Extensions: Configure Recommended Extensions (Workspace Folder) from the command palette (Ctrl+Shift+P). Adding the following code in the extensions.json file, will help your newcomers when checking out the repository, to get the recommendations and install the extensions belonging to the workspace:
{
"recommendations": ["ms-vscode.powershell", "oderwat.indent-rainbow", "eamodio.gitlens", "vscode-icons-team.vscode-icons", "gruntfuggly.todo-tree"]
}
Scaffolding project files with Sampler
Scaffolding is building a common directory and file structure, just like creating a Visual Studio project where you have that /bin and /obj folder created for you by the editor. While scaffolding is fairly new for PowerShell module development, there have been some popular scaffolding modules available for PowerShell. One of these scaffolding modules is called Sampler. Let’s see that in action.
In your PowerShell terminal, you can install Sampler by using the Install-Module cmdlet:
Install-Module -Name 'Sampler' -Scope 'CurrentUser' -Repository PSGallery
Sampler has a bunch of cmdlets available in the module, but the most interesting one is the New-SampleModule cmdlet. Let’s create a module with minimal structure and automation by running the following code:
$newSampleModuleParameters = @{
DestinationPath = 'C:\source'
ModuleType = 'SimpleModule' # There are different module types you can give
ModuleName = 'PoweringUp'
ModuleAuthor = 'Gijs Reijn'
ModuleDescription = 'Powering up Powershell module development'
}
New-SampleModule @newSampleModuleParameters
If you’ve ran the cmdlet and opened the project in VS Code, you can see that a bunch of files have been created.

Let’s quickly cover the files and folders that have been created by Sampler:
- build.ps1: entry point to invoke tasks leveraging the Invoke-Build task runner
- build.yaml: defines several workflows that can be called when executing .\build.ps1 -Tasks <Workflow_or_task_Name>
- azure-pipelines.yml: predefined build and release pipeline for Azure DevOps
- CHANGELOG.md: changelog that contains changes for each version of the project
- GitVersion.yml: tool that generates Semantic Version numbering
- source: contains Public and Private folders to store your PowerShell functions
- tests: Pester tests that can be grouped to be executed
- output: consists out of required modules and the final produced PoweringUp module
To list the available tasks, you can run the ./build -Tasks ? in your PowerShell terminal. Before you build the module, let’s introduce a new function in the Public folder and Private folder to see the behavior what happens when you build the module. Both these scripts come from the old Technet Gallery, but are slightly modified.
- In the Public folder, create a file called Get-PendingReboot.ps1
- Paste the following code inside the file and save it
Function Get-PendingReboot {
[CmdletBinding()]
param(
[Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias("CN", "Computer")]
[String[]]$ComputerName = "$env:COMPUTERNAME"
)
Begin { }## End Begin Script Block
Process {
Foreach ($Computer in $ComputerName) {
Try {
## Setting pending values to false to cut down on the number of else statements
$CompPendRen, $PendFileRename, $Pending, $SCCM = $false, $false, $false, $false
## Setting CBSRebootPend to null since not all versions of Windows has this value
$CBSRebootPend = $null
## Querying WMI for build version
$WMI_OS = Get-CimInstance -Class Win32_OperatingSystem -Property BuildNumber, CSName -ComputerName $Computer -ErrorAction Stop
## Making registry connection to the local/remote computer
$HKLM = [UInt32] "0x80000002"
$WMI_Reg = [WMIClass] "\\$Computer\root\default:StdRegProv"
## If Vista/2008 & Above query the CBS Reg Key
If ([Int32]$WMI_OS.BuildNumber -ge 6001) {
$RegSubKeysCBS = $WMI_Reg.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\")
$CBSRebootPend = $RegSubKeysCBS.sNames -contains "RebootPending"
}
## Query WUAU from the registry
$RegWUAURebootReq = $WMI_Reg.EnumKey($HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\")
$WUAURebootReq = $RegWUAURebootReq.sNames -contains "RebootRequired"
## Query PendingFileRenameOperations from the registry
$RegSubKeySM = $WMI_Reg.GetMultiStringValue($HKLM, "SYSTEM\CurrentControlSet\Control\Session Manager\", "PendingFileRenameOperations")
$RegValuePFRO = $RegSubKeySM.sValue
## Query JoinDomain key from the registry - These keys are present if pending a reboot from a domain join operation
$Netlogon = $WMI_Reg.EnumKey($HKLM, "SYSTEM\CurrentControlSet\Services\Netlogon").sNames
$PendDomJoin = ($Netlogon -contains 'JoinDomain') -or ($Netlogon -contains 'AvoidSpnSet')
## Query ComputerName and ActiveComputerName from the registry
$ActCompNm = $WMI_Reg.GetStringValue($HKLM, "SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName\", "ComputerName")
$CompNm = $WMI_Reg.GetStringValue($HKLM, "SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName\", "ComputerName")
If (($ActCompNm -ne $CompNm) -or $PendDomJoin) {
$CompPendRen = $true
}
## If PendingFileRenameOperations has a value set $RegValuePFRO variable to $true
If ($RegValuePFRO) {
$PendFileRename = $true
}
## Determine SCCM 2012 Client Reboot Pending Status
## To avoid nested 'if' statements and unneeded WMI calls to determine if the CCM_ClientUtilities class exist, setting EA = 0
$CCMClientSDK = $null
$CCMSplat = @{
NameSpace = 'ROOT\ccm\ClientSDK'
Class = 'CCM_ClientUtilities'
Name = 'DetermineIfRebootPending'
ComputerName = $Computer
ErrorAction = 'Stop'
}
## Try CCMClientSDK
Try {
$CCMClientSDK = Invoke-CimMethod @CCMSplat
}
Catch [System.UnauthorizedAccessException] {
$CcmStatus = Get-Service -Name CcmExec -ComputerName $Computer -ErrorAction SilentlyContinue
If ($CcmStatus.Status -ne 'Running') {
LogWrite -Message "$Computer`: Error - CcmExec service is not running."
$CCMClientSDK = $null
}
}
Catch {
$CCMClientSDK = $null
}
If ($CCMClientSDK) {
If ($CCMClientSDK.ReturnValue -ne 0) {
LogWrite -Message "Error: DetermineIfRebootPending returned error code $($CCMClientSDK.ReturnValue)"
}
If ($CCMClientSDK.IsHardRebootPending -or $CCMClientSDK.RebootPending) {
$SCCM = $true
}
}
Else {
$SCCM = $null
}
## Creating Custom PSObject and Select-Object Splat
$SelectSplat = @{
Property = (
'Computer',
'CBServicing',
'WindowsUpdate',
'CCMClientSDK',
'PendComputerRename',
'PendFileRename',
'PendFileRenVal',
'RebootPending'
)
}
New-Object -TypeName PSObject -Property @{
Computer = $WMI_OS.CSName
CBServicing = $CBSRebootPend
WindowsUpdate = $WUAURebootReq
CCMClientSDK = $SCCM
PendComputerRename = $CompPendRen
PendFileRename = $PendFileRename
PendFileRenVal = $RegValuePFRO
RebootPending = ($CompPendRen -or $CBSRebootPend -or $WUAURebootReq -or $SCCM -or $PendFileRename)
} | Select-Object @SelectSplat
}
Catch {
LogWrite -Message "$Computer`: $_"
}
}## End Foreach ($Computer in $ComputerName)
}## End Process
End { }## End End
}## End Function Get-PendingReboot
- In the private folder, create a file called LogWrite.ps1
- Inside the file, paste the following code
Function LogWrite {
[CmdletBinding()]
Param
(
[Parameter(Mandatory = $true,
ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[Alias("LogContent")]
[string]$Message,
[Parameter(Mandatory = $false)]
[Alias('LogPath')]
[string]$Path = 'C:\Logs\PowerShellLog.log',
[Parameter(Mandatory = $false)]
[ValidateSet("Error", "Warn", "Info")]
[string]$Level = "Info",
[Parameter(Mandatory = $false)]
[switch]$NoClobber
)
Begin {
# Set VerbosePreference to Continue so that verbose messages are displayed.
$VerbosePreference = 'Continue'
}
Process {
# If the file already exists and NoClobber was specified, do not write to the log.
if ((Test-Path $Path) -AND $NoClobber) {
Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
Return
}
# If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
elseif (!(Test-Path $Path)) {
Write-Verbose "Creating $Path."
$NewLogFile = New-Item $Path -Force -ItemType File
}
else {
# Nothing to see here yet.
}
# Format Date for our Log File
$FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
# Write message to error, warning, or verbose pipeline and specify $LevelText
switch ($Level) {
'Error' {
Write-Error $Message
$LevelText = 'ERROR:'
}
'Warn' {
Write-Warning $Message
$LevelText = 'WARNING:'
}
'Info' {
Write-Verbose $Message
$LevelText = 'INFO:'
}
}
# Write log entry to $Path
"$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
}
End {
}
}
Let’s start the build workflow by running ./build.ps1 -Tasks build from your PowerShell terminal. Below you’ll see the following output in the output directory.

Open up a new PowerShell terminal and import the module manifest file with the Import-Module cmdlet.

Fantastic! Now you see that only the Get-PendingReboot function is available to you, but is missing the LogWrite function. Then why is the LogWrite function not available? Glad you asked. When the module is built with the ModuleBuilder module, which separates both Public and Private functions by adding only the Public functions in the manifest file in the FunctionsToExport entry. If you open the PoweringUp.psd1 in the output directory, you’ll spot that specific entry.

You can debate about the design and whether to call it Public and Private or Functions and Internal, since both options are common in the PowerShell community. Both options do the same trick by hiding functions that are internal to the module and not available to the end users.
A word about rules with PSScriptAnalyzer
As mentioned before, both the scripts come from the Technet Gallery and were created by awesome folks. The original LogWrite function was called Write-Log, but why was the name changed? You probably haven’t noticed any warnings popping up in your VS Code editor, and you can argue that LogWrite isn’t an approved verb to be used. That’s exactly what is demonstrated here, because if you change it to Log-Write, you’ll see a warning from the editor:

The analyzer, called PSScriptAnalyzer, runs these rules, which are baked into to the PowerShell extension that you installed in the beginning if you’re following along. PSScriptAnalyzer is a static code checker for PowerShell modules and scripts. It can check the quality of the code by running against a set of rules. If you want to control the behavior of the analyzer, you can introduce a settings file where you can specify the rules you want to check against.
- In the root of the repository, create a file called PSScriptAnalyzerSettings.psd1
- For demonstrating purposes, add the following content without the PSUseApprovedVerbs
@{
IncludeRules = @('PSAvoidDefaultValueSwitchParameter',
'PSMisleadingBacktick',
'PSMissingModuleManifestField',
'PSReservedCmdletChar',
'PSReservedParams',
'PSShouldProcess',
'PSAvoidUsingCmdletAliases',
'PSUseDeclaredVarsMoreThanAssignments')
}
- Restart the editor
You’ll notice after the editor is restarted, there are no more squiggly lines telling you that it is using an unapproved verb. If you want to get a full list of available rules, you can simple run the Get-ScriptAnalyzerRule cmdlet or visit the documentation. For now, make sure you add the rule and change the function name to the original.
Conclusion
You’ve already covered quite some ground. You saw by installing extensions in VS Code, you can create an awesome developer experience. Not only for yourself, but also for newcomers when they check out your repository. After that, you went through the scaffolding of the project files to start introducing new PowerShell scripts that get build into a so called module.
In the next part of this series, you will cover even more by checking out the different formatting styles, building your own verb-noun pair and ensuring greater quality with Pester tests. If you are already up for an exercise, can you spot how the build workflow works and runs? See you in the next part!
References
- Test framework for PowerShell | Pester
- Build automation for PowerShell | Invoke-Build
- Scaffolding project folders and files | Sampler
- Static code checker for PowerShell | PSScriptAnalyzer
- Code editor Visual Studio Code | VS Code