Why I Built envfmt
Explains the "why" behind the crate, like why I chose a Context trait and kept it simple.
I’ve already mentioned that I built envfmt
because I couldn’t find a simple
crate for shell-style variable expansion. But there’s a bit more to it than
that. This page explains some of the thinking behind its design.
Keep It Simple
My main goal was to keep the crate as simple as possible. I only needed a few
common features: $VAR
, ${VAR}
, and ${VAR:-default}
. A lot of other
templating and formatting libraries out there are amazing, but they do way more
than I needed. They come with loops, conditionals, and complex parsing. That’s
great for a full templating engine, but for my use case in cargo-eth
, it was
too much.
I wanted something with no dependencies, so it wouldn’t add bloat to my
projects. Right now, envfmt
only uses thiserror
, and that’s just for making
the error types a bit nicer. The core logic is all standard library Rust.
The Context
Trait
The biggest design decision was to create the Context
trait. At first, I was
just going to write a function that took a HashMap
. That would have worked,
but it felt a bit limiting.
What if my data wasn’t in a HashMap
? I’d have to convert it first. That’s
boilerplate code I don’t want to write. And what about environment variables?
I’d have to load them all into a HashMap
just to pass them to the function.
That felt wasteful.
So, I came up with this trait:
pub trait Context {
fn get(&self, key: &str) -> Option<String>;
}
This is an abstraction. It separates the formatting logic from the data source.
The format_with()
function doesn’t care where the data comes from. It just
knows how to ask for it. This is why it can work with environment variables,
HashMap
s, and custom structs without any changes.
This approach also made the crate much easier to test. I can pass a simple
HashMap
in my tests instead of having to set and unset real environment
variables, which can be slow and messy.
What I Left Out
There are a lot of other shell expansion features I didn’t include, like
${VAR:?error}
, ${VAR:=default}
, or ${VAR#prefix}
. I left them out on
purpose to stick to the “keep it simple” rule. The features I did include cover
what I think is the most common 90% of use cases for this kind of simple
substitution.
If I ever find myself really needing one of them, I might add it. But for now, I like that the crate is small and does one thing well.