Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .inherignore
  • cargo-inherit – the CLI that adds aliases, defaults, caching, and interactive prompts

> Read more

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 caching
  • inherit-core and cargo-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”.

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 use inherit for brevity. In practice you can run cargo inherit if you have cargo-inherit installed (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:

  1. Clone https://github.com/rust-lib/cargo-lib (cached for next time).
  2. Read Inherit.toml and scan all files for @VARIABLES@.
  3. Ask you for values (with helpful descriptions and defaults from config).
  4. Generate my-project/ with all placeholders replaced.
  5. Run git init and execute any post_create hooks.

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

CommandDescription
inherit <template> to alias <name>Create an alias for a template
inherit alias listShow 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

CommandDescription
inherit default for <VAR>Set a default value for a variable
inherit default listShow 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

CommandDescription
inherit cache listShow cached templates and their disk usage
inherit cache cleanDelete 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

VariableDescription
INHERIT_CONFIGFull path to config file (overrides default)
INHERIT_CACHE_DIRDirectory for cached templates (overrides cache_dir)
INHERIT_NON_INTERACTIVEIf 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:

  1. Resolveuser/repo is transformed into a Git URL (https://github.com/user/repo.git) unless it’s an alias or a full URL.
  2. Fetchgit clone --depth 1 is used to download the template into the cache (keyed by the canonical URL). Subsequent runs reuse the cached copy.
  3. Loadinherit-core reads Inherit.toml and scans all files for @VAR@ placeholders, respecting .inherignore.
  4. Prompt – For every variable found, the CLI shows a prompt with its description (from [variables]) and your configured default (from [defaults]).
  5. Process – All files and folders are copied to the target directory, with @VAR@ replaced in both contents and filenames. Binary files are copied unchanged.
  6. Finalise – If init_git = true, a fresh git init is run in the target. Then any post_create hooks from the template are executed (using sh -c on Unix, cmd /C on Windows).
  7. Open – If open_with is 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_create commands 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 1 is used to keep the cache small.
  • To update a cached template, delete it from the cache (inherit cache clean will nuke everything) or manually run git pull inside 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

ProblemSolution
error: Manifest "..." not foundThe 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 failedCheck your network, the repository URL, and your GitHub token if private.
cannot determine config directorydirs crate failed. Set INHERIT_CONFIG explicitly.
Invalid variable nameVariable 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:

  1. Write the template in a repo mycorp/microservice-template.

  2. Add aliases for your team:

    inherit mycorp/microservice-template to alias mcsrv
    
  3. Set company defaults:

    inherit default for AUTHOR
    # -> type "Engineering Team <eng@mycorp.com>"
    inherit default for LICENSE
    # -> type "MIT OR Apache-2.0"
    
  4. Generate a new service:

    inherit mcsrv to payment-service
    

    The tool will ask only for the project‑specific variables (e.g. SERVICE_NAME, PORT). The company defaults are already filled.

  5. Automatically open in VS Code and run cargo build via 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.

crates.io docs.rs

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

ConceptDescription
TemplateA directory containing an Inherit.toml manifest and arbitrary files with @VAR@ placeholders.
ManifestTOML file that declares variables (with descriptions) and optional hooks.
Placeholder syntax@UPPER_SNAKE_CASE@ – powered by the [kissreplace] crate.
.inherignoreGit‑ignore style file to exclude certain paths from processing.
Post‑create hooksShell commands (sh or cmd) run after the project is materialised.

Note: inherit-core does 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   |
+--------------+       +------------------+           +--------------+
  1. Load – Read Inherit.toml and scan all template files to collect every @VAR@ occurrence.
  2. Prompt (outside the crate) – The caller collects concrete values from the user.
  3. Process – Copy every file/folder, replacing placeholders in content and path names, respecting .inherignore.
  4. Finalise – Optionally run git init and execute post_create hooks.

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:

  1. Always ignored"Inherit.toml", ".inherignore", ".git" (and anything inside .git/).
  2. User‑defined – via a .inherignore file in the template root, using .gitignore syntax.
#![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 provided
  • var_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_]*$ – by kissreplace’s rules).
  • Checks that all required variables are present and non‑empty.
  • Creates the target directory.
  • Walks the source, respecting always‑ignored and .inherignore entries.
  • 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_git is true -> runs git init -q in the target.
  • If opts.run_hooks is true -> executes each post_create command 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 Unix
  • cmd /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-lib example template end‑to‑end.
  • Verify missing variables trigger the right error.
  • Test variable replacement inside file names (the test_variable_in_filename case).

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!

crates.io docs.rs

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, where VAR follows 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:

  1. Look for the next '@'.
  2. From that position, search forward for a closing '@'.
  3. Check if the text between them is a valid variable name.
  4. If yes – replace it with the value from the map (or leave @VAR@ if missing).
  5. 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:

MethodDescription
replace_str(&self, input: &str) -> StringCore 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 a PathBuf to 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:

InputVariablesOutput
"@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_mut reuses the existing Vec capacity, reducing allocations when you process many strings.
  • The scanner for extract_vars also performs a single pass and uses a HashSet to 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 wantHow kissreplace helps
Replace @VAR@ placeholdersvars.replace_str("...")
Process many strings efficientlyreplace_mut(&mut vec)
Work with file pathsreplace_paths(vec![...])
Discover which variables are usedscan::extract_vars("...")
Validate variable namesvalid::is_valid_var_name("...")
Stay dependency‑lightOnly 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!

crates.io docs.rs

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_id exists.
  • If yes -> return its path immediately.
  • If no:
    • Create a temporary directory .artifact_id-tmp inside cache_dir.
    • Call fetch_fn with that temp directory.
    • If fetch_fn succeeds -> atomically rename temp dir to the final name.
    • If fetch_fn fails -> delete the temp directory and propagate the error.

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:

  1. Check existence – If target_dir exists, we’re done.
  2. Prepare tempcache_dir/.artifact_id-tmp. If it already exists from a previous interrupted run, it gets deleted.
  3. Run your fetch – You write files into the temp directory. If you need to download multiple files or unpack an archive, do it there.
  4. Commitstd::fs::rename (or tokio::fs::rename). On most filesystems this is atomic – either the rename happens or it doesn’t. No reader will ever see an incomplete directory.
  5. 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 tokio dependency and provides async_fetch / async_refetch. You get tokio::fs and tokio::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_std environment (this crate uses std heavily).

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!

crates.io docs.rs

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:

  1. Strips the first three arguments
  2. Stores the real compiler path in info.rustc
  3. Parses the remainder as rustc arguments

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:

FlagParsed FieldType
--emit=llvm-ir,objinfo.emitVec<EmitKind>
--extern crate=pathinfo.externsVec<Extern>
-L kind=pathinfo.libpathsVec<LibrarySearchPath>
-l kind:+mods=name:renameinfo.linksVec<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.unknown unchanged
  • 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:

  1. Known flags -> parsed into typed fields
  2. Unknown flags -> stored in info.unknown as raw strings
  3. 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, and info.print early in your main(). If any are set, forward arguments to rustc immediately 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, or argh)
  • rustc driver plugins (use rustc_driver APIs)
  • Parsing cargo arguments (use cargo_metadata or dedicated parsers)

crates.io docs.rs

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:

  1. The first occurrence of the original tool’s name and version is replaced with the fake name and fake version. If save_orig is true, the original version is appended in parentheses.
  2. 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

FieldTypeDescription
orig_nameStringThe original tool’s name (e.g., "rustc")
fake_nameStringThe name to display instead (e.g., "dustc")
fake_verStringThe version string to display (e.g., "2.0.0")
save_origboolWhether 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)
}

crates.io docs.rs