rust-ise 0.2.0

Fast Rust-native ISEScan-equivalent insertion-sequence (IS) scanner for bacterial (meta)genomes: rustygal ORFs + MMseqs2 profile search + native affine-SW terminal inverted repeats.
Documentation
// rust-ise CLI — a thin wrapper over the `rust_ise` library.
//
// Parses args, runs the detection pipeline (`rust_ise::run`), and writes the
// TSV/GFF/.sum outputs. All detection logic lives in the library so it can be
// consumed in-process (e.g. by bactars).
use rust_ise::{run, write_gff, write_sum, write_tsv, IseConfig};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;

// `build-db` subcommand: reproducible-from-source build of the union IS profile DB
// (rustygal ORFs + mmseqs cluster + in-process poasta MSA). Bin-only; the scanner
// library (consumed in-process by bactars) does not depend on it.
mod build_db;

fn main() {
    let t0 = Instant::now();
    let args: Vec<String> = std::env::args().collect();
    // Subcommand: `rust-ise build-db …` rebuilds the union IS profile DB from source.
    if args.get(1).map(String::as_str) == Some("build-db") {
        run_build_db(&args[2..]);
        return;
    }
    let mut seqfile = String::new();
    let mut output = String::new();
    let mut threads = 16usize;
    let mut keep = false;
    let mut strict = false;
    let mut db_dir: Option<PathBuf> = None; // resolve from --db, then $RUSTISE_DB
    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "-i" | "--seqfile" => {
                i += 1;
                seqfile = args[i].clone();
            }
            "-o" | "--output" => {
                i += 1;
                output = args[i].clone();
            }
            "-t" | "--threads" => {
                i += 1;
                threads = match args[i].parse() {
                    Ok(n) => n,
                    Err(_) => {
                        eprintln!("error: --threads expects a number, got '{}'", args[i]);
                        std::process::exit(2);
                    }
                };
            }
            "--keep" => keep = true,
            "--strict" => strict = true, // drop fpFlag=host calls (host-lineage FPs)
            "--db" => {
                i += 1;
                db_dir = Some(PathBuf::from(args[i].clone()));
            }
            _ => {
                eprintln!("unknown arg {}", args[i]);
                std::process::exit(2);
            }
        }
        i += 1;
    }
    if seqfile.is_empty() || output.is_empty() {
        eprintln!("usage: rust-ise -i <contigs.fasta> -o <out_dir> [-t N] [--db <dir>] [--keep] [--strict]");
        eprintln!("error: -i/--seqfile and -o/--output are required.");
        std::process::exit(2);
    }

    // DB is external data (not bundled). Resolve --db, else $RUSTISE_DB, else error.
    let db_dir = db_dir
        .or_else(|| std::env::var_os("RUSTISE_DB").map(PathBuf::from))
        .unwrap_or_else(|| {
            eprintln!("error: IS database not set. Pass --db <dir> or set RUSTISE_DB (see README).");
            std::process::exit(2);
        });

    rayon::ThreadPoolBuilder::new()
        .num_threads(threads)
        .build_global()
        .ok();
    fs::create_dir_all(&output).unwrap();

    let config = IseConfig { threads, strict, db_dir };
    let calls = match run(Path::new(&seqfile), Path::new(&output), &config) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("error: {e}");
            std::process::exit(1);
        }
    };

    let stem = Path::new(&output);
    write_tsv(&calls, &stem.join("rust-ise.tsv"));
    write_gff(&calls, &stem.join("rust-ise.gff"));
    write_sum(&calls, &stem.join("rust-ise.sum"));
    if !keep {
        let _ = fs::remove_dir_all(stem.join("rust-ise_tmp"));
    }
    eprintln!(
        "[{:.1}s] DONE -> {}/rust-ise.tsv  ({} IS)",
        t0.elapsed().as_secs_f64(),
        output,
        calls.len()
    );
}

// ---------------------------------------------------------- build-db subcommand
fn run_build_db(rest: &[String]) {
    use build_db::BuildArgs;
    let mut a: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
    let mut out: Option<PathBuf> = None;
    let mut work: Option<PathBuf> = None;
    let mut strict_version = false;
    let mut i = 0;
    while i < rest.len() {
        match rest[i].as_str() {
            "--isosdb-fna" => { i += 1; a.insert("iso", rest[i].clone()); }
            "--fam-annot" => { i += 1; a.insert("fam", rest[i].clone()); }
            "--isfinder-faa" => { i += 1; a.insert("faa", rest[i].clone()); }
            "--isfinder-csv" => { i += 1; a.insert("csv", rest[i].clone()); }
            "--out" | "--db" => { i += 1; out = Some(PathBuf::from(&rest[i])); }
            "--work" => { i += 1; work = Some(PathBuf::from(&rest[i])); }
            "--strict-mmseqs-version" => strict_version = true,
            "-h" | "--help" => {
                eprintln!("usage: rust-ise build-db --isosdb-fna F --fam-annot F --isfinder-faa F --isfinder-csv F --out DIR [--work DIR] [--strict-mmseqs-version]");
                return;
            }
            x => { eprintln!("unknown build-db arg {x}"); std::process::exit(2); }
        }
        i += 1;
    }
    let get = |k: &str, flag: &str| -> PathBuf {
        match a.get(k) {
            Some(v) => PathBuf::from(v),
            None => { eprintln!("build-db needs {flag}"); std::process::exit(2); }
        }
    };
    let out_dir = out.unwrap_or_else(|| { eprintln!("build-db needs --out DIR"); std::process::exit(2); });
    let work = work.unwrap_or_else(|| out_dir.join("work"));
    let cfg = BuildArgs {
        isosdb_fna: get("iso", "--isosdb-fna"),
        fam_annot: get("fam", "--fam-annot"),
        isfinder_faa: get("faa", "--isfinder-faa"),
        isfinder_csv: get("csv", "--isfinder-csv"),
        out_dir,
        work,
        strict_version,
    };
    if let Err(e) = build_db::run(&cfg) {
        eprintln!("build-db error: {e}");
        std::process::exit(1);
    }
}