Powershell Script Boilerplate

I’m a big fan of PowerShell, but scripts can require some annoying boilerplate. Here’s what I’m using lately for new scripts:

#!/usr/bin/env pwsh
param(
    # cli args declared here
)

function main() {
    # Script goes here

    # To call external binaries and fail on non-zero exit codes, wrap in `exec {}`:
    exec { git merge foo/bar }
}

function exec($block) {
    & $block
    if($LASTEXITCODE -ne 0) { throw "Non-zero exit code $LASTEXITCODE" }
}

$ErrorActionPreference = 'Stop'
try {
    $_pwd = $pwd
    cd $PSScriptRoot
    main
} finally {
    cd $_pwd
}

Breaking it down

The shebang is for Linux; does nothing on Windows.

Setting $ErrorActionPreference is like bash’s set -e but for PowerShell commands. When a command fails, it throws an error, effectively ending the script unless caught. It can be overridden on a command-by-command basis with the -ErrorAction flag.

try {} finally allows us to cd to the script’s directory and guarantee we’ll go back upon termination. PowerShell runs scripts a bit differently from bash. In bash, the default is to spawn a new process unless you explicitly dot-source something. In PowerShell the default is to run in-process unless you explicitly spawn a new shell. cding in a script affects the shell’s $pwd unless we explicitly cd back to where we started.

The exec function exists to run external binaries and convert non-zero exit codes into thrown errors. Again, this is a big difference between bash and PowerShell. In bash almost every command is an external process, and non-zero exit codes are interpreted as an error via set -e. In PowerShell most commands are in-process and have proper failure handling – they throw errors – but exit codes of external processes are ignored. PowerShell inherits this quirk from Windows, where exit codes do not carry the significance they do on Unix. To get bash-like behavior for external processes, we use this wrapper function. Technically, it will execute any script block, but by convention we pass a script block that runs a single external command.

Gotchas

$PSScriptRoot is only defined in files with a .ps1 extension, even with a shebang, even on Linux. I dunno why; the file needs that extension or else that variable will not be set.

Updated:

Leave a Comment