Skip to content

recursively load .env files up to root #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
51 changes: 29 additions & 22 deletions dotenv/src/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,39 @@ impl<'a> Finder<'a> {
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))
pub fn find(self) -> Result<Vec<(PathBuf, Iter<File>)>> {
let paths = find(&env::current_dir().map_err(Error::Io)?, self.filename)?;

paths
.into_iter()
.map(|path| match File::open(&path) {
Ok(file) => Ok((path, Iter::new(file))),
Err(err) => Err(Error::Io(err))
})
.collect::<Result<Vec<_>>>()
}
}

/// Searches for `filename` in `directory` and parent directories until found or root is reached.
pub fn find(directory: &Path, filename: &Path) -> Result<PathBuf> {
let candidate = directory.join(filename);

match fs::metadata(&candidate) {
Ok(metadata) => if metadata.is_file() {
return Ok(candidate);
},
Err(error) => {
if error.kind() != io::ErrorKind::NotFound {
return Err(Error::Io(error));
}
}
pub fn find(directory: &Path, filename: &Path) -> Result<Vec<PathBuf>> {

let results = directory
.ancestors()
.map(|path| path.join(filename))
.filter_map(|candidate| match fs::metadata(&candidate) {
Ok(metadata) if metadata.is_file() => {
Some(Ok(candidate))
},
Err(error) if error.kind() != io::ErrorKind::NotFound => {
Some(Err(Error::Io(error)))
},
_ => None,
})
.collect::<Result<Vec<_>>>()?;

if results.is_empty() {
return Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, "path not found")));
}

if let Some(parent) = directory.parent() {
find(parent, filename)
} else {
Err(Error::Io(io::Error::new(io::ErrorKind::NotFound, "path not found")))
}
Ok(results)
}
32 changes: 32 additions & 0 deletions dotenv/src/iter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::collections::HashMap;
use std::env;
use std::io::{BufReader, Lines};
Expand Down Expand Up @@ -50,3 +51,34 @@ impl<R: Read> Iterator for Iter<R> {
}
}
}

pub struct DistinctEnvIter<T> {
seen_keys: HashSet<String>,
inner: T,
}

impl<T: Iterator<Item = Result<(String, String)>>> DistinctEnvIter<T> {
pub fn new(inner_iter: T) -> DistinctEnvIter<T> {
DistinctEnvIter {
seen_keys: HashSet::new(),
inner: inner_iter
}
}
}

impl<T: Iterator<Item = Result<(String, String)>>> Iterator for DistinctEnvIter<T> {
type Item = Result<(String, String)>;

fn next(&mut self) -> Option<Self::Item> {
loop {
match self.inner.next() {
Some(Ok((key, value))) if !self.seen_keys.contains(&key) => {
self.seen_keys.insert(key.clone());
return Some(Ok((key, value)));
},
Some(Ok(_)) => continue,
otherwise => return otherwise,
}
}
}
}
47 changes: 33 additions & 14 deletions dotenv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::path::{Path, PathBuf};
use std::sync::{Once};

pub use crate::errors::*;
use crate::iter::Iter;
use crate::iter::{Iter, DistinctEnvIter};
use crate::find::Finder;

static START: Once = Once::new();
Expand Down Expand Up @@ -120,10 +120,15 @@ pub fn from_path_iter<P: AsRef<Path>>(path: P) -> Result<Iter<File>> {
/// use dotenv;
/// dotenv::from_filename(".env").ok();
/// ```
pub fn from_filename<P: AsRef<Path>>(filename: P) -> Result<PathBuf> {
let (path, iter) = Finder::new().filename(filename.as_ref()).find()?;
iter.load()?;
Ok(path)
pub fn from_filename<P: AsRef<Path>>(filename: P) -> Result<Vec<PathBuf>> {
let results = Finder::new().filename(filename.as_ref()).find()?;
results
.into_iter()
.map(|(path, iter)| -> Result<PathBuf> {
iter.load()?;
Ok(path)
})
.collect()
}

/// Like `from_filename`, but returns an iterator over variables instead of loading into environment.
Expand All @@ -147,9 +152,13 @@ pub fn from_filename<P: AsRef<Path>>(filename: P) -> Result<PathBuf> {
/// }
/// ```
#[deprecated(since = "0.14.1", note = "please use `from_path` in conjunction with `var` instead")]
pub fn from_filename_iter<P: AsRef<Path>>(filename: P) -> Result<Iter<File>> {
let (_, iter) = Finder::new().filename(filename.as_ref()).find()?;
Ok(iter)
pub fn from_filename_iter<P: AsRef<Path>>(filename: P) -> Result<impl Iterator<Item = Result<(String, String)>>> {
let results = Finder::new().filename(filename.as_ref()).find()?;
Ok(DistinctEnvIter::new(results
.into_iter()
.flat_map(|(_, iter)| {
iter
})))
}

/// This is usually what you want.
Expand All @@ -161,9 +170,14 @@ pub fn from_filename_iter<P: AsRef<Path>>(filename: P) -> Result<Iter<File>> {
/// dotenv::dotenv().ok();
/// ```
pub fn dotenv() -> Result<PathBuf> {
let (path, iter) = Finder::new().find()?;
iter.load()?;
Ok(path)
let results = Finder::new().find()?;
results
.into_iter()
.map(|(path, iter)| -> Result<PathBuf> {
iter.load()?;
Ok(path)
})
.collect()
}

/// Like `dotenv`, but returns an iterator over variables instead of loading into environment.
Expand All @@ -178,7 +192,12 @@ pub fn dotenv() -> Result<PathBuf> {
/// }
/// ```
#[deprecated(since = "0.14.1", note = "please use `from_path` in conjunction with `var` instead")]
pub fn dotenv_iter() -> Result<iter::Iter<File>> {
let (_, iter) = Finder::new().find()?;
Ok(iter)
pub fn dotenv_iter() -> Result<impl Iterator<Item = Result<(String, String)>>> {
let results = Finder::new().find()?;

Ok(DistinctEnvIter::new(results
.into_iter()
.flat_map(|(_, iter)| {
iter
})))
}
23 changes: 22 additions & 1 deletion dotenv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result<TempDir> {
}

pub fn make_test_dotenv() -> io::Result<TempDir> {
tempdir_with_dotenv("TESTKEY=test_val")
tempdir_with_dotenv("TESTKEY=test_val")
}

pub fn make_layered_test_dotenv() -> io::Result<TempDir> {
let dir = tempdir()?;
env::set_current_dir(dir.path())?;

let dotenv_path = dir.path().join(".env");
let mut dotenv_file = File::create(dotenv_path)?;
dotenv_file.write_all("TESTKEY=test_val\nTESTKEY2=test_val_outer".as_bytes())?;
dotenv_file.sync_all()?;

let inner_dir = dir.path().join("inner");
std::fs::create_dir(&inner_dir)?;
env::set_current_dir(&inner_dir)?;

let inner_dotenv_path = inner_dir.join(".env");
let mut inner_dotenv_file = File::create(inner_dotenv_path)?;
inner_dotenv_file.write_all("TESTKEY2=test_val_inner".as_bytes())?;
inner_dotenv_file.sync_all()?;

Ok(dir)
}

18 changes: 15 additions & 3 deletions dotenv/tests/test-dotenv-iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@ fn test_dotenv_iter() {

let iter = dotenv_iter().unwrap();

assert!(env::var("TESTKEY").is_err());
iter.filter_map(Result::ok).any(|(key, value)| key == "TESTKEY" && value == "test_val");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant to be an assertion?


iter.load().ok();
env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
}

#[test]
#[allow(deprecated)]
fn test_dotenv_subdir_iter() {
let dir = make_layered_test_dotenv().unwrap();

let iter = dotenv_iter().unwrap();

assert_eq!(env::var("TESTKEY").unwrap(), "test_val");
let pairs = iter.filter_map(Result::ok).collect::<Vec<_>>();

assert!(pairs.contains(&("TESTKEY".into(), "test_val".into())));
assert!(pairs.contains(&("TESTKEY2".into(), "test_val_inner".into())));

env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
Expand Down
19 changes: 16 additions & 3 deletions dotenv/tests/test-from-filename-iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,25 @@ fn test_from_filename_iter() {

let iter = from_filename_iter(".env").unwrap();

assert!(env::var("TESTKEY").is_err());
iter.filter_map(Result::ok).any(|(key, value)| key == "TESTKEY" && value == "test_val");

iter.load().ok();
env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
}

assert_eq!(env::var("TESTKEY").unwrap(), "test_val");
#[test]
#[allow(deprecated)]
fn test_from_filename_subdir_iter() {
let dir = make_layered_test_dotenv().unwrap();

let iter = from_filename_iter(".env").unwrap();

let pairs = iter.filter_map(Result::ok).collect::<Vec<_>>();

assert!(pairs.contains(&("TESTKEY".into(), "test_val".into())));
assert!(pairs.contains(&("TESTKEY2".into(), "test_val_inner".into())));

env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
}

15 changes: 14 additions & 1 deletion dotenv/tests/test-from-filename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,23 @@ use crate::common::*;
fn test_from_filename() {
let dir = make_test_dotenv().unwrap();

from_filename(".env").ok();
from_filename(".env").unwrap();

assert_eq!(env::var("TESTKEY").unwrap(), "test_val");

env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
}

#[test]
fn test_from_filename_subdir() {
let dir = make_layered_test_dotenv().unwrap();

from_filename(".env").unwrap();

assert_eq!(env::var("TESTKEY").unwrap(), "test_val");
assert_eq!(env::var("TESTKEY2").unwrap(), "test_val_inner");

env::set_current_dir(dir.path().parent().unwrap()).unwrap();
dir.close().unwrap();
}