-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
"PowerShell Polish, Part 1: Perfecting Your Scripts"
by Colin Cogle
Written Saturday, June 24, 2023.
Available online at
https://colincogle.name/posh1
ABSTRACT
Here's how to take your PowerShell script and turn it into the best
version of itself.
INTRODUCTION
If you know what PowerShell is, then there's a good chance that you're a
Windows power user or a sysadmin of some sort. Moreover, if you're work-
ing with PowerShell on a daily or weekly basis, you've probably written
a really nice script. Whether you made it to put a new tool in your vir-
tual tool belt, to automate some tedious task, or just for a programming
challenge; congratulations on writing a cool script.
Now, you might be tempted to toss it deep inside your Documents folder
and call it done. However, if your code is like anything ever written,
you'll inevitably be faced with two hurdles: how to share your file,
and how to manage version control. With a little work up front, you can
transform your script from something static and unchanging into some-
thing that you'll want to share with the entire world.
In the PowerShell world, there are two ways to package something for
mass consumption: scripts and modules. While modules are a better way to
package and share multiple files, we'll save those for another article.
Today, we're going to focus on scripts, single files that contain all of
your code.
While you can read the article by itself, you're free to use this as a
guide to update one of your own creations. If you have a script of yours
in mind, go ahead and open it up in your favorite code editor. (If you
are looking for one, I recommend Visual Studio Code.)
OUR EXAMPLE SCRIPT
For the purposes of this article, I'm going to be working on this little
script. It's nothing more than a quick game of guess the number, a rite
of passage for any computer science student, somewhere in between saying
"Hello World!" and hunting a wumpus. To illustrate some concepts as we
go along, I've tried to violate some best practices and make some garish
styling decisions that I otherwise would not do.
----------------------------------------------------------------------
# version 1.3
echo "Welcome to Gues the Number!"
$x = Get-Random -min 1 6
Do{
$guess=Read-Host "what's your guess?"
if ($guess -eq $x) {write-host "You win!"}
else
{Write-host "Wrong!"}
} while($guess -ne $x)
----------------------------------------------------------------------
Figure 1: Our working script before making any changes.
GIT GOOD: VERSIONING AND SOURCE CONTROL
If you're following along at home, I'm going to have you make a lot of
changes to your script as we go along. Thus, I'm bumping this up to step
number one. You'll see why.
Eventually, you (or one of your users) will find a bug in your script,
and you'll have to fix it. The biggest problem with managing a script is
keeping track of your changes. You might be thinking to yourself, "Why
would I ever need an old version of my script?" Well, let's assume that,
over time, you make a version 1.1, then a version 1.2, and then a ver-
sion 1.3 that has a bug. How do you know _when_ the bug was introduced?
Could it be something you changed recently, or could it be something you
changed last week?
This is where a version control system comes into play. The most popular
one right now is called Git. You can quickly install it on your system:
* Windows users should download and install Git for Windows. (This
is what I use at work.)
* macOS users have a few options, including downloading Xcode from the
Mac App Store (which is a lot, unless you install only the command-
line tools, but it's a pretty nice IDE, too).
* Linux users might already have it installed, but if not, something
like `sudo apt install git` might do the trick.
Git only works on entire folders, so create a new folder and put your
script inside it. Once you've done that, we're going to initiate a new
repository, add your file to Git's tracking, and commit it to a version
1.3 tag.
----------------------------------------------------------------------
PS C:\> mkdir GuessTheNumber
PS C:\> mv GuessTheNumber.ps1 GuessTheNumber
PS C:\> cd GuessTheNumber
PS C:\> git init
PS C:\> git add -A
PS C:\> git commit -m "Initial commit"
PS C:\> git tag v1.3 -m "This was the first version I put into Git."
----------------------------------------------------------------------
Figure 2: Congratulations! We've initiated a new Git repository, and
committed our file into it.
Now that we have our initial version safely tucked away in Git, we can
work to our heart's content!
Further Reading
I don't have time to go into Git in-depth here. One could write a whole
book explaining how Git works, but if you're looking for a good tutori-
al, I can personally recommend the Microsoft Developer Tools Challenge.
STYLING YOUR CODE
Our first goal is to clean up our spaghetti code and make it look pret-
ty. We won't be changing any functionality, but by making it look nicer,
it will be easier for you (or others) to read and maintain.
Everyone has or develops their own coding style. As someone who can code
in C, C++, and Java, I tend to follow some parts of the GNU Standard C
style whenever it makes sense and whenever it makes the code the easiest
to read. Some tips I recommend:
* Comment your code wherever appropriate! (If this is something that
you're going to share with the world, consider writing your comments
in English, as it's the lingua franca of the programming world.)
* Give your variables useful names. If you're not declaring a loop or
a simple three-line function, then `$x` is probably a terrible name.
* Stick in a line break if it makes things look cleaner.
* Indent lines when opening a new block, and
* Put curly braces on their own line.
* Put spaces around operators.
If you're contributing to someone else's project, look for a CONTRIBU-
TING file in the source code. That will tell you what coding style the
project expects from all contributors. Please follow that, instead of
what you and I think best.
Regardless, no matter what, your PowerShell cmdlets and statements
should be properly capitalized, matching however you see them in offi-
cial documentation. It also goes without saying that you should check
your spelling and grammar.
----------------------------------------------------------------------
# This is version 1.3
echo "Welcome to Guess the Number!"
# Pick a random number between one and six, inclusive.
$theNumber = Get-Random -min 1 6
Do
{
$guess = Read-Host "What's your guess?"
If ($guess -eq $theNumber)
{
Write-Host "You win!"
}
Else
{
Write-Host "Wrong!"
}
} While ($guess -ne $theNumber)
----------------------------------------------------------------------
Figure 3: Our code after fixing spelling, grammar, and poor style.
That's so much easier to read!
Now, let's commit our changes.
----------------------------------------------------------------------
PS C:\> git commit -m "Fixed spelling, grammar, and style."
----------------------------------------------------------------------
Figure 4: Commit your changes so you can track them later. You should
write better commit messages, too, but this isn't a Git best
practices tutorial. I'm just teaching you enough to get you
up and running.
PSSCRIPTANALYZER; OR, HOW TO CONFORM TO POWERSHELL'S BEST PRACTICES
Microsoft has developed an automated tool called PSScriptAnalyzer to
scan your PowerShell files for common mistakes, bad practices, and other
things you should avoid.
Figure 5: Downloading and running PSScriptAnalyzer.
----------------------------------------------------------------------
PS C:\> Install-Module PSScriptAnalyzer
PS C:\> Invoke-ScriptAnalyzer ./GuessTheNumber.ps1
RuleName : PSAvoidUsingCmdletAliases
Severity : Warning
Line : 2
Column : 1
Message : 'echo' is an alias of 'Write-Output'. Alias can introduce
possible problems and make scripts hard to maintain. Please
consider changing alias to its full content.
----------------------------------------------------------------------
As you can see, it complained that we used an alias in our code. While
it's much easier to type out interactively (and something I do frequent-
ly), you should always avoid that when writing scripts.
Likewise, we should always spell out our parameters for clarity, and
provide default and implied ones for clarity, too. For example, in our
call to Get-Random, we'd used `-min` when we should have used `-Minimum`
and we can improve readability by adding in the implied `-Maximum`.
Remember, _you_ might be a seasoned PowerShell expert, but that doesn't
mean the next person reading your code will be.
----------------------------------------------------------------------
# This is version 1.3
Write-Output "Welcome to Guess the Number!"
# Pick a random number between one and six, inclusive.
$theNumber = Get-Random -Minimum 1 -Maximum 6
Do
{
$guess = Read-Host "What's your guess?"
If ($guess -eq $theNumber)
{
Write-Host "You win!"
}
Else
{
Write-Host "Wrong!"
}
} While ($guess -ne $theNumber)
----------------------------------------------------------------------
Figure 6: Cleaning up some common PowerShell trouble.
----------------------------------------------------------------------
PS C:\> git commit -m "Resolved PSScriptAnalyzer's complaints."
----------------------------------------------------------------------
Figure 7: Get into the habit of making a commit whenever you complete a
small step.
ADDING METADATA
This is where we start to move into the more PowerShell-specific things
you can do to make your script a little easier to work on. PowerShell
has a comment block known as `PSScriptInfo` where you can specify meta-
data — that is, information about your script.
Rather than type mere comments into your editor, you can and should de-
fine your metadata in a standard way (especially if you have dreams of
this script being in the PowerShell Gallery someday). You can use the
`Update-ScriptFileInfo` cmdlet to add a metadata block to your script,
or you can edit most of it in your text editor:
----------------------------------------------------------------------
PS C:\> Update-ScriptFileInfo GuessTheNumber.ps1 -Author "Colin Cogle" `
-Version 1.3.0 -Description "Play a game of guess the number."
PS C:\> Get-Content GuessTheNumber.ps1
<#PSScriptInfo
VERSION 1.0
GUID ff00ae24-9a16-4d6f-9845-ed7a7d8133b1
AUTHOR colin
COMPANYNAME
COPYRIGHT
TAGS
LICENSEURI
PROJECTURI
ICONURI
EXTERNALMODULEDEPENDENCIES
REQUIREDSCRIPTS
EXTERNALSCRIPTDEPENDENCIES
RELEASENOTES
PRIVATEDATA
#>
<#
DESCRIPTION
foo
#>
Param()
[…]
----------------------------------------------------------------------
Figure 8: If you just want to add the comment block and fill it in in
your IDE, then you're like me; know that the only required
parameter to make this run is `-Description`.
Let's go ahead and fill out some of these fields. You can remove any
lines that you're not going to use or that don't apply.
----------------------------------------------------------------------
<#PSScriptInfo
VERSION 1.3.0
GUID ff00ae24-9a16-4d6f-9845-ed7a7d8133b1
AUTHOR Colin Cogle
COPYRIGHT © 2023 Colin Cogle. All Rights Reserved.
TAGS number, guessing, game, tutorial, Windows, macOS, Linux
LICENSEURI
https://www.gnu.org/licenses/agpl-3.0.en.html
RELEASENOTES
This release of GuessTheNumber.ps1 contains many bug fixes:
- - - Implement PSScriptInfo and help.
- - - Code cleanup.
#>
<#
DESCRIPTION
foo
#>
Param()
[…]
----------------------------------------------------------------------
Figure 9: This looks a lot nicer now that we've filled in the ScriptInfo
fields. We'll take care of the rest shortly. (Note that I
removed the extra line breaks as a manner of preference.)
Now, you have metadata defined in the proper manner, via two comment
blocks. This data can be read by Test-ScriptFileInfo, online galleries,
or by any eyeballs that happen to perusing your source code. Be sure
to update the version number and release notes whenever you decide it's
time.
GET HELP FOR `Get-Help`
How often have you looked at someone else's script or code and wondered
how to use it? Perhaps that won't be a problem with our script, but it's
like I say at work: **always** document your work.
We can use `Get-Help` to learn more about this script:
----------------------------------------------------------------------
PS C:\> Get-Help .\GuessTheNumber.ps1
GuessTheNumber.ps1
----------------------------------------------------------------------
Figure 10: The output of `Get-Help`, by default. There isn't much to
look at.
If you haven't already, open up your PowerShell script and take a look
at the new blocks in your script. The second one is where we'll be fo-
cusing our attention. What you're looking at is PowerShell's comment-
based help system. This block consists of several paragraphs of plain
text occasionally marked up with headings that begin with a dot. (Though
PowerShell was developed by Microsoft, this syntax looks a lot like
`troff` commands from the UNIX world.)
Let's go ahead and use some of the most common keywords to help describe
our script. At the very least, `.DESCRIPTION` was filled in for you, but
we can add and edit others. Note that we won't be using all of these.
SYNOPSIS
A one-line summary of what your script does.
DESCRIPTION
A longer summary of what your code does. You can write multiple
paragraphs.
PARAMETER InputObject
If your script accepts command-line parameters, you should explain
what they do. If we had a parameter named InputObject, we would pro-
ceed to describe it. You can specify this as many times as you need.
INPUTS and .OUTPUTS
If your script accepts pipeline input or generates pipeline output,
provide the data type and a description.
EXAMPLE
This is the only structured help item. On the first line after this,
show the command line. Then, after a blank line, describe what that
will do. (Ironically, I will show the example later.) You can speci-
fy this as many times as you want.
NOTES
Is there anything else your users should know? For example, do they
need to run a command before attempting to run your script? Does it
only support certain data types?
LINK
You can use this (multiple times, if you'd like) to recommend users
read other help topics. You can name other cmdlets, conceptual help
topics, or specify a URL. If you specify at least one URL, the first
one will be used for `Get-Help -Online`.
Edit the second comment block to look something like this:
----------------------------------------------------------------------
<#
SYNOPSIS
Play a game of guess the number.
DESCRIPTION
This script plays a game of guess the number. The computer will pick a
number between one and six, and you will be prompted to guess it.
EXAMPLE
PS C:\> .\GuessTheNumber.ps1
Begins a game. This script runs interactively, and it does not accept
pipeline input or parameters.
NOTES
If you wish to stop playing, press Control-C to break.
LINK
https://colincogle.name/PoSH
#>
Param()
----------------------------------------------------------------------
Figure 11: The second comment block fleshed out with some helpful infor-
mation. Note that an empty parameter definition was automati-
cally added. This is required when using `PSScriptInfo`,
comment-based help, or both.
Let's try that command again!
----------------------------------------------------------------------
PS C:\> Get-Help .\GuessTheNumber.ps1
NAME
C:\GuessTheNumber.ps1
SYNOPSIS
Play a game of guess the number.
SYNTAX
GuessTheNumber.ps1 [<CommonParameters>]
DESCRIPTION
This script plays a game of guess the number. The computer will pick a
number between one and six, and you will be prompted to guess it.
RELATED LINKS
https://colincogle.name/PoSH
REMARKS
To see the examples, type: "Get-Help GuessTheNumber.ps1 -Examples"
For more information, type: "Get-Help GuessTheNumber.ps1 -Detailed"
For technical information, type: "Get-Help GuessTheNumber.ps1 -Full"
For online help, type: "Get-Help GuessTheNumber.ps1 -Online"
----------------------------------------------------------------------
Figure 12: The output of `Get-Help`, thanks to our hard work!
Doesn't that look so much better?
Beautiful! If you don't see everything you typed, that's normal; try
running `Get-Help -Full` to learn everything. `git add`, `git commit`,
and let's wrap this up!
GOING THE DISTANCE: SIGNING YOUR CODE
We have worked hard on our script, and now, we've got something that is
ready to share with the world. Authors put their name on the cover,
artists sign or tag their work, and PowerShell scripters also have the
option to sign their code. While this _is_ optional, and you can run
unsigned code perfectly well, there are many benefits to slapping a dig-
ital signature on your script:
* Windows PowerShell or Windows SmartScreen may show a warning when
you try to run an unsigned script.
* In some corporate environments, an administrator may have used
InTune or Group Policy to go one step further, and modify Windows
PowerShell's execution policy such that unsigned scripts will not
run.
* If anyone modifies your script, the signature will be invalid, which
Windows will treat as **worse** than unsigned.
* You get to look professional (and enjoy a small ego boost) by seeing
your name attached to your script. This goes double if you're a bus-
iness or if you code for a living.
Where To Find Magic Bits
Now, we have a problem. Where does someone get a code signing certifi-
cate? While the ISRG and Let's Encrypt have effectively democratized
server certificates by making them available for free, they have no
plans to do the same for other types — and that includes code signing
certificates. (I'll complain about S/MIME certificates in another blog
post.)
You can use a self-signed certificate for small deployments. You can
share your certificate with your family and friends, and build your own
little web of trust, but that is not scalable. If someone asked me to
install a certificate in my root store to run their script, I would do
one of two things: either click through the unsigned code warnings, or
simply not run their script.
Businesses might run their own internal certificate authority; Active
Directory Certificate Services is fairly popular. If your company has
its own private CA, you could ask your IT department to take a leap of
faith and issue you a code signing certificate. Having both run the CA
and used it to sign code, that's a wonderful option for code that will
only run on company-owned computers. For sharing with a wider audience,
though, we're back to square one.
You could detach-sign your code with PGP keys or distribute hashes, and
hope your users verify everything before running it. There's also some
chatter about doing _something_ with blockchains, but it's going to be
years before this can even be a contender. Ultimately, if you want to
sign your code *and* sign it properly so that anyone in the world can
trust your code, then you will need to bite the bullet and pay someone
money for these magic bits that make operating systems happy. I was
quite happy with SignMyCode.com, but you should find a vendor that's
right for you.
In my case, I made an ECDSA private key, then gave my vendor a certifi-
cate signing request and a copy of my legal identification. Once they
verified that I'm me, I was the proud owner of a globally-trusted code
signing certificate.
How to Sign a Script; Or, How to Turn Magic Bits Into Other Magic Bits
You're going to need a Windows computer for the following step, as
Microsoft doesn't make all of Microsoft.PowerShell.Security's cmdlets —
namely, `Set-AuthenticodeSignature ` — available on other platforms.
This little code block assumes that you have your one and only code
signing certificate imported into your Personal store.
----------------------------------------------------------------------
Set-AuthenticodeSignature `
-Certificate (Get-ChildItem Cert:\CurrentUser\My -CodeSigning)[0] `
-IncludeChain NotRoot `
-TimestampServer
http://timestamp.digicert.com `
-HashAlgorithm SHA256 `
-FilePath .\GuessTheNumber.ps1
----------------------------------------------------------------------
Figure 13: How to sign your code. If, for some reason, you actually
have opinions about timestamp servers, you can substitute
your own.
If you look at your script, you will see a long comment block at the
very end. This is the digital signature and timestamp, encoded in a por-
table format. If you right-click your script and choose Properties, you
can also see your name on the new Digital Signatures tab!
IN CONCLUSION
Congratulations on making it this far! If you've been following along
at home, you've now taken one of your own scripts and turned it into the
best possible version of itself. Go ahead and share it far and wide.
You might have noticed that I didn't talk much about the PowerShell
Gallery. While you can publish and install scripts from it, it really
focuses on modules, which I'll be touching on in a future blog post.
========================================================================
"PowerShell Polish, Part 1: Perfecting Your Scripts" by Colin Cogle is
licensed under Creative Commons Attribution-ShareAlike 4.0 International
(CC-BY-SA).
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQQ7NZ6ap/Bjr/sGU4FSrfh98PoTfwUCZJo+4AAKCRBSrfh98PoT
f4W1AQDNMVQyMmcdL9mNBHmw7tp+GE1yEmYCjw+UOFL+LXMJvwD/WMi4+0Nld0Ky
7S+lwOs/YM9DhxJeTbC7LHinq8kKpQQ=
=PXO7
-----END PGP SIGNATURE-----