kibble 0.1.0

chew through any source into clean datasets — a fast ingestion, RAG & fine-tuning toolkit
Documentation
//! Minimal `.env` loader — no external crate.
//!
//! Reads `KEY=VALUE` lines from the nearest `.env` (the current directory,
//! walking up to the filesystem root) and sets any variable that is not
//! already present in the process environment. Real environment variables
//! always win, so `export FOO=...` in your shell overrides a `.env` `FOO`.

use std::path::{Path, PathBuf};

/// Locate the nearest `.env`, starting at `start` and walking up to the root.
fn find_env_file(start: &Path) -> Option<PathBuf> {
    let mut dir = Some(start);
    while let Some(d) = dir {
        let candidate = d.join(".env");
        if candidate.is_file() {
            return Some(candidate);
        }
        dir = d.parent();
    }
    None
}

/// Parse one `.env` line into a `(key, value)` pair, or `None` for a
/// blank line, a `#` comment, or a line without a valid key.
fn parse_line(line: &str) -> Option<(String, String)> {
    let line = line.trim();
    if line.is_empty() || line.starts_with('#') {
        return None;
    }
    // Allow an optional leading `export ` (so a shell-sourceable file works).
    let line = line.strip_prefix("export ").unwrap_or(line);
    let (key, val) = line.split_once('=')?;
    let key = key.trim();
    if key.is_empty() {
        return None;
    }
    let mut val = val.trim();
    // Strip one matching pair of surrounding quotes.
    if val.len() >= 2
        && ((val.starts_with('"') && val.ends_with('"'))
            || (val.starts_with('\'') && val.ends_with('\'')))
    {
        val = &val[1..val.len() - 1];
    }
    Some((key.to_string(), val.to_string()))
}

/// Load the nearest `.env` (from the current directory upward) into the
/// process environment without overriding variables that are already set.
/// Returns the number of variables actually set.
pub fn load() -> usize {
    let Ok(cwd) = std::env::current_dir() else {
        return 0;
    };
    load_from(&cwd)
}

/// Testable core: search from `start`, set vars, return the count set.
fn load_from(start: &Path) -> usize {
    let Some(path) = find_env_file(start) else {
        return 0;
    };
    let Ok(content) = std::fs::read_to_string(&path) else {
        return 0;
    };
    let mut n = 0;
    for line in content.lines() {
        if let Some((k, v)) = parse_line(line) {
            if std::env::var_os(&k).is_none() {
                std::env::set_var(&k, &v);
                n += 1;
            }
        }
    }
    if n > 0 {
        eprintln!("kibble: loaded {n} env var(s) from {}", path.display());
    }
    n
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_basic() {
        assert_eq!(parse_line("FOO=bar"), Some(("FOO".into(), "bar".into())));
    }

    #[test]
    fn parse_trims_spaces_around_equals() {
        assert_eq!(parse_line("  FOO = bar  "), Some(("FOO".into(), "bar".into())));
    }

    #[test]
    fn parse_export_prefix() {
        assert_eq!(parse_line("export TOKEN=abc"), Some(("TOKEN".into(), "abc".into())));
    }

    #[test]
    fn parse_strips_matching_quotes() {
        assert_eq!(parse_line(r#"K="a b""#), Some(("K".into(), "a b".into())));
        assert_eq!(parse_line("K='a b'"), Some(("K".into(), "a b".into())));
        // mismatched / single quote char is kept verbatim
        assert_eq!(parse_line(r#"K="a b'"#), Some(("K".into(), r#""a b'"#.into())));
    }

    #[test]
    fn parse_value_may_contain_equals() {
        assert_eq!(parse_line("URL=a=b=c"), Some(("URL".into(), "a=b=c".into())));
    }

    #[test]
    fn parse_skips_blank_and_comment() {
        assert_eq!(parse_line(""), None);
        assert_eq!(parse_line("   "), None);
        assert_eq!(parse_line("# a comment"), None);
        assert_eq!(parse_line("=novalue"), None);
    }

    #[test]
    fn find_walks_up_to_parent() {
        let base = std::env::temp_dir().join(format!("kibble_dotenv_find_{}", std::process::id()));
        let nested = base.join("a").join("b");
        std::fs::create_dir_all(&nested).unwrap();
        std::fs::write(base.join(".env"), "X=1\n").unwrap();
        let found = find_env_file(&nested).unwrap();
        assert_eq!(found, base.join(".env"));
        std::fs::remove_dir_all(&base).ok();
    }

    #[test]
    fn load_sets_and_respects_existing() {
        let dir = std::env::temp_dir().join(format!("kibble_dotenv_load_{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        // NEW var gets set; PRESET var is NOT overridden (real env wins).
        std::fs::write(
            dir.join(".env"),
            "KIBBLE_DOTENV_NEW=fromfile\nKIBBLE_DOTENV_PRESET=fromfile\n",
        )
        .unwrap();
        std::env::set_var("KIBBLE_DOTENV_PRESET", "fromenv");
        let n = load_from(&dir);
        assert_eq!(n, 1); // only the NEW one
        assert_eq!(std::env::var("KIBBLE_DOTENV_NEW").unwrap(), "fromfile");
        assert_eq!(std::env::var("KIBBLE_DOTENV_PRESET").unwrap(), "fromenv");
        std::env::remove_var("KIBBLE_DOTENV_NEW");
        std::env::remove_var("KIBBLE_DOTENV_PRESET");
        std::fs::remove_dir_all(&dir).ok();
    }
}