An ultra-fast, nearly stateless, and failure-tolerant parser for .env files, written in Rust.
Designed for high-performance tooling (LSPs, linters, formatters) and applications that need deep introspection into environment configuration files.
- 🚀 Blazingly Fast: Heavily optimized using nearly zero-copy parsing (
Cowstrings) and SIMD-friendly slice iterators. - 📍 Introspective: Tracks exact line and column positions (spans) for keys, values, and comments.
- 💬 Comment Support: First-class support for parsing and preserving comments, including commented-out key-value pairs.
- 🛡️ Failure Tolerant: Continues parsing after errors, collecting all issues instead of halting on the first one.
- 📦 Zero Runtime Dependencies: Lightweight and easy to drop into any project.
[dependencies]
korni = "0.1.4"use korni::{parse, ParseOptions};
fn main() {
let input = r#"
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
"#;
// Simple parsing (fast mode)
let entries = parse(input);
for entry in entries {
if let Some(pair) = entry.as_pair() {
println!("Key: {}, Value: {}", pair.key, pair.value);
}
}
// Advanced parsing with positions and comments
let full_entries = korni::parse_with_options(input, ParseOptions::full());
}The main parsing result type representing a single line in an .env file:
pub enum Entry<'a> {
Comment(Span), // A comment line (# ...)
Pair(KeyValuePair<'a>), // A key-value pair (KEY=value)
Error(Error), // A parsing error
}Represents a parsed key-value pair with full introspection:
pub struct KeyValuePair<'a> {
pub key: Cow<'a, str>, // The key name
pub key_span: Option<Span>, // Span of the key
pub value: Cow<'a, str>, // The parsed value
pub value_span: Option<Span>, // Span of the value
pub quote: QuoteType, // Single, Double, or None
pub open_quote_pos: Option<Position>,
pub close_quote_pos: Option<Position>,
pub equals_pos: Option<Position>, // Position of the '='
pub is_exported: bool, // Whether 'export' keyword was used
pub is_comment: bool, // Whether this was in a comment (# KEY=value)
}Configure parsing behavior:
pub struct ParseOptions {
pub include_comments: bool, // Parse & include comments in output
pub track_positions: bool, // Track line/col/offset positions
}
// Presets
ParseOptions::fast() // Default: no comments, no positions
ParseOptions::full() // Comments + positions enabledFor precise location tracking:
pub struct Position {
pub line: usize, // 0-indexed line number
pub col: usize, // 0-indexed column number
pub offset: usize, // Byte offset from file start
}
pub struct Span {
pub start: Position,
pub end: Position,
}Indicates how a value was quoted:
pub enum QuoteType {
Single, // 'value'
Double, // "value"
None, // value (unquoted)
}Fast parsing - returns key-value pairs only, no position tracking:
let entries = korni::parse("KEY=value");Configurable parsing with custom options:
let entries = korni::parse_with_options(input, ParseOptions::full());The builder API provides a fluent interface for parsing from various sources:
use korni::Korni;
let env = Korni::from_str("KEY=value")
.preserve_comments()
.track_positions()
.parse()?;
println!("{}", env.get("KEY").unwrap());let env = Korni::from_file(".env")
.preserve_comments()
.parse()?;Searches current directory and ancestors for the file:
let env = Korni::find_file(".env")?
.parse()?;let bytes = b"KEY=value";
let env = Korni::from_bytes(bytes).parse()?;use std::io::Cursor;
let reader = Cursor::new("KEY=value");
let env = Korni::from_reader(reader).parse()?;The Environment struct provides a HashMap-like interface:
use korni::Korni;
let env = Korni::from_str("
DB_HOST=localhost
DB_PORT=5432
").parse()?;
// Get a value
let host = env.get("DB_HOST"); // Option<&str>
let port = env.get_or("DB_PORT", "3306"); // &str with default
// Get full entry with metadata
if let Some(entry) = env.get_entry("DB_HOST") {
println!("Quoted: {:?}", entry.quote);
println!("Exported: {}", entry.is_exported);
}
// Iterate all pairs
for pair in env.iter() {
println!("{} = {}", pair.key, pair.value);
}
// Check for errors
if env.has_errors() {
for error in env.errors() {
eprintln!("Error: {}", error);
}
}
// Export to HashMap<String, String>
let map = env.to_map();Stream entries one at a time for memory efficiency:
use korni::{Parser, EnvIterator};
let parser = Parser::new("KEY1=a\nKEY2=b");
let iter = EnvIterator::new(parser);
for entry in iter {
// Process entry
}All parsing errors include byte offsets for precise error reporting:
pub enum Error {
InvalidUtf8 { offset: usize, reason: String },
UnclosedQuote { quote_type: &'static str, offset: usize },
InvalidKey { offset: usize, reason: String },
ForbiddenWhitespace { location: &'static str, offset: usize },
DoubleEquals { offset: usize },
InvalidBom { offset: usize },
Expected { offset: usize, expected: &'static str },
Generic { offset: usize, message: String },
Io(String),
}
// Get the byte offset of any error
let offset = error.offset();- Must contain only ASCII alphanumeric characters and underscores (
[A-Za-z0-9_]) - Must NOT start with a digit
- No whitespace allowed between key and
=
VALID_KEY=value
_also_valid=value
# Invalid: 123_KEY=value (starts with digit)
# Invalid: KEY = value (whitespace around =)- Terminate at first whitespace or end of line
- Support line continuation with trailing backslash
KEY=simple_value
MULTI=line1\
line2- Literal strings - no escape processing
- Must be closed on the same line
KEY='raw value with $VAR not expanded'- Support escape sequences:
\n,\r,\t,\\,\",\$ - Unknown escapes are preserved literally
KEY="line1\nline2"
ESCAPED="contains \"quotes\""- Lines starting with
#are comments - Inline comments:
KEY=value # comment(requires whitespace before#) - Commented-out pairs (
# KEY=value) are parsed withis_comment: true
The optional export prefix is supported:
export DATABASE_URL=postgres://localhost/db- UTF-8 BOM (
\xEF\xBB\xBF) at file start is silently skipped - BOM in middle of file produces an error
dotenvy is the standard, battle-tested crate for loading environment variables in Rust applications. korni serves a different, more specialized purpose.
| Feature | dotenvy |
korni |
|---|---|---|
| Primary Goal | Load env vars into std::env |
Parse env files into structured data |
| Output | Modifies process environment | AST / Structured Iterators |
| Introspection | None (opaque loading) | Full (Spans, Line/Col, Offsets) |
| Comments | Ignored | Parsed & Preserved |
| Error Handling | Stops on first error | Failure-tolerant (collects all errors) |
Modifies std::env |
✅ Yes | ❌ No (Pure parsing) |
| Use Case | Application Configuration | Tooling (IDEs, Linters), Complex Configs |
- You just want
cargo runto pick up your.envfile. - You need standard, 12-factor app configuration.
- You are building an IDE plugin, linter, or formatter.
- You need to analyze the structure of an
.envfile (e.g. "where isDB_PORTdefined?"). - You need performance-critical parsing of massive files.
- You want to manually control how environment variables are applied.
This parser implements the EDF (Ecolog Dotenv File Format) 1.0.1 specification.
korni aims for EDF 1.0.1 compliance. Per the specification's compliance requirements:
A parser implementation claiming EDF 1.0.1 Compliance MUST adhere to ALL requirements specified in the specification. This is a strict, all-or-nothing compliance model.
- ✅ Parsing Rules: Keys, values (quoted/unquoted), comments, and multiline handling per Section 4
- ✅ Error Handling: Failure-tolerant parsing with detailed error messages including byte offsets
- ✅ Security: UTF-8 validation, BOM handling, error message safety
- ✅ Edge Cases: Empty values, escape sequences, line continuations
This implementation varies in:
- Performance: Heavily optimized with focus on zero-copy parsing (but it is not absolutely zero-copy) and SIMD-friendly iterators
- API Design: Rust-idiomatic with
Cowstrings, builders, and iterators - Additional Features: Position tracking, comment parsing, and
is_commentflag for commented-out pairs
- Specification Version: EDF 1.0.1
- Semantic Versioning: This library follows semver. Major version bumps indicate potential parsing behavior changes.
MIT