main.rs is the entry point for the weaveback-tangle binary. It parses command-line arguments with clap, builds a `Clip`, reads the input files, writes all @file chunks to disk via `SafeFileWriter`, and merges the run’s database into the path given by --db (default weaveback.db).

See weaveback_tangle.adoc for the module map and architecture.adoc for the pipeline overview.

Arguments

Flag Default Description

--gen DIR

gen

Base directory for generated output files.

--open-delim STR

<[

Chunk open delimiter.

--close-delim STR

]>

Chunk close delimiter.

--chunk-end STR

@

Marker that closes a chunk definition.

--comment-markers LIST

#,//

Comma-separated comment prefixes.

--formatter EXT=CMD

Formatter to run on files with a given extension.

--allow-home

off

Allow @file ~/… chunks to write outside gen/.

--strict

off

Treat undefined chunk references as fatal errors (default: expand to nothing).

--dry-run

off

Print output paths without writing anything.

--db FILE

weaveback.db

Path to the persistent source-map database.

--chunks LIST

Named chunks to extract to stdout (or --output).

--output FILE

stdout

Destination for --chunks output.

FILES…

Input literate source files (- for stdin).

// <[@file weaveback-tangle/src/main.rs]>=
use weaveback_tangle::{WeavebackError, Clip, SafeFileWriter, SafeWriterConfig};
use clap::Parser;
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Write};
use std::path::PathBuf;

#[derive(Parser)]
#[command(
    name = "weaveback",
    about = "Expand chunks like noweb - A literate programming tool",
    version
)]
struct Args {
    /// Output file for --chunks [default: stdout]
    #[arg(long)]
    output: Option<PathBuf>,

    /// Names of chunks to extract (comma separated)
    #[arg(long)]
    chunks: Option<String>,

    /// Base directory of generated files
    #[arg(long = "gen", default_value = "gen")]
    gen_dir: PathBuf,

    /// Delimiter used to open a chunk
    #[arg(long, default_value = "<[")]
    open_delim: String,

    /// Delimiter used to close a chunk definition
    #[arg(long, default_value = "]>")]
    close_delim: String,

    /// Delimiter for chunk-end lines
    #[arg(long, default_value = "@")]
    chunk_end: String,

    /// Comment markers (comma separated)
    #[arg(long, default_value = "#,//")]
    comment_markers: String,

    /// Formatter command per file extension, e.g. --formatter rs=rustfmt
    /// Can be repeated: --formatter rs=rustfmt --formatter ts="prettier --write"
    #[arg(long, value_name = "EXT=CMD")]
    formatter: Vec<String>,

    /// Allow @file ~/... chunks to write outside the gen/ directory
    #[arg(long)]
    allow_home: bool,

    /// Treat references to undefined chunks as fatal errors
    #[arg(long)]
    strict: bool,

    /// Show what would be written without writing anything
    #[arg(long)]
    dry_run: bool,

    /// Path to the persistent source-map database
    #[arg(long, default_value = "weaveback.db")]
    db: PathBuf,

    /// Input files (use - for stdin)
    #[arg(required = true)]
    files: Vec<PathBuf>,
}

fn write_chunks<W: Write>(
    clipper: &mut Clip,
    chunks: &[&str],
    writer: &mut W,
) -> Result<(), WeavebackError> {
    for chunk in chunks {
        clipper.get_chunk(chunk, writer)?;
        writeln!(writer)?;
    }
    Ok(())
}

fn run(args: Args) -> Result<(), WeavebackError> {
    let comment_markers: Vec<String> = args
        .comment_markers
        .split(',')
        .map(|s| s.trim().to_string())
        .collect();

    let formatters: HashMap<String, String> = args
        .formatter
        .iter()
        .filter_map(|s| {
            s.split_once('=')
                .map(|(e, c)| (e.to_string(), c.to_string()))
        })
        .collect();

    let safe_writer = SafeFileWriter::with_config(
        &args.gen_dir,
        SafeWriterConfig {
            formatters,
            allow_home: args.allow_home,
            ..SafeWriterConfig::default()
        },
    )?;
    let mut clipper = Clip::new(
        safe_writer,
        &args.open_delim,
        &args.close_delim,
        &args.chunk_end,
        &comment_markers,
    );

    clipper.set_strict_undefined(args.strict);
    clipper.read_files(&args.files)?;

    if args.dry_run {
        for path in clipper.list_output_files() {
            println!("{}", path.display());
        }
        return Ok(());
    }

    clipper.write_files()?;

    if let Some(chunks) = args.chunks {
        let chunks: Vec<&str> = chunks.split(',').collect();
        if let Some(output_path) = args.output {
            let mut file = File::create(output_path)?;
            write_chunks(&mut clipper, &chunks, &mut file)?;
        } else {
            let stdout = io::stdout();
            let mut handle = stdout.lock();
            write_chunks(&mut clipper, &chunks, &mut handle)?;
        }
    }

    clipper.finish(&args.db)?;

    Ok(())
}

fn main() {
    env_logger::init();
    let args = Args::parse();

    if let Err(e) = run(args) {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
}
// @@

Tests

Integration tests for the weaveback-tangle CLI binary. They use assert_cmd to invoke the built binary as a subprocess and exercise the full argument-parsing / chunk-extraction contract:

  • test_no_arguments_fails — no args → non-zero exit with "required"

  • test_basic_chunk_extraction@file chunk written to gen/ directory

  • test_extract_specific_chunk_to_stdout--chunks prints named chunk

  • test_extract_chunk_to_file--chunks --output writes chunk to file

// <[@file weaveback-tangle/tests/main_tests.rs]>=
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::fs;
use std::io::Write;
use std::process::Command;
use tempfile::tempdir;

#[test]
fn test_no_arguments_fails() -> Result<(), Box<dyn std::error::Error>> {
    // Running the binary with no arguments should fail and print usage or an error.
    let mut cmd = Command::cargo_bin("weaveback-tangle")?;
    cmd.assert()
        .failure()
        .stderr(predicate::str::contains("required"));

    Ok(())
}

#[test]
fn test_basic_chunk_extraction() -> Result<(), Box<dyn std::error::Error>> {
    let dir = tempdir()?;
    let input_file = dir.path().join("input.nw");
    let mut file = fs::File::create(&input_file)?;
    writeln!(file, "<[@file test.txt]>=")?;
    writeln!(file, "Hello, world!")?;
    writeln!(file, "@")?;

    let gen_dir = dir.path().join("gen");
    fs::create_dir_all(&gen_dir)?;

    let mut cmd = Command::cargo_bin("weaveback-tangle")?;
    cmd.arg("--gen")
        .arg(&gen_dir)
        .arg(&input_file)
        .current_dir(dir.path());

    cmd.assert().success();

    let output_path = gen_dir.join("test.txt");
    let output_content = fs::read_to_string(output_path)?;
    assert_eq!(output_content, "Hello, world!\n");

    Ok(())
}

#[test]
fn test_extract_specific_chunk_to_stdout() -> Result<(), Box<dyn std::error::Error>> {
    let dir = tempdir()?;
    let input_file = dir.path().join("input.nw");
    let mut file = fs::File::create(&input_file)?;
    writeln!(file, "<[chunk1]>=")?;
    writeln!(file, "Chunk 1 content")?;
    writeln!(file, "@")?;
    writeln!(file, "<[chunk2]>=")?;
    writeln!(file, "Chunk 2 content")?;
    writeln!(file, "@")?;

    let gen_dir = dir.path().join("gen");
    fs::create_dir_all(&gen_dir)?;

    let mut cmd = Command::cargo_bin("weaveback-tangle")?;
    cmd.arg("--gen")
        .arg(&gen_dir)
        .arg("--chunks")
        .arg("chunk2")
        .arg(&input_file)
        .current_dir(dir.path());

    cmd.assert()
        .success()
        .stdout(predicate::str::contains("Chunk 2 content"));

    Ok(())
}

#[test]
fn test_extract_chunk_to_file() -> Result<(), Box<dyn std::error::Error>> {
    let dir = tempdir()?;
    let input_file = dir.path().join("input.nw");
    {
        let mut file = fs::File::create(&input_file)?;
        writeln!(file, "<[chunk3]>=")?;
        writeln!(file, "This is chunk 3.")?;
        writeln!(file, "@")?;
    }

    let output_file = dir.path().join("chunk3_output.txt");
    let gen_dir = dir.path().join("gen");
    fs::create_dir_all(&gen_dir)?;

    let mut cmd = Command::cargo_bin("weaveback-tangle")?;
    cmd.arg("--gen")
        .arg(&gen_dir)
        .arg("--chunks")
        .arg("chunk3")
        .arg("--output")
        .arg(&output_file)
        .arg(&input_file)
        .current_dir(dir.path());

    cmd.assert().success();

    let content = fs::read_to_string(&output_file)?;
    assert!(content.contains("This is chunk 3."));

    Ok(())
}
// @@