Loading PowerShell Args from JSON Configuration
I was working on a PowerShell script this afternoon, and found that I was passing the same parameters over and over again. As a developer, I’d normally just wrap calls to the desired function, and handle any necessary abstraction in my wrapper function for common parameters, but PowerShell has this wonderful language feature called Splatting that is really well suited to solving this problem in a really concise way. I also decided I wanted to lift some of this wonderful goodness to a JSON configuration file, because that enables some other really interesting scenarios. Let’s dig in!
When I write PowerShell scripts that I intend to share, I will generally lift some of the configuration out of the script, and store it externally. In the dev space, we have this super-fancy pattern for this sort of thing called External Configuration Store. It’s actually not all that fancy, though. We’re just decoupling certain configuration data from our application/script/whatever it is code so that they can be versioned semi-independently. As a rule of thumb, the only time the two ought to change together is when the configuration parameters (schema) changes, as in the case of adding/removing/renaming a configuration setting. I tend to prefer JavaScript Object Notation (JSON) for storing this sort of data for several reasons:
- I find JSON to be much more readable than most XML
- XML has too much ceremony for my taste in the most trivial samples
- There exist JSON scheme generators and validators, just like in XML, so you’re not losing any benefits in that area
- JSON and XML are both ubiquitous serialization formats, which is really good for platform interoperability
- JSON is understood natively in every major web browser, which creates additional future scenarios for customers
The readability thing takes the cake for me primarily. JSON is just simple, and most of the time, I’m not writing anything that’s uber-complicated.
Whenever I create a JSON configuration file, I’ll usually include the following lines of code in my PowerShell scripts:
$configPath = Join-Path $PSScriptRoot "config.json"
$configuration = Get-Content $configPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
This is lovely. If the user has done something to muck with my configuration, I’ll just let everything blow up. It’s not terribly graceful, but in most cases, the distance between my users and I is a few desks, so triaging this sort of error is fairly trivial.
For this post, we’re going to do something really simple: create a table of all PowerShell scripts in a directory, detailing the file name, the directory name (aka, the system file path), the length in megabytes, the last modified time (UTC), and the creation time (also UTC). This can all be achieved in two PowerShell commands, and allows me to demonstrate the primary concept of the blog post - splatting from a JSON configuration.
For starters, let’s we’ll just hard-code our script:
Get-ChildItem -Path "C:\Temp" -Filter "*.ps1" -Recurse
But that’s ugly: what if this one-line script is super long, and we call Get-ChildItem multiple times, and we want to pass a set of common parameters in each case, or we just don’t like having really long argument lists to cmdlets because our monitors can’t fit the function call on a single line so we have to resort to either word wrapping or horizontal scrolling? How can we deal with this madness?! Splatting. That’s how. Here’s what that looks like:
$listFilesArgs = [hashtable]@{
Path = "C:\Temp"
Filter = "*.ps1"
Recurse = $true
}
Get-ChildItem @listFilesArgs
Isn’t that so much better? Probably not in this example, because I’ve just added the use of a potentially
unfamiliar language feature to a really simple script. In my real-world example, I want to pass common
arguments to the Invoke-WebRequest
and Invoke-RestMethod
cmdlets, each of which is executed a bunch of
times in my script, so it makes a lot more sense. This is just an example, though, so let’s roll with it.
Now in case you’re not familiar with splatting, here’s the gist: the PowerShell runtime will happily allow
you to bind an arbitrary list of parameters and their values, which it well then “bind” to the callee (in
this case, the Get-ChildItem
cmdlet). This is achieved with the @listFileArgs
syntax above. The line
Get-ChildItem @listFilesArgs
basically says:
I want to call the
Get-ChildItem
cmdlet. When this cmdlet is called, I would like to take any parameters from the$listFilesArgs
variable, and map them to the equivalent parameters in the cmdlet.
There are some rules to doing this. You can’t, for example, add a key-value pair of BogusArg = 0..10
. That
is to say, PowerShell will see the key name BogusArg
, attempt to resolve it in the cmdlet metadata, determine
that no parameter exists with the name BogusArg
, and pitch a fit saying something along the lines of
Get-ChildItem : A parameter cannot be found that matches parameter name 'BogusArg'.
.
Now, how do we lift all of this out of our script? That’s actually pretty straight forward. First, we’ll
create a file in the same directory as our PowerShell script called script-config.json
. Then we’ll open it in
our favorite editor (for me, either Notepad or VSCode), and insert the following text:
{
"GetChildItemArgs": {
"Path" = "C:\\Temp"
"Filter" = "*.ps1"
"Recurse" = true
}
}
Next, we’ll update our PowerShell script so that it reads:
$configPath = Join-Path $PSScriptRoot "script-config.json"
$configuration = Get-Content $configPath -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
$listFilesArgs = $configuration.GetChildItemArgs -as [hashtable]
Get-ChildItem @listFilesArgs
That’s pretty straight forward, right? Our script now says:
- Form the path to the configuration file
- Deserialize the content from the configuration file
- Convert the
GetChildItemArgs
property from the converted JSON to a hashtable - Call the
Get-ChildItem
cmdlet.
I know the example is somewhat contrived (like most examples), but the purpose is to demonstrate a technique that may be useful for you in your scripting. I’m personally going to adopt this in some of my scripts because it really simplifies what I’m doing, and alleviates me from having to ensure that I’ve supplied the default parameters for a particular cmdlet that I intend to call a bunch of times with a common set of base-arguments.
I’d love to get some feedback about this technique if you feel inclined to do so. Please visit the footer of this page to find contact information, and reach out with questions.
Happy coding!
- Brian
References: