Autoconf’s revenge: ad-hoc shell templates
Blog System/5
SubscribeSign in
Autoconf’s revenge: ad-hoc shell templates<br>How to leverage an old dirty trick in your Bazel wrapper for great effect
Julio Merino<br>Jul 08, 2026
Share
As powerful as Bazel is, sometimes it’s not featureful enough. When using this build system, it’s common practice to wrap it in a launcher script—and in fact, this is natively supported by Bazelisk, Bazel’s native dispatcher that stands for the bazel binary in the user’s PATH. Bazelisk will first download the version of Bazel requested by the project, and then, if tools/bazel exists, invoke it instead of the downloaded binary. tools/bazel is what’s known as a Bazel wrapper and is the point of today’s article.<br>Well, not quite. The actual point of today’s article is to demonstrate a simple trick I learned from the GNU Autoconf and Automake days to implement full-blown conditionals in an ad-hoc template system. But because such trick is trivial once you see it, I have to present it in the context of a modern real-world scenario. So what I’m going to do is guide you through the creation of your very own Bazel wrapper to customize Bazel’s .bazelrc configuration file in ways that the native Bazel tool doesn’t support.<br>Let’s get started.<br>But wait! Take a moment to subscribe. I’m sure you’ll enjoy future posts, and it’s the only way for me to know that they are worth writing in the first place!
Subscribe
The context
Template systems are everywhere. Take any static blog generation system and you’ll find some. Take system management tools like Ansible and you’ll find others. Take a cloud orchestration service like Kubernetes and you’ll find Helm. Heck, even Go’s standard library provides a full blown text template system out of the box.<br>There is clear benefit and appetite for these and, surely enough, it’s tempting to use any pre-existing such system in your own project… but if all you need are a bunch of variable replacements, some of which may be only conditionally applied, you can go a long way by not taking any dependencies. A call to sed or the substring function of your language of choice is all you need.<br>To put this in context, let’s say you have Bazel’s .bazelrc configuration file, which is not very flexible, and that you need to set some arguments based on dynamic values that depend on the environment. E.g. something like this:<br># Developer builds<br>startup --host_jvm_args=-Xmx4g<br>build --jobs=200
# CI builds<br>startup --host_jvm_args=-Xmx16g<br>build --jobs=1000If you know a little bit about .bazelrc, however, you may squint at that and say: “That’s silly! Make those flags conditional on a configuration and you’re set!” So you try something like this:<br># Developer builds<br>startup:dev --host_jvm_args=-Xmx4g<br>build:dev --jobs=200
# CI builds<br>startup:ci --host_jvm_args=-Xmx16g<br>build:ci --jobs=1000
# Enable dev as default configuration<br>common --config=dev<br># ... but CI can pass --config=ci on the CLI to override.And… this does not work. Gotcha! Startup flags cannot be placed behind a configuration so there is no way for you to parameterize the JVM’s max heap value passed in -Xmx. And having to remember to pass --config=ci from CI all the time is fragile, because you might forget and not get the desired configuration in place.<br>Basic string replacements
Solving the above is not difficult if we could parameterize the configuration. We might want to write something like this instead:<br>startup --host_jvm_args=-Xmx@MAX_HEAP@<br>build --jobs=@JOBS@… and then have @MAX_HEAP@ and @JOBS@ be replaced dynamically depending on some runtime arbitrary logic. We can do that via the Bazel wrapper, and this sort of dynamic configuration is a common thing to do from it. So let’s do this.<br>Let’s start with the template logic:<br>declare -A SUBSTS
subst() {<br>local var="${1}"; shift<br>local value="${1}"; shift
SUBSTS["${var}"]="${value}"
generate() {<br>local in_file="${1}"; shift<br>local out_file="${1}"; shift
local args=()<br>for var in "${!SUBSTS[@]}"; do<br>local value="${SUBSTS[$var]}"<br>args+=( -e "s|@${var}@|${value}|g" )<br>done<br>sed "${args[@]}" "${in_file}" >"${out_file}"
Ugly(?) bash syntax but nothing too complicated:<br>The global SUBSTS hashmap tracks variable names and their replacement values.
The subst function inserts a new key/value pair into SUBSTS. (It’s important that the values given to subst don’t contain sed-special characters like &, backslashes, or the | separator we chose—but we control the generation of those values so we are good.)
The generate function transforms the hashmap into a set of sed arguments of the form -e s|@VAR@|VALUE|g and then calls sed to process the given input file into the given output file.
Then, we can plug everything together into a minimal Bazel wrapper:<br>#! /bin/bash
set -euo pipefail
WORKSPACE="$(cd $(dirname "${0}")/.. && pwd -P)"<br>readonly WORKSPACE
# ... insert template logic from above.
run() {<br>local bazelrc<br>bazelrc="$(mktemp -p "${TMPDIR:-/tmp}" bazelrc.XXXXXXXX)"<br>trap "rm...