💾 Archived View for colincogle.name › blog › powershell-polish-1 captured on 2024-03-21 at 14:50:23. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-07-10)
-=-=-=-=-=-=-
by Colin Cogle
Published Saturday, June 24, 2023.
Here's how to take your PowerShell script and turn it into the best version of itself.
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 working 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 virtual 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 something 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're looking for one, I recommend Visual Studio Code.)
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 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.
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 version 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:
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.
mkdir GuessTheNumber mv GuessTheNumber.ps1 GuessTheNumber cd GuessTheNumber git init git add -A git commit -m "Initial commit" 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!
I don't have time to go into Git in-depth here. One could write an entire book explaining how Git works, but if you're looking for a good tutorial, I can personally recommend the:
Microsoft Developer Tools Challenge
Our first goal is to clean up our spaghetti code and make it look pretty. 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:
whenever it makes sense and whenever it makes the code the easiest to read. Some tips I recommend:
If you're contributing to someone else's project, look for a CONTRIBUTING 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 official 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.
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.
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 frequently), 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.
git commit -m "Resolved PSScriptAnalyzer's complaints."
Figure 7: Get into the habit of making a commit whenever you complete a small step.
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 metadata — that is, information about your script.
Rather than type mere comments into your editor, you can and should define 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.
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 focusing 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 proceed 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 specify 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 HTTP or HTTPS 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 information. Note that an empty parameter definition was automatically 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!
We've worked hard on our script, and now we've got something that's 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 digital 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 business or if you code for a living.
Now, we have a problem. Where does someone get a code signing certificate? 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 can 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 certificate 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.
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 portable format. If you right-click your script and choose Properties, you can also see your name on the new Digital Signatures tab!
Figure 14: See your name in the programmers' equivalent of shining lights.
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.
=================================================================
Previous Blog: "Watching Baseball in 2023"
=================================================================
"PowerShell Polish, Part 1: Perfecting Your Scripts" by Colin Cogle is licensed under:
Creative Commons Attribution-ShareAlike 4.0 International (CC-BY-SA)