diff --git a/dotenv/examples/multiple_files.rs b/dotenv/examples/multiple_files.rs new file mode 100644 index 0000000..4fec59a --- /dev/null +++ b/dotenv/examples/multiple_files.rs @@ -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"), + } +} diff --git a/dotenv/src/find.rs b/dotenv/src/find.rs index dd60728..bb4faf1 100644 --- a/dotenv/src/find.rs +++ b/dotenv/src/find.rs @@ -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)> { - 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::>()), + ))) } } diff --git a/dotenv/src/lib.rs b/dotenv/src/lib.rs index f2b1b0a..c06334d 100644 --- a/dotenv/src/lib.rs +++ b/dotenv/src/lib.rs @@ -150,6 +150,48 @@ pub fn from_filename_iter>(filename: P) -> Result> { 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>(filenames: &[P]) -> Result { + 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>(filenames: &[P]) -> Result> { + 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. /// diff --git a/dotenv/tests/test-from-filenames.rs b/dotenv/tests/test-from-filenames.rs new file mode 100644 index 0000000..5c21d8d --- /dev/null +++ b/dotenv/tests/test-from-filenames.rs @@ -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(¤t).unwrap(); + temp.close().unwrap(); +}