Abstracting away side-effects with Higher-Order Functions in PowerShell

Cleaner, safer PowerShell code using functional abstractions

I generally write PowerShell with a pathological aversion to state and side-effects. In reality, these undesirable patterns are often unavoidable, especially in DevOps. Unavoidable CLIs like git and compilers require the stateful shell cursor to move to a different directory before the CLI can be invoked; mutable collections must be maintained; integration tests and infrastructure code often requires provisioning of temporary resources in the cloud — all of these operations change the state of our system and subsequently put the system in an unknown, difficult-to-manage state.

In this article, we review a few examples of leveraging higher-order functions to abstract away side-effect management from our scripts.

Stay stateless, my friends.

Invoke-InDirectory

The Problem

Often in DevOps, you will need to run a command inside a specific directory. For example, git (unlike most PowerShell commands) does not take a directory path argument and expects you to cd into the correct directory before executing. Similarly, application building CLIs like npm , docker , dotnet , etc., generally operate best in the directory root.

We could always Push-Location or Set-Location into the directory we need, but then we would always have to remember to come back out when the script completes. Worse, we need to add boilerplate try / finally statements in case our script fails — otherwise, the person running the script will unexpectedly find their terminal in a different location than they expected.

The Solution

We can write a higher-order function named Invoke-InDirectory to abstract away the boilerplate required to invoke a command inside a given directory without causing side-effects (the terminal is in an unexpected location if the command errors).

Now we can declaratively code the what (invoking a command in a directory) instead of the how ( try / finally statements and Push-Location / Pop-Location calls).

Invoke-InContext

The Problem

In large PowerShell scripts, script output can become an unreadable mess. It can be nearly impossible to tell exactly what line of code is emitting a given log line, or what line threw an error.

The Solution

We can implement a contextual logger that maintains a stack of context strings and logs them out when your script emits output, such that you always know exactly which context block threw the error. I usually use the Requirements framework for this very purpose; however, we can implement the same pattern in a single function, maintaining a stack to track our position in the program.

Now we can add consistent logging to our scripts, such as in this example React app build script.

All the logged output will be prefixed by a timestamp and the context stack that you can easily correlate to the tree of Invoke-InContext calls in your code.

Invoke-With<Insert your Resource>

The Problem

When deploying services, you often want to allocate temporary storage space to stage artifacts, but after the deployment, this space should be deallocated to save costs. Implementing the code to provision and delete storage can be tedious and if your script does not handle failures, your storage can remain allocated.

The generic case of this problem arises frequently in DevOps. For example, integration test pipelines may want to build expensive production infrastructure resources before running the integration tests, then clear the resources when the tests have completed.

The Solution

Rather than using our higher-order function pattern to maintain a stack, as in our two previous examples, we will instead use our pattern to run cleanup code after our scriptblock finishes executing. In this example, we create a function that automatically supplies a new storage context to the user. The user does not need to understand how to create a storage account and manage cleanup — it is all abstracted away.

Next Steps