Why I Built EnvUtils
TL;DR You can find EnvUtils module on Github.
Before experimenting with using Windows as a daily driver for work, I used to use Ubuntu.1
One thing I like about the Unix shell is that you can prefix a command with an environment variable, allowing the environment value to be overridden solely for that specific command.
# assuming FAVOURITE_FRUIT was not set before
FAVOURITE_FRUIT=kiwi python -c 'import os; print(os.getenv("FAVOURITE_FRUIT))'
kiwi
This situation might be familiar to those who have used the express web
framework, where setting NODE_ENV
to production
, for instance, allows validation of production
settings.
NODE_ENV=production node index.js
The cool thing about this feature is that it will restore the variable to whatever value it had before you ran the command.
# let's set this to a value
export FAVOURITE_FRUIT=apple
FAVOURITE_FRUIT=kiwi python -c 'import os; print(os.getenv("FAVOURITE_FRUIT))'
kiwi
# the value should be restored
echo $FAVOURITE_FRUIT
apple
How about Windows?
While you cannot do the same thing on Windows, here is an attempt using cmd
:
set FAVOURITE_FRUIT=kiwi & python -c "import os; print(os.getenv('FAVOURITE_FRUIT'))" & set FAVOURITE_FRUIT=
kiwi
And another using PowerShell:
$env:FAVOURITE_FRUIT='kiwi'; python -c 'import os; print(os.getenv("FAVOURITE_FRUIT"))'; $env:FAVOURITE_FRUIT=''
In both cases, we can get close to the Unix example, but it is important to note that we lose the ability to restore the variable to its previous value.
While you can modify the examples to achieve this functionality, they will become less convenient to type as one-liners and will also become less readable.
Exploring a solution using PowerShell
I was already experimenting with using PowerShell as my main shell, while occasionally using WSL as an escape hatch, for when I couldn’t be bothered to learn the proper way to do something.2
Looking at the PowerShell docs, there was a built in command that looked similar to what I wanted to
achieve: Invoke-Command
.
This cmdlet allowed you to run commands on a local or remote computer. What made me particularly
interested in it, was the the -ScriptBlock
parameter:
Invoke-Command -ScriptBlock { <# write your command here #> }
What if I could write something similar, that took the environment overrides as a parameter, much like our original example.
Building my own cmdlet
After learning enough PowerShell to become dangerous, I came up with the following:
# let's give it a value, to prove it restores the environment
$env:FAVOURITE_FRUIT = "apple"
# run the one-liner
Invoke-Environment -Environment @{ FAVOURITE_FRUIT = "kiwi" } { python -c "import os; print(os.getenv('FAVOURITE_FRUIT'))" }
kiwi
# check that the value was restored
$env:FAVOURITE_FRUIT
apple
Invoke-Environment
covers all my use cases:
- It is short enough to be easy to type
- It is readable and easy to remember what it does
- It keeps the environment intact between calls
Making it work with dotenv
files
After using Invoke-Environment
a couple of times, I wanted to keep a record of all the environment
overrides in a file, so that I could also use it on docker. For that, I had to support dotenv
files.
Here is an illustration of a typical dotenv
file usage with docker:
# filename: .env
LOG_LEVEL=debug
TARGET_URL=https://example.com
# you can inject the above dotenv file into a container
docker run --env-file .env <image>
And with Invoke-Environment
:
Invoke-Environment -EnvironmentFile .env { <# write your command here #> }
Enabling support for the dotenv
format involved writing a basic parser to convert the file into a
PowerShell Hash Table. This was such a useful feature that I exposed it as a cmdlet:
$Environment = ConvertFrom-Environment .env
$Environment["LOG_LEVEL"]
debug
Make the environment last between commands
One last common pattern that I wanted to be able to do was the ability to source a dotenv
file for
a given shell session. This would enable the use of overrides across command invocations. I still
aimed to keep the dotenv
file format, and avoid needing to preface each line with a set
solely
for running call .env
.
That being said, I added the following commands to support this workflow: New-Environment
,
Get-Environment
, and Remove-Environment
.
Here is an example workflow using these cmdlets:
# load the env into the shell session
New-Environment -EnvironmentFile .env
# run your commands
# command-1 arg1 agr2
# command-2 arg1
# you can view your session overrides at any point
Get-Environment
# you can restore the environment to the previous state
# by closing your shell or by running
Remove-Environment
Packaging it as a PowerShell module
You can find all the above cmdlets packaged as a module on Github.
Clone the project into a folder in your env:$PSModulePath
to start using it, and let me know
if you find any bugs.
I have kept the destructive commands well behaved e.g by supporting -WhatIf
and -Confirm
switches.
A PowerShell gallery will soon follow (possibly with a 1.0.0
release).