subst

Shell-like $VAR and ${VAR:default} variable substitution for Rust strings, byte strings, and JSON/TOML/YAML documents.

Library
Cargo
v0.3.8
20stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
22/100Needs Attention
Development Activity0
Maintenance0
Community20
Maturity56
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture80
Code Quality88
Innovation62
Learning Curve85

subst is a small, dependency-light Rust crate that implements shell-style variable substitution: $NAME, ${NAME}, and ${NAME:default} placeholders, with recursive expansion inside default values and backslash escaping for literal dollar signs, braces, and colons. It works on both &str and &[u8], accepts any HashMap/BTreeMap-like variable source (including the live process environment via Env), and can walk every string value inside a parsed JSON, TOML, or YAML document when the matching optional feature is enabled.

Rather than a full templating language, it targets the narrow, well-understood need of expanding environment-style placeholders in config files, CLI arguments, or generated text — the same syntax shells and tools like envsubst or Docker Compose use, but as a typed library with precise, span-aware parse errors instead of silent misbehavior.

What You Get

  • One-shot substitute() and substitute_bytes() functions for expanding a source string or byte slice against a variable map in a single call.
  • Reusable Template, TemplateBuf, ByteTemplate, and ByteTemplateBuf types that parse once and expand many times, avoiding repeated parsing overhead.
  • An Env (and EnvBytes on Unix) adapter so the live process environment can be used directly as a variable source without building an intermediate map.
  • Optional json, toml, and yaml features that recursively substitute every string value inside a parsed document of that format.
  • An indexmap feature that implements VariableMap for indexmap::IndexMap, for callers who need insertion-order-preserving variable sources.
  • Typed, span-aware errors (InvalidEscapeSequence, MissingVariableName, UnexpectedCharacter, MissingClosingBrace, NoSuchVariable) with a source_highlighting() helper that renders a ^^^ caret pointer under the offending substring for readable diagnostics.

Common Use Cases

  • Expanding ${HOME}-style placeholders in config files (TOML/YAML/JSON) at load time before deserializing them into application structs.
  • Interpolating environment variables into CLI argument strings or generated shell commands without shelling out to envsubst.
  • Templating small pieces of user-facing or log text where a full template engine would be overkill.
  • Building config-loading pipelines that need default values and recursive fallbacks, e.g. ${XDG_CONFIG_HOME:$HOME/.config}/app/config.toml.
  • Producing precise, human-readable parse-error messages (with caret highlighting) when a user-supplied template string is malformed.

Under The Hood

Architecture The public API in src/lib.rs (substitute(), substitute_bytes()) is a thin convenience wrapper around the Template/ByteTemplate types defined in src/template/mod.rs, which in turn delegate parsing and expansion to an internal raw::Template representation in src/template/raw/. Parsing happens once and produces an intermediate representation that can be expanded repeatedly against different VariableMap implementations (src/map/mod.rs), which is the trait that unifies HashMap, BTreeMap, slices, arrays, the live environment (Env/EnvBytes), and optional indexmap::IndexMap support behind one interface. A non_aliasing module handles the unsafe-adjacent lifetime bookkeeping needed to let borrowed templates and owned template buffers share the same expansion code path. The features/ module (json.rs, toml.rs, yaml.rs, indexmap.rs) layers document-aware string substitution on top of the core template engine via serde, kept behind Cargo feature flags so the dependency-free core stays minimal.

Tech Stack Pure Rust, edition 2021, with a deliberately small mandatory dependency set: memchr for fast byte scanning during parsing and unicode-width for correctly measuring multi-byte characters in error-highlighting output. Everything else — indexmap, serde, serde_json, serde_yaml, toml — is optional and gated behind Cargo features (indexmap, json, toml, yaml, plus a preserve-order flag and a nightly-only doc-cfg flag for docs.rs). Dev-dependencies use assert2 for its check!/let_assert! macros. Published to crates.io with docs.rs configured to build with all-features enabled.

Code Quality The crate has an extensive first-party test suite embedded via #[cfg(test)] modules across lib.rs, error.rs, template/mod.rs, template/raw/parse.rs, and each optional-feature module (json.rs, toml.rs, yaml.rs, indexmap.rs) — covering both success paths and exact error messages/caret positions for malformed input, including Unicode edge cases. CI (.github/workflows/rust.yml) runs cargo build/test with every feature enabled, clippy with the actions-rs/clippy-check action, and additionally runs the full test suite under cargo miri on nightly to catch undefined behavior in the crate’s unsafe lifetime-transmute code (non_aliasing.rs, Template::transmute_lifetime). Errors are modeled as typed enums (Error, ParseError, ExpandError) rather than strings, and the crate denies missing documentation and missing Debug impls via #![warn(missing_docs, missing_debug_implementations)].

What Makes It Unique Most Rust substitution crates either wrap a full templating engine or reimplement envsubst as a one-shot string function; subst instead exposes both a one-shot function and a reusable parsed-template type so repeated expansion of the same source avoids re-parsing, while still supporting byte-string input (not just &str) for callers working with non-UTF-8 or binary-adjacent data. Its standout feature is span-aware error reporting: every parse/expand error carries enough position information to render a caret (^^^) under the exact offending characters via source_highlighting(), which is unusually thorough for a library this small. Recursive substitution inside default values (${a:cruel ${b:world}}) is also handled correctly to arbitrary nesting depth, which naive substitution implementations typically get wrong.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search