Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions dotenv/examples/multiple_files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use dotenv;
use std::env;

fn main() {
// Try to load from multiple files in priority order
// This will load from .env.local first, then fall back to .env if .env.local doesn't exist
match dotenv::from_filenames(&[".env.local", ".env"]) {
Ok(path) => println!("Loaded environment from: {:?}", path),
Err(e) => println!("Failed to load any environment file: {}", e),
}

// Now you can use environment variables as usual
match env::var("MY_VAR") {
Ok(val) => println!("MY_VAR = {}", val),
Err(_) => println!("MY_VAR not found in environment"),
}
}
42 changes: 35 additions & 7 deletions dotenv/src/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,54 @@ use crate::errors::*;
use crate::iter::Iter;

pub struct Finder<'a> {
filename: &'a Path,
filenames: Vec<&'a Path>,
}

impl<'a> Finder<'a> {
pub fn new() -> Self {
Finder {
filename: Path::new(".env"),
filenames: vec![Path::new(".env")],
}
}

pub fn filename(mut self, filename: &'a Path) -> Self {
self.filename = filename;
self.filenames = vec![filename];
self
}

pub fn filenames(mut self, filenames: Vec<&'a Path>) -> Self {
self.filenames = filenames;
self
}

pub fn find(self) -> Result<(PathBuf, Iter<File>)> {
let path = find(&env::current_dir().map_err(Error::Io)?, self.filename)?;
let file = File::open(&path).map_err(Error::Io)?;
let iter = Iter::new(file);
Ok((path, iter))
if self.filenames.is_empty() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
"No filenames specified",
)));
}

let current_dir = &env::current_dir().map_err(Error::Io)?;

for filename in &self.filenames {
match find(current_dir, filename) {
Ok(path) => {
let file = File::open(&path).map_err(Error::Io)?;
let iter = Iter::new(file);
return Ok((path, iter));
}
Err(e) if e.not_found() => continue,
Err(e) => return Err(e),
}
}

// If we get here, none of the files were found
Err(Error::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("None of the specified dotenv files found: {:?}",
self.filenames.iter().map(|p| p.to_string_lossy()).collect::<Vec<_>>()),
)))
}
}

Expand Down
42 changes: 42 additions & 0 deletions dotenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,48 @@ pub fn from_filename_iter<P: AsRef<Path>>(filename: P) -> Result<Iter<File>> {
Ok(iter)
}

/// Loads the first available file from the specified list of filenames from the environment's current directory or its parents in sequence.
///
/// This function will try each filename in order until it finds one that exists, then load that file.
/// If none of the files exist, it returns an error.
///
/// # Examples
/// ```
/// use dotenv;
///
/// // Try to load from .env.local first, then .env if .env.local doesn't exist
/// dotenv::from_filenames(&[".env.local", ".env"]).ok();
/// ```
pub fn from_filenames<P: AsRef<Path>>(filenames: &[P]) -> Result<PathBuf> {
let paths: Vec<&Path> = filenames.iter().map(|p| p.as_ref()).collect();
let (path, iter) = Finder::new().filenames(paths).find()?;
iter.load()?;
Ok(path)
}

/// Like `from_filenames`, but returns an iterator over variables instead of loading into environment.
///
/// This function will try each filename in order until it finds one that exists, then return an iterator for that file.
/// If none of the files exist, it returns an error.
///
/// # Examples
/// ```no_run
/// use dotenv;
///
/// // Try to load from .env.local first, then .env if .env.local doesn't exist
/// let iter = dotenv::from_filenames_iter(&[".env.local", ".env"]).unwrap();
///
/// for item in iter {
/// let (key, val) = item.unwrap();
/// println!("{}={}", key, val);
/// }
/// ```
pub fn from_filenames_iter<P: AsRef<Path>>(filenames: &[P]) -> Result<Iter<File>> {
let paths: Vec<&Path> = filenames.iter().map(|p| p.as_ref()).collect();
let (_, iter) = Finder::new().filenames(paths).find()?;
Ok(iter)
}

/// This is usually what you want.
/// It loads the .env file located in the environment's current directory or its parents in sequence.
///
Expand Down
38 changes: 38 additions & 0 deletions dotenv/tests/test-from-filenames.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::env;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;

use dotenv::*;

#[test]
fn test_from_filenames_priority() {
let temp = TempDir::new().unwrap();
let current = env::current_dir().unwrap();
env::set_current_dir(temp.path()).unwrap();

let mut env_file = File::create(".env").unwrap();
env_file.write_all(b"TESTKEY=test_val\n").unwrap();
env_file.sync_all().unwrap();

// clear it to start fresh
env::remove_var("TESTKEY");

// should find .env when .env.local doesn't exist
from_filenames(&[".env.local", ".env"]).unwrap();
assert_eq!(env::var("TESTKEY").unwrap(), "test_val");

let mut local_file = File::create(".env.local").unwrap();
local_file.write_all(b"TESTKEY=local_val\n").unwrap();
local_file.sync_all().unwrap();

env::remove_var("TESTKEY");

// should find .env.local first (higher priority)
from_filenames(&[".env.local", ".env"]).unwrap();
assert_eq!(env::var("TESTKEY").unwrap(), "local_val");

// restore
env::set_current_dir(&current).unwrap();
temp.close().unwrap();
}