Introduction
Welcome to my digital workshop.
You’ve stumbled upon a living collection of code, ideas, and experiments - part documentation, part developer’s journal, and entirely a reflection of what keeps me tinkering late into the night.
My name is Ivan Chetchasov. I write systems in Rust, design programming languages for fun, and occasionally argue with compilers about borrow checking. This book is where I gather the fruits of that labour: libraries, utilities, and whatever else emerges from the intersection of curiosity and caffeine.
What You’ll Find Inside
This isn’t a single‑product manual. It’s a workshop — each chapter stands on its own, but together they show a consistent philosophy:
- Practicality first – If it doesn’t solve a real problem, it doesn’t belong here.
- Educational by accident – I write code that’s readable, and I explain why things work the way they do.
- Experimental without apology – Some projects are production‑ready; others are playgrounds. Both are valuable.
Here’s a taste of what’s already inside (and what’s coming):
The inherit Ecosystem
A Git‑native templating system that turns any repository into a reusable project generator.
No more copy‑paste‑search‑replace. Just cargo inherit user/template to my-project.
inherit-core– the engine that scans, replaces, and respects.inherignorecargo-inherit– the CLI that adds aliases, defaults, caching, and interactive prompts
A Rust Dialect (Working Title: “Dust”)
This is a secret so far ;P
Utility Crates
kissreplace– stupid‑simple placeholder replacement (scan, replace, validate)lazyget– lazy loading with cachinginherit-coreandcargo-inherit(already mentioned)- And more that will appear as I write them
> Read more on kissreplace > Read more on lazyget
Blog‑Style Posts
From time to time I’ll drop a chapter that isn’t code‑heavy but reflects on:
- “Why I rewrote the template scanner three times”
- “Lessons from building a small GUI library”
- “The joy and pain of custom Rust-like syntax”
Think of it as a technical blog embedded in a book.
How to Read This Book
The chapters are arranged roughly by maturity:
- Done ✅ –
inherit-core,cargo-inherit(full-featured guides for existing crates) - In Progress ⏳ – you already can read it, but it can contain disinfo or typos
- Experimental ⚠️ – sort of chapters not for wide audicy
- Blog ❤️ – random thoughts and post‑mortems
You can jump directly to any chapter. The sidebar navigation is your friend.
Contact & Contributions
I love hearing from readers — whether it’s a bug report, a question, or just “hey, this helped me”.
- Email: vi.is.chapmann@gmail.com
- Telegram: @viqxq
- WhatsApp: +7(993)3533292
If you find a typo, a broken link, or a code snippet that doesn’t compile, please let me know.
Better yet, open a pull request on the GitHub repository for this book.
A Note on “Ivan Chetchasov”
Yes, that’s my real name. Yes, it’s a mouthful. You can call me Ivan or Vi — I answer to both. The vi.is.chapmann email address is a tiny homage to my past.
Ready?
Let’s build something interesting.
“The best way to predict the future is to implement it.”
— Alan Kay (sort of)
Proceed to the next chapter, or pick whatever catches your eye. The code is waiting.
My Crates
This part of the book describes my crates. Guides and documentation: all what you need to use my crates. Documented are extra-tiny crates too: no one question should appear, I hope.
cargo-inherit – The Inherit CLI
Welcome to cargo-inherit – the command‑line tool that turns any Git repository into a
reusable, customisable project template. If you’ve ever copied a boilerplate project and
then manually replaced all the placeholders, this tool is for you.
cargo-inherit is a thin but powerful wrapper around inherit-core.
It adds:
- Template discovery – from GitHub or any Git URL
- Aliases – short names for long template paths
- Default values – pre‑fill variables like
AUTHOR - Smart caching – cloned templates are stored for speed
- Interactive prompts – ask for missing variables with nice defaults
- Post‑creation hooks – run shell commands after generation
(e.g.
cargo fmt,git add .) - Configuration file – keep your preferences and secrets safe
Let’s dive in!
Installation
cargo-inherit is a Rust binary. Install it from crates.io:
cargo install cargo-inherit
Make sure ~/.cargo/bin is in your PATH. After installation, the command is cargo-inherit – but you can also use inherit if you add an alias or rename the binary.
Note: The binary is named
cargo-inherit, but the examples in this chapter will useinheritfor brevity. In practice you can runcargo inheritif you havecargo-inheritinstalled (Cargo forwards subcommands).
Quick Start
Generate a new Rust library from the hypothetical cargo-lib template:
inherit rust-lib/cargo-lib to my-project
The CLI will:
- Clone
https://github.com/rust-lib/cargo-lib(cached for next time). - Read
Inherit.tomland scan all files for@VARIABLES@. - Ask you for values (with helpful descriptions and defaults from config).
- Generate
my-project/with all placeholders replaced. - Run
git initand execute anypost_createhooks.
If you have a local template directory, you can use a file:// URL or an absolute path:
inherit /home/me/my-templates/rust-lib
Commands Reference
generate – The Main Event
Syntax: inherit <template> [to <directory>]
template– can be:user/repo(GitHub shorthand)- full URL (
https://...,file://..., or absolute path) - an alias (see below)
directory– where to create the project (defaults to current directory)
Example:
inherit alice/awesome-template to my-app
If alice/awesome-template requires variables like PROJECT_NAME and AUTHOR, you’ll be prompted interactively. Values from your config’s [defaults] table are offered as suggestions.
alias – Shorten Your Favourite Templates
| Command | Description |
|---|---|
inherit <template> to alias <name> | Create an alias for a template |
inherit alias list | Show all aliases |
inherit alias remove <name> | Delete an alias |
Example:
# Add alias
inherit rust-lib/cargo-lib to alias rlib
# Use it later
inherit rlib to new-project
# See all aliases
inherit alias list
# Remove it
inherit alias remove rlib
Aliases are stored in your config file under [aliases].
default – Pre‑set Variable Values
| Command | Description |
|---|---|
inherit default for <VAR> | Set a default value for a variable |
inherit default list | Show all defaults |
inherit default unset <VAR> | Remove a default |
Defaults are used as the initial suggestion when the prompt appears. You can still override them by typing a different value.
Example:
# Set your name once
inherit default for AUTHOR
# > prompts: "Default value for AUTHOR [Your Name <you@example.com>]:"
# Type a new value or press Enter to keep the current one.
# Later, every template that asks for AUTHOR will suggest this value.
Defaults are stored in [defaults] in the config file.
cache – Manage Downloaded Templates
| Command | Description |
|---|---|
inherit cache list | Show cached templates and their disk usage |
inherit cache clean | Delete everything in the cache |
Templates are cached after the first clone. This speeds up subsequent generations and allows offline use. The cache directory is configurable (see below).
Configuration File
On first run, cargo-inherit creates a config file at:
- Linux / macOS:
~/.config/inherit/config.toml - Windows:
%APPDATA%\inherit\config.toml
You can override the location with the INHERIT_CONFIG environment variable.
Here’s a full annotated example:
# Default values for template variables.
# When a template requires a variable listed here, its value is used as the
# default suggestion — you can still override it via the interactive prompt.
[defaults]
AUTHOR = "Your Name <you@example.com>"
VERSION = "0.1.0"
# Short aliases for templates.
[aliases]
rlib = "rust-lib/cargo-lib"
blog = "https://github.com/my/blog-template.git"
# Directory used to cache downloaded templates.
# Supports ~/ expansion.
cache_dir = "~/.cache/inherit"
# GitHub personal access token (for private repositories).
# Must have "repo" scope.
github_token = "ghp_..."
# Whether to automatically run `git init` in generated projects.
init_git = true
# Whether to execute `post_create` hooks defined by templates.
run_hooks = true
# Command to open the project after generation (e.g., "code", "nvim", "idea").
open_with = "code"
Environment Variables
| Variable | Description |
|---|---|
INHERIT_CONFIG | Full path to config file (overrides default) |
INHERIT_CACHE_DIR | Directory for cached templates (overrides cache_dir) |
INHERIT_NON_INTERACTIVE | If set to 1, never prompt – fails if defaults missing |
Non‑interactive mode is useful in CI or scripts. Example:
INHERIT_NON_INTERACTIVE=1 inherit user/repo to output
How It Works Under the Hood
When you run inherit user/repo:
- Resolve –
user/repois transformed into a Git URL (https://github.com/user/repo.git) unless it’s an alias or a full URL. - Fetch –
git clone --depth 1is used to download the template into the cache (keyed by the canonical URL). Subsequent runs reuse the cached copy. - Load –
inherit-corereadsInherit.tomland scans all files for@VAR@placeholders, respecting.inherignore. - Prompt – For every variable found, the CLI shows a prompt with its description (from
[variables]) and your configured default (from[defaults]). - Process – All files and folders are copied to the target directory, with
@VAR@replaced in both contents and filenames. Binary files are copied unchanged. - Finalise – If
init_git = true, a freshgit initis run in the target. Then anypost_createhooks from the template are executed (usingsh -con Unix,cmd /Con Windows). - Open – If
open_withis set, the CLI launches that command with the target directory as an argument.
Template Manifest (Inherit.toml)
Example for hypothetical cargo-lib template:
[template]
name = "cargo-lib"
description = "Minimal Rust library template"
[variables]
PROJECT_NAME = "Name of the project"
AUTHOR = "Author name and email"
VERSION = "Initial version"
DESCRIPTION = "Short description of the library"
[hooks]
post_create = ["cargo fmt", "git add ."]
[variables]keys are the placeholders (without@). Their values are descriptions shown to the user.post_createcommands are run in the generated project directory.
The .inherignore File
Just like .gitignore, but for excluding files from the template. Example:
target/
Cargo.lock
.git/
Any file or folder matching these patterns will not be copied into the new project. This keeps your template clean of build artifacts or tool‑specific clutter.
Advanced Topics
Using Private GitHub Repositories
Set github_token in your config. The token is inserted into the clone URL:
https://<token>@github.com/user/repo.git
Make sure your token has at least repo scope.
Local Templates Without Git
You can point directly to a directory:
inherit /absolute/path/to/template
The tool will not try to clone it; it will use the directory as‑is. This is perfect for rapid iteration.
Caching Behaviour
- Each template is cached under a stable identifier derived from its canonical URL.
git clone --depth 1is used to keep the cache small.- To update a cached template, delete it from the cache (
inherit cache cleanwill nuke everything) or manually rungit pullinside the cache directory.
Post‑Create Hooks Security
Hooks are just shell commands. They run with the same privileges as the user. Use them responsibly – do not clone untrusted templates without reviewing their Inherit.toml.
Troubleshooting
| Problem | Solution |
|---|---|
error: Manifest "..." not found | The template directory is missing an Inherit.toml file. Every valid template must have one (even if empty). |
error: The following required variables are missing: [...] | You ran in non‑interactive mode without providing defaults for those variables. Either set them in [defaults] or run without INHERIT_NON_INTERACTIVE. |
git clone failed | Check your network, the repository URL, and your GitHub token if private. |
cannot determine config directory | dirs crate failed. Set INHERIT_CONFIG explicitly. |
Invalid variable name | Variable names must match [A-Z][A-Z0-9_]* (uppercase, underscore allowed). |
Putting It All Together – A Real‑World Workflow
Imagine you maintain a company template for microservices:
-
Write the template in a repo
mycorp/microservice-template. -
Add aliases for your team:
inherit mycorp/microservice-template to alias mcsrv -
Set company defaults:
inherit default for AUTHOR # -> type "Engineering Team <eng@mycorp.com>" inherit default for LICENSE # -> type "MIT OR Apache-2.0" -
Generate a new service:
inherit mcsrv to payment-serviceThe tool will ask only for the project‑specific variables (e.g.
SERVICE_NAME,PORT). The company defaults are already filled. -
Automatically open in VS Code and run
cargo buildvia a hook – your team is productive in seconds.
Conclusion
cargo-inherit brings the power of templating to your terminal, with a focus on simplicity and reusability. Whether you’re scaffolding a personal blog, a microservice, or an entire monorepo, this tool will save you from tedious copy‑paste and search‑replace.
The CLI is the friendly face of the inherit-core engine. Together they form a lightweight, Git‑native templating system that fits naturally into your development workflow.
Give it a try – your future self will thank you.
Links
The Engine Behind Inherit Templates: inherit-core
Welcome to the core library that powers Inherit!
If you’ve ever wished for a simple, Git‑friendly way to stamp out project templates with
dynamic placeholders, you’re in the right place. inherit-core does all the heavy lifting:
scanning files, replacing variables, respecting .inherignore rules, and even running
post‑creation hooks.
In this chapter we’ll explore the library’s design, how to use it programmatically, and peek under the hood at its main components.
What Problem Does It Solve?
Copy‑pasting a template project leads to stale copies, inconsistent naming, and tedious search‑and‑replace. A better approach:
- Keep a source template with placeholders like
@PROJECT_NAME@. - Let the tool scan the template to discover which variables are needed.
- Ask the user (or a script) for concrete values.
- Generate a new project with all placeholders replaced – including file names and folder names!
inherit-core implements exactly that pipeline, while being completely agnostic about the
user interface. The CLI tool cargo-inherit uses it to ask questions interactively, but
you could also drive it from a build script or a GUI.
Core Concepts
| Concept | Description |
|---|---|
| Template | A directory containing an Inherit.toml manifest and arbitrary files with @VAR@ placeholders. |
| Manifest | TOML file that declares variables (with descriptions) and optional hooks. |
| Placeholder syntax | @UPPER_SNAKE_CASE@ – powered by the [kissreplace] crate. |
.inherignore | Git‑ignore style file to exclude certain paths from processing. |
| Post‑create hooks | Shell commands (sh or cmd) run after the project is materialised. |
Note:
inherit-coredoes not prompt the user for missing variables. That’s the caller’sresponsibility. The library only validates that all required variables are supplied and non‑empty.
A Bird’s‑Eye View of the Pipeline
+-------------+ +---------------------+ +--------------+
| Template |------->│ load_template |------->| Context |
| Directory | │ (scan + manifest) | | (vars + desc)|
+-------------+ +---------------------+ +------+-------+
|
V
+--------------+ +------------------+ +--------------+
| Final Values |------>| process_template |---------->| New Project |
| (Variables) | | (replace, copy) | | Directory |
+--------------+ +------------------+ +--------------+
- Load – Read
Inherit.tomland scan all template files to collect every@VAR@occurrence. - Prompt (outside the crate) – The caller collects concrete values from the user.
- Process – Copy every file/folder, replacing placeholders in content and
path names, respecting
.inherignore. - Finalise – Optionally run
git initand executepost_createhooks.
The Modules in Detail
error.rs – Clear, actionable errors
All fallible operations return Result<T, InheritError>. The error enum distinguishes between:
- Missing manifest (
ManifestNotFound) - Parse failures (
ManifestParse) - Missing variables (
MissingVariables) - IO and command failures
#![allow(unused)]
fn main() {
pub enum InheritError {
Io(#[from] std::io::Error),
ManifestNotFound(PathBuf),
ManifestParse(#[from] toml::de::Error),
MissingVariables(Vec<String>),
InvalidVariable(String),
CommandFailed { cmd: String, status: ExitStatus },
KissReplace(#[from] kissreplace::KissReplaceError),
}
}
manifest.rs – The template’s configuration
Deserialises Inherit.toml with three optional sections:
[template]
name = "cargo-lib"
description = "Minimal Rust library template"
[variables]
PROJECT_NAME = "Name of the project"
AUTHOR = "Author name and email"
[hooks]
post_create = ["cargo fmt", "echo 'Done!'"]
The variables map serves two purposes:
- It defines which variables the template expects (extra variables found in files are also required).
- The string value is a description (shown to the user when prompting).
#[serde(default)]makes every field optional – a template can have no manifest at all (though you’d lose descriptions and hooks).
ignore.rs – What to skip
inherit-core respects two layers of ignoring:
- Always ignored –
"Inherit.toml",".inherignore",".git"(and anything inside.git/). - User‑defined – via a
.inherignorefile in the template root, using.gitignoresyntax.
#![allow(unused)]
fn main() {
let ignore = InheritIgnore::load(template_dir);
if ignore.is_ignored(relative_path, is_dir) {
continue; // skip this file/folder
}
}
You can exclude build artifacts, lock files, or any generated content that shouldn’t be copied into new projects.
scanner.rs – Discovering variables
The scanner walks the template directory (respecting ignores) and reads every text file.
It uses kissreplace::scan::extract_vars to find all @...@ placeholders.
The result is a HashSet<String> of required variable names.
Why scan? Because a template author might forget to list a variable in [variables].
The scanner ensures nothing is missed – the union of manifest‑declared and scanned
variables becomes the final required set.
pipeline.rs – The heart of the operation
Two public functions drive everything:
load_template(source_dir: &Path) -> Result<TemplateContext>
Returns a TemplateContext containing:
- The parsed
Manifest required_vars– all variables that must eventually be providedvar_descriptions– descriptions from the manifest (empty string if not declared)
You’d call this first to show the user a list of what they need to fill in.
process_template(source_dir, target_dir, final_vars, opts) -> Result<ProcessResult>
This is the real workhorse. It:
- Validates variable names (must be
^[A-Z][A-Z0-9_]*$– bykissreplace’s rules). - Checks that all required variables are present and non‑empty.
- Creates the target directory.
- Walks the source, respecting always‑ignored and
.inherignoreentries. - For each file:
- If it’s a directory -> create it in the target (after replacing placeholders in its name).
- If it’s a file:
- Try to read as UTF‑8 -> replace placeholders in the content, write as text.
- On failure (binary file) -> copy byte‑for‑byte (no replacement).
- If
opts.init_gitis true -> runsgit init -qin the target. - If
opts.run_hooksis true -> executes eachpost_createcommand in order.
The function returns counts of processed text files and copied binary files.
Putting It All Together – A Complete Example
Let’s simulate what the CLI would do. We’ll use the built‑in cargo-lib example template.
use inherit_core::{load_template, process_template, ProcessOptions, Variables};
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let template_dir = "./examples/cargo-lib";
let target_dir = "./my-new-lib";
// 1. Load template to know what variables are needed
let ctx = load_template(template_dir.as_ref())?;
println!("Required variables: {:?}", ctx.required_vars);
// 2. Collect values (normally you'd ask the user)
let mut vars = Variables::new();
vars.insert("PROJECT_NAME".into(), "my_awesome_lib".into());
vars.insert("AUTHOR".into(), "Jane Doe <jane@example.com>".into());
vars.insert("VERSION".into(), "0.1.0".into());
vars.insert("DESCRIPTION".into(), "Does something cool".into());
// 3. Process the template
let opts = ProcessOptions::default(); // init_git = true, run_hooks = true
let result = process_template(
template_dir.as_ref(),
target_dir.as_ref(),
&vars,
opts,
)?;
println!("Generated {} text files, {} binary files",
result.processed_files, result.binary_files);
// Check that placeholders are gone
let cargo_toml = fs::read_to_string(target_dir.join("Cargo.toml"))?;
assert!(!cargo_toml.contains('@'));
Ok(())
}
When you run this, the target directory will contain a fresh Rust library
project with name = "my_awesome_lib" and a .git folder
(because init_git defaulted to true).
Advanced Features
Placeholders in File and Folder Names
The replacement isn’t limited to file contents – it also applies to paths. This template file:
src/@PROJECT_NAME@/mod.rs
will be created as src/my_awesome_lib/mod.rs. Very useful for language‑specific
layouts (e.g. Python packages, Java namespaces).
Binary Files are Copied Unchanged
If a file cannot be read as UTF‑8, inherit-core assumes it’s binary and performs
a byte‑wise copy. No placeholder replacement happens, so your images or
compiled assets stay intact.
Post‑Create Hooks on Windows and Unix
The hooks.post_create commands are executed using:
sh -c "command"on Unixcmd /C "command"on Windows
This gives you maximum portability. A typical hook might run cargo fmt, git add ., or npm install.
Error Handling in Practice
The CLI tool uses InheritError to produce user‑friendly messages. For example:
- MissingVariables – prints the list of variables the user forgot to provide.
- CommandFailed – shows which hook failed and its exit status.
- ManifestNotFound – suggests maybe the path isn’t a valid template directory.
Because every error implements std::error::Error, you can use anyhow or thiserror in your own wrapper.
Testing Strategy
The crate includes integration tests that:
- Run the
cargo-libexample template end‑to‑end. - Verify missing variables trigger the right error.
- Test variable replacement inside file names (the
test_variable_in_filenamecase).
These tests use tempfile::tempdir() to avoid polluting the source tree. They also disable
init_git and hooks to keep tests fast and deterministic.
Why kissreplace?
The placeholder engine was deliberately kept tiny and fast. [kissreplace] provides:
- Scanning – extract all
@VAR@names from a string. - Replacement – efficient, single‑pass substitution.
Its “kiss” philosophy aligns perfectly with inherit-core: no regex magic, no accidental
partial replacements, just clear semantics.
When to Use inherit-core Directly
You might bypass the cargo-inherit CLI if you want to:
- Integrate templating into a larger build system (e.g. a workspace generator).
- Provide a different user interface – a TUI, a web form, or environment‑variable driven generation.
- Automate template instantiation in CI/CD pipelines.
Simply add inherit-core as a dependency, and you get the entire templating engine without any interactive baggage.
cargo add inherit-core
Conclusion
inherit-core is a focused, well‑tested library that turns any directory into a reusable, parameterised template.
It respects ignore files, replaces placeholders everywhere (even in paths), and runs hooks to finalise the generated
project. Whether you’re building the official cargo-inherit tool or your own bespoke generator, this crate gives
you a solid foundation – and keeps the magic behind @YOUR_VARIABLES@.
Now go ahead, create some templates, and let inherit-core do the repetitive work for you!
Links
Kissreplace: A Minimalist Template Engine
Welcome to kissreplace – a tiny, no‑nonsense template engine that lives by the KISS (Keep It Simple, Stupid!) principle.
If you need to replace placeholders like @VAR@ in strings, file paths, or whole collections, this crate does exactly that and
nothing more. No complex DSLs, no runtime overhead you didn’t ask for - just plain, predictable substitution.
What it does?
- Finds every occurrence of
@VAR@in a string, whereVARfollows a simple naming rule (letters, digits, underscore, and must start with a letter or underscore). - Replaces it with a value from a hash map (
HashMap<String, String>). - Leaves invalid or missing variables untouched (e.g.
@123@,@var-name@or@UNKNOWN@stay as they are). - Works on single strings, whole vectors (in‑place or by value), and file paths.
Add to your project
cargo add kissreplace
Optional async support (enables tokio as a dependency, useful when you need async I/O around replacement):
cargo add kissreplace --features async
Quick start
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use kissreplace::{KissReplace, Variables};
let mut vars = Variables::new();
vars.insert("NAME".to_string(), "World".to_string());
vars.insert("PROJECT".to_string(), "kissreplace".to_string());
let template = "Hello @NAME@, you're reading @PROJECT@ docs!";
let result = vars.replace_str(template);
println!("{}", result);
// Output: Hello World, you're reading kissreplace docs!
}
Under the hood Variables is just a type alias for HashMap<String, String>, so you can build it any way you like.
How replacement works
The function replace_str scans the input from left to right:
- Look for the next
'@'. - From that position, search forward for a closing
'@'. - Check if the text between them is a valid variable name.
- If yes – replace it with the value from the map (or leave
@VAR@if missing). - If not – treat the first
'@'as a literal character and continue scanning.
Because the scan is single‑pass and no recursive expansion is performed, nested‑looking variables
like @A@ where A maps to "@B@" are not expanded further – you get exactly one substitution per placeholder.
Valid variable names
- Must not be empty.
- First character must be an ASCII letter (
a-z,A-Z) or underscore_. - Following characters can be ASCII letters, digits (
0-9), or underscore.
#![allow(unused)]
fn main() {
use kissreplace::valid::is_valid_var_name;
assert!(is_valid_var_name("PROJECT_2"));
assert!(is_valid_var_name("_private"));
assert!(!is_valid_var_name("123start"));
assert!(!is_valid_var_name("with-dash"));
}
The KissReplace trait
This trait is implemented for Variables (HashMap<String, String>) and gives you several convenience methods:
| Method | Description |
|---|---|
replace_str(&self, input: &str) -> String | Core method – replaces placeholders in a single string. |
replace(&self, sources: Vec<String>) -> Vec<String> | Apply to every element of a vector, returning a new vector. |
replace_mut(&self, sources: &mut Vec<String>) | In‑place version – more allocation‑efficient. |
replace_paths(&self, paths: Vec<PathBuf>) -> Vec<PathBuf> | Works on file paths (converts to string, replaces, then back to PathBuf). |
Example: replacing many strings
#![allow(unused)]
fn main() {
let vars = /* ... */;
let mut lines = vec![
"name = @NAME@".to_string(),
"version = @VERSION@".to_string(),
];
vars.replace_mut(&mut lines);
// lines now contains the replaced values
}
Example: file paths
#![allow(unused)]
fn main() {
let vars = /* ... */;
let paths = vec![
PathBuf::from("src/@PROJECT@/main.rs"),
PathBuf::from("config/@PROJECT@.toml"),
];
let new_paths = vars.replace_paths(paths);
}
Scanning for variables
If you only need to know which variables appear in a template (without replacing them), use scan::extract_vars:
#![allow(unused)]
fn main() {
use kissreplace::scan;
let template = "Hello @NAME@, your @PROJECT@ is version @VERSION@";
let vars = extract_vars(template);
// vars = {"NAME", "PROJECT", "VERSION"}
}
It returns a HashSet<String> of unique, valid variable names found. The scanning logic is exactly the same as in replace_str, so you can trust that the reported names would be replaced when you later call replace_str.
Error handling
The crate defines its own KissReplaceError enum. Currently two variants exist:
InvalidVariableName(String)– returned by functions that validate names (if you build your own validation logic).InvalidUtf8– used when converting aPathBufto a string fails (the path is not valid UTF‑8).
Most replacement methods are infallible (they don’t return Result). Errors only appear if you explicitly call into the valid module or handle paths with non‑UTF‑8 components.
#![allow(unused)]
fn main() {
use kissreplace::{KissReplaceError, valid};
if let Err(e) = some_validation_function("1invalid") {
println!("Error: {}", e);
}
}
Async feature
When you enable the async feature, the crate pulls in tokio as an optional dependency. The replacement logic itself is synchronous – this feature simply makes tokio available for your own async I/O tasks, for example:
- Reading hundreds of template files concurrently with
tokio::fs. - Replacing variables in each file, then writing the results.
Nothing in kissreplace is async by itself, but the feature lets you keep your dependency list tidy if you’re already using tokio.
Testing & edge cases
The crate comes with a thorough test suite. Here are some behaviours you can rely on:
| Input | Variables | Output |
|---|---|---|
"@NAME@ and @MISSING@" | NAME=Alice | "Alice and @MISSING@" |
"@@X@@" | X=Y | "@Y@" |
"@var-name@ and @123@" | – | unchanged (invalid names) |
"hello @X and @X@" | X=Y | "hello @X and Y" (unclosed @ left as literal) |
"@A@@B@" | A=1, B=2 | "12" |
"@A@" | A="@B@", B=X | "@B@" (no nested expansion) |
The no‑nested‑expansion rule is intentional – it keeps complexity low and avoids infinite loops.
Performance considerations
- Single pass over the input –
O(n)time. replace_mutreuses the existingVeccapacity, reducing allocations when you process many strings.- The scanner for
extract_varsalso performs a single pass and uses aHashSetto store unique names.
If you need to replace the same template hundreds of times with different variable sets, consider pre‑scanning for variable names and then doing replacements via String::replace or a manual loop – but for most use cases, calling replace_str directly is perfectly fine.
Philosophy – why KISS?
Many template engines grow organically: conditionals, loops, filters, partials… and suddenly your “simple” templating is a full‑blown language. kissreplace deliberately stops at placeholder substitution. It’s ideal for:
- Configuration file generation (e.g.
config.@ENV@.toml->config.production.toml) - Simple email or notification templates
- Environment variable expansion in custom CLIs
- Teaching the concept of templating without distractions
If you need logic, you can always combine it with Rust’s own control flow – that keeps both the template syntax and your code simple.
Summary
| What you want | How kissreplace helps |
|---|---|
Replace @VAR@ placeholders | vars.replace_str("...") |
| Process many strings efficiently | replace_mut(&mut vec) |
| Work with file paths | replace_paths(vec![...]) |
| Discover which variables are used | scan::extract_vars("...") |
| Validate variable names | valid::is_valid_var_name("...") |
| Stay dependency‑light | Only uses std + thiserror (async optional) |
kissreplace is a small, focused tool – and that’s its superpower. Go ahead, sprinkle some @VAR@ placeholders into your strings, and let this crate do the rest. Happy templating!
Links
Lazy Loading, Smart Caching: The lazyget Crate
Welcome to the lazyget guide! If you’ve ever found yourself re-downloading the same huge asset over and over,
or copy-pasting fragile caching code between projects, you’re in the right place.
lazyget is a tiny but mighty Rust crate that solves a big problem: how to fetch an artifact (file, binary,
dataset – anything) once, cache it locally, and never worry about it again.
It does one thing and does it well: given a cache directory and an artifact identifier, it will either return the existing cached path, or run your custom fetch logic exactly once, store the result, and give you back the path – all with atomic updates and automatic cleanup on failure.
Let’s dive in.
What Problem Does lazyget Solve?
Imagine you’re writing a CLI tool that needs a large AI model file, a game engine that downloads asset packs, or a build script that pulls a specific toolchain. You want:
- No redundant downloads – if the artifact is already on disk, just use it.
- No stale caches – sometimes you must refresh the artifact.
- No half‑written files – if the download fails, the old (or partial) version should never be used.
- No boilerplate – you shouldn’t have to write temp directory dances and error handling every time.
lazyget handles all of that for you. You just tell it how to fetch the artifact (a closure or async
function), and it takes care of the rest.
Quick Start
Add lazyget to your Cargo.toml:
cargo add lazyget
If you need asynchronous support (using tokio), enable the async feature:
cargo add lazyget --features async
Synchronous Example
#![allow(unused)]
fn main() {
use lazyget::{fetch, make_id};
use std::fs;
use std::path::Path;
// Where to store cached artifacts (here: system cache directory)
let cache_dir = dirs::cache_dir().unwrap().join("my-app");
// A stable ID for your artifact – can be based on URL and commit hash
let id = make_id("https://github.com/example/model", Some("v1.2.3"));
let artifact_path = fetch(&cache_dir, &id, |temp_dir: &Path| {
// This closure runs only when the artifact is NOT already cached.
// `temp_dir` is a scratch directory that will become the final cache location.
// Download or generate your artifact here:
let response = ureq::get("https://example.com/model.bin").call()?;
let mut reader = response.into_reader();
let mut file = fs::File::create(temp_dir.join("model.bin"))?;
std::io::copy(&mut reader, &mut file)?;
Ok(())
})?;
println!("Artifact ready at: {}", artifact_path.display());
}
If you run this twice, the closure runs only the first time. The second call immediately returns the cached path.
Asynchronous Example (with tokio)
Enable the async feature and use async_fetch:
#![allow(unused)]
fn main() {
use lazyget::async_fetch;
use tokio::fs;
use tokio::io::AsyncWriteExt;
let cache_dir = dirs::cache_dir().unwrap().join("my-app");
let id = lazyget::make_id("https://github.com/example/model", Some("v1.2.3"));
let artifact_path = async_fetch(&cache_dir, &id, |temp_dir| async move {
let url = "https://example.com/model.bin";
let response = reqwest::get(url).await?;
let bytes = response.bytes().await?;
let mut file = tokio::fs::File::create(temp_dir.join("model.bin")).await?;
file.write_all(&bytes).await?;
Ok(())
}).await?;
}
Core Concepts
1. Artifact Identifier
Every artifact is identified by a directory name under your cache root. The simplest way is to use a human-readable string:
#![allow(unused)]
fn main() {
let id = "my-cool-model-v2";
}
But you can also generate a deterministic hash from a URL and an optional tag (like a Git commit) using
make_id:
#![allow(unused)]
fn main() {
let id = make_id("https://github.com/example/repo", Some("abc123def"));
// => "a6b4c3e2..." (64 hex characters)
}
make_id computes a SHA‑256 of url + ":" + tag (if tag is Some). This is perfect when your
artifact’s source changes over time and you want to invalidate the cache automatically.
2. fetch / async_fetch – The Lazy Workhorse
#![allow(unused)]
fn main() {
fn fetch<P, F>(cache_dir: P, artifact_id: &str, fetch_fn: F) -> Result<PathBuf>
where
P: AsRef<Path>,
F: FnOnce(&Path) -> Result<(), Box<dyn Error + Send + Sync>>,
}
Behaviour:
- Check if
cache_dir/artifact_idexists. - If yes -> return its path immediately.
- If no:
- Create a temporary directory
.artifact_id-tmpinsidecache_dir. - Call
fetch_fnwith that temp directory. - If
fetch_fnsucceeds -> atomically rename temp dir to the final name. - If
fetch_fnfails -> delete the temp directory and propagate the error.
- Create a temporary directory
This guarantees that you never see a partially written or corrupted artifact.
3. refetch / async_refetch – Force Refresh
Sometimes you want to ignore the existing cache and re‑fetch the artifact, even
if it’s present. That’s what refetch is for:
#![allow(unused)]
fn main() {
fn refetch<P, F>(cache_dir: P, artifact_id: &str, fetch_fn: F) -> Result<PathBuf>
}
It deletes the existing cached directory (if any) and then calls fetch internally.
Error Handling
All fallible operations return a Result<T, LazyGetError>. LazyGetError
is a thiserror enum covering:
Io– I/O errors (file system).Fetch– Your own closure returned an error (wrapped in a boxed trait object).CacheCreate– Failed to create the root cache directory.AtomicRename– The final rename step failed (very rare, but possible on some filesystems).
This means you can pattern‑match to handle specific cases or use ? to bubble errors up.
#![allow(unused)]
fn main() {
use lazyget::{LazyGetError, fetch};
match fetch(cache_dir, "my-id", |dir| Ok(())) {
Ok(path) => println!("Got {}", path.display()),
Err(LazyGetError::Fetch(e)) => eprintln!("Download logic failed: {}", e),
Err(e) => eprintln!("Caching system error: {}", e),
}
}
Under the Hood: How Atomic Caching Works
lazyget follows a simple but robust protocol:
- Check existence – If
target_direxists, we’re done. - Prepare temp –
cache_dir/.artifact_id-tmp. If it already exists from a previous interrupted run, it gets deleted. - Run your fetch – You write files into the temp directory. If you need to download multiple files or unpack an archive, do it there.
- Commit –
std::fs::rename(ortokio::fs::rename). On most filesystems this is atomic – either the rename happens or it doesn’t. No reader will ever see an incomplete directory. - Cleanup – If your closure returns an error, the temp directory is removed automatically.
This approach works on Linux, macOS, and Windows.
Complete Example: Downloading a Zip Archive
Here’s a real‑world synchronous example that downloads a zip file, extracts it, and caches the result:
#![allow(unused)]
fn main() {
use lazyget::{fetch, make_id, LazyGetError};
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::Path;
use zip::ZipArchive;
fn fetch_and_extract(temp_dir: &Path) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Download zip
let mut resp = ureq::get("https://example.com/assets.zip").call()?;
let mut bytes = Vec::new();
resp.into_reader().read_to_end(&mut bytes)?;
// Unzip into temp_dir
let mut archive = ZipArchive::new(Cursor::new(bytes))?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let out_path = temp_dir.join(file.mangled_name());
if file.is_dir() {
std::fs::create_dir_all(&out_path)?;
} else {
let mut outfile = File::create(&out_path)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
Ok(())
}
let cache_dir = std::env::temp_dir().join("my-cache");
let id = make_id("https://example.com/assets.zip", Some("v2"));
let path = fetch(&cache_dir, &id, fetch_and_extract)?;
println!("Assets ready at: {}", path.display());
}
Testing Strategies
lazyget is easy to test because you can provide any FnOnce – including one that counts
how many times it was called. The crate itself uses tempfile::tempdir() to
create temporary cache roots for tests.
Example test pattern:
#![allow(unused)]
fn main() {
#[test]
fn test_caching_behaviour() {
let tmp = tempfile::tempdir().unwrap();
let id = "test-id";
let mut counter = 0;
// First call: runs closure
let _ = fetch(tmp.path(), id, |_dir| { counter += 1; Ok(()) }).unwrap();
assert_eq!(counter, 1);
// Second call: uses cache
let _ = fetch(tmp.path(), id, |_dir| { counter += 1; Ok(()) }).unwrap();
assert_eq!(counter, 1);
}
}
Feature Flags
- default – no extra features.
- async – enables the
tokiodependency and providesasync_fetch/async_refetch. You gettokio::fsandtokio::process, but the runtime is not started automatically – you need a Tokio runtime in your application.
When Not to Use lazyget
- You need to cache very large numbers of tiny files (the per‑artifact directory overhead is minimal, but if you have millions, consider a database).
- You are writing a
no_stdenvironment (this crate usesstdheavily).
Conclusion
lazyget gives you bulletproof, atomic, lazy caching with an API that fits in your head.
It’s the kind of crate that disappears into your code – you only notice it when it works perfectly.
Go ahead, stop re‑downloading that 2 GB model file on every CI run, and let lazyget take the wheel.
Happy lazy fetching!
Links
wrapc: Parsing rustc Arguments for RUSTC_WRAPPER Tools
Overview
When you build a tool that intercepts Rust compilation — be it a compilation cache, a custom profiler,
a static analyzer, or a linker-flag injector — you inevitably need to parse rustc’s command-line arguments.
This is harder than it sounds.
rustc’s CLI is a moving target: it mixes = and space-separated values, embeds complex sub-syntaxes
for linking (-l static:+bundle,+whole-archive=name:renamed), and evolves constantly with nightly-only
flags. General-purpose CLI parsers like clap are too rigid, too heavy, and fundamentally mismatched
for the wrapper protocol.
wrapc exists to solve exactly this problem.
It provides a strongly-typed, protocol-aware parser that:
- Understands Cargo’s
<wrapper> - <rustc> <args...>invocation format - Parses complex flags into structured Rust types (
--emit,--extern,-L,-l, etc.) - Guarantees flawless round-trip reconstruction via
Info::to_args() - Handles unknown/nightly flags gracefully by bucketing them into
info.unknown - Avoids panics on edge cases like the
-separator or missing values
In short: wrapc lets you focus on what your wrapper does, not on fighting argument parsing.
Quick Start
Add the dependency:
cargo add wrapc
Then, in your wrapper’s main.rs:
use std::process::Command;
use wrapc::fetch;
fn main() {
// 1. Parse `std::env::args()` according to the wrapper protocol
let mut info = fetch().expect("Failed to parse rustc arguments");
// 2. Handle passthrough commands early (help/version/sysroot probes)
if info.help || info.version || info.print.is_some() {
let rustc = info.rustc.unwrap_or_else(|| "rustc".to_string());
let status = Command::new(rustc)
.args(info.to_args())
.status()
.expect("Failed to spawn rustc");
std::process::exit(status.code().unwrap_or(1));
}
// 3. Inspect or mutate the compilation context
if info.crate_name.as_deref() == Some("legacy_crate") {
info.codegen_opts.push("opt-level=1".to_string());
}
// 4. Resolve the real rustc path and reconstruct arguments
let rustc_path = info.rustc.unwrap_or_else(|| "rustc".to_string());
let args = info.to_args();
// 5. Execute the actual compiler
let status = Command::new(rustc_path)
.args(&args)
.status()
.expect("Failed to spawn rustc");
std::process::exit(status.code().unwrap_or(1));
}
That’s the entire skeleton. Five clear steps. No string splitting, no fragile indexing, zero data loss.
Core Concepts
The Wrapper Protocol and the - Separator
Cargo invokes wrappers using a strict format:
<wrapper_binary> - <actual_rustc_path> <rustc_args...>
wrapc::fetch() automatically:
- Strips the first three arguments
- Stores the real compiler path in
info.rustc - Parses the remainder as
rustcarguments
The - Edge Case
Tools sometimes pass - as an input filename to tell rustc to read from stdin.
Because the wrapper protocol also uses - as a separator, naive parsers break here.
wrapc handles this contextually:
#![allow(unused)]
fn main() {
// Invocation: `my_wrapper - rustc -`
let info = wrapc::fetch().unwrap();
assert_eq!(info.rustc, Some("rustc".to_string()));
assert_eq!(info.inputs, vec![std::path::PathBuf::from("-")]);
}
The first - is the protocol separator; the second is correctly identified as an input file.
Type-Safe Flag Parsing
Instead of returning a Vec<String>, wrapc decomposes complex flags into structured types:
| Flag | Parsed Field | Type |
|---|---|---|
--emit=llvm-ir,obj | info.emit | Vec<EmitKind> |
--extern crate=path | info.externs | Vec<Extern> |
-L kind=path | info.libpaths | Vec<LibrarySearchPath> |
-l kind:+mods=name:rename | info.links | Vec<LinkLib> |
This lets you write logic like:
#![allow(unused)]
fn main() {
use wrapc::{LibrarySearchPathKind, LinkLibKind};
for lib_path in &info.libpaths {
if lib_path.kind == LibrarySearchPathKind::Native {
println!("Native search path: {:?}", lib_path.path);
}
}
for link in &info.links {
if matches!(link.kind, Some(LinkLibKind::Static)) {
println!("Static lib: {} (mods: {:?})", link.name, link.modifiers);
}
}
}
No regexes. No manual splitting. Just typed data.
Flawless Round-Tripping
A wrapper must never corrupt the build. When you mutate a subset of arguments, everything else must pass through exactly as Cargo intended.
wrapc guarantees this via Info::to_args():
- Preserves original spacing and
=vs. space-separated forms - Maintains argument order
- Re-emits unrecognized flags from
info.unknownunchanged - Handles edge cases like quoted values or embedded spaces
You mutate what you need; wrapc handles the rest.
Graceful Degradation for Nightly Flags
Rust evolves. Nightly compilers introduce new flags daily.
wrapc doesn’t pretend to know them all. Instead:
- Known flags -> parsed into typed fields
- Unknown flags -> stored in
info.unknownas raw strings - On
to_args()-> all flags re-emitted exactly as received
Your wrapper won’t crash on a new nightly, and you won’t silently drop experimental flags.
API Highlights
fetch() -> Result<Info, ParseError>
The entry point. Reads std::env::args(), parses according to the wrapper protocol,
and returns a strongly-typed Info struct.
Info Struct (selected fields)
#![allow(unused)]
fn main() {
pub struct Info {
// Protocol metadata
pub rustc: Option<String>, // Path to real rustc, if provided
// Common flags
pub crate_name: Option<String>,
pub edition: Option<String>,
pub target: Option<String>,
pub profile: Option<String>, // "debug" or "release"
// Action flags
pub help: bool,
pub version: bool,
pub print: Option<String>, // --print=<kind>
// Compilation units
pub inputs: Vec<PathBuf>, // Source files
pub emit: Vec<EmitKind>, // --emit=...
pub out_dir: Option<PathBuf>,
// Dependency management
pub externs: Vec<Extern>, // --extern crate=path
pub libpaths: Vec<LibrarySearchPath>, // -L kind=path
pub links: Vec<LinkLib>, // -l kind:+mods=name:rename
// Code generation and diagnostics
pub codegen_opts: Vec<String>, // -C flag values
pub cfg: Vec<String>, // --cfg values
pub features: Vec<String>, // --cfg feature=...
// Catch-all for unknown/nightly flags
pub unknown: Vec<String>,
}
}
Info::to_args(&self) -> Vec<String>
Reconstructs the original argument list with zero data loss. Use this when forwarding to the real rustc.
ParseError
A lightweight error type that indicates why parsing failed (e.g., missing value, malformed flag).
Most wrapper tools can safely .expect() on fetch() since Cargo guarantees well-formed
invocations — but the error is there if you need it.
Advanced: Mutating Compilation Context
Because Info owns its data, you can safely mutate fields before reconstruction:
#![allow(unused)]
fn main() {
// Force a specific codegen option for all crates
info.codegen_opts.push("target-cpu=native".to_string());
// Inject a cfg flag conditionally
if info.profile.as_deref() == Some("release") {
info.cfg.push("feature=\"optimised\"".to_string());
}
// Replace an extern dependency path
for ext in &mut info.externs {
if ext.name == "legacy_dep" {
ext.path = PathBuf::from("/new/path/liblegacy_dep.rlib");
}
}
}
All changes are reflected in to_args() output.
Testing Your Wrapper
You don’t need to publish or install globally to test. Use RUSTC_WRAPPER:
# Build your wrapper
cargo build --release
# Point Cargo to your binary
export RUSTC_WRAPPER="$(pwd)/target/release/my_wrapper"
# Run any cargo command — your wrapper will intercept rustc calls
cargo build
Pro tip: Always check
info.help,info.version, andinfo.printearly in yourmain(). If any are set, forward arguments torustcimmediately and exit. This prevents your wrapper from interfering with Cargo’s internal compiler probes.
When to Use wrapc
Ideal for:
- Compilation caches (
sccache-like tools) - Build-time telemetry or profiling
- Static analysis wrappers
- Linker-flag or environment injectors
- Any tool implementing
RUSTC_WRAPPER
Not intended for:
- General-purpose CLI parsing (use
clap,bpaf, orargh) rustcdriver plugins (userustc_driverAPIs)- Parsing
cargoarguments (usecargo_metadataor dedicated parsers)
Links
wrapcli: Fake Command Identity With Ease
wrapcli is a Rust library that wraps an existing CLI tool and rewrites its output on the fly, making it appear as if a different tool produced the output.
Features
- Stream real-time output rewriting (line by line)
- Capture full output for post-processing
- Customizable rewriting rules
- Preserve original version information (optional)
Use Cases
- Create branded variants of CLI tools
- Mask internal tool names in user-facing output
- Add version information without changing the underlying tool
- Integrate legacy tools into modern workflows
Getting Started
Installation
Add wrapcli to your dependencies:
cargo add wrapcli
Basic Usage
use wrapcli::{run_streaming, WrapConfig};
fn main() -> std::io::Result<()> {
let cfg = WrapConfig {
orig_name: "rustc".into(),
fake_name: "dustc".into(),
fake_ver: "2.0.0".into(),
save_orig: true,
};
let args: Vec<String> = std::env::args().skip(1).collect();
let status = run_streaming(&cfg, args)?;
std::process::exit(status.code().unwrap_or(1));
}
How It Works
When the wrapped tool outputs a line containing the original name, wrapcli intercepts it and applies rewriting rules:
- The first occurrence of the original tool’s name and version is replaced with the fake name and fake version. If
save_origistrue, the original version is appended in parentheses. - Any subsequent occurrences of the original name in usage lines are replaced with the fake name.
This ensures consistent output masking without breaking the tool’s functionality.
Configuration
The WrapConfig struct controls the rewriting behavior.
Fields
| Field | Type | Description |
|---|---|---|
orig_name | String | The original tool’s name (e.g., "rustc") |
fake_name | String | The name to display instead (e.g., "dustc") |
fake_ver | String | The version string to display (e.g., "2.0.0") |
save_orig | bool | Whether to append the original version in parentheses |
Example
#![allow(unused)]
fn main() {
use wrapcli::WrapConfig;
let cfg = WrapConfig {
orig_name: "git".into(),
fake_name: "gitter".into(),
fake_ver: "3.0.0".into(),
save_orig: false,
};
}
Examples
Streaming Output
use wrapcli::{run_streaming, WrapConfig};
fn main() -> std::io::Result<()> {
let cfg = WrapConfig {
orig_name: "cargo".into(),
fake_name: "pargo".into(),
fake_ver: "2.0.0".into(),
save_orig: true,
};
let args = vec!["--version".to_string()];
run_streaming(&cfg, args)?;
Ok(())
}
Capturing Output
use wrapcli::{run_capture, WrapConfig};
fn main() -> std::io::Result<()> {
let cfg = WrapConfig {
orig_name: "rustc".into(),
fake_name: "dustc".into(),
fake_ver: "2.0.0".into(),
save_orig: false,
};
let args = vec!["--version".to_string()];
let result = run_capture(&cfg, args)?;
println!("Captured stdout: {}", String::from_utf8_lossy(&result.stdout));
println!("Captured stderr: {}", String::from_utf8_lossy(&result.stderr));
Ok(())
}
Real-World Example: Wrapping Git
use wrapcli::{run_streaming, WrapConfig};
use std::env;
fn main() -> std::io::Result<()> {
let cfg = WrapConfig {
orig_name: "git".into(),
fake_name: "gitter".into(),
fake_ver: "3.0.0".into(),
save_orig: true,
};
let args: Vec<String> = env::args().skip(1).collect();
run_streaming(&cfg, args)
}