Wesley Dean
mktext, a focused text-substitution library image

mktext, a focused text-substitution library

6 min read

While working on adrctl, I found myself needing a small piece of functionality that was useful there but was not inherently about Architecture Decision Records: substitute caller-supplied values into text.

adrctl needed that capability in two closely related places:

  • users should be able to define patterns for ADR filenames;
  • users should be able to substitute values into ADR body templates.

Both cases involve text containing named tokens that need to be replaced with values. That is also how Nat Pryce’s adr-tools handles ADR templates. Since one of adrctl’s goals is compatibility with adr-tools, existing templates using bare tokens such as TITLE, NUMBER, DATE, and STATUS needed to keep working.

I looked at several libraries and template engines. They were capable tools, but most did considerably more than I wanted. I did not need a programming language hidden inside a template. I needed predictable text substitution with a very small semantic surface.

So, mktext was born.

The architectural boundary is captured in mktext ADR-001, while adrctl’s decision to consume mktext as a pinned, verified build dependency is documented in adrctl ADR-003. Those decisions explain much of what mktext deliberately does not do, which is at least as important as what it does.

What it looks like

mktext uses a caller-owned Bash associative array as a rendering context. The caller prepares the values, then mktext substitutes them into the input text:

declare -A context=()

mktext set context TITLE "Fewer Incidents"
mktext set context NUMBER4 "0042"

printf '%s\n' 'ADR {NUMBER4}: {TITLE}' | mktext render context

The result is:

ADR 0042: Fewer Incidents

That small example describes the central design: the caller owns the values; mktext owns the substitution.

Primary objectives

Pure Bash

One of the design goals for mktext was to keep its runtime requirements very small. Ordinary mktext operations require Bash 4.3 or newer and do not invoke external runtime commands. Template text is data, not shell code, and mktext does not evaluate it.

That makes mktext practical to embed in another Bash tool. adrctl, for example, incorporates a pinned and SHA-256-verified mktext release artifact into its own generated distribution, so adrctl users do not need to install mktext separately.

No built-in values

There are no “special” tokens such as timestamp, hostname, uuid, or git_branch. If a developer wants a value substituted, the caller must acquire or calculate that value and put it into the context explicitly.

I like the mental model used in mktext’s architecture documentation:

Acquisition    -> caller
Transformation -> caller
Rendering      -> mktext

If a project needs the current date, it obtains the date. If it needs a slug, it creates the slug. If it needs a padded number, it pads the number. mktext does not need to know why any of those values exist or what they mean.

Predictable, repeatable, deterministic behavior

Given the same context and the same input text, mktext should produce the same output. Rendering is lexical, literal, and single-pass. Replacement values are inserted exactly as supplied, and inserted values are not scanned again for more macros.

For example, if {A} is replaced with the literal value {B}, mktext leaves the result as {B} rather than performing another substitution pass. That behavior keeps rendering easier to reason about, test, and safely compose.

Configurable delimiters

Native mktext templates use braces by default, such as {TITLE} and {NUMBER4}. The delimiters are configurable per render operation, however, and they are treated as literal strings rather than regular expressions.

That matters to adrctl because legacy adr-tools templates use bare tokens with no delimiters. mktext supports that mode by allowing both delimiters to be set to empty strings. It can also support conventions such as ``, [[TITLE]], or other literal delimiter pairs without adding a second rendering engine.

Bare-token mode is deliberately lexical rather than a naive search-and-replace. A TITLE token does not accidentally match the TITLE portion of SUBTITLE. That distinction is especially important when templates contain ordinary prose.

No filters or transformations

Some template engines support expressions such as `` or provide built-in formatting, arithmetic, case conversion, padding, and other transformations. mktext deliberately avoids all of them.

Formatting belongs to the caller. If an ADR number needs to be padded to three digits, for example, Bash can prepare the value before rendering:

printf -v number3 '%03d' "$number"
mktext set context NUMBER3 "$number3"

mktext then receives the already formatted value. This keeps policy out of the renderer and makes each transformation explicit where the relevant context is known.

Avoid guessing

I wanted invalid API usage to fail rather than invite mktext to guess what the caller meant. The delimiter pair is a good example: both delimiters may be non-empty, or both may be empty, but a one-sided empty delimiter is rejected.

At the same time, ordinary template text is preserved conservatively. Unknown recognized macros remain unchanged, malformed template text remains unchanged, and unrelated text is not treated as an instruction. The goal is predictable behavior, not clever recovery.

UNIX-like reasoning

A hallmark of UNIX-like design is that a tool should own a small slice of work and do that work well. mktext takes that approach seriously. It substitutes named values into text and leaves acquisition, filtering, preprocessing, post-processing, translation, formatting, and policy to the caller.

That narrow scope is the feature. A caller remains free to compose mktext with whatever other logic the application requires without asking the rendering library to understand the application itself.

mktext is available on GitHub, including its specification, Architecture Decision Records, tests, and release artifacts.

Next, I’ll look at another component tool: bash-doxygen.

Tags