In this article, our guest blogger, Brent Challis, demonstrates how to use an incremental building block approach to PowerShell script design, allowing flexibility while keeping sight of the desired result.
Splitting code between the functional blocks and the code you interact with to complete a task is often called “Tools and Controllers”. As I am an engineer originally—my bachelor’s degree is in Electrical Engineering—this approach is familiar to me.
The concept here is like getting your toolbox from the garage when you need to complete a task. Just as you have different collections of tools in your toolbox, ranging from general purpose to specialized, we do the same thing with our PowerShell modules.

Tools
In PowerShell, the toolboxes are generally modules. The commands in the modules are the tools. These commands should:
- Have comment-based help so they will look and feel like all the other tools that are made available.
- Take input on the command-line. What is needed should be made as clear as possible. This is mainly done through the use of the Help, meaningful names, and being strongly typed.
- Be robust. As they are likely to be used in multiple contexts, possibly by multiple users, they must handle different inputs. The code should check that the input values work as expected; if not, either prompt for values (if that is appropriate) or return a meaningful error message identifying the problem.
- Be a single output type. This can be returned as a value or by writing it to the pipeline. If you need to provide multiple pieces of information in the output, you should create a custom object—ideally as an instance of class—and have it contain all the information needed.
- Be as flexible as possible and still be focused. In PowerShell, we should never, in my opinion, have a ‘Swiss army knife’ command.
Controllers
The controller is the code that users will run and interact with to achieve a task.
When I want to do some work around the home, I start by collecting the tools needed, then working out the steps required and planning how I will complete the task. The controller script works the same way. Hopefully, you will already have the tools/commands already included in a module ready to be called. If you need to write a command and it seems to be specific to the task at hand, it should still be written in the same way as you would write a tool. That way, it can be moved to a module if it seems likely that it can be used more broadly
Commands written in controllers are generally not visible to any other code.
The controller script may get all the information from the command-line and only run through the code when you run the script. This makes it look like the code for a tool. There are still differences, such as it:
- Is focused on the task—the bigger picture.
- Will be responsible for setting up the context for the work.
- May ask for more information if there is anything missing.
- Should check that the required resources are available, such as files and network connections.
- May be a single run-through or several options may be available as steps. There may likely be a menu option or UI so that it can be run several times.
- Should be documented either as comment-based help or comment blocks as a header so that when someone else (or you in 6 months) looks at the code, you can quickly understand what is going on.
Looking at the UI side of things, it may be implemented as a text menu or as a Windows application written in PowerShell, as shown in the examples below:
The screenshots above are from a utility I developed using PowerShell Studio, enabling me to work with photos. The controller element—the GUI—lets me select a folder containing photos, select the photo I want, and then exam the metadata (including Exif data).
There are several sets of tools at play here. PowerShell Studio has its own Toolbox containing components to design the GUI:

The image above shows part of the PowerShell Studio Toolbox. The elements listed here expose classes from the .NET Framework and make them easier to work with. These classes can also be accessed directly from your code (see the case study below).
I build the GUI by dragging items from the Toolbox onto the design canvas, configuring them if necessary, and writing the code that the elements work with.
The other tool sets are the general commands that PowerShell provides to work with files, and a custom toolset I have written that includes a command to extract metadata from a file. This command wraps a command-line utility that extracts the Exif data. By creating a tool for this functionality rather than embedding it in the controller, I can easily utilize it in multiple control environments or as a standalone command to examine a single file.
Text-based menus are simple to write. For example, the following code:
Clear-Host
[String]$selection = ""
do
{
Clear-Host
Write-Host "#############################"
Write-Host "# Ledger Processing Utility #"
Write-Host "#############################`n"
Write-Host "D. Display The Full Ledger"
Write-Host "S. Display The SMSF Ledger"
Write-Host "C. Display The Consultancy Ledger"
Write-Host "`n###############################################`n"
Write-Host "R. Report on Ledger"
Write-Host "N. Create New Ledger"
Write-Host "M. MYOB Chart of Accounts"
Write-Host "Q. Quit"
Write-Host "`n###############################################`n"
$selection = Read-Host "Enter Selection"
if ($selection -ne "Q")
{
$selectedTransactions = $null
switch($selection)
{
"D" {$selectedTransactions = (Get-TheLedger |
Out-GridView -Title "Full Ledger" -OutputMode Multiple)}
"S" {$selectedTransactions = (Get-TheLedger -AccountHolder SMSF |
Out-GridView -Title "SMSF Ledger" -OutputMode Multiple)}
"C" {$selectedTransactions = (Get-TheLedger -AccountHolder Consultancy |
Out-GridView -Title "Consultancy Ledger" -OutputMode Multiple)}
## Processing options
"R" {Get-LedgerReport | Out-GridView -Title "Latest Transaction Report."}
"N" {New-FullLedger -Verbose}
"M" {Get-MYOBChartOfAccounts}
default {Write-Host "Unable to recognise $selection"}
}
Read-Host "Press Enter to continue"
}
}until ($selection -eq "Q")
Produces the following output:

The more powerful, complex interface is the GUI (although a good IDE makes it moderately simple).
A Case Study
As a Microsoft Certified Trainer, I download resources for delivering courses. The materials are periodically updated, making it necessary to check if I have already downloaded the latest version. While there are date stamps and comments about the changes on the download site, I frequently want to compare the new files (the lab files in particular) with the ones I have on my computer. To do this, I need to compare the files from two folders (the folder I am currently using and the one with the latest downloads) so I can see what, if anything, has changed.
Requirements
I require two tools for the comparison:
- A browser to select the folders that I want to compare.
- A tool to compare two files to see if they are the same.
The controller script will use the file browser class from the .NET Framework to identify the reference folder (the one I am currently using) and the comparison folder (the latest download) if I have not provided the paths on the command-line. This approach allows me to avoid having to hard code or type long paths when I run the utility.
It will then go through each file in the reference folder, find the file with the matching name in the comparison folder, and then call the comparison function to check if they are the same.
It will also provide a list of file names present in one folder but not the other.
In this first instance, I simply output the results of the comparison to the screen as I do not need to display the differences between the files where they exist.
The Solution
The code for the two tools:
<#
.SYNOPSIS
Compare-File takes two file references and will test to see if the file
contents are identical.
.DESCRIPTION
Compare-File initially checks that the two files are identical in length and
then performs a byte comparison to see if the contents are identical.
.PARAMETER ReferenceFile
A System.IO.FileInfo reference to a file on which to base the comparison.
.PARAMETER ComparisonFile
A System.IO.FileInfo reference to a file to compare with the reference file.
.EXAMPLE
Compare-File -ReferenceFile (Get-Item "C:\Data\File1.txt”) -ComparisonFile (Get-Item "C:\Data\File2.txt”)
.OUTPUTS
A Boolean value to indicate if the files are identical
.NOTES
Confirm Impact: Low
Supports Positional Binding
#>
function Compare-File
{
[CmdletBinding(ConfirmImpact='Low')]
Param
(
[System.IO.FileInfo]$ReferenceFile,
[System.IO.FileInfo]$ComparisonFile
)
Begin
{
if ((Test-Path $ReferenceFile.FullName) -and (Test-Path $ComparisonFile.FullName))
{
[bool]$identical = $true
[int]$offset = 0
[Int64]$bytesRead = 0
[Int64]$bytesToRead = 0
}
else
{
Throw ("Unabe to find either {0} or{1}." -f $ReferenceFile.FullName,$ComparisonFile.FullName)
}
}
Process
{
if ($ReferenceFile.Length -eq $ComparisonFile.Length)
{
$bytesToRead = $ReferenceFile.Length
[System.IO.FileStream]$reference = $ReferenceFile.OpenRead()
[System.IO.FileStream]$comparison = $ComparisonFile.OpenRead()
[Byte[]]$refBuffer = New-Object Byte[] $bytesToRead
[Byte[]]$compBuffer = New-Object Byte[] $bytesToRead
While (($bytesToRead -gt 0) -and $identical)
{
$bytesRead = $reference.Read($refBuffer, $offset, $bytesToRead)
$comparison.Read($compBuffer,$offset, $bytesToRead) | out-null
if ( -Not [System.Linq.Enumerable]::SequenceEqual($refBuffer, $compBuffer))
{
$identical = $false
}
if ($bytesRead -gt 0)
{
$offset += $bytesRead
$bytesToRead -= $bytesRead
}
}
$reference.Close()
$comparison.Close()
}
else
{
$identical = $false
}
}
End
{
return $identical
}
}
<#
.SYNOPSIS
Get-FolderPath is a utility that launches a FolderBrowserDialog to enable
the user to browse and select a folder.
.DESCRIPTION
Get-FolderPath uses the FolderBrowserDialog from the .NET Framework
to launch an explorer dialog to enable to user to select a folder.
.PARAMETER BrowserTitle
Default Value: "Select Folder"
A string to provide a prompt to the user regarding the type of folder
being sought.
.EXAMPLE
Get-FolderPath -BrowserTitle "Select the folder for the log files."
.OUTPUTS
Either a null if the dialog is cancelled or a string representing the
selected folder path.
.NOTES
Confirm Impact: Low
Supports Positional Binding
#>
function Get-FolderPath
{
[CmdletBinding(ConfirmImpact='Low')]
Param
(
[String]$BrowserTitle = "Select Folder"
)
Begin
{
[System.Windows.Forms.FolderBrowserDialog]$ofbd = New-Object System.Windows.Forms.FolderBrowserDialog
$ofbd.Description = $BrowserTitle
}
Process
{
[String]$folderPath = $null
if ($ofbd.ShowDialog() -eq "OK")
{
$folderPath = $ofbd.SelectedPath
}
return $folderPath
}
}
The actual work is carried out by:
function Compare-Folder
{
[CmdletBinding(ConfirmImpact='Low')]
Param
(
[String]$ReferenceFolderPath = (Get-FolderPath -BrowserTitle "Select Reference Folder"),
[String]$ComparisonFolderPath = (Get-FolderPath -BrowserTitle "Select Comparison Folder")
)
Begin
{
if ((Test-Path $ReferenceFolderPath) -and (Test-Path $ComparisonFolderPath))
{
$referenceFiles = Get-ChildItem $ReferenceFolderPath -File
$comparisonFiles = Get-ChildItem $ComparisonFolderPath -File
$refFileNames = $referenceFiles | Select-Object -ExpandProperty Name
$compFileNames = $comparisonFiles | Select-Object -ExpandProperty Name
}
else
{
Throw "There was a problem with the folder paths"
}
}
Process
{
Write-Host ([String]::Format("Comparing files in folder {0}`nwith`nfiles in Folder {1}`n",$ReferenceFolderPath,$ComparisonFolderPath))
Write-Host "`nFiles present in both folders" -BackgroundColor Cyan
foreach ($refFile in $referenceFiles)
{
foreach ($compFile in $comparisonFiles)
{
if ($refFile.Name -eq $compFile.Name)
{
Write-Host ("Comparing File {0}" -f $refFile.Name)
$result = Compare-File -ReferenceFile $refFile -ComparisonFile $compFile
if ($result)
{
Write-Host "The files are the same" -BackgroundColor Green
}
else
{
Write-Host "The files are different." -BackgroundColor Yellow -ForegroundColor Black
}
}
}
}
Write-Host "`nFiles in the Reference Folder but not in Comparison Folder" -BackgroundColor Cyan
foreach ($refFileName in $refFileNames)
{
if ($refFileName -notin $compFileNames)
{
Write-Host $refFileName
}
}
Write-Host "`nFiles in the Comparison Folder but not in Reference Folder" -BackgroundColor Cyan
foreach ($compFileName in $compFileNames)
{
if ($compFileName -notin $refFileNames)
{
Write-Host $compFileName
}
}
}
}
Clear-Host
Compare-Folder
I often structure my control code as a function and then call it in the same script so, it is straightforward if I decide to move it to a module later.
Note: Both PowerShell Studio and PrimalScript include ScriptMerge, a stand-alone application that compares files and folders and allows you to apply differences to either of the two compared items.
Summary
Splitting the focus between the building blocks and the bigger picture helps me design for greater flexibility by adding to my toolset. The control script becomes essentially the story of what needs to be done, making it easier to develop incrementally and to understand.
Reference
Other blog articles by guest blogger Brent Challis:
- Where is the Documents Folder?
- Sudo for PowerShell – or – I meant to open the shell with elevated priveleges!
- A simple fix for problems with Windows Forms WebBrowser
Feedback
We love to hear from you! Please comment below, or in our feedback forum and reference this post.