Flags are configuration
One of the recurring problems we have in the software industry is something I like to call “code but worse,” where we constantly build tiny, domain-specific languages to solve problems, with the idea that simply writing more code would be too verbose or too burdensome. Assertion libraries, configuration languages, template processors, and build systems, to name a few.
The trouble is, as they gain more functionality, they become just as complicated as the code they were meant to abstract away. They rediscover from first principles all the reasons why their parent languages seemed verbose or complicated. Eventually, if this goes on for long enough, parallel tools start to emerge, which look suspiciously like linters, or test runners, or formatters, or type-checkers. At this point, the ecosystem has officially graduated to Code But Worse.
And yet there are some problems for which the answer can’t reasonably be “write a program.” For example, it’s possible to express any template you like as a string-concatenating program, but doing so doesn’t make it easier to read or write. On the other hand, a template language like Jinja or Liquid definitely falls under the umbrella of Code But Worse. Where do you draw the line?
Using templating here as an illustrative example, I think the line is most clearly drawn by envsubst (or os.ExpandEnv in Go): you can substitute variables with values, but you cannot express any logic - no if-then statements. If you do want to add logic, you do that in your program’s host language instead of trying to shoehorn it into a template. That’s not only easier to understand, it’s actually testable, and the templates become easier to read. This lets the template system do what it’s good at (presentation) and the programming language do what it’s good at (logic). Each side plays to its strengths and the delineation is clear.
I’ve been searching for a while for a similar answer for configuration. The landscape is a mess. dhall is likely the best answer for declarative config; Lua remains the gold standard for imperative config; everything else sits awkwardly in the middle, simultaneously too heavy-handed for quick setting tweaks yet not nearly expressive enough for complex systems.
I’m not interested in the complex cases because I believe that any sufficiently advanced configuration is indistinguishable from code and should therefore just live as code. But for the simple cases, what can you do? What’s the most minimalistic possible configuration system?
For a while I thought the answer might be environment variables. And environment variables are very useful. You don’t have to make special files for them - the user is responsible for inserting them into their environment in some way or another. You just have to read them. They can be injected pretty much anywhere by any system, and it’s easy to spawn multiple copies of an application with different configurations without having to start down the path of making, naming, and tracking multiple configuration files.
But environment variables are also global, and this makes them clumsy to use. You can only type MYAPP_SETTING=value so many times before you get sick of it.
They’re also easy to get wrong. Set MYAPPSETTING instead and there’s nothing that can tell you what you did wrong, since the namespace is global, and for all any program knows that’s a valid key for some other application.
But there’s another configuration system every bit as basic and as ubiquitous as environment variables: the humble CLI -flag.
Sometimes, when a system works well enough, it becomes such a dull fact of life that it just fades into the background. And that’s what CLI flags were for me. I’d been searching for configuration systems for so long that I forgot about the one that had been quietly working for decades.
Flags-as-configuration is so common, and works so well, that odds are good that typing alias ls or alias grep will bring up aliases you didn’t even know you had.
1
2
3
4
5
6
7
# From a fresh Ubuntu container.
$ alias ls
alias ls='ls --color=auto'
# From a Mac with oh-my-zsh installed.
$ alias grep
alias grep='grep --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox,.venv,venv}'
And that’s the beauty of flags-as-config: “adding configuration” is exactly the same as adding an alias. Most flag-aware programs will warn you if a flag is invalid, or set to an invalid value, and most respond to -h with helpful documentation on how best to configure your call.
So, knowing that, is there really a need for anything else? I’m starting to think the answer to that might be no. Flag parsers exist in every programming language I can think of. If you’ve written a tool you suspect needs to have directory-local configuration, make a .toolrc or .toolflags file that just contains… flags. Read it, toss it at your language’s flag parser, then append the rest of argv, and you’re done. Or just re-exec yourself with the flags appended if you’re feeling really lazy. No new config language or dependency required, and your configuration is already documented in your tool’s help message. Plus, it’s easy to mess around with when experimenting. No file edits necessary.
There are also conventions that make flags even more flexible. Go, for instance, nests flags in parts of its toolchain. go vet is a command-line tool for linting Go code. vet itself runs many other linters, each of which has its own flags. You can address a specific linter’s flag with a dotted name. For example, go vet -printf.funcs=myprintf allows users to customize the funcs analyzed by the printf linter. It’s easy to see how this could generalize to arbitrary degrees or types of nesting.
The only downside I can imagine is that, for a tool with lots of configuration options, you might end up with a lot of flags. But is that really such a big deal? The longest command invocation on most systems is a couple of megabytes. And if you have that much configuration, you should definitely have written a library instead.
I’m testing how far this idea can go today with go.rc, a Go project configuration file using the smallest and most minimal syntax I think I can get away with: environment variables and flags. As far as I’m concerned, the project’s syntax is already “done.” Because the moment I step outside of envsubst and basic flags, I’m well on my way back to yet another configuration language, and that way lies Code But Worse.
The bet I’m making is that flags are the envsubst of configuration. They can express a lot on their own, but they resist any kind of if-then logic. They’re easy to write, easy to read, easy to parse, and they have no practical limit. So, if you’re debating between JSON, YAML, INI, and TOML in your own projects, consider picking none of the above and going with flags instead. You might need less than you think.