From a45eedae6b90edaad8ccf10b29412b2f72f35919 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:54:07 -0400 Subject: [PATCH 01/39] Use 2024 edition, mark unsafe `set::env_set_var` is unsafe in the 2024 edition. All functions calling it are marked as unsafe. --- dotenv/Cargo.toml | 8 +- dotenv/src/iter.rs | 8 +- dotenv/src/lib.rs | 73 ++++++++++--------- dotenv/src/parse.rs | 4 +- dotenv/tests/common/mod.rs | 10 ++- dotenv/tests/integration/util/testenv.rs | 16 ++-- dotenv/tests/test-child-dir.rs | 4 +- .../tests/test-default-location-override.rs | 4 +- dotenv/tests/test-default-location.rs | 4 +- dotenv/tests/test-dotenv-iter.rs | 4 +- dotenv/tests/test-from-filename-iter.rs | 4 +- dotenv/tests/test-from-filename-override.rs | 4 +- dotenv/tests/test-from-filename.rs | 4 +- dotenv/tests/test-from-path-iter.rs | 4 +- dotenv/tests/test-from-path-override.rs | 4 +- dotenv/tests/test-from-path.rs | 4 +- dotenv/tests/test-from-read-override.rs | 6 +- dotenv/tests/test-from-read.rs | 6 +- dotenv/tests/test-ignore-bom.rs | 6 +- dotenv/tests/test-multiline-comment.rs | 11 ++- dotenv/tests/test-multiline.rs | 8 +- dotenv/tests/test-var.rs | 4 +- dotenv/tests/test-variable-substitution.rs | 22 +++--- dotenv/tests/test-vars.rs | 4 +- dotenv_codegen/Cargo.toml | 8 +- dotenv_codegen/src/lib.rs | 10 +-- 26 files changed, 131 insertions(+), 113 deletions(-) diff --git a/dotenv/Cargo.toml b/dotenv/Cargo.toml index f6dc182e..8e52f81c 100644 --- a/dotenv/Cargo.toml +++ b/dotenv/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["edition2024"] + [package] name = "dotenvy" version = "0.15.7" @@ -18,8 +20,8 @@ keywords = ["dotenv", "env", "environment", "settings", "config"] categories = ["configuration"] license = "MIT" repository = "https://github.com/allan2/dotenvy" -edition = "2021" -rust-version = "1.72.0" +edition = "2024" +#rust-version = "1.72.0" [[bin]] name = "dotenvy" @@ -32,4 +34,4 @@ clap = { version = "4.5.16", features = ["derive"], optional = true } tempfile = "3.12.0" [features] -cli = ["clap"] +cli = ["dep:clap"] diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs index 05f4ddd5..1129c3f4 100644 --- a/dotenv/src/iter.rs +++ b/dotenv/src/iter.rs @@ -28,13 +28,13 @@ impl Iter { /// /// If a variable is specified multiple times within the reader's data, /// then the first occurrence is applied. - pub fn load(mut self) -> Result<()> { + pub unsafe fn load(mut self) -> Result<()> { self.remove_bom()?; for item in self { let (key, value) = item?; if env::var(&key).is_err() { - env::set_var(&key, value); + unsafe { env::set_var(&key, value) }; } } @@ -46,12 +46,12 @@ impl Iter { /// /// If a variable is specified multiple times within the reader's data, /// then the last occurrence is applied. - pub fn load_override(mut self) -> Result<()> { + pub unsafe fn load_override(mut self) -> Result<()> { self.remove_bom()?; for item in self { let (key, value) = item?; - env::set_var(key, value); + unsafe { env::set_var(key, value) }; } Ok(()) diff --git a/dotenv/src/lib.rs b/dotenv/src/lib.rs index c9425ba5..4038b645 100644 --- a/dotenv/src/lib.rs +++ b/dotenv/src/lib.rs @@ -1,5 +1,10 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] -#![allow(clippy::missing_errors_doc, clippy::too_many_lines)] +#![allow( + clippy::missing_errors_doc, + clippy::too_many_lines, + clippy::missing_safety_doc +)] +#![deny(clippy::uninlined_format_args)] //! [`dotenv`]: https://crates.io/crates/dotenv //! A well-maintained fork of the [`dotenv`] crate @@ -35,14 +40,14 @@ static START: Once = Once::new(); /// /// ```no_run /// # fn main() -> Result<(), Box> { -/// let value = dotenvy::var("HOME")?; +/// let value = unsafe { dotenvy::var("HOME") }?; /// println!("{}", value); // prints `/home/foo` /// # Ok(()) /// # } /// ``` -pub fn var>(key: K) -> Result { +pub unsafe fn var>(key: K) -> Result { START.call_once(|| { - dotenv().ok(); + unsafe { dotenv() }.ok(); }); env::var(key).map_err(Error::EnvVar) } @@ -55,11 +60,11 @@ pub fn var>(key: K) -> Result { /// ```no_run /// use std::io; /// -/// let result: Vec<(String, String)> = dotenvy::vars().collect(); +/// let result: Vec<(String, String)> = unsafe { dotenvy::vars() }.collect(); /// ``` -pub fn vars() -> Vars { +pub unsafe fn vars() -> Vars { START.call_once(|| { - dotenv().ok(); + unsafe { dotenv() }.ok(); }); env::vars() } @@ -81,13 +86,14 @@ pub fn vars() -> Vars { /// use std::path::Path; /// /// # fn main() -> Result<(), Box> { -/// dotenvy::from_path(Path::new("path/to/.env"))?; +/// let path = Path::new("path/to/.env"); +/// unsafe { dotenvy::from_path(path) }?; /// # Ok(()) /// # } /// ``` -pub fn from_path>(path: P) -> Result<()> { +pub unsafe fn from_path>(path: P) -> Result<()> { let iter = Iter::new(File::open(path).map_err(Error::Io)?); - iter.load() + unsafe { iter.load() } } /// Loads environment variables from the specified path, @@ -106,13 +112,14 @@ pub fn from_path>(path: P) -> Result<()> { /// use std::path::Path; /// /// # fn main() -> Result<(), Box> { -/// dotenvy::from_path_override(Path::new("path/to/.env"))?; +/// let path = Path::new("path/to/.env"); +/// unsafe { dotenvy::from_path_override(path) }?; /// # Ok(()) /// # } /// ``` -pub fn from_path_override>(path: P) -> Result<()> { +pub unsafe fn from_path_override>(path: P) -> Result<()> { let iter = Iter::new(File::open(path).map_err(Error::Io)?); - iter.load_override() + unsafe { iter.load_override() } } /// Returns an iterator over environment variables from the specified path. @@ -148,7 +155,7 @@ pub fn from_path_iter>(path: P) -> Result> { /// # Examples /// ```no_run /// # fn main() -> Result<(), Box> { -/// dotenvy::from_filename("custom.env")?; +/// unsafe { dotenvy::from_filename("custom.env") }?; /// # Ok(()) /// # } /// ``` @@ -157,13 +164,13 @@ pub fn from_path_iter>(path: P) -> Result> { /// /// ``` /// # fn main() -> Result<(), Box> { -/// dotenvy::from_filename(".env")?; +/// unsafe { dotenvy::from_filename(".env") }?; /// # Ok(()) /// # } /// ``` -pub fn from_filename>(filename: P) -> Result { +pub unsafe fn from_filename>(filename: P) -> Result { let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; - iter.load()?; + unsafe { iter.load() }?; Ok(path) } @@ -180,7 +187,7 @@ pub fn from_filename>(filename: P) -> Result { /// # Examples /// ```no_run /// # fn main() -> Result<(), Box> { -/// dotenvy::from_filename_override("custom.env")?; +/// unsafe { dotenvy::from_filename_override("custom.env") }?; /// # Ok(()) /// # } /// ``` @@ -189,13 +196,13 @@ pub fn from_filename>(filename: P) -> Result { /// /// ``` /// # fn main() -> Result<(), Box> { -/// dotenvy::from_filename_override(".env")?; +/// unsafe { dotenvy::from_filename_override(".env")}?; /// # Ok(()) /// # } /// ``` -pub fn from_filename_override>(filename: P) -> Result { +pub unsafe fn from_filename_override>(filename: P) -> Result { let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; - iter.load_override()?; + unsafe { iter.load_override() }?; Ok(path) } @@ -243,13 +250,13 @@ pub fn from_filename_iter>(filename: P) -> Result> { /// # #[cfg(unix)] /// let mut stream = UnixStream::connect("/some/socket")?; /// # #[cfg(unix)] -/// dotenvy::from_read(stream)?; +/// unsafe { dotenvy::from_read(stream) }?; /// # Ok(()) /// # } /// ``` -pub fn from_read(reader: R) -> Result<()> { +pub unsafe fn from_read(reader: R) -> Result<()> { let iter = Iter::new(reader); - iter.load()?; + unsafe { iter.load() }?; Ok(()) } @@ -277,13 +284,13 @@ pub fn from_read(reader: R) -> Result<()> { /// # #[cfg(unix)] /// let mut stream = UnixStream::connect("/some/socket")?; /// # #[cfg(unix)] -/// dotenvy::from_read_override(stream)?; +/// unsafe { dotenvy::from_read_override(stream) }?; /// # Ok(()) /// # } /// ``` -pub fn from_read_override(reader: R) -> Result<()> { +pub unsafe fn from_read_override(reader: R) -> Result<()> { let iter = Iter::new(reader); - iter.load_override()?; + unsafe { iter.load_override() }?; Ok(()) } @@ -329,13 +336,13 @@ pub fn from_read_iter(reader: R) -> Iter { /// /// ``` /// # fn main() -> Result<(), Box> { -/// dotenvy::dotenv()?; +/// unsafe { dotenvy::dotenv() }?; /// # Ok(()) /// # } /// ``` -pub fn dotenv() -> Result { +pub unsafe fn dotenv() -> Result { let (path, iter) = Finder::new().find()?; - iter.load()?; + unsafe { iter.load() }?; Ok(path) } @@ -352,13 +359,13 @@ pub fn dotenv() -> Result { /// # Examples /// ``` /// # fn main() -> Result<(), Box> { -/// dotenvy::dotenv_override()?; +/// unsafe { dotenvy::dotenv_override() }?; /// # Ok(()) /// # } /// ``` -pub fn dotenv_override() -> Result { +pub unsafe fn dotenv_override() -> Result { let (path, iter) = Finder::new().find()?; - iter.load_override()?; + unsafe { iter.load_override() }?; Ok(path) } diff --git a/dotenv/src/parse.rs b/dotenv/src/parse.rs index 397c73d5..568bd7a6 100644 --- a/dotenv/src/parse.rs +++ b/dotenv/src/parse.rs @@ -512,14 +512,14 @@ mod variable_substitution_tests { #[test] fn substitute_variable_from_env_variable() { - env::set_var("KEY11", "test_user_env"); + unsafe { env::set_var("KEY11", "test_user_env") }; assert_parsed_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]); } #[test] fn substitute_variable_env_variable_overrides_dotenv_in_substitution() { - env::set_var("KEY11", "test_user_env"); + unsafe { env::set_var("KEY11", "test_user_env") }; assert_parsed_string( r#" diff --git a/dotenv/tests/common/mod.rs b/dotenv/tests/common/mod.rs index d4df7b3b..338c0a5b 100644 --- a/dotenv/tests/common/mod.rs +++ b/dotenv/tests/common/mod.rs @@ -5,8 +5,8 @@ use std::{ }; use tempfile::{tempdir, TempDir}; -pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { - env::set_var("EXISTING", "from_env"); +pub unsafe fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { + unsafe { env::set_var("EXISTING", "from_env") }; let dir = tempdir()?; env::set_current_dir(dir.path())?; let dotenv_path = dir.path().join(".env"); @@ -17,6 +17,8 @@ pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { } #[allow(dead_code)] -pub fn make_test_dotenv() -> io::Result { - tempdir_with_dotenv("TESTKEY=test_val\nTESTKEY=test_val_overridden\nEXISTING=from_file") +pub unsafe fn make_test_dotenv() -> io::Result { + unsafe { + tempdir_with_dotenv("TESTKEY=test_val\nTESTKEY=test_val_overridden\nEXISTING=from_file") + } } diff --git a/dotenv/tests/integration/util/testenv.rs b/dotenv/tests/integration/util/testenv.rs index 23b0ff6f..616954aa 100644 --- a/dotenv/tests/integration/util/testenv.rs +++ b/dotenv/tests/integration/util/testenv.rs @@ -45,7 +45,7 @@ pub struct KeyVal { /// /// Resets the environment variables, loads the [`TestEnv`], then runs the test /// closure. Ensures only one thread has access to the process environment. -pub fn test_in_env(test_env: TestEnv, test: F) +pub unsafe fn test_in_env(test_env: TestEnv, test: F) where F: FnOnce(), { @@ -55,7 +55,7 @@ where let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner); // we reset the environment anyway upon acquiring the lock reset_env(&original_env); - create_env(&test_env); + unsafe { create_env(&test_env) }; test(); // drop the lock and the `TestEnv` - should delete the tempdir } @@ -77,12 +77,12 @@ where /// /// Notice that file has the potential to override `TEST_EXISTING_KEY` depending /// on the what's being tested. -pub fn test_in_default_env(test: F) +pub unsafe fn test_in_default_env(test: F) where F: FnOnce(), { let test_env = TestEnv::default(); - test_in_env(test_env, test); + unsafe { test_in_env(test_env, test) }; } impl TestEnv { @@ -294,17 +294,17 @@ fn reset_env(original_env: &EnvMap) { // remove keys if they weren't in the original environment env::vars() .filter(|(key, _)| !original_env.contains_key(key)) - .for_each(|(key, _)| env::remove_var(key)); + .for_each(|(key, _)| unsafe { env::remove_var(key) }); // ensure original keys have their original values original_env .iter() - .for_each(|(key, value)| env::set_var(key, value)); + .for_each(|(key, value)| unsafe { env::set_var(key, value) }); } /// Create an environment to run tests in. /// /// Writes the envfile, sets the working directory, and sets environment vars. -fn create_env(test_env: &TestEnv) { +unsafe fn create_env(test_env: &TestEnv) { // only create the envfile if its contents has been set if let Some(contents) = test_env.envfile_contents() { create_envfile(&test_env.envfile_path, contents); @@ -313,7 +313,7 @@ fn create_env(test_env: &TestEnv) { env::set_current_dir(&test_env.work_dir).expect("setting working directory"); for KeyVal { key, value } in &test_env.env_vars { - env::set_var(key, value) + unsafe { env::set_var(key, value) } } } diff --git a/dotenv/tests/test-child-dir.rs b/dotenv/tests/test-child-dir.rs index e2ffef77..8a11f859 100644 --- a/dotenv/tests/test-child-dir.rs +++ b/dotenv/tests/test-child-dir.rs @@ -5,13 +5,13 @@ use std::{env, error, fs}; #[test] fn test_child_dir() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; fs::create_dir("child")?; env::set_current_dir("child")?; - dotenvy::dotenv()?; + unsafe { dotenvy::dotenv() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); env::set_current_dir(dir.path().parent().unwrap())?; diff --git a/dotenv/tests/test-default-location-override.rs b/dotenv/tests/test-default-location-override.rs index 464ee527..9110bc14 100644 --- a/dotenv/tests/test-default-location-override.rs +++ b/dotenv/tests/test-default-location-override.rs @@ -5,9 +5,9 @@ use std::{env, error}; #[test] fn test_default_location_override() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - dotenvy::dotenv_override()?; + unsafe { dotenvy::dotenv_override() }?; assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); assert_eq!(env::var("EXISTING")?, "from_file"); diff --git a/dotenv/tests/test-default-location.rs b/dotenv/tests/test-default-location.rs index 43616f0f..59ee2f53 100644 --- a/dotenv/tests/test-default-location.rs +++ b/dotenv/tests/test-default-location.rs @@ -5,9 +5,9 @@ use std::{env, error}; #[test] fn test_default_location() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - dotenvy::dotenv()?; + unsafe { dotenvy::dotenv() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenv/tests/test-dotenv-iter.rs b/dotenv/tests/test-dotenv-iter.rs index 17fb1ac1..416b5db6 100644 --- a/dotenv/tests/test-dotenv-iter.rs +++ b/dotenv/tests/test-dotenv-iter.rs @@ -5,12 +5,12 @@ use std::{env, error}; #[test] fn test_dotenv_iter() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; let iter = dotenvy::dotenv_iter()?; assert!(env::var("TESTKEY").is_err()); - iter.load()?; + unsafe { iter.load() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); env::set_current_dir(dir.path().parent().unwrap())?; diff --git a/dotenv/tests/test-from-filename-iter.rs b/dotenv/tests/test-from-filename-iter.rs index bcc33cd4..8c61db5d 100644 --- a/dotenv/tests/test-from-filename-iter.rs +++ b/dotenv/tests/test-from-filename-iter.rs @@ -5,13 +5,13 @@ use std::{env, error}; #[test] fn test_from_filename_iter() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; let iter = dotenvy::from_filename_iter(".env")?; assert!(env::var("TESTKEY").is_err()); - iter.load()?; + unsafe { iter.load() }?; assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); diff --git a/dotenv/tests/test-from-filename-override.rs b/dotenv/tests/test-from-filename-override.rs index 560f101d..78172c88 100644 --- a/dotenv/tests/test-from-filename-override.rs +++ b/dotenv/tests/test-from-filename-override.rs @@ -5,9 +5,9 @@ use std::{env, error}; #[test] fn test_from_filename_override() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - dotenvy::from_filename_override(".env")?; + unsafe { dotenvy::from_filename_override(".env") }?; assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); assert_eq!(env::var("EXISTING")?, "from_file"); diff --git a/dotenv/tests/test-from-filename.rs b/dotenv/tests/test-from-filename.rs index 83e25607..23e89413 100644 --- a/dotenv/tests/test-from-filename.rs +++ b/dotenv/tests/test-from-filename.rs @@ -6,9 +6,9 @@ use std::{env, error}; #[test] fn test_from_filename() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - from_filename(".env")?; + unsafe { from_filename(".env") }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenv/tests/test-from-path-iter.rs b/dotenv/tests/test-from-path-iter.rs index d19f953f..22f18561 100644 --- a/dotenv/tests/test-from-path-iter.rs +++ b/dotenv/tests/test-from-path-iter.rs @@ -6,7 +6,7 @@ use std::{env, error}; #[test] fn test_from_path_iter() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; let mut path = env::current_dir()?; path.push(".env"); @@ -15,7 +15,7 @@ fn test_from_path_iter() -> Result<(), Box> { assert!(env::var("TESTKEY").is_err()); - iter.load()?; + unsafe { iter.load() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); diff --git a/dotenv/tests/test-from-path-override.rs b/dotenv/tests/test-from-path-override.rs index 82080013..79a9782b 100644 --- a/dotenv/tests/test-from-path-override.rs +++ b/dotenv/tests/test-from-path-override.rs @@ -8,12 +8,12 @@ use crate::common::make_test_dotenv; #[test] fn test_from_path_override() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; let mut path = env::current_dir()?; path.push(".env"); - from_path_override(&path)?; + unsafe { from_path_override(&path) }?; assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); assert_eq!(env::var("EXISTING")?, "from_file"); diff --git a/dotenv/tests/test-from-path.rs b/dotenv/tests/test-from-path.rs index 6c6d3fc3..df77c34c 100644 --- a/dotenv/tests/test-from-path.rs +++ b/dotenv/tests/test-from-path.rs @@ -5,12 +5,12 @@ use std::{env, error}; #[test] fn test_from_path() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; let mut path = env::current_dir()?; path.push(".env"); - dotenvy::from_path(&path)?; + unsafe { dotenvy::from_path(&path) }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenv/tests/test-from-read-override.rs b/dotenv/tests/test-from-read-override.rs index 9f8429f2..da771d39 100644 --- a/dotenv/tests/test-from-read-override.rs +++ b/dotenv/tests/test-from-read-override.rs @@ -5,9 +5,9 @@ use std::{env, error, fs::File}; #[test] fn test_from_read_override() -> Result<(), Box> { - let dir = make_test_dotenv()?; - - dotenvy::from_read_override(File::open(".env")?)?; + let dir = unsafe { make_test_dotenv() }?; + let rdr = File::open(".env")?; + unsafe { dotenvy::from_read_override(rdr) }?; assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); assert_eq!(env::var("EXISTING")?, "from_file"); diff --git a/dotenv/tests/test-from-read.rs b/dotenv/tests/test-from-read.rs index d7efed33..2d0ef368 100644 --- a/dotenv/tests/test-from-read.rs +++ b/dotenv/tests/test-from-read.rs @@ -5,9 +5,9 @@ use std::{env, error, fs::File}; #[test] fn test_from_read() -> Result<(), Box> { - let dir = make_test_dotenv()?; - - dotenvy::from_read(File::open(".env")?)?; + let dir = unsafe { make_test_dotenv() }?; + let rdr = File::open(".env")?; + unsafe { dotenvy::from_read(rdr) }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenv/tests/test-ignore-bom.rs b/dotenv/tests/test-ignore-bom.rs index 35f1819f..5acbbbed 100644 --- a/dotenv/tests/test-ignore-bom.rs +++ b/dotenv/tests/test-ignore-bom.rs @@ -5,13 +5,13 @@ use std::{env, error}; #[test] fn test_ignore_bom() -> Result<(), Box> { - let bom = "\u{feff}"; - let dir = tempdir_with_dotenv(&format!("{}TESTKEY=test_val", bom))?; + let txt = format!("\u{feff}TESTKEY=test_val"); + let dir = unsafe { tempdir_with_dotenv(&txt) }?; let mut path = env::current_dir()?; path.push(".env"); - dotenvy::from_path(&path)?; + unsafe { dotenvy::from_path(&path) }?; assert_eq!(env::var("TESTKEY")?, "test_val"); diff --git a/dotenv/tests/test-multiline-comment.rs b/dotenv/tests/test-multiline-comment.rs index 8f67dc21..eb95b773 100644 --- a/dotenv/tests/test-multiline-comment.rs +++ b/dotenv/tests/test-multiline-comment.rs @@ -5,8 +5,7 @@ use common::tempdir_with_dotenv; #[test] fn test_issue_12() { - let _f = tempdir_with_dotenv( - r#" + let txt = r#" # Start of .env file # Comment line with single ' quote # Comment line with double " quote @@ -22,11 +21,11 @@ TESTKEY5="Line 4 Line 6 " # 5 Multiline "' comment # End of .env file -"#, - ) - .expect("should write test env"); +"#; - dotenvy::dotenv().expect("should succeed"); + let _f = unsafe { tempdir_with_dotenv(txt) }.expect("should write test env"); + + unsafe { dotenvy::dotenv() }.expect("should succeed"); assert_eq!( env::var("TESTKEY1").expect("testkey1 env key not set"), "test_val" diff --git a/dotenv/tests/test-multiline.rs b/dotenv/tests/test-multiline.rs index e730e522..b79f9f48 100644 --- a/dotenv/tests/test-multiline.rs +++ b/dotenv/tests/test-multiline.rs @@ -7,7 +7,8 @@ use std::{env, error}; fn test_multiline() -> Result<(), Box> { let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\""; let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\""; - let dir = tempdir_with_dotenv(&format!( + + let txt = format!( r#" KEY=my\ cool\ value KEY3="awesome \"stuff\" @@ -20,9 +21,10 @@ WEAK="{}" STRONG='{}' "#, value, value - ))?; + ); - dotenvy::dotenv()?; + let dir = unsafe { tempdir_with_dotenv(&txt) }?; + unsafe { dotenvy::dotenv() }?; assert_eq!(env::var("KEY")?, r#"my cool value"#); assert_eq!( env::var("KEY3")?, diff --git a/dotenv/tests/test-var.rs b/dotenv/tests/test-var.rs index 43b68ddc..eb57844e 100644 --- a/dotenv/tests/test-var.rs +++ b/dotenv/tests/test-var.rs @@ -5,9 +5,9 @@ use std::{env, error}; #[test] fn test_var() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - assert_eq!(dotenvy::var("TESTKEY")?, "test_val"); + assert_eq!(unsafe { dotenvy::var("TESTKEY") }?, "test_val"); env::set_current_dir(dir.path().parent().unwrap())?; dir.close()?; diff --git a/dotenv/tests/test-variable-substitution.rs b/dotenv/tests/test-variable-substitution.rs index 6bde0f5c..742a8be9 100644 --- a/dotenv/tests/test-variable-substitution.rs +++ b/dotenv/tests/test-variable-substitution.rs @@ -1,3 +1,5 @@ +#![deny(clippy::uninlined_format_args)] + mod common; use crate::common::tempdir_with_dotenv; @@ -5,27 +7,29 @@ use std::{env, error}; #[test] fn test_variable_substitutions() -> Result<(), Box> { - std::env::set_var("KEY", "value"); - std::env::set_var("KEY1", "value1"); + unsafe { + env::set_var("KEY", "value"); + env::set_var("KEY1", "value1"); + } let substitutions_to_test = [ "$ZZZ", "$KEY", "$KEY1", "${KEY}1", "$KEY_U", "${KEY_U}", "\\$KEY", ]; let common_string = substitutions_to_test.join(">>"); - let dir = tempdir_with_dotenv(&format!( + let txt = format!( r#" KEY1=new_value1 KEY_U=$KEY+valueU -SUBSTITUTION_FOR_STRONG_QUOTES='{}' -SUBSTITUTION_FOR_WEAK_QUOTES="{}" -SUBSTITUTION_WITHOUT_QUOTES={} +SUBSTITUTION_FOR_STRONG_QUOTES='{common_string}' +SUBSTITUTION_FOR_WEAK_QUOTES="{common_string}" +SUBSTITUTION_WITHOUT_QUOTES={common_string} "#, - common_string, common_string, common_string - ))?; + ); + let dir = unsafe { tempdir_with_dotenv(&txt) }?; - dotenvy::dotenv()?; + unsafe { dotenvy::dotenv() }?; assert_eq!(env::var("KEY")?, "value"); assert_eq!(env::var("KEY1")?, "value1"); diff --git a/dotenv/tests/test-vars.rs b/dotenv/tests/test-vars.rs index 4f513344..e8776bdc 100644 --- a/dotenv/tests/test-vars.rs +++ b/dotenv/tests/test-vars.rs @@ -5,9 +5,9 @@ use std::{collections::HashMap, env, error}; #[test] fn test_vars() -> Result<(), Box> { - let dir = make_test_dotenv()?; + let dir = unsafe { make_test_dotenv() }?; - let vars: HashMap = dotenvy::vars().collect(); + let vars: HashMap = unsafe { dotenvy::vars() }.collect(); assert_eq!(vars["TESTKEY"], "test_val"); diff --git a/dotenv_codegen/Cargo.toml b/dotenv_codegen/Cargo.toml index c8c57ce9..577e04f9 100644 --- a/dotenv_codegen/Cargo.toml +++ b/dotenv_codegen/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["edition2024"] + [lib] proc-macro = true @@ -19,11 +21,11 @@ license = "MIT" homepage = "https://github.com/allan2/dotenvy" repository = "https://github.com/allan2/dotenvy" description = "A macro for compile time dotenv inspection" -edition = "2021" -rust-version = "1.72.0" +edition = "2024" +#rust-version = "1.72.0" [dependencies] proc-macro2 = "1" quote = "1" syn = "1" -dotenvy = { version = "0.15", path = "../dotenv" } +dotenvy = { path = "../dotenv" } diff --git a/dotenv_codegen/src/lib.rs b/dotenv_codegen/src/lib.rs index 418c2dfe..573ff417 100644 --- a/dotenv_codegen/src/lib.rs +++ b/dotenv_codegen/src/lib.rs @@ -1,16 +1,16 @@ -#![forbid(unsafe_code)] - use quote::quote; use std::env::{self, VarError}; use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned, Token}; #[proc_macro] +/// TODO: add safety warning pub fn dotenv(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - dotenv_inner(input.into()).into() + let input = input.into(); + unsafe { dotenv_inner(input) }.into() } -fn dotenv_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - if let Err(err) = dotenvy::dotenv() { +unsafe fn dotenv_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + if let Err(err) = unsafe { dotenvy::dotenv() } { let msg = format!("Error loading .env file: {}", err); return quote! { compile_error!(#msg); From fa19b77fa20b139ddc6a0d5fc12cfabd87d637d7 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:38:54 -0400 Subject: [PATCH 02/39] New API - dotenv moved to dotenvy folder - dotenvy-macros crate created for runtime macros - temporary 2024 edition to check for unsafe `set_var` usage - new API with `EnvLoader` builder, `load`, and `load_and_modify` - `load` attribute macro that is thread-safe and async-compatible - many examples added - some irrelevant tests removed --- .gitignore | 1 + CHANGELOG.md | 7 + Cargo.toml | 2 +- README.md | 82 +++- dotenv/LICENSE | 1 - dotenv/src/find.rs | 57 --- dotenv/src/iter.rs | 201 --------- dotenv/src/lib.rs | 388 ------------------ dotenv/tests/integration/main.rs | 1 - dotenv/tests/test-dotenv-iter.rs | 19 - dotenv/tests/test-from-filename-iter.rs | 21 - dotenv/tests/test-from-filename-override.rs | 18 - dotenv/tests/test-from-filename.rs | 19 - dotenv/tests/test-from-path-iter.rs | 25 -- dotenv/tests/test-from-path-override.rs | 24 -- dotenv/tests/test-from-path.rs | 21 - dotenv/tests/test-from-read-override.rs | 18 - dotenv/tests/test-from-read.rs | 18 - dotenv/tests/test-var.rs | 15 - dotenv/tests/test-vars.rs | 17 - dotenv_codegen/Cargo.toml | 10 +- dotenv_codegen/LICENSE | 22 +- dotenv_codegen/README.md | 1 + dotenv_codegen/src/lib.rs | 13 +- dotenvy-macros/Cargo.toml | 26 ++ dotenvy-macros/src/lib.rs | 97 +++++ {dotenv => dotenvy}/Cargo.toml | 3 + dotenvy/LICENSE | 21 + {dotenv => dotenvy}/README.md | 29 +- {dotenv => dotenvy}/src/bin/dotenvy.rs | 6 +- dotenv/src/errors.rs => dotenvy/src/err.rs | 77 ++-- dotenvy/src/iter.rs | 203 +++++++++ dotenvy/src/lib.rs | 174 ++++++++ {dotenv => dotenvy}/src/parse.rs | 15 +- .../common/mod.rs => dotenvy/tests/common.rs | 10 +- .../tests/integration/main.rs | 30 +- .../tests/integration}/testenv.rs | 49 ++- {dotenv => dotenvy}/tests/test-child-dir.rs | 6 +- .../tests/test-default-location-override.rs | 4 +- .../tests/test-default-location.rs | 4 +- {dotenv => dotenvy}/tests/test-ignore-bom.rs | 5 +- .../tests/test-multiline-comment.rs | 3 +- {dotenv => dotenvy}/tests/test-multiline.rs | 4 +- .../tests/test-variable-substitution.rs | 4 +- dotenvy/tests/test-vars.rs | 17 + examples/.env | 1 - examples/Cargo.toml | 4 - examples/dev-prod/Cargo.toml | 4 +- examples/dev-prod/src/main.rs | 69 ++-- examples/env-example | 1 + examples/env-example-2 | 1 + examples/find/Cargo.toml | 8 + examples/find/src/main.rs | 46 +++ examples/modify-macro/Cargo.toml | 8 + examples/modify-macro/src/main.rs | 7 + examples/modify-tokio-macro/Cargo.toml | 14 + examples/modify-tokio-macro/src/main.rs | 10 + examples/modify-tokio/Cargo.toml | 9 + examples/modify-tokio/src/main.rs | 24 ++ examples/modify/Cargo.toml | 8 + examples/modify/print_host.py | 4 + examples/modify/src/main.rs | 22 + examples/multiple-files/Cargo.toml | 8 + examples/multiple-files/src/main.rs | 19 + examples/optional/Cargo.toml | 8 + examples/optional/src/main.rs | 20 + 66 files changed, 1058 insertions(+), 1025 deletions(-) mode change 120000 => 100644 README.md delete mode 120000 dotenv/LICENSE delete mode 100644 dotenv/src/find.rs delete mode 100644 dotenv/src/iter.rs delete mode 100644 dotenv/src/lib.rs delete mode 100644 dotenv/tests/integration/main.rs delete mode 100644 dotenv/tests/test-dotenv-iter.rs delete mode 100644 dotenv/tests/test-from-filename-iter.rs delete mode 100644 dotenv/tests/test-from-filename-override.rs delete mode 100644 dotenv/tests/test-from-filename.rs delete mode 100644 dotenv/tests/test-from-path-iter.rs delete mode 100644 dotenv/tests/test-from-path-override.rs delete mode 100644 dotenv/tests/test-from-path.rs delete mode 100644 dotenv/tests/test-from-read-override.rs delete mode 100644 dotenv/tests/test-from-read.rs delete mode 100644 dotenv/tests/test-var.rs delete mode 100644 dotenv/tests/test-vars.rs mode change 120000 => 100644 dotenv_codegen/LICENSE create mode 100644 dotenvy-macros/Cargo.toml create mode 100644 dotenvy-macros/src/lib.rs rename {dotenv => dotenvy}/Cargo.toml (88%) create mode 100644 dotenvy/LICENSE rename {dotenv => dotenvy}/README.md (86%) rename {dotenv => dotenvy}/src/bin/dotenvy.rs (90%) rename dotenv/src/errors.rs => dotenvy/src/err.rs (50%) create mode 100644 dotenvy/src/iter.rs create mode 100644 dotenvy/src/lib.rs rename {dotenv => dotenvy}/src/parse.rs (98%) rename dotenv/tests/common/mod.rs => dotenvy/tests/common.rs (61%) rename dotenv/tests/integration/util/mod.rs => dotenvy/tests/integration/main.rs (61%) rename {dotenv/tests/integration/util => dotenvy/tests/integration}/testenv.rs (89%) rename {dotenv => dotenvy}/tests/test-child-dir.rs (81%) rename {dotenv => dotenvy}/tests/test-default-location-override.rs (72%) rename {dotenv => dotenvy}/tests/test-default-location.rs (80%) rename {dotenv => dotenvy}/tests/test-ignore-bom.rs (80%) rename {dotenv => dotenvy}/tests/test-multiline-comment.rs (92%) rename {dotenv => dotenvy}/tests/test-multiline.rs (92%) rename {dotenv => dotenvy}/tests/test-variable-substitution.rs (95%) create mode 100644 dotenvy/tests/test-vars.rs delete mode 100644 examples/.env delete mode 100644 examples/Cargo.toml create mode 100644 examples/env-example create mode 100644 examples/env-example-2 create mode 100644 examples/find/Cargo.toml create mode 100644 examples/find/src/main.rs create mode 100644 examples/modify-macro/Cargo.toml create mode 100644 examples/modify-macro/src/main.rs create mode 100644 examples/modify-tokio-macro/Cargo.toml create mode 100644 examples/modify-tokio-macro/src/main.rs create mode 100644 examples/modify-tokio/Cargo.toml create mode 100644 examples/modify-tokio/src/main.rs create mode 100644 examples/modify/Cargo.toml create mode 100644 examples/modify/print_host.py create mode 100644 examples/modify/src/main.rs create mode 100644 examples/multiple-files/Cargo.toml create mode 100644 examples/multiple-files/src/main.rs create mode 100644 examples/optional/Cargo.toml create mode 100644 examples/optional/src/main.rs diff --git a/.gitignore b/.gitignore index 2c96eb1b..1ec7ed7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 68838494..cc6cc035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - update to 2021 edition - update MSRV to 1.72.0 + + +- **breaking**: `dotenvy::Result` is now private +- **breaking**: deprecate `dotenvy::var`, `dotenvy::from_filename*` +- `Error` is now `From { - filename: &'a Path, -} - -impl<'a> Finder<'a> { - pub fn new() -> Self { - Finder { - filename: Path::new(".env"), - } - } - - pub const fn filename(mut self, filename: &'a Path) -> Self { - self.filename = filename; - 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)) - } -} - -/// Searches for `filename` in `directory` and parent directories until found or root is reached. -pub fn find(mut directory: &Path, filename: &Path) -> Result { - loop { - let candidate = directory.join(filename); - - match fs::metadata(&candidate) { - Ok(metadata) if metadata.is_file() => return Ok(candidate), - Ok(_) => {} - Err(error) if matches!(error.kind(), io::ErrorKind::NotFound) => {} - Err(error) => return Err(Error::Io(error)), - } - - if let Some(parent) = directory.parent() { - directory = parent; - } else { - return Err(Error::Io(io::Error::new( - io::ErrorKind::NotFound, - "path not found", - ))); - } - } -} diff --git a/dotenv/src/iter.rs b/dotenv/src/iter.rs deleted file mode 100644 index 1129c3f4..00000000 --- a/dotenv/src/iter.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::{ - errors::{Error, Result}, - parse, -}; -use std::{ - collections::HashMap, - env, - io::{BufRead, BufReader, Read}, -}; - -pub struct Iter { - lines: QuotedLines>, - substitution_data: HashMap>, -} - -impl Iter { - pub fn new(reader: R) -> Self { - Self { - lines: QuotedLines { - buf: BufReader::new(reader), - }, - substitution_data: HashMap::new(), - } - } - - /// Loads all variables found in the `reader` into the environment, - /// preserving any existing environment variables of the same name. - /// - /// If a variable is specified multiple times within the reader's data, - /// then the first occurrence is applied. - pub unsafe fn load(mut self) -> Result<()> { - self.remove_bom()?; - - for item in self { - let (key, value) = item?; - if env::var(&key).is_err() { - unsafe { env::set_var(&key, value) }; - } - } - - Ok(()) - } - - /// Loads all variables found in the `reader` into the environment, - /// overriding any existing environment variables of the same name. - /// - /// If a variable is specified multiple times within the reader's data, - /// then the last occurrence is applied. - pub unsafe fn load_override(mut self) -> Result<()> { - self.remove_bom()?; - - for item in self { - let (key, value) = item?; - unsafe { env::set_var(key, value) }; - } - - Ok(()) - } - - fn remove_bom(&mut self) -> Result<()> { - let buffer = self.lines.buf.fill_buf().map_err(Error::Io)?; - // https://www.compart.com/en/unicode/U+FEFF - if buffer.starts_with(&[0xEF, 0xBB, 0xBF]) { - // remove the BOM from the bufreader - self.lines.buf.consume(3); - } - Ok(()) - } -} - -struct QuotedLines { - buf: B, -} - -enum ParseState { - Complete, - Escape, - StrongOpen, - StrongOpenEscape, - WeakOpen, - WeakOpenEscape, - Comment, - WhiteSpace, -} - -fn eval_end_state(prev_state: ParseState, buf: &str) -> (usize, ParseState) { - let mut cur_state = prev_state; - let mut cur_pos: usize = 0; - - for (pos, c) in buf.char_indices() { - cur_pos = pos; - cur_state = match cur_state { - ParseState::WhiteSpace => match c { - '#' => return (cur_pos, ParseState::Comment), - '\\' => ParseState::Escape, - '"' => ParseState::WeakOpen, - '\'' => ParseState::StrongOpen, - _ => ParseState::Complete, - }, - ParseState::Escape => ParseState::Complete, - ParseState::Complete => match c { - c if c.is_whitespace() && c != '\n' && c != '\r' => ParseState::WhiteSpace, - '\\' => ParseState::Escape, - '"' => ParseState::WeakOpen, - '\'' => ParseState::StrongOpen, - _ => ParseState::Complete, - }, - ParseState::WeakOpen => match c { - '\\' => ParseState::WeakOpenEscape, - '"' => ParseState::Complete, - _ => ParseState::WeakOpen, - }, - ParseState::WeakOpenEscape => ParseState::WeakOpen, - ParseState::StrongOpen => match c { - '\\' => ParseState::StrongOpenEscape, - '\'' => ParseState::Complete, - _ => ParseState::StrongOpen, - }, - ParseState::StrongOpenEscape => ParseState::StrongOpen, - // Comments last the entire line. - ParseState::Comment => panic!("should have returned early"), - }; - } - (cur_pos, cur_state) -} - -impl Iterator for QuotedLines { - type Item = Result; - - fn next(&mut self) -> Option> { - let mut buf = String::new(); - let mut cur_state = ParseState::Complete; - let mut buf_pos; - let mut cur_pos; - loop { - buf_pos = buf.len(); - match self.buf.read_line(&mut buf) { - Ok(0) => { - if matches!(cur_state, ParseState::Complete) { - return None; - } - let len = buf.len(); - return Some(Err(Error::LineParse(buf, len))); - } - Ok(_n) => { - // Skip lines which start with a # before iteration - // This optimizes parsing a bit. - if buf.trim_start().starts_with('#') { - return Some(Ok(String::with_capacity(0))); - } - let result = eval_end_state(cur_state, &buf[buf_pos..]); - cur_pos = result.0; - cur_state = result.1; - - match cur_state { - ParseState::Complete => { - if buf.ends_with('\n') { - buf.pop(); - if buf.ends_with('\r') { - buf.pop(); - } - } - return Some(Ok(buf)); - } - ParseState::Escape - | ParseState::StrongOpen - | ParseState::StrongOpenEscape - | ParseState::WeakOpen - | ParseState::WeakOpenEscape - | ParseState::WhiteSpace => {} - ParseState::Comment => { - buf.truncate(buf_pos + cur_pos); - return Some(Ok(buf)); - } - } - } - Err(e) => return Some(Err(Error::Io(e))), - } - } - } -} - -impl Iterator for Iter { - type Item = Result<(String, String)>; - - fn next(&mut self) -> Option { - loop { - let line = match self.lines.next() { - Some(Ok(line)) => line, - Some(Err(err)) => return Some(Err(err)), - None => return None, - }; - - match parse::parse_line(&line, &mut self.substitution_data) { - Ok(Some(result)) => return Some(Ok(result)), - Ok(None) => {} - Err(err) => return Some(Err(err)), - } - } - } -} diff --git a/dotenv/src/lib.rs b/dotenv/src/lib.rs deleted file mode 100644 index 4038b645..00000000 --- a/dotenv/src/lib.rs +++ /dev/null @@ -1,388 +0,0 @@ -#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] -#![allow( - clippy::missing_errors_doc, - clippy::too_many_lines, - clippy::missing_safety_doc -)] -#![deny(clippy::uninlined_format_args)] - -//! [`dotenv`]: https://crates.io/crates/dotenv -//! A well-maintained fork of the [`dotenv`] crate -//! -//! This library loads environment variables from a *.env* file. This is convenient for dev environments. - -mod errors; -mod find; -mod iter; -mod parse; - -use std::env::{self, Vars}; -use std::ffi::OsStr; -use std::fs::File; -use std::io; -use std::path::{Path, PathBuf}; -use std::sync::Once; - -pub use crate::errors::*; -use crate::find::Finder; -pub use crate::iter::Iter; - -static START: Once = Once::new(); - -/// Gets the value for an environment variable. -/// -/// The value is `Ok(s)` if the environment variable is present and valid unicode. -/// -/// Note: this function gets values from any visible environment variable key, -/// regardless of whether a *.env* file was loaded. -/// -/// # Examples: -/// -/// ```no_run -/// # fn main() -> Result<(), Box> { -/// let value = unsafe { dotenvy::var("HOME") }?; -/// println!("{}", value); // prints `/home/foo` -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn var>(key: K) -> Result { - START.call_once(|| { - unsafe { dotenv() }.ok(); - }); - env::var(key).map_err(Error::EnvVar) -} - -/// Returns an iterator of `(key, value)` pairs for all environment variables of the current process. -/// The returned iterator contains a snapshot of the process's environment variables at the time of invocation. Modifications to environment variables afterwards will not be reflected. -/// -/// # Examples: -/// -/// ```no_run -/// use std::io; -/// -/// let result: Vec<(String, String)> = unsafe { dotenvy::vars() }.collect(); -/// ``` -pub unsafe fn vars() -> Vars { - START.call_once(|| { - unsafe { dotenv() }.ok(); - }); - env::vars() -} - -/// Loads environment variables from the specified path. -/// -/// If variables with the same names already exist in the environment, then their values will be -/// preserved. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* -/// file, the *first one* is applied. -/// -/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables -/// already existing in the environment, then use [`from_path_override`] instead. -/// -/// # Examples -/// -/// ```no_run -/// use std::path::Path; -/// -/// # fn main() -> Result<(), Box> { -/// let path = Path::new("path/to/.env"); -/// unsafe { dotenvy::from_path(path) }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_path>(path: P) -> Result<()> { - let iter = Iter::new(File::open(path).map_err(Error::Io)?); - unsafe { iter.load() } -} - -/// Loads environment variables from the specified path, -/// overriding existing environment variables. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* file, the -/// *last one* is applied. -/// -/// If you want the existing environment to take precedence, -/// or if you want to be able to override environment variables on the command line, -/// then use [`from_path`] instead. -/// -/// # Examples -/// -/// ```no_run -/// use std::path::Path; -/// -/// # fn main() -> Result<(), Box> { -/// let path = Path::new("path/to/.env"); -/// unsafe { dotenvy::from_path_override(path) }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_path_override>(path: P) -> Result<()> { - let iter = Iter::new(File::open(path).map_err(Error::Io)?); - unsafe { iter.load_override() } -} - -/// Returns an iterator over environment variables from the specified path. -/// -/// # Examples -/// -/// ```no_run -/// use std::path::Path; -/// -/// # fn main() -> Result<(), Box> { -/// for item in dotenvy::from_path_iter(Path::new("path/to/.env"))? { -/// let (key, val) = item?; -/// println!("{}={}", key, val); -/// } -/// # Ok(()) -/// # } -/// ``` -pub fn from_path_iter>(path: P) -> Result> { - Ok(Iter::new(File::open(path).map_err(Error::Io)?)) -} - -/// Loads environment variables from the specified file. -/// -/// If variables with the same names already exist in the environment, then their values will be -/// preserved. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* -/// file, the *first one* is applied. -/// -/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables -/// already existing in the environment, then use [`from_filename_override`] instead. -/// -/// # Examples -/// ```no_run -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::from_filename("custom.env") }?; -/// # Ok(()) -/// # } -/// ``` -/// -/// It is also possible to load from a typical *.env* file like so. However, using [`dotenv`] is preferred. -/// -/// ``` -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::from_filename(".env") }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_filename>(filename: P) -> Result { - let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; - unsafe { iter.load() }?; - Ok(path) -} - -/// Loads environment variables from the specified file, -/// overriding existing environment variables. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* file, the -/// *last one* is applied. -/// -/// If you want the existing environment to take precedence, -/// or if you want to be able to override environment variables on the command line, -/// then use [`from_filename`] instead. -/// -/// # Examples -/// ```no_run -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::from_filename_override("custom.env") }?; -/// # Ok(()) -/// # } -/// ``` -/// -/// It is also possible to load from a typical *.env* file like so. However, using [`dotenv_override`] is preferred. -/// -/// ``` -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::from_filename_override(".env")}?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_filename_override>(filename: P) -> Result { - let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; - unsafe { iter.load_override() }?; - Ok(path) -} - -/// Returns an iterator over environment variables from the specified file. -/// -/// # Examples -/// -/// ```no_run -/// # fn main() -> Result<(), Box> { -/// for item in dotenvy::from_filename_iter("custom.env")? { -/// let (key, val) = item?; -/// println!("{}={}", key, val); -/// } -/// # Ok(()) -/// # } -/// ``` -pub fn from_filename_iter>(filename: P) -> Result> { - let (_, iter) = Finder::new().filename(filename.as_ref()).find()?; - Ok(iter) -} - -/// Loads environment variables from [`io::Read`]. -/// -/// This is useful for loading environment variables from IPC or the network. -/// -/// If variables with the same names already exist in the environment, then their values will be -/// preserved. -/// -/// Where multiple declarations for the same environment variable exist in your `reader`, -/// the *first one* is applied. -/// -/// If you wish to ensure all variables are loaded from your `reader`, ignoring variables -/// already existing in the environment, then use [`from_read_override`] instead. -/// -/// For regular files, use [`from_path`] or [`from_filename`]. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::Read; -/// # #[cfg(unix)] -/// use std::os::unix::net::UnixStream; -/// -/// # fn main() -> Result<(), Box> { -/// # #[cfg(unix)] -/// let mut stream = UnixStream::connect("/some/socket")?; -/// # #[cfg(unix)] -/// unsafe { dotenvy::from_read(stream) }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_read(reader: R) -> Result<()> { - let iter = Iter::new(reader); - unsafe { iter.load() }?; - Ok(()) -} - -/// Loads environment variables from [`io::Read`], -/// overriding existing environment variables. -/// -/// This is useful for loading environment variables from IPC or the network. -/// -/// Where multiple declarations for the same environment variable exist in your `reader`, the -/// *last one* is applied. -/// -/// If you want the existing environment to take precedence, -/// or if you want to be able to override environment variables on the command line, -/// then use [`from_read`] instead. -/// -/// For regular files, use [`from_path_override`] or [`from_filename_override`]. -/// -/// # Examples -/// ```no_run -/// use std::io::Read; -/// # #[cfg(unix)] -/// use std::os::unix::net::UnixStream; -/// -/// # fn main() -> Result<(), Box> { -/// # #[cfg(unix)] -/// let mut stream = UnixStream::connect("/some/socket")?; -/// # #[cfg(unix)] -/// unsafe { dotenvy::from_read_override(stream) }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn from_read_override(reader: R) -> Result<()> { - let iter = Iter::new(reader); - unsafe { iter.load_override() }?; - Ok(()) -} - -/// Returns an iterator over environment variables from [`io::Read`]. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::Read; -/// # #[cfg(unix)] -/// use std::os::unix::net::UnixStream; -/// -/// # fn main() -> Result<(), Box> { -/// # #[cfg(unix)] -/// let mut stream = UnixStream::connect("/some/socket")?; -/// -/// # #[cfg(unix)] -/// for item in dotenvy::from_read_iter(stream) { -/// let (key, val) = item?; -/// println!("{}={}", key, val); -/// } -/// # Ok(()) -/// # } -/// ``` -pub fn from_read_iter(reader: R) -> Iter { - Iter::new(reader) -} - -/// Loads the *.env* file from the current directory or parents. This is typically what you want. -/// -/// If variables with the same names already exist in the environment, then their values will be -/// preserved. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* -/// file, the *first one* is applied. -/// -/// If you wish to ensure all variables are loaded from your *.env* file, ignoring variables -/// already existing in the environment, then use [`dotenv_override`] instead. -/// -/// An error will be returned if the file is not found. -/// -/// # Examples -/// -/// ``` -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::dotenv() }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn dotenv() -> Result { - let (path, iter) = Finder::new().find()?; - unsafe { iter.load() }?; - Ok(path) -} - -/// Loads all variables found in the `reader` into the environment, -/// overriding any existing environment variables of the same name. -/// -/// Where multiple declarations for the same environment variable exist in your *.env* file, the -/// *last one* is applied. -/// -/// If you want the existing environment to take precedence, -/// or if you want to be able to override environment variables on the command line, -/// then use [`dotenv`] instead. -/// -/// # Examples -/// ``` -/// # fn main() -> Result<(), Box> { -/// unsafe { dotenvy::dotenv_override() }?; -/// # Ok(()) -/// # } -/// ``` -pub unsafe fn dotenv_override() -> Result { - let (path, iter) = Finder::new().find()?; - unsafe { iter.load_override() }?; - Ok(path) -} - -/// Returns an iterator over environment variables. -/// -/// # Examples -/// -/// ``` -/// # fn main() -> Result<(), Box> { -/// for item in dotenvy::dotenv_iter()? { -/// let (key, val) = item?; -/// println!("{}={}", key, val); -/// } -/// # Ok(()) -/// # } -/// ``` -pub fn dotenv_iter() -> Result> { - let (_, iter) = Finder::new().find()?; - Ok(iter) -} diff --git a/dotenv/tests/integration/main.rs b/dotenv/tests/integration/main.rs deleted file mode 100644 index 83c8c0aa..00000000 --- a/dotenv/tests/integration/main.rs +++ /dev/null @@ -1 +0,0 @@ -mod util; diff --git a/dotenv/tests/test-dotenv-iter.rs b/dotenv/tests/test-dotenv-iter.rs deleted file mode 100644 index 416b5db6..00000000 --- a/dotenv/tests/test-dotenv-iter.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_dotenv_iter() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let iter = dotenvy::dotenv_iter()?; - assert!(env::var("TESTKEY").is_err()); - - unsafe { iter.load() }?; - assert_eq!(env::var("TESTKEY")?, "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-filename-iter.rs b/dotenv/tests/test-from-filename-iter.rs deleted file mode 100644 index 8c61db5d..00000000 --- a/dotenv/tests/test-from-filename-iter.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_from_filename_iter() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let iter = dotenvy::from_filename_iter(".env")?; - - assert!(env::var("TESTKEY").is_err()); - - unsafe { iter.load() }?; - - assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-filename-override.rs b/dotenv/tests/test-from-filename-override.rs deleted file mode 100644 index 78172c88..00000000 --- a/dotenv/tests/test-from-filename-override.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_from_filename_override() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - unsafe { dotenvy::from_filename_override(".env") }?; - - assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); - assert_eq!(env::var("EXISTING")?, "from_file"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-filename.rs b/dotenv/tests/test-from-filename.rs deleted file mode 100644 index 23e89413..00000000 --- a/dotenv/tests/test-from-filename.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use dotenvy::from_filename; -use std::{env, error}; - -#[test] -fn test_from_filename() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - unsafe { from_filename(".env") }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - assert_eq!(env::var("EXISTING")?, "from_env"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-path-iter.rs b/dotenv/tests/test-from-path-iter.rs deleted file mode 100644 index 22f18561..00000000 --- a/dotenv/tests/test-from-path-iter.rs +++ /dev/null @@ -1,25 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use dotenvy::from_path_iter; -use std::{env, error}; - -#[test] -fn test_from_path_iter() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let mut path = env::current_dir()?; - path.push(".env"); - - let iter = from_path_iter(&path)?; - - assert!(env::var("TESTKEY").is_err()); - - unsafe { iter.load() }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-path-override.rs b/dotenv/tests/test-from-path-override.rs deleted file mode 100644 index 79a9782b..00000000 --- a/dotenv/tests/test-from-path-override.rs +++ /dev/null @@ -1,24 +0,0 @@ -mod common; - -use std::{env, error}; - -use dotenvy::from_path_override; - -use crate::common::make_test_dotenv; - -#[test] -fn test_from_path_override() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let mut path = env::current_dir()?; - path.push(".env"); - - unsafe { from_path_override(&path) }?; - - assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); - assert_eq!(env::var("EXISTING")?, "from_file"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-path.rs b/dotenv/tests/test-from-path.rs deleted file mode 100644 index df77c34c..00000000 --- a/dotenv/tests/test-from-path.rs +++ /dev/null @@ -1,21 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_from_path() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let mut path = env::current_dir()?; - path.push(".env"); - - unsafe { dotenvy::from_path(&path) }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - assert_eq!(env::var("EXISTING")?, "from_env"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-read-override.rs b/dotenv/tests/test-from-read-override.rs deleted file mode 100644 index da771d39..00000000 --- a/dotenv/tests/test-from-read-override.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error, fs::File}; - -#[test] -fn test_from_read_override() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - let rdr = File::open(".env")?; - unsafe { dotenvy::from_read_override(rdr) }?; - - assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); - assert_eq!(env::var("EXISTING")?, "from_file"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-from-read.rs b/dotenv/tests/test-from-read.rs deleted file mode 100644 index 2d0ef368..00000000 --- a/dotenv/tests/test-from-read.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error, fs::File}; - -#[test] -fn test_from_read() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - let rdr = File::open(".env")?; - unsafe { dotenvy::from_read(rdr) }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - assert_eq!(env::var("EXISTING")?, "from_env"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-var.rs b/dotenv/tests/test-var.rs deleted file mode 100644 index eb57844e..00000000 --- a/dotenv/tests/test-var.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_var() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - assert_eq!(unsafe { dotenvy::var("TESTKEY") }?, "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv/tests/test-vars.rs b/dotenv/tests/test-vars.rs deleted file mode 100644 index e8776bdc..00000000 --- a/dotenv/tests/test-vars.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{collections::HashMap, env, error}; - -#[test] -fn test_vars() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let vars: HashMap = unsafe { dotenvy::vars() }.collect(); - - assert_eq!(vars["TESTKEY"], "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenv_codegen/Cargo.toml b/dotenv_codegen/Cargo.toml index 577e04f9..a6de4e6f 100644 --- a/dotenv_codegen/Cargo.toml +++ b/dotenv_codegen/Cargo.toml @@ -1,8 +1,5 @@ cargo-features = ["edition2024"] -[lib] -proc-macro = true - [package] name = "dotenvy_macro" version = "0.15.7" @@ -24,8 +21,11 @@ description = "A macro for compile time dotenv inspection" edition = "2024" #rust-version = "1.72.0" +[lib] +proc-macro = true + [dependencies] proc-macro2 = "1" quote = "1" -syn = "1" -dotenvy = { path = "../dotenv" } +syn = "2" +dotenvy = { path = "../dotenvy" } diff --git a/dotenv_codegen/LICENSE b/dotenv_codegen/LICENSE deleted file mode 120000 index ea5b6064..00000000 --- a/dotenv_codegen/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/dotenv_codegen/LICENSE b/dotenv_codegen/LICENSE new file mode 100644 index 00000000..11e157d4 --- /dev/null +++ b/dotenv_codegen/LICENSE @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2014 Santiago Lapresta and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/dotenv_codegen/README.md b/dotenv_codegen/README.md index e054288f..efa8f41d 100644 --- a/dotenv_codegen/README.md +++ b/dotenv_codegen/README.md @@ -6,3 +6,4 @@ A macro for compile time dotenv inspection. This is a well-maintained fork of `dotenv_codegen`. + \ No newline at end of file diff --git a/dotenv_codegen/src/lib.rs b/dotenv_codegen/src/lib.rs index 573ff417..a4baee45 100644 --- a/dotenv_codegen/src/lib.rs +++ b/dotenv_codegen/src/lib.rs @@ -1,17 +1,20 @@ +use dotenvy::EnvLoader; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use std::env::{self, VarError}; use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned, Token}; #[proc_macro] /// TODO: add safety warning -pub fn dotenv(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +pub fn dotenv(input: TokenStream) -> TokenStream { let input = input.into(); unsafe { dotenv_inner(input) }.into() } -unsafe fn dotenv_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { - if let Err(err) = unsafe { dotenvy::dotenv() } { - let msg = format!("Error loading .env file: {}", err); +unsafe fn dotenv_inner(input: TokenStream2) -> TokenStream2 { + if let Err(e) = unsafe { EnvLoader::new().load_and_modify() } { + let msg = format!("Error loading .env file: {}", e); return quote! { compile_error!(#msg); }; @@ -23,7 +26,7 @@ unsafe fn dotenv_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStr } } -fn expand_env(input_raw: proc_macro2::TokenStream) -> syn::Result { +fn expand_env(input_raw: TokenStream2) -> syn::Result { let args = >::parse_terminated .parse(input_raw.into()) .expect("expected macro to be called with a comma-separated list of string literals"); diff --git a/dotenvy-macros/Cargo.toml b/dotenvy-macros/Cargo.toml new file mode 100644 index 00000000..d5281222 --- /dev/null +++ b/dotenvy-macros/Cargo.toml @@ -0,0 +1,26 @@ +cargo-features = ["edition2024"] + +[package] +name = "dotenvy-macros" +version = "0.1.0" +authors = ["Allan"] +readme = "README.md" +keywords = ["dotenv", "env", "environment", "settings", "config"] +categories = ["configuration"] +license = "MIT" +homepage = "https://github.com/allan2/dotenvy" +repository = "https://github.com/allan2/dotenvy" +description = "Runtime macros for dotenvy" +edition = "2024" +#rust-version = "1.72.0" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "1", features = ["full"] } + +[dev-dependencies] +dotenvy = { path = "../dotenvy" } \ No newline at end of file diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs new file mode 100644 index 00000000..61065049 --- /dev/null +++ b/dotenvy-macros/src/lib.rs @@ -0,0 +1,97 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![deny(clippy::uninlined_format_args, clippy::wildcard_imports)] + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta}; + +/// Loads environment variables from a file and modifies the environment. +/// +/// Three optional arguments are supported: `path`, `required`, and `override`. +/// Usage is like `#[dotenvy::load(path = ".env", required = true, override = true)]`. +/// +/// The default path is ".env". The default sequence is `EnvSequence::InputThenEnv`. +#[proc_macro_attribute] +pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as AttributeArgs); + let input = parse_macro_input!(item as ItemFn); + + let mut path = ".env".to_owned(); + let mut required = true; + let mut override_ = false; + + for arg in args { + if let NestedMeta::Meta(Meta::NameValue(v)) = arg { + if v.path.is_ident("path") { + if let Lit::Str(lit_str) = v.lit { + path = lit_str.value(); + } + } else if v.path.is_ident("required") { + if let Lit::Bool(lit_bool) = v.lit { + required = lit_bool.value(); + } + } else if v.path.is_ident("override") { + if let Lit::Bool(lit_bool) = v.lit { + override_ = lit_bool.value(); + } + } + } + } + + let load_env = quote! { + use dotenvy::{EnvLoader, EnvSequence}; + use std::{error::Error, io::{self, ErrorKind}, process}; + + let seq = if #override_ { + EnvSequence::InputOnly + } else { + EnvSequence::InputThenEnv + }; + let mut loader = EnvLoader::from_path(#path).sequence(seq); + if let Err(e) = unsafe { loader.load_and_modify() } { + if let Some(io_err) = e.source().and_then(|src| src.downcast_ref::()) { + if io_err.kind() == io::ErrorKind::NotFound && !#required { + // `required` is false and file not found, so continue + } + } + eprintln!("Failed to load env file from path '{}': {e}", #path); + process::exit(1); + } + }; + + let attrs = &input.attrs; + let block = &input.block; + let sig = &input.sig; + let vis = &input.vis; + let fn_name = &input.sig.ident; + let output = &input.sig.output; + let new_fn_name = format_ident!("{fn_name}_inner"); + + let expanded = if sig.asyncness.is_some() { + // this works with `tokio::main`` but not `async_std::main`` + quote! { + // non-async wrapper function + #vis fn #fn_name() #output { + #load_env + #new_fn_name() + } + + // orig async function, but renamed + #(#attrs)* + #vis async fn #new_fn_name() #output { + #block + } + } + } else { + // not using async, just inject `load_env` at the start + quote! { + #(#attrs)* + #vis #sig { + #load_env + #block + } + } + }; + + TokenStream::from(expanded) +} diff --git a/dotenv/Cargo.toml b/dotenvy/Cargo.toml similarity index 88% rename from dotenv/Cargo.toml rename to dotenvy/Cargo.toml index 8e52f81c..bd141b2b 100644 --- a/dotenv/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -29,9 +29,12 @@ required-features = ["cli"] [dependencies] clap = { version = "4.5.16", features = ["derive"], optional = true } +dotenvy-macros = { path = "../dotenvy-macros", optional = true } [dev-dependencies] tempfile = "3.12.0" [features] +default = ["cli", "macros"] cli = ["dep:clap"] +macros = ["dep:dotenvy-macros"] \ No newline at end of file diff --git a/dotenvy/LICENSE b/dotenvy/LICENSE new file mode 100644 index 00000000..11e157d4 --- /dev/null +++ b/dotenvy/LICENSE @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2014 Santiago Lapresta and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/dotenv/README.md b/dotenvy/README.md similarity index 86% rename from dotenv/README.md rename to dotenvy/README.md index 42d55329..bd1ea040 100644 --- a/dotenv/README.md +++ b/dotenvy/README.md @@ -20,22 +20,40 @@ This library loads environment variables from a _.env_ file. This is convenient ## Usage -### Loading at runtime +## + +Safe API: ```rs -use dotenvy::dotenv; +use dotenvy::EnvLoader; use std::env; fn main() { - // load environment variables from .env file - dotenv().expect(".env file not found"); + let env_map = EnvLoader::from_path("env-file").load()?; - for (key, value) in env::vars() { + for (key, value) in env { println!("{key}: {value}"); } } ``` +Modify API: + +```rs + +use std::{error, env}; + +fn main() -> Result<(), Box> { + let is_dev_mode = env::var("APP_ENV")? == "dev"; + // loads the.env file from the currenty directory + let env = dotenvy::modify::Config().required(is_dev_mode).load()?; + + for (key, value) in env { + println!("{key}: {value}"); + } +} + + ### Loading at compile time The `dotenv!` macro provided by `dotenvy_macro` crate can be used. @@ -80,3 +98,4 @@ Thank you very much for considering to contribute to this project! See Legend has it that the Lost Maintainer will return, merging changes from `dotenvy` into `dotenv` with such thrust that all `Cargo.toml`s will lose one keystroke. Only then shall the Rust dotenv crateverse be united in true harmony. Until then, this repo dutifully carries on the dotenv torch. It is actively maintained. +``` diff --git a/dotenv/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs similarity index 90% rename from dotenv/src/bin/dotenvy.rs rename to dotenvy/src/bin/dotenvy.rs index c07006d5..6dbee199 100644 --- a/dotenv/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -10,6 +10,7 @@ //! //! will output `bar`. use clap::{Parser, Subcommand}; +use dotenvy::EnvLoader; use std::{error, os::unix::process::CommandExt, path::PathBuf, process}; macro_rules! die { @@ -39,7 +40,7 @@ fn mk_cmd(program: &str, args: &[String]) -> process::Command { allow_external_subcommands = true )] struct Cli { - #[arg(short, long, default_value = "./.env")] + #[arg(short, long, default_value = ".env")] file: PathBuf, #[clap(subcommand)] subcmd: Subcmd, @@ -55,7 +56,8 @@ fn main() -> Result<(), Box> { let cli = Cli::parse(); // load the file - if let Err(e) = dotenvy::from_path(&cli.file) { + let loader = EnvLoader::from_path(&cli.file); + if let Err(e) = unsafe { loader.load_and_modify() } { die!("Failed to load {path}: {e}", path = cli.file.display()); } diff --git a/dotenv/src/errors.rs b/dotenvy/src/err.rs similarity index 50% rename from dotenv/src/errors.rs rename to dotenvy/src/err.rs index cb7a28a3..e488403c 100644 --- a/dotenv/src/errors.rs +++ b/dotenvy/src/err.rs @@ -1,6 +1,9 @@ -use std::{env, error, fmt, io}; +use std::{ + env::{self}, + error, fmt, io, result, +}; -pub type Result = std::result::Result; +pub type Result = result::Result; #[derive(Debug)] #[non_exhaustive] @@ -8,46 +11,61 @@ pub enum Error { LineParse(String, usize), Io(io::Error), EnvVar(env::VarError), + /// When `load_and_modify` is called with `EnvSequence::EnvOnly` + /// + /// There is nothing to modify, so we consider this an invalid operation because of the unnecessary unsafe call. + InvalidOp, + /// When a load function is called with no path or reader. + /// + /// Only `EnvLoader::default` would have no path or reader. + NoInput, } +impl From for Error { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} impl Error { #[must_use] pub fn not_found(&self) -> bool { - if let Self::Io(ref io_error) = *self { - return io_error.kind() == io::ErrorKind::NotFound; + if let Self::Io(e) = self { + e.kind() == io::ErrorKind::NotFound + } else { + false } - false } } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { - Self::Io(err) => Some(err), - Self::EnvVar(err) => Some(err), - _ => None, + Self::Io(e) => Some(e), + Self::EnvVar(e) => Some(e), + Self::InvalidOp | Self::LineParse(_, _) | Self::NoInput => None, } } } impl fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Io(e) => write!(fmt, "{e}"), - Self::EnvVar(e) => write!(fmt, "{e}"), + Self::Io(e) => e.fmt(f), + Self::EnvVar(e) => e.fmt(f), Self::LineParse(line, index) => write!( - fmt, + f, "Error parsing line: '{line}', error at line index: {index}", ), + Self::InvalidOp => write!(f, "Modify is not permitted with `EnvSequence::EnvOnly`"), + Self::NoInput => write!(f, "No input provided"), } } } #[cfg(test)] mod test { - use std::error::Error as StdError; - - use super::*; + use super::Error; + use std::{env, error::Error as StdError, io}; #[test] fn test_io_error_source() { @@ -68,50 +86,43 @@ mod test { } #[test] - fn test_lineparse_error_source() { - let err = Error::LineParse("test line".to_string(), 2); - assert!(err.source().is_none()); + fn test_line_parse_error_source() { + let e = Error::LineParse("test line".to_string(), 2); + assert!(e.source().is_none()); } #[test] fn test_error_not_found_true() { - let err = Error::Io(io::ErrorKind::NotFound.into()); - assert!(err.not_found()); + let e = Error::Io(io::ErrorKind::NotFound.into()); + assert!(e.not_found()); } #[test] fn test_error_not_found_false() { - let err = Error::Io(io::ErrorKind::PermissionDenied.into()); - assert!(!err.not_found()); + let e = Error::Io(io::ErrorKind::PermissionDenied.into()); + assert!(!e.not_found()); } #[test] fn test_io_error_display() { let err = Error::Io(io::ErrorKind::PermissionDenied.into()); let io_err: io::Error = io::ErrorKind::PermissionDenied.into(); - - let err_desc = format!("{err}"); - let io_err_desc = format!("{io_err}"); - assert_eq!(io_err_desc, err_desc); + assert_eq!(err.to_string(), io_err.to_string()); } #[test] fn test_envvar_error_display() { let err = Error::EnvVar(env::VarError::NotPresent); let var_err = env::VarError::NotPresent; - - let err_desc = format!("{err}"); - let var_err_desc = format!("{var_err}"); - assert_eq!(var_err_desc, err_desc); + assert_eq!(err.to_string(), var_err.to_string()); } #[test] fn test_lineparse_error_display() { - let err = Error::LineParse("test line".to_string(), 2); - let err_desc = format!("{err}"); + let err = Error::LineParse("test line".to_owned(), 2); assert_eq!( "Error parsing line: 'test line', error at line index: 2", - err_desc + err.to_string() ); } } diff --git a/dotenvy/src/iter.rs b/dotenvy/src/iter.rs new file mode 100644 index 00000000..8e2cad3e --- /dev/null +++ b/dotenvy/src/iter.rs @@ -0,0 +1,203 @@ +use crate::{ + err::{Error, Result}, + parse, EnvMap, +}; +use std::{ + collections::HashMap, + env::{self}, + io::{self, BufRead}, + result::Result as StdResult, +}; + +pub struct Iter { + lines: Lines, + substitution_data: HashMap>, +} + +impl Iter { + pub fn new(buf: B) -> Self { + Self { + lines: Lines(buf), + substitution_data: HashMap::new(), + } + } + + fn internal_load(mut self, mut load_fn: F) -> Result + where + F: FnMut(String, String, &mut EnvMap), + { + self.remove_bom()?; + let mut map = HashMap::new(); + for item in self { + let (k, v) = item?; + load_fn(k, v, &mut map); + } + Ok(map) + } + + pub fn load(self) -> Result { + self.internal_load(|k, v, map| { + map.insert(k, v); + }) + } + + pub unsafe fn load_and_modify(self) -> Result { + self.internal_load(|k, v, map| { + if env::var(&k).is_err() { + unsafe { env::set_var(&k, &v) }; + } + map.insert(k, v); + }) + } + + pub unsafe fn load_and_modify_override(self) -> Result { + self.internal_load(|k, v, map| { + unsafe { env::set_var(&k, &v) }; + map.insert(k, v); + }) + } + + /// Removes the BOM from the reader if it exists. + /// + /// For more details, see the [Unicode BOM character](https://www.compart.com/en/unicode/U+FEFF). + fn remove_bom(&mut self) -> StdResult<(), io::Error> { + let buf = self.lines.0.fill_buf()?; + + if buf.starts_with(&[0xEF, 0xBB, 0xBF]) { + self.lines.0.consume(3); + } + Ok(()) + } +} + +struct Lines(B); + +enum ParseState { + Complete, + Escape, + StrongOpen, + StrongOpenEscape, + WeakOpen, + WeakOpenEscape, + Comment, + WhiteSpace, +} + +impl ParseState { + fn eval_end(self, buf: &str) -> (usize, Self) { + let mut cur_state = self; + let mut cur_pos = 0; + + for (pos, c) in buf.char_indices() { + cur_pos = pos; + cur_state = match cur_state { + Self::WhiteSpace => match c { + '#' => return (cur_pos, Self::Comment), + '\\' => Self::Escape, + '"' => Self::WeakOpen, + '\'' => Self::StrongOpen, + _ => Self::Complete, + }, + Self::Escape => Self::Complete, + Self::Complete => match c { + c if c.is_whitespace() && c != '\n' && c != '\r' => Self::WhiteSpace, + '\\' => Self::Escape, + '"' => Self::WeakOpen, + '\'' => Self::StrongOpen, + _ => Self::Complete, + }, + Self::WeakOpen => match c { + '\\' => Self::WeakOpenEscape, + '"' => Self::Complete, + _ => Self::WeakOpen, + }, + Self::WeakOpenEscape => Self::WeakOpen, + Self::StrongOpen => match c { + '\\' => Self::StrongOpenEscape, + '\'' => Self::Complete, + _ => Self::StrongOpen, + }, + Self::StrongOpenEscape => Self::StrongOpen, + // Comments last the entire line. + Self::Comment => unreachable!("should have returned already"), + }; + } + (cur_pos, cur_state) + } +} + +impl Iterator for Lines { + type Item = Result; + + fn next(&mut self) -> Option> { + let mut buf = String::new(); + let mut cur_state = ParseState::Complete; + let mut buf_pos; + let mut cur_pos; + loop { + buf_pos = buf.len(); + match self.0.read_line(&mut buf) { + Ok(0) => { + if matches!(cur_state, ParseState::Complete) { + return None; + } + let len = buf.len(); + return Some(Err(Error::LineParse(buf, len))); + } + Ok(_n) => { + // Skip lines which start with a `#` before iteration + // This optimizes parsing a bit. + if buf.trim_start().starts_with('#') { + return Some(Ok(String::with_capacity(0))); + } + let result = cur_state.eval_end(&buf[buf_pos..]); + cur_pos = result.0; + cur_state = result.1; + + match cur_state { + ParseState::Complete => { + if buf.ends_with('\n') { + buf.pop(); + if buf.ends_with('\r') { + buf.pop(); + } + } + return Some(Ok(buf)); + } + ParseState::Escape + | ParseState::StrongOpen + | ParseState::StrongOpenEscape + | ParseState::WeakOpen + | ParseState::WeakOpenEscape + | ParseState::WhiteSpace => {} + ParseState::Comment => { + buf.truncate(buf_pos + cur_pos); + return Some(Ok(buf)); + } + } + } + Err(e) => return Some(Err(Error::Io(e))), + } + } + } +} + +impl Iterator for Iter { + type Item = Result<(String, String)>; + + fn next(&mut self) -> Option { + loop { + let line = match self.lines.next() { + Some(Ok(line)) => line, + Some(Err(e)) => return Some(Err(e)), + None => return None, + }; + + match parse::parse_line(&line, &mut self.substitution_data) { + Ok(Some(res)) => return Some(Ok(res)), + Ok(None) => {} + Err(e) => return Some(Err(e)), + } + } + } +} diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs new file mode 100644 index 00000000..ffd1a2e0 --- /dev/null +++ b/dotenvy/src/lib.rs @@ -0,0 +1,174 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow( + clippy::missing_errors_doc, + clippy::too_many_lines, + clippy::missing_safety_doc +)] +#![deny(clippy::uninlined_format_args, clippy::wildcard_imports)] + +//! [`dotenv`]: https://crates.io/crates/dotenv +//! A well-maintained fork of the [`dotenv`] crate. +//! +//! This library allows for loading environment variables from an env file or a reader. +use crate::iter::Iter; +use std::{ + collections::HashMap, + env, + fs::File, + io::{BufReader, Read}, + path::{Path, PathBuf}, +}; + +mod err; +mod iter; +mod parse; + +/// The map that stores the environment. +/// +/// For internal use only. +pub type EnvMap = HashMap; + +pub use crate::err::{Error, Result}; + +#[cfg(feature = "macros")] +pub use dotenvy_macros::*; + +/// The sequence in which to load environment variables. +/// +/// Values in the latter override values in the former. +#[derive(Default, Debug, PartialEq, Eq, Clone)] +pub enum EnvSequence { + /// Inherit the existing environment without loading from input. + EnvOnly, + /// Inherit the existing environment, and then load from input, overriding existing values. + EnvThenInput, + /// Load from input only. + InputOnly, + /// Load from input and then inherit the existing environment. Values in the existing environment are not overwritten. + #[default] + InputThenEnv, +} + +#[derive(Default)] +pub struct EnvLoader<'a> { + path: Option, + reader: Option>, + sequence: EnvSequence, +} + +impl<'a> EnvLoader<'a> { + #[must_use] + /// Creates a new `EnvLoader` with the path set to `env` in the current directory. + pub fn new() -> Self { + Self::from_path(".env") + } + + /// Creates a new `EnvLoader` from a path. + /// + /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. + pub fn from_path>(path: P) -> Self { + Self { + path: Some(path.as_ref().to_owned()), + ..Default::default() + } + } + + /// Creates a new `EnvLoader` from a reader. + /// + /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. + pub fn from_reader(rdr: R) -> Self { + Self { + reader: Some(Box::new(rdr)), + ..Default::default() + } + } + + /// Sets the sequence in which to load environment variables. + #[must_use] + pub const fn sequence(mut self, sequence: EnvSequence) -> Self { + self.sequence = sequence; + self + } + + fn buf(self) -> Result>> { + let rdr = if let Some(path) = self.path { + Box::new(File::open(path)?) + } else if let Some(rdr) = self.reader { + rdr + } else { + // only `EnvLoader::default` would have no path or reader + return Err(Error::NoInput); + }; + Ok(BufReader::new(rdr)) + } + + fn load_input(self) -> Result { + let iter = Iter::new(self.buf()?); + iter.load() + } + + unsafe fn load_input_and_modify(self) -> Result { + let iter = Iter::new(self.buf()?); + unsafe { iter.load_and_modify() } + } + + unsafe fn load_input_and_modify_override(self) -> Result { + let iter = Iter::new(self.buf()?); + unsafe { iter.load_and_modify_override() } + } + + /// Loads environment variables into a hash map. + /// + /// This is the primary method for loading environment variables. + pub fn load(self) -> Result { + match self.sequence { + EnvSequence::EnvOnly => Ok(env::vars().collect()), + EnvSequence::EnvThenInput => { + let mut existing: EnvMap = env::vars().collect(); + let input = self.load_input()?; + existing.extend(input); + Ok(existing) + } + EnvSequence::InputOnly => self.load_input(), + EnvSequence::InputThenEnv => { + let mut input = self.load_input()?; + input.extend(env::vars()); + Ok(input) + } + } + } + + + /// Loads environment variables into a hash map, modifying the existing environment. + /// + /// This calls `std::env::set_var` internally and is not thread-safe. + pub unsafe fn load_and_modify(self) -> Result { + match self.sequence { + // nothing to modify + EnvSequence::EnvOnly => Err(Error::InvalidOp), + // override existing env with input, returning entire env + EnvSequence::EnvThenInput => { + let mut existing: EnvMap = env::vars().collect(); + let input = unsafe { self.load_input_and_modify_override() }?; + existing.extend(input); + Ok(existing) + } + // override existing env with input, returning input only + EnvSequence::InputOnly => unsafe { self.load_input_and_modify_override() }, + // load input into env, but don't override existing + EnvSequence::InputThenEnv => { + let existing: EnvMap = env::vars().collect(); + + let mut input = unsafe { self.load_input_and_modify() }?; + + for k in input.keys() { + if !existing.contains_key(k) { + unsafe { env::set_var(k, &input[k]) }; + } + } + input.extend(existing); + Ok(input) + } + } + } +} diff --git a/dotenv/src/parse.rs b/dotenvy/src/parse.rs similarity index 98% rename from dotenv/src/parse.rs rename to dotenvy/src/parse.rs index 568bd7a6..970866fb 100644 --- a/dotenv/src/parse.rs +++ b/dotenvy/src/parse.rs @@ -1,15 +1,12 @@ #![allow(clippy::module_name_repetitions)] -use crate::errors::{Error, Result}; +use crate::{Error, Result}; use std::{collections::HashMap, env}; -// for readability's sake -pub type ParsedLine = Result>; - pub fn parse_line( line: &str, substitution_data: &mut HashMap>, -) -> ParsedLine { +) -> Result> { let mut parser = LineParser::new(line, substitution_data); parser.parse_line() } @@ -35,7 +32,7 @@ impl<'a> LineParser<'a> { Error::LineParse(self.original_line.into(), self.pos) } - fn parse_line(&mut self) -> ParsedLine { + fn parse_line(&mut self) -> Result> { self.skip_whitespace(); // if its an empty line or a comment, skip it if self.line.is_empty() || self.line.starts_with('#') { @@ -268,9 +265,7 @@ fn apply_substitution( #[cfg(test)] mod test { - use crate::iter::Iter; - - use super::*; + use crate::{iter::Iter, Result}; #[test] fn test_parse_line_env() { @@ -560,7 +555,7 @@ mod variable_substitution_tests { #[cfg(test)] mod error_tests { - use crate::errors::Error::LineParse; + use crate::err::Error::LineParse; use crate::iter::Iter; #[test] diff --git a/dotenv/tests/common/mod.rs b/dotenvy/tests/common.rs similarity index 61% rename from dotenv/tests/common/mod.rs rename to dotenvy/tests/common.rs index 338c0a5b..97f4cab5 100644 --- a/dotenv/tests/common/mod.rs +++ b/dotenvy/tests/common.rs @@ -5,14 +5,14 @@ use std::{ }; use tempfile::{tempdir, TempDir}; -pub unsafe fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { +pub unsafe fn tempdir_with_dotenv(text: &str) -> io::Result { unsafe { env::set_var("EXISTING", "from_env") }; 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(dotenv_text.as_bytes())?; - dotenv_file.sync_all()?; + let path = dir.path().join(".env"); + let mut file = File::create(path)?; + file.write_all(text.as_bytes())?; + file.sync_all()?; Ok(dir) } diff --git a/dotenv/tests/integration/util/mod.rs b/dotenvy/tests/integration/main.rs similarity index 61% rename from dotenv/tests/integration/util/mod.rs rename to dotenvy/tests/integration/main.rs index 12430b63..81d02b33 100644 --- a/dotenv/tests/integration/util/mod.rs +++ b/dotenvy/tests/integration/main.rs @@ -1,9 +1,7 @@ -#![allow(dead_code)] +use std::env::{self, VarError}; mod testenv; -use std::env::{self, VarError}; - /// Default key used in envfile pub const TEST_KEY: &str = "TESTKEY"; /// Default value used in envfile @@ -18,19 +16,13 @@ pub const TEST_OVERRIDING_VALUE: &str = "from_file"; #[inline(always)] pub fn create_default_envfile() -> String { - format!( - "{}={}\n{}={}", - TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE - ) + format!("{TEST_KEY}={TEST_VALUE}\n{TEST_EXISTING_KEY}={TEST_OVERRIDING_VALUE}",) } /// missing equals #[inline(always)] pub fn create_invalid_envfile() -> String { - format!( - "{}{}\n{}{}", - TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE - ) + format!("{TEST_KEY}{TEST_VALUE}\n{TEST_EXISTING_KEY}{TEST_OVERRIDING_VALUE}",) } /// Assert that an environment variable is set and has the expected value. @@ -38,13 +30,11 @@ pub fn assert_env_var(key: &str, expected: &str) { match env::var(key) { Ok(actual) => assert_eq!( expected, actual, - "\n\nFor Environment Variable `{}`:\n EXPECTED: `{}`\n ACTUAL: `{}`\n", - key, expected, actual + "\n\nFor Environment Variable `{key}`:\n EXPECTED: `{expected}`\n ACTUAL: `{actual}`\n", ), - Err(VarError::NotPresent) => panic!("env var `{}` not found", key), + Err(VarError::NotPresent) => panic!("env var `{key}` not found"), Err(VarError::NotUnicode(val)) => panic!( - "env var `{}` currently has invalid unicode: `{}`", - key, + "env var `{key}` currently has invalid unicode: `{}`", val.to_string_lossy() ), } @@ -53,13 +43,9 @@ pub fn assert_env_var(key: &str, expected: &str) { /// Assert that an environment variable is not currently set. pub fn assert_env_var_unset(key: &str) { match env::var(key) { - Ok(actual) => panic!( - "env var `{}` should not be set, currently it is: `{}`", - key, actual - ), + Ok(actual) => panic!("env var `{key}` should not be set, currently it is: `{actual}`",), Err(VarError::NotUnicode(val)) => panic!( - "env var `{}` should not be set, currently has invalid unicode: `{}`", - key, + "env var `{key}` should not be set, currently has invalid unicode: `{}`", val.to_string_lossy() ), _ => (), diff --git a/dotenv/tests/integration/util/testenv.rs b/dotenvy/tests/integration/testenv.rs similarity index 89% rename from dotenv/tests/integration/util/testenv.rs rename to dotenvy/tests/integration/testenv.rs index 616954aa..a5965f85 100644 --- a/dotenv/tests/integration/util/testenv.rs +++ b/dotenvy/tests/integration/testenv.rs @@ -1,6 +1,6 @@ use super::{create_default_envfile, TEST_EXISTING_KEY, TEST_EXISTING_VALUE}; +use dotenvy::EnvMap; use std::{ - collections::HashMap, env, fs, io::{self, Write}, path::{Path, PathBuf}, @@ -8,16 +8,13 @@ use std::{ }; use tempfile::{tempdir, TempDir}; -/// Env var convenience type. -type EnvMap = HashMap; - /// Initialized in [`get_env_locker`] static ENV_LOCKER: OnceLock>> = OnceLock::new(); /// A test environment. /// /// Will create a new temporary directory. Use its builder methods to configure -/// the directory structure, preset variables, envfile name and contents, and +/// the directory structure, preset variables, env file name and contents, and /// the working directory to run the test from. /// /// Creation methods: @@ -30,8 +27,8 @@ pub struct TestEnv { temp_dir: TempDir, work_dir: PathBuf, env_vars: Vec, - envfile_contents: Option, - envfile_path: PathBuf, + env_file_contents: Option, + env_file_path: PathBuf, } /// Simple key value struct for representing environment variables @@ -99,8 +96,8 @@ impl TestEnv { temp_dir: tempdir, work_dir, env_vars: Default::default(), - envfile_contents: None, - envfile_path, + env_file_contents: None, + env_file_path: envfile_path, } } @@ -117,16 +114,16 @@ impl TestEnv { /// Change the name of the default `.env` file. /// /// It will still be placed in the root temporary directory. If you need to - /// put the envfile in a different directory, use + /// put the env file in a different directory, use /// [`set_envfile_path`](TestEnv::set_envfile_path) instead. pub fn set_envfile_name(&mut self, name: impl AsRef) -> &mut Self { - self.envfile_path = self.temp_path().join(name); + self.env_file_path = self.temp_path().join(name); self } /// Change the absolute path to the envfile. pub fn set_envfile_path(&mut self, path: PathBuf) -> &mut Self { - self.envfile_path = path; + self.env_file_path = path; self } @@ -135,9 +132,9 @@ impl TestEnv { /// If this is the only change to the [`TestEnv`] being made, use /// [`new_with_envfile`](TestEnv::new_with_envfile). /// - /// Setting it to an empty string will cause an empty envfile to be created + /// Setting it to an empty string will cause an empty env file to be created pub fn set_envfile_contents(&mut self, contents: impl ToString) -> &mut Self { - self.envfile_contents = Some(contents.to_string()); + self.env_file_contents = Some(contents.to_string()); self } @@ -198,7 +195,7 @@ impl TestEnv { /// Create a child folder within the temporary directory. /// /// This will not change the working directory the test is run in, or where - /// the envfile is created. + /// the env file is created. /// /// Will create parent directories if they are missing. pub fn add_child_dir_all(&self, rel_path: impl AsRef) -> PathBuf { @@ -233,14 +230,14 @@ impl TestEnv { /// Get a reference to the string that will be placed in the envfile. /// - /// If `None` is returned, an envfile will not be created + /// If `None` is returned, an env file will not be created pub fn envfile_contents(&self) -> Option<&str> { - self.envfile_contents.as_deref() + self.env_file_contents.as_deref() } /// Get a reference to the path of the envfile. pub fn envfile_path(&self) -> &Path { - &self.envfile_path + &self.env_file_path } } @@ -258,8 +255,8 @@ impl Default for TestEnv { temp_dir, work_dir, env_vars, - envfile_contents, - envfile_path, + env_file_contents: envfile_contents, + env_file_path: envfile_path, } } } @@ -305,9 +302,9 @@ fn reset_env(original_env: &EnvMap) { /// /// Writes the envfile, sets the working directory, and sets environment vars. unsafe fn create_env(test_env: &TestEnv) { - // only create the envfile if its contents has been set + // only create the env file if its contents has been set if let Some(contents) = test_env.envfile_contents() { - create_envfile(&test_env.envfile_path, contents); + create_env_filefile(&test_env.env_file_path, contents); } env::set_current_dir(&test_env.work_dir).expect("setting working directory"); @@ -317,10 +314,10 @@ unsafe fn create_env(test_env: &TestEnv) { } } -/// Create an envfile for use in tests. -fn create_envfile(path: &Path, contents: &str) { +/// Create an env file for use in tests. +fn create_env_filefile(path: &Path, contents: &str) { if path.exists() { - panic!("envfile `{}` already exists", path.display()) + panic!("env file `{}` already exists", path.display()) } // inner function to group together io::Results fn create_env_file_inner(path: &Path, contents: &str) -> io::Result<()> { @@ -331,6 +328,6 @@ fn create_envfile(path: &Path, contents: &str) { // call inner function if let Err(err) = create_env_file_inner(path, contents) { // handle any io::Result::Err - panic!("error creating envfile `{}`: {}", path.display(), err); + panic!("error creating env file `{}`: {}", path.display(), err); } } diff --git a/dotenv/tests/test-child-dir.rs b/dotenvy/tests/test-child-dir.rs similarity index 81% rename from dotenv/tests/test-child-dir.rs rename to dotenvy/tests/test-child-dir.rs index 8a11f859..b68cfce0 100644 --- a/dotenv/tests/test-child-dir.rs +++ b/dotenvy/tests/test-child-dir.rs @@ -1,17 +1,17 @@ mod common; +use dotenvy::EnvLoader; + use crate::common::make_test_dotenv; use std::{env, error, fs}; #[test] fn test_child_dir() -> Result<(), Box> { let dir = unsafe { make_test_dotenv() }?; - fs::create_dir("child")?; - env::set_current_dir("child")?; - unsafe { dotenvy::dotenv() }?; + unsafe { EnvLoader::from_path("../.env").load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); env::set_current_dir(dir.path().parent().unwrap())?; diff --git a/dotenv/tests/test-default-location-override.rs b/dotenvy/tests/test-default-location-override.rs similarity index 72% rename from dotenv/tests/test-default-location-override.rs rename to dotenvy/tests/test-default-location-override.rs index 9110bc14..3a04ea64 100644 --- a/dotenv/tests/test-default-location-override.rs +++ b/dotenvy/tests/test-default-location-override.rs @@ -1,13 +1,15 @@ mod common; use crate::common::make_test_dotenv; +use dotenvy::{EnvLoader, EnvSequence}; use std::{env, error}; #[test] fn test_default_location_override() -> Result<(), Box> { let dir = unsafe { make_test_dotenv() }?; - unsafe { dotenvy::dotenv_override() }?; + let loader = EnvLoader::new().sequence(EnvSequence::EnvThenInput); + unsafe { loader.load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); assert_eq!(env::var("EXISTING")?, "from_file"); diff --git a/dotenv/tests/test-default-location.rs b/dotenvy/tests/test-default-location.rs similarity index 80% rename from dotenv/tests/test-default-location.rs rename to dotenvy/tests/test-default-location.rs index 59ee2f53..671dcfd0 100644 --- a/dotenv/tests/test-default-location.rs +++ b/dotenvy/tests/test-default-location.rs @@ -1,5 +1,7 @@ mod common; +use dotenvy::EnvLoader; + use crate::common::make_test_dotenv; use std::{env, error}; @@ -7,7 +9,7 @@ use std::{env, error}; fn test_default_location() -> Result<(), Box> { let dir = unsafe { make_test_dotenv() }?; - unsafe { dotenvy::dotenv() }?; + unsafe { EnvLoader::from_path("./.env").load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenv/tests/test-ignore-bom.rs b/dotenvy/tests/test-ignore-bom.rs similarity index 80% rename from dotenv/tests/test-ignore-bom.rs rename to dotenvy/tests/test-ignore-bom.rs index 5acbbbed..a118303b 100644 --- a/dotenv/tests/test-ignore-bom.rs +++ b/dotenvy/tests/test-ignore-bom.rs @@ -1,5 +1,7 @@ mod common; +use dotenvy::EnvLoader; + use crate::common::tempdir_with_dotenv; use std::{env, error}; @@ -11,7 +13,8 @@ fn test_ignore_bom() -> Result<(), Box> { let mut path = env::current_dir()?; path.push(".env"); - unsafe { dotenvy::from_path(&path) }?; + let config = EnvLoader::from_path(path); + unsafe { config.load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); diff --git a/dotenv/tests/test-multiline-comment.rs b/dotenvy/tests/test-multiline-comment.rs similarity index 92% rename from dotenv/tests/test-multiline-comment.rs rename to dotenvy/tests/test-multiline-comment.rs index eb95b773..76bc3384 100644 --- a/dotenv/tests/test-multiline-comment.rs +++ b/dotenvy/tests/test-multiline-comment.rs @@ -2,6 +2,7 @@ mod common; use std::env; use common::tempdir_with_dotenv; +use dotenvy::EnvLoader; #[test] fn test_issue_12() { @@ -25,7 +26,7 @@ Line 6 let _f = unsafe { tempdir_with_dotenv(txt) }.expect("should write test env"); - unsafe { dotenvy::dotenv() }.expect("should succeed"); + unsafe { EnvLoader::new().load_and_modify() }.expect("should succeed"); assert_eq!( env::var("TESTKEY1").expect("testkey1 env key not set"), "test_val" diff --git a/dotenv/tests/test-multiline.rs b/dotenvy/tests/test-multiline.rs similarity index 92% rename from dotenv/tests/test-multiline.rs rename to dotenvy/tests/test-multiline.rs index b79f9f48..91b54e4b 100644 --- a/dotenv/tests/test-multiline.rs +++ b/dotenvy/tests/test-multiline.rs @@ -1,5 +1,7 @@ mod common; +use dotenvy::EnvLoader; + use crate::common::tempdir_with_dotenv; use std::{env, error}; @@ -24,7 +26,7 @@ STRONG='{}' ); let dir = unsafe { tempdir_with_dotenv(&txt) }?; - unsafe { dotenvy::dotenv() }?; + unsafe { EnvLoader::new().load_and_modify() }?; assert_eq!(env::var("KEY")?, r#"my cool value"#); assert_eq!( env::var("KEY3")?, diff --git a/dotenv/tests/test-variable-substitution.rs b/dotenvy/tests/test-variable-substitution.rs similarity index 95% rename from dotenv/tests/test-variable-substitution.rs rename to dotenvy/tests/test-variable-substitution.rs index 742a8be9..f5ffc39a 100644 --- a/dotenv/tests/test-variable-substitution.rs +++ b/dotenvy/tests/test-variable-substitution.rs @@ -2,6 +2,8 @@ mod common; +use dotenvy::EnvLoader; + use crate::common::tempdir_with_dotenv; use std::{env, error}; @@ -29,7 +31,7 @@ SUBSTITUTION_WITHOUT_QUOTES={common_string} ); let dir = unsafe { tempdir_with_dotenv(&txt) }?; - unsafe { dotenvy::dotenv() }?; + unsafe { EnvLoader::new().load_and_modify() }?; assert_eq!(env::var("KEY")?, "value"); assert_eq!(env::var("KEY1")?, "value1"); diff --git a/dotenvy/tests/test-vars.rs b/dotenvy/tests/test-vars.rs new file mode 100644 index 00000000..684694e6 --- /dev/null +++ b/dotenvy/tests/test-vars.rs @@ -0,0 +1,17 @@ +mod common; + +use crate::common::make_test_dotenv; +use std::{collections::HashMap, env, error}; + +// #[test] +// fn test_vars() -> Result<(), Box> { +// let dir = unsafe { make_test_dotenv() }?; + +// let vars: HashMap = unsafe { dotenvy::modify::vars() }.collect(); + +// assert_eq!(vars["TESTKEY"], "test_val"); + +// env::set_current_dir(dir.path().parent().unwrap())?; +// dir.close()?; +// Ok(()) +// } diff --git a/examples/.env b/examples/.env deleted file mode 100644 index 95f961dc..00000000 --- a/examples/.env +++ /dev/null @@ -1 +0,0 @@ -HOST=dev.com \ No newline at end of file diff --git a/examples/Cargo.toml b/examples/Cargo.toml deleted file mode 100644 index 66964292..00000000 --- a/examples/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[workspace] -members = ["*"] -exclude = ["target"] -resolver = "2" diff --git a/examples/dev-prod/Cargo.toml b/examples/dev-prod/Cargo.toml index 6bf98bc5..3aea642c 100644 --- a/examples/dev-prod/Cargo.toml +++ b/examples/dev-prod/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "example-dev-prod" +name = "dev-prod-example" version = "0.1.0" edition = "2021" publish = false [dependencies] -dotenvy = { path = "../../dotenv" } +dotenvy = { path = "../../dotenvy" } diff --git a/examples/dev-prod/src/main.rs b/examples/dev-prod/src/main.rs index 75aecbbc..dd51b5bc 100644 --- a/examples/dev-prod/src/main.rs +++ b/examples/dev-prod/src/main.rs @@ -1,43 +1,52 @@ -use std::{env, fmt}; +//! This example loads from an env file in development but from the environment only in production. +//! +/// Commands to try: +/// 1) `cargo run` +/// 2) `APP_ENV=prod cargo run` +/// 3) `APP_ENV=prod HOST=prod.com cargo run` +use dotenvy::{EnvLoader, EnvSequence}; +use std::{env, error, str::FromStr}; + +fn main() -> Result<(), Box> { + let app_env = env::var("APP_ENV") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(AppEnv::Dev); + + let env_map = EnvLoader::from_path("../env-example") + .sequence(app_env.into()) + .load()?; + + if let Some(v) = env_map.get("HOST") { + println!("Host: {v}"); + } else { + println!("HOST not set"); + } + Ok(()) +} -#[derive(PartialEq)] enum AppEnv { Dev, Prod, } -/// A common setup that: -/// - loads from a .env file in dev mode -/// - loads from the environment in prod mode -/// -/// A few commands to try: -/// 1) `cargo run` -/// 2) `APP_ENV=prod cargo run` -/// 3) `APP_ENV=prod HOST=prod.com cargo run` -fn main() { - let app_env = match env::var("APP_ENV") { - Ok(v) if v == "prod" => AppEnv::Prod, - _ => AppEnv::Dev, - }; +impl FromStr for AppEnv { + type Err = String; - println!("Running in {app_env} mode"); - - if app_env == AppEnv::Dev { - match dotenvy::dotenv() { - Ok(path) => println!(".env read successfully from {}", path.display()), - Err(e) => println!("Could not load .env file: {e}"), - }; + fn from_str(s: &str) -> Result { + match s { + "dev" => Ok(Self::Dev), + "prod" => Ok(Self::Prod), + s => Err(format!("Invalid AppEnv: {s}")), + } } - - let host = env::var("HOST").expect("HOST not set"); - println!("Host: {host}"); } -impl fmt::Display for AppEnv { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - AppEnv::Dev => write!(f, "dev"), - AppEnv::Prod => write!(f, "prod"), +impl From for EnvSequence { + fn from(v: AppEnv) -> Self { + match v { + AppEnv::Dev => Self::InputThenEnv, + AppEnv::Prod => Self::EnvOnly, } } } diff --git a/examples/env-example b/examples/env-example new file mode 100644 index 00000000..d8dd4252 --- /dev/null +++ b/examples/env-example @@ -0,0 +1 @@ +HOST=dev.com diff --git a/examples/env-example-2 b/examples/env-example-2 new file mode 100644 index 00000000..83502d36 --- /dev/null +++ b/examples/env-example-2 @@ -0,0 +1 @@ +HOST=dev2.com diff --git a/examples/find/Cargo.toml b/examples/find/Cargo.toml new file mode 100644 index 00000000..1eac6f2c --- /dev/null +++ b/examples/find/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "find-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy" } diff --git a/examples/find/src/main.rs b/examples/find/src/main.rs new file mode 100644 index 00000000..5341644f --- /dev/null +++ b/examples/find/src/main.rs @@ -0,0 +1,46 @@ +//! This example shows finding an env file by filename. +use dotenvy::EnvLoader; +use std::{ + env, error, fs, io, + path::{Path, PathBuf}, +}; + +fn main() -> Result<(), Box> { + let filename = "env-example"; + + println!("Looking for env file with filename: `{filename}`"); + let path = find(env::current_dir()?.as_path(), filename)?; + println!("Env file found at `{}`", path.display()); + + let env_map = EnvLoader::from_path(path).load()?; + if let Some(v) = env_map.get("HOST") { + println!("HOST={v}"); + } + Ok(()) +} + +/// Searches for the filename in the directory and parent directories until the file is found or the filesystem root is reached. +pub fn find(mut dir: &Path, filename: &str) -> Result { + loop { + let candidate = dir.join(filename); + + match fs::metadata(&candidate) { + Ok(metadata) => { + if metadata.is_file() { + return Ok(candidate); + } + } + Err(e) => { + if e.kind() != io::ErrorKind::NotFound { + return Err(e); + } + } + } + + if let Some(parent) = dir.parent() { + dir = parent; + } else { + return Err(io::ErrorKind::NotFound.into()); + } + } +} diff --git a/examples/modify-macro/Cargo.toml b/examples/modify-macro/Cargo.toml new file mode 100644 index 00000000..491bcbf2 --- /dev/null +++ b/examples/modify-macro/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "modify-macro-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy", features = ["macros"] } \ No newline at end of file diff --git a/examples/modify-macro/src/main.rs b/examples/modify-macro/src/main.rs new file mode 100644 index 00000000..fb449340 --- /dev/null +++ b/examples/modify-macro/src/main.rs @@ -0,0 +1,7 @@ +use std::{env, error}; + +#[dotenvy::load(path = "../env-example", required = true, override = true)] +fn main() -> Result<(), Box> { + println!("HOST={}", env::var("HOST")?); + Ok(()) +} diff --git a/examples/modify-tokio-macro/Cargo.toml b/examples/modify-tokio-macro/Cargo.toml new file mode 100644 index 00000000..8715a6d2 --- /dev/null +++ b/examples/modify-tokio-macro/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "modify-tokio-macro-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy", features = ["macros"] } +tokio = { version = "1", features = [ + "process", + "rt-multi-thread", + "rt", + "macros", +] } diff --git a/examples/modify-tokio-macro/src/main.rs b/examples/modify-tokio-macro/src/main.rs new file mode 100644 index 00000000..6ab8c181 --- /dev/null +++ b/examples/modify-tokio-macro/src/main.rs @@ -0,0 +1,10 @@ +//! `#[dotenvy::load]` must go before `#[tokio::main]`. + +use std::{env, error}; + +#[dotenvy::load(path = "../env-example")] +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("HOST={}", env::var("HOST")?); + Ok(()) +} diff --git a/examples/modify-tokio/Cargo.toml b/examples/modify-tokio/Cargo.toml new file mode 100644 index 00000000..4d2b4ad6 --- /dev/null +++ b/examples/modify-tokio/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "modify-tokio-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy" } +tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"]} \ No newline at end of file diff --git a/examples/modify-tokio/src/main.rs b/examples/modify-tokio/src/main.rs new file mode 100644 index 00000000..d036b6e3 --- /dev/null +++ b/examples/modify-tokio/src/main.rs @@ -0,0 +1,24 @@ +use dotenvy::EnvLoader; +use std::{ + env::{self, VarError}, + error, +}; + +// `load_and_modify` uses `std::env::set_var` internally, which is not thread-safe. +// As such, loading must be done before the async runtime is spawned. +// This is why we don't use `#[tokio::main]` here. +fn main() -> Result<(), Box> { + let loader = EnvLoader::from_path("../env-example"); + unsafe { loader.load_and_modify() }?; + + // this is the expansion of `#[tokio::main]` + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()? + .block_on(async { + println!("HOST={}", env::var("HOST")?); + Ok::<_, VarError>(()) + })?; + + Ok(()) +} diff --git a/examples/modify/Cargo.toml b/examples/modify/Cargo.toml new file mode 100644 index 00000000..ee61f5f7 --- /dev/null +++ b/examples/modify/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "modify-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy" } diff --git a/examples/modify/print_host.py b/examples/modify/print_host.py new file mode 100644 index 00000000..6eb71b9f --- /dev/null +++ b/examples/modify/print_host.py @@ -0,0 +1,4 @@ +import os + +print('Hello, Python world') +print(f'HOST={os.environ.get("HOST")}') \ No newline at end of file diff --git a/examples/modify/src/main.rs b/examples/modify/src/main.rs new file mode 100644 index 00000000..2368611c --- /dev/null +++ b/examples/modify/src/main.rs @@ -0,0 +1,22 @@ +//! This example modifies the existing environment. +//! +//! This makes environment varaibles from available to subprocesses, e.g., a Python script. +use dotenvy::{EnvLoader, EnvSequence}; +use std::{env, error, fs, io, process::Command}; + +fn main() -> Result<(), Box> { + // to override, set sequence to `EnvThenInput` or `InputOnly` + let loader = EnvLoader::from_path("../env-example").sequence(EnvSequence::InputThenEnv); + unsafe { loader.load_and_modify() }?; + + println!("HOST={}", env::var("HOST")?); + print_host_py()?; + Ok(()) +} + +fn print_host_py() -> Result<(), io::Error> { + let script = fs::read_to_string("print_host.py")?; + let output = Command::new("python3").arg("-c").arg(script).output()?; + print!("{}", String::from_utf8_lossy(&output.stdout)); + Ok(()) +} diff --git a/examples/multiple-files/Cargo.toml b/examples/multiple-files/Cargo.toml new file mode 100644 index 00000000..60193ed3 --- /dev/null +++ b/examples/multiple-files/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "multiple-files-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy" } diff --git a/examples/multiple-files/src/main.rs b/examples/multiple-files/src/main.rs new file mode 100644 index 00000000..d413c731 --- /dev/null +++ b/examples/multiple-files/src/main.rs @@ -0,0 +1,19 @@ +use dotenvy::{EnvLoader, EnvSequence}; +use std::error; + +fn main() -> Result<(), Box> { + let map_a = EnvLoader::from_path("../env-example") + .sequence(EnvSequence::EnvThenInput) + .load()?; + let map_b = EnvLoader::from_path("../env-example-2") + .sequence(EnvSequence::InputOnly) // we already loaded from the environment in map_a + .load()?; + + let mut env_map = map_a.clone(); + env_map.extend(map_b); + + if let Some(v) = env_map.get("HOST") { + println!("HOST={v}"); + } + Ok(()) +} diff --git a/examples/optional/Cargo.toml b/examples/optional/Cargo.toml new file mode 100644 index 00000000..f4063dc4 --- /dev/null +++ b/examples/optional/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "optional-example" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +dotenvy = { path = "../../dotenvy" } diff --git a/examples/optional/src/main.rs b/examples/optional/src/main.rs new file mode 100644 index 00000000..b32d217e --- /dev/null +++ b/examples/optional/src/main.rs @@ -0,0 +1,20 @@ +//! This example loads an env file only if the file exists. + +use dotenvy::{EnvLoader, EnvSequence}; +use std::{error, path::Path}; + +fn main() -> Result<(), Box> { + let path = Path::new("../.env"); + let seq = if path.exists() { + EnvSequence::InputThenEnv + } else { + EnvSequence::EnvOnly + }; + + let env_map = EnvLoader::from_path(path).sequence(seq).load()?; + + if let Some(v) = env_map.get("HOST") { + println!("Host: {v}"); + } + Ok(()) +} From 5012a99f5f7adb0adb39818b9cac986ab5ce947f Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:20:22 -0400 Subject: [PATCH 03/39] Rename `from_path` to `with_path` This naming better reflects the fact that we are configuring a loader with an optional path, with deferred I/O. --- CHANGELOG.md | 3 +-- README.md | 2 +- dotenvy-macros/src/lib.rs | 2 +- dotenvy/README.md | 2 +- dotenvy/src/bin/dotenvy.rs | 2 +- dotenvy/src/err.rs | 2 ++ dotenvy/src/lib.rs | 19 +++++++++---------- dotenvy/tests/test-child-dir.rs | 2 +- dotenvy/tests/test-default-location.rs | 2 +- dotenvy/tests/test-ignore-bom.rs | 2 +- examples/dev-prod/src/main.rs | 2 +- examples/find/src/main.rs | 2 +- examples/modify-tokio/src/main.rs | 2 +- examples/modify/src/main.rs | 2 +- examples/multiple-files/src/main.rs | 4 ++-- examples/optional/src/main.rs | 2 +- 16 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6cc035..eb142f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,9 +15,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - **breaking**: `dotenvy::Result` is now private - **breaking**: deprecate `dotenvy::var`, `dotenvy::from_filename*` - `Error` is now `From TokenStream { } else { EnvSequence::InputThenEnv }; - let mut loader = EnvLoader::from_path(#path).sequence(seq); + let mut loader = EnvLoader::with_path(#path).sequence(seq); if let Err(e) = unsafe { loader.load_and_modify() } { if let Some(io_err) = e.source().and_then(|src| src.downcast_ref::()) { if io_err.kind() == io::ErrorKind::NotFound && !#required { diff --git a/dotenvy/README.md b/dotenvy/README.md index bd1ea040..94e3b16b 100644 --- a/dotenvy/README.md +++ b/dotenvy/README.md @@ -29,7 +29,7 @@ use dotenvy::EnvLoader; use std::env; fn main() { - let env_map = EnvLoader::from_path("env-file").load()?; + let env_map = EnvLoader::new().load()?; for (key, value) in env { println!("{key}: {value}"); diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index 6dbee199..f92ea9fe 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -56,7 +56,7 @@ fn main() -> Result<(), Box> { let cli = Cli::parse(); // load the file - let loader = EnvLoader::from_path(&cli.file); + let loader = EnvLoader::with_path(&cli.file); if let Err(e) = unsafe { loader.load_and_modify() } { die!("Failed to load {path}: {e}", path = cli.file.display()); } diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index e488403c..ea995908 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -9,6 +9,7 @@ pub type Result = result::Result; #[non_exhaustive] pub enum Error { LineParse(String, usize), + /// An IO error may be encountered when reading from a file or reader. Io(io::Error), EnvVar(env::VarError), /// When `load_and_modify` is called with `EnvSequence::EnvOnly` @@ -21,6 +22,7 @@ pub enum Error { NoInput, } + impl From for Error { fn from(e: io::Error) -> Self { Self::Io(e) diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index ffd1a2e0..5676bb81 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -60,23 +60,23 @@ impl<'a> EnvLoader<'a> { #[must_use] /// Creates a new `EnvLoader` with the path set to `env` in the current directory. pub fn new() -> Self { - Self::from_path(".env") + Self::with_path(".env") } - /// Creates a new `EnvLoader` from a path. - /// + /// Creates a new `EnvLoader` with the specified path. + /// /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. - pub fn from_path>(path: P) -> Self { + pub fn with_path>(path: P) -> Self { Self { path: Some(path.as_ref().to_owned()), ..Default::default() } } - /// Creates a new `EnvLoader` from a reader. - /// + /// Creates a new `EnvLoader` with the specified reader. + /// /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. - pub fn from_reader(rdr: R) -> Self { + pub fn with_reader(rdr: R) -> Self { Self { reader: Some(Box::new(rdr)), ..Default::default() @@ -118,7 +118,7 @@ impl<'a> EnvLoader<'a> { } /// Loads environment variables into a hash map. - /// + /// /// This is the primary method for loading environment variables. pub fn load(self) -> Result { match self.sequence { @@ -138,9 +138,8 @@ impl<'a> EnvLoader<'a> { } } - /// Loads environment variables into a hash map, modifying the existing environment. - /// + /// /// This calls `std::env::set_var` internally and is not thread-safe. pub unsafe fn load_and_modify(self) -> Result { match self.sequence { diff --git a/dotenvy/tests/test-child-dir.rs b/dotenvy/tests/test-child-dir.rs index b68cfce0..5709f4c7 100644 --- a/dotenvy/tests/test-child-dir.rs +++ b/dotenvy/tests/test-child-dir.rs @@ -11,7 +11,7 @@ fn test_child_dir() -> Result<(), Box> { fs::create_dir("child")?; env::set_current_dir("child")?; - unsafe { EnvLoader::from_path("../.env").load_and_modify() }?; + unsafe { EnvLoader::with_path("../.env").load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); env::set_current_dir(dir.path().parent().unwrap())?; diff --git a/dotenvy/tests/test-default-location.rs b/dotenvy/tests/test-default-location.rs index 671dcfd0..0a22721f 100644 --- a/dotenvy/tests/test-default-location.rs +++ b/dotenvy/tests/test-default-location.rs @@ -9,7 +9,7 @@ use std::{env, error}; fn test_default_location() -> Result<(), Box> { let dir = unsafe { make_test_dotenv() }?; - unsafe { EnvLoader::from_path("./.env").load_and_modify() }?; + unsafe { EnvLoader::with_path("./.env").load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); assert_eq!(env::var("EXISTING")?, "from_env"); diff --git a/dotenvy/tests/test-ignore-bom.rs b/dotenvy/tests/test-ignore-bom.rs index a118303b..b16ff7bf 100644 --- a/dotenvy/tests/test-ignore-bom.rs +++ b/dotenvy/tests/test-ignore-bom.rs @@ -13,7 +13,7 @@ fn test_ignore_bom() -> Result<(), Box> { let mut path = env::current_dir()?; path.push(".env"); - let config = EnvLoader::from_path(path); + let config = EnvLoader::with_path(path); unsafe { config.load_and_modify() }?; assert_eq!(env::var("TESTKEY")?, "test_val"); diff --git a/examples/dev-prod/src/main.rs b/examples/dev-prod/src/main.rs index dd51b5bc..210c240c 100644 --- a/examples/dev-prod/src/main.rs +++ b/examples/dev-prod/src/main.rs @@ -13,7 +13,7 @@ fn main() -> Result<(), Box> { .and_then(|v| v.parse().ok()) .unwrap_or(AppEnv::Dev); - let env_map = EnvLoader::from_path("../env-example") + let env_map = EnvLoader::with_path("../env-example") .sequence(app_env.into()) .load()?; diff --git a/examples/find/src/main.rs b/examples/find/src/main.rs index 5341644f..003099eb 100644 --- a/examples/find/src/main.rs +++ b/examples/find/src/main.rs @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { let path = find(env::current_dir()?.as_path(), filename)?; println!("Env file found at `{}`", path.display()); - let env_map = EnvLoader::from_path(path).load()?; + let env_map = EnvLoader::with_path(path).load()?; if let Some(v) = env_map.get("HOST") { println!("HOST={v}"); } diff --git a/examples/modify-tokio/src/main.rs b/examples/modify-tokio/src/main.rs index d036b6e3..d496ecb4 100644 --- a/examples/modify-tokio/src/main.rs +++ b/examples/modify-tokio/src/main.rs @@ -8,7 +8,7 @@ use std::{ // As such, loading must be done before the async runtime is spawned. // This is why we don't use `#[tokio::main]` here. fn main() -> Result<(), Box> { - let loader = EnvLoader::from_path("../env-example"); + let loader = EnvLoader::with_path("../env-example"); unsafe { loader.load_and_modify() }?; // this is the expansion of `#[tokio::main]` diff --git a/examples/modify/src/main.rs b/examples/modify/src/main.rs index 2368611c..4a8cf833 100644 --- a/examples/modify/src/main.rs +++ b/examples/modify/src/main.rs @@ -6,7 +6,7 @@ use std::{env, error, fs, io, process::Command}; fn main() -> Result<(), Box> { // to override, set sequence to `EnvThenInput` or `InputOnly` - let loader = EnvLoader::from_path("../env-example").sequence(EnvSequence::InputThenEnv); + let loader = EnvLoader::with_path("../env-example").sequence(EnvSequence::InputThenEnv); unsafe { loader.load_and_modify() }?; println!("HOST={}", env::var("HOST")?); diff --git a/examples/multiple-files/src/main.rs b/examples/multiple-files/src/main.rs index d413c731..b993cbb0 100644 --- a/examples/multiple-files/src/main.rs +++ b/examples/multiple-files/src/main.rs @@ -2,10 +2,10 @@ use dotenvy::{EnvLoader, EnvSequence}; use std::error; fn main() -> Result<(), Box> { - let map_a = EnvLoader::from_path("../env-example") + let map_a = EnvLoader::with_path("../env-example") .sequence(EnvSequence::EnvThenInput) .load()?; - let map_b = EnvLoader::from_path("../env-example-2") + let map_b = EnvLoader::with_path("../env-example-2") .sequence(EnvSequence::InputOnly) // we already loaded from the environment in map_a .load()?; diff --git a/examples/optional/src/main.rs b/examples/optional/src/main.rs index b32d217e..54ba8aa3 100644 --- a/examples/optional/src/main.rs +++ b/examples/optional/src/main.rs @@ -11,7 +11,7 @@ fn main() -> Result<(), Box> { EnvSequence::EnvOnly }; - let env_map = EnvLoader::from_path(path).sequence(seq).load()?; + let env_map = EnvLoader::with_path(path).sequence(seq).load()?; if let Some(v) = env_map.get("HOST") { println!("Host: {v}"); From 0f9ad5fec5e5c7070faf743cf45094552461cba4 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:50:47 -0400 Subject: [PATCH 04/39] New `Error` type with `NotPresent(String)` This removes the `EnvVar` variant which wraps `VarError`. The internals of `VarError` are now variants in `Error`. This makes it more ergonomic for users who are propogating errors to know which varaible is truly missing. --- dotenvy/src/err.rs | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index ea995908..822657c1 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -1,17 +1,14 @@ -use std::{ - env::{self}, - error, fmt, io, result, -}; +use std::{error, ffi::OsString, fmt, io, result}; pub type Result = result::Result; #[derive(Debug)] -#[non_exhaustive] pub enum Error { LineParse(String, usize), /// An IO error may be encountered when reading from a file or reader. Io(io::Error), - EnvVar(env::VarError), + NotPresent(String), + NotUnicode(OsString), /// When `load_and_modify` is called with `EnvSequence::EnvOnly` /// /// There is nothing to modify, so we consider this an invalid operation because of the unnecessary unsafe call. @@ -22,7 +19,6 @@ pub enum Error { NoInput, } - impl From for Error { fn from(e: io::Error) -> Self { Self::Io(e) @@ -43,8 +39,11 @@ impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Self::Io(e) => Some(e), - Self::EnvVar(e) => Some(e), - Self::InvalidOp | Self::LineParse(_, _) | Self::NoInput => None, + Self::LineParse(_, _) + | Self::NotPresent(_) + | Self::NotUnicode(_) + | Self::InvalidOp + | Self::NoInput => None, } } } @@ -53,11 +52,14 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Io(e) => e.fmt(f), - Self::EnvVar(e) => e.fmt(f), Self::LineParse(line, index) => write!( f, "Error parsing line: '{line}', error at line index: {index}", ), + Self::NotPresent(s) => write!(f, "environment variable not found: {s}"), + Self::NotUnicode(s) => { + write!(f, "environment variable was not valid unicode: {s:?}",) + } Self::InvalidOp => write!(f, "Modify is not permitted with `EnvSequence::EnvOnly`"), Self::NoInput => write!(f, "No input provided"), } @@ -67,7 +69,7 @@ impl fmt::Display for Error { #[cfg(test)] mod test { use super::Error; - use std::{env, error::Error as StdError, io}; + use std::{error::Error as StdError, io}; #[test] fn test_io_error_source() { @@ -76,17 +78,6 @@ mod test { assert_eq!(io::ErrorKind::PermissionDenied, io_err.kind()); } - #[test] - fn test_envvar_error_source() { - let err = Error::EnvVar(env::VarError::NotPresent); - let var_err = err - .source() - .unwrap() - .downcast_ref::() - .unwrap(); - assert_eq!(&env::VarError::NotPresent, var_err); - } - #[test] fn test_line_parse_error_source() { let e = Error::LineParse("test line".to_string(), 2); @@ -112,13 +103,6 @@ mod test { assert_eq!(err.to_string(), io_err.to_string()); } - #[test] - fn test_envvar_error_display() { - let err = Error::EnvVar(env::VarError::NotPresent); - let var_err = env::VarError::NotPresent; - assert_eq!(err.to_string(), var_err.to_string()); - } - #[test] fn test_lineparse_error_display() { let err = Error::LineParse("test line".to_owned(), 2); From b585c416aa501b81f05e33c5f700809f785eac8d Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:57:05 -0400 Subject: [PATCH 05/39] Add `dotenvy::var` with new error type This reintroduces `var` as a copy of `std::env::var` with more info. The signature is different because in this crate, we only handle valid UTF-8. Calling `var` now gives the key name in the error. --- CHANGELOG.md | 1 + dotenvy/src/lib.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb142f7c..5e1e9eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - update MSRV to 1.72.0 +- **breaking**: `dotenvy::var` no longer calls `load` internally - **breaking**: `dotenvy::Result` is now private - **breaking**: deprecate `dotenvy::var`, `dotenvy::from_filename*` - `Error` is now `From println!("{key}: {val:?}"), +/// Err(e) => println!("couldn't interpret {key}: {e}"), +/// } +/// ``` +pub fn var(key: &str) -> Result { + env::var(key).map_err(|e| match e { + VarError::NotPresent => Error::NotPresent(key.to_owned()), + VarError::NotUnicode(s) => Error::NotUnicode(s), + }) +} + /// The sequence in which to load environment variables. /// /// Values in the latter override values in the former. From 13231f6e9ddb2528b8f28bc218bf700fa0ab7134 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:18:23 -0400 Subject: [PATCH 06/39] Make a `EnvMap` a newtype with `EnvMap::var` This change `EnvMap` to a newtype with a `var` function that returns `Result`. This mirrors the behaviour of `dotenvy::var`. --- dotenvy/src/iter.rs | 2 +- dotenvy/src/lib.rs | 49 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/dotenvy/src/iter.rs b/dotenvy/src/iter.rs index 8e2cad3e..051e8567 100644 --- a/dotenvy/src/iter.rs +++ b/dotenvy/src/iter.rs @@ -27,7 +27,7 @@ impl Iter { F: FnMut(String, String, &mut EnvMap), { self.remove_bom()?; - let mut map = HashMap::new(); + let mut map = EnvMap::new(); for item in self { let (k, v) = item?; load_fn(k, v, &mut map); diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 8d87c3ab..7489a471 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -16,6 +16,7 @@ use std::{ env::{self, VarError}, fs::File, io::{BufReader, Read}, + ops::{Deref, DerefMut}, path::{Path, PathBuf}, }; @@ -23,10 +24,52 @@ mod err; mod iter; mod parse; -/// The map that stores the environment. +/// A map of environment variables. /// -/// For internal use only. -pub type EnvMap = HashMap; +/// This is a newtype around `HashMap` with one additional function, `var`. +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub struct EnvMap(HashMap); + +impl Deref for EnvMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EnvMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromIterator<(String, String)> for EnvMap { + fn from_iter>(iter: I) -> Self { + Self(HashMap::from_iter(iter)) + } +} + +impl IntoIterator for EnvMap { + type Item = (String, String); + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl EnvMap { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn var(&self, key: &str) -> Result { + self.get(key) + .cloned() + .ok_or_else(|| Error::NotPresent(key.to_owned())) + } +} pub use crate::err::{Error, Result}; From 2fac95568abacbd5d1ef6e9b8385b2d189b3c9e0 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:21:50 -0400 Subject: [PATCH 07/39] Add version display for CLI --- dotenvy/src/bin/dotenvy.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index f92ea9fe..e3dfa5cc 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -35,6 +35,7 @@ fn mk_cmd(program: &str, args: &[String]) -> process::Command { #[derive(Parser)] #[command( name = "dotenvy", + version, about = "Run a command using an environment loaded from a .env file", arg_required_else_help = true, allow_external_subcommands = true From 6334ceb5ae9b861ac2f72a8863ef9ce5bf09e962 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:50:56 -0400 Subject: [PATCH 08/39] Add required and override to CLI This makes the CLI feature aligned with the attribute macro. The optional example is also updated to be similar to the CLI. `Path::exists` is no longer used because of a potential race condition. --- dotenvy/Cargo.toml | 2 +- dotenvy/src/bin/dotenvy.rs | 35 +++++++++++++++++++++++++++-------- examples/optional/src/main.rs | 24 ++++++++++++++++-------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/dotenvy/Cargo.toml b/dotenvy/Cargo.toml index bd141b2b..2b80b69f 100644 --- a/dotenvy/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -37,4 +37,4 @@ tempfile = "3.12.0" [features] default = ["cli", "macros"] cli = ["dep:clap"] -macros = ["dep:dotenvy-macros"] \ No newline at end of file +macros = ["dep:dotenvy-macros"] diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index e3dfa5cc..8c0a68ee 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -1,4 +1,4 @@ -//! A CLI tool that loads a *.env* file before running a command. +//! A CLI tool that loads an env file before running a command. //! //! # Example //! @@ -10,8 +10,8 @@ //! //! will output `bar`. use clap::{Parser, Subcommand}; -use dotenvy::EnvLoader; -use std::{error, os::unix::process::CommandExt, path::PathBuf, process}; +use dotenvy::{EnvLoader, EnvSequence}; +use std::{error, fs::File, io::ErrorKind, os::unix::process::CommandExt, path::PathBuf, process}; macro_rules! die { ($fmt:expr) => ({ @@ -45,6 +45,10 @@ struct Cli { file: PathBuf, #[clap(subcommand)] subcmd: Subcmd, + #[arg(long, default_value_t = true)] + required: bool, + #[arg(long, default_value_t = false)] + r#override: bool, } #[derive(Subcommand)] @@ -56,11 +60,26 @@ enum Subcmd { fn main() -> Result<(), Box> { let cli = Cli::parse(); - // load the file - let loader = EnvLoader::with_path(&cli.file); - if let Err(e) = unsafe { loader.load_and_modify() } { - die!("Failed to load {path}: {e}", path = cli.file.display()); - } + match File::open(&cli.file) { + Ok(file) => { + let seq = if cli.r#override { + EnvSequence::EnvThenInput + } else { + EnvSequence::InputThenEnv + }; + + // load the file + let loader = EnvLoader::with_reader(file).sequence(seq); + if let Err(e) = unsafe { loader.load_and_modify() } { + die!("Failed to load {path}: {e}", path = cli.file.display()); + } + } + Err(e) => { + if cli.required && e.kind() == ErrorKind::NotFound { + die!("Failed to load {path}: {e}", path = cli.file.display()); + } + } + }; // prepare the command let Subcmd::External(args) = cli.subcmd; diff --git a/examples/optional/src/main.rs b/examples/optional/src/main.rs index 54ba8aa3..3e55cd8a 100644 --- a/examples/optional/src/main.rs +++ b/examples/optional/src/main.rs @@ -1,17 +1,25 @@ //! This example loads an env file only if the file exists. - +//! +//! `HOST=abc cargo run` use dotenvy::{EnvLoader, EnvSequence}; -use std::{error, path::Path}; +use std::{error, fs::File, path::Path}; fn main() -> Result<(), Box> { - let path = Path::new("../.env"); - let seq = if path.exists() { - EnvSequence::InputThenEnv - } else { - EnvSequence::EnvOnly + let path = Path::new("non-existent-env"); + + // rather than checking with `Path::exists` and then opening the file handle, we call `File::open` directly to avoid a race condition where the file is inaccessible between the exist check and open + let loader = match File::open(path) { + Ok(file) => EnvLoader::with_reader(file).sequence(EnvSequence::InputThenEnv), + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + EnvLoader::default().sequence(EnvSequence::EnvOnly) + } else { + return Err(e.into()); + } + } }; - let env_map = EnvLoader::with_path(path).sequence(seq).load()?; + let env_map = loader.load()?; if let Some(v) = env_map.get("HOST") { println!("Host: {v}"); From 9ad10450ffc186a998c8b309b10cfb89336d7619 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:57:04 -0400 Subject: [PATCH 09/39] Add variable name to Unicode error Error messages are also adjusted. --- dotenvy/src/err.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index 822657c1..5edded50 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -7,8 +7,10 @@ pub enum Error { LineParse(String, usize), /// An IO error may be encountered when reading from a file or reader. Io(io::Error), + /// The variable was not found in the environment. The `String` is the name of the variable. NotPresent(String), - NotUnicode(OsString), + /// The variable was not valid unicode. The `String` is the name of the variable. + NotUnicode(OsString, String), /// When `load_and_modify` is called with `EnvSequence::EnvOnly` /// /// There is nothing to modify, so we consider this an invalid operation because of the unnecessary unsafe call. @@ -41,7 +43,7 @@ impl error::Error for Error { Self::Io(e) => Some(e), Self::LineParse(_, _) | Self::NotPresent(_) - | Self::NotUnicode(_) + | Self::NotUnicode(_, _) | Self::InvalidOp | Self::NoInput => None, } @@ -54,14 +56,14 @@ impl fmt::Display for Error { Self::Io(e) => e.fmt(f), Self::LineParse(line, index) => write!( f, - "Error parsing line: '{line}', error at line index: {index}", + "error parsing line: '{line}', error at line index: {index}", ), - Self::NotPresent(s) => write!(f, "environment variable not found: {s}"), - Self::NotUnicode(s) => { - write!(f, "environment variable was not valid unicode: {s:?}",) + Self::NotPresent(s) => write!(f, "{s} is not set"), + Self::NotUnicode(os_str, s) => { + write!(f, "{s} is not valid Unicode: {os_str:?}",) } - Self::InvalidOp => write!(f, "Modify is not permitted with `EnvSequence::EnvOnly`"), - Self::NoInput => write!(f, "No input provided"), + Self::InvalidOp => write!(f, "modify is not permitted with `EnvSequence::EnvOnly`"), + Self::NoInput => write!(f, "no input provided"), } } } From c4263c84687f6e7461ddbcce92e6b10f4889af63 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:59:45 -0400 Subject: [PATCH 10/39] Fix error usage --- dotenvy/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 7489a471..6a090db1 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -103,7 +103,7 @@ pub use dotenvy_macros::*; pub fn var(key: &str) -> Result { env::var(key).map_err(|e| match e { VarError::NotPresent => Error::NotPresent(key.to_owned()), - VarError::NotUnicode(s) => Error::NotUnicode(s), + VarError::NotUnicode(os_str) => Error::NotUnicode(os_str, key.to_owned()), }) } From 81dd772bb4ec6ae542c1aaaeac33357c02d68a13 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Fri, 6 Sep 2024 11:33:20 -0400 Subject: [PATCH 11/39] cargo fmt --- examples/optional/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/optional/src/main.rs b/examples/optional/src/main.rs index 3e55cd8a..f0f6a8c4 100644 --- a/examples/optional/src/main.rs +++ b/examples/optional/src/main.rs @@ -1,5 +1,5 @@ //! This example loads an env file only if the file exists. -//! +//! //! `HOST=abc cargo run` use dotenvy::{EnvLoader, EnvSequence}; use std::{error, fs::File, path::Path}; From cb5be8d98d22c7594529cacb38fa8b350c5d1138 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:39:33 -0400 Subject: [PATCH 12/39] Add file path to IO error variant This creates an internal `ParseBufError` to handle line parsing errors without the path context. Loading a file reports the file path error if an attempt was made to read from it and it is not found. --- CHANGELOG.md | 2 +- dotenvy/src/err.rs | 45 ++++++++++++++++++++++++++-------------- dotenvy/src/iter.rs | 49 +++++++++++++++++++++++++++----------------- dotenvy/src/lib.rs | 31 ++++++++++++++++------------ dotenvy/src/parse.rs | 41 +++++++++++++++++++----------------- 5 files changed, 101 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1e9eae..ad344987 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - update to 2021 edition - update MSRV to 1.72.0 - +- **breaking**: `dotenvy::Error` now includes IO file path info and variable name info - **breaking**: `dotenvy::var` no longer calls `load` internally - **breaking**: `dotenvy::Result` is now private - **breaking**: deprecate `dotenvy::var`, `dotenvy::from_filename*` diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index 5edded50..cbc6cd6c 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -1,12 +1,12 @@ -use std::{error, ffi::OsString, fmt, io, result}; +use std::{error, ffi::OsString, fmt, io, path::PathBuf}; -pub type Result = result::Result; +use crate::iter::ParseBufError; #[derive(Debug)] pub enum Error { LineParse(String, usize), /// An IO error may be encountered when reading from a file or reader. - Io(io::Error), + Io(io::Error, Option), /// The variable was not found in the environment. The `String` is the name of the variable. NotPresent(String), /// The variable was not valid unicode. The `String` is the name of the variable. @@ -21,15 +21,10 @@ pub enum Error { NoInput, } -impl From for Error { - fn from(e: io::Error) -> Self { - Self::Io(e) - } -} impl Error { #[must_use] pub fn not_found(&self) -> bool { - if let Self::Io(e) = self { + if let Self::Io(e, _) = self { e.kind() == io::ErrorKind::NotFound } else { false @@ -40,7 +35,7 @@ impl Error { impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { - Self::Io(e) => Some(e), + Self::Io(e, _) => Some(e), Self::LineParse(_, _) | Self::NotPresent(_) | Self::NotUnicode(_, _) @@ -53,7 +48,13 @@ impl error::Error for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::Io(e) => e.fmt(f), + Self::Io(e, path) => { + if let Some(path) = path { + write!(f, "error reading '{}':, {e}", path.to_string_lossy()) + } else { + e.fmt(f) + } + } Self::LineParse(line, index) => write!( f, "error parsing line: '{line}', error at line index: {index}", @@ -68,6 +69,20 @@ impl fmt::Display for Error { } } +impl From<(io::Error, PathBuf)> for Error { + fn from((e, path): (io::Error, PathBuf)) -> Self { + Self::Io(e, Some(path)) + } +} +impl From<(ParseBufError, Option)> for Error { + fn from((e, path): (ParseBufError, Option)) -> Self { + match e { + ParseBufError::LineParse(line, index) => Self::LineParse(line, index), + ParseBufError::Io(e) => Self::Io(e, path), + } + } +} + #[cfg(test)] mod test { use super::Error; @@ -75,7 +90,7 @@ mod test { #[test] fn test_io_error_source() { - let err = Error::Io(io::ErrorKind::PermissionDenied.into()); + let err = Error::Io(io::ErrorKind::PermissionDenied.into(), None); let io_err = err.source().unwrap().downcast_ref::().unwrap(); assert_eq!(io::ErrorKind::PermissionDenied, io_err.kind()); } @@ -88,19 +103,19 @@ mod test { #[test] fn test_error_not_found_true() { - let e = Error::Io(io::ErrorKind::NotFound.into()); + let e = Error::Io(io::ErrorKind::NotFound.into(), None); assert!(e.not_found()); } #[test] fn test_error_not_found_false() { - let e = Error::Io(io::ErrorKind::PermissionDenied.into()); + let e = Error::Io(io::ErrorKind::PermissionDenied.into(), None); assert!(!e.not_found()); } #[test] fn test_io_error_display() { - let err = Error::Io(io::ErrorKind::PermissionDenied.into()); + let err = Error::Io(io::ErrorKind::PermissionDenied.into(), None); let io_err: io::Error = io::ErrorKind::PermissionDenied.into(); assert_eq!(err.to_string(), io_err.to_string()); } diff --git a/dotenvy/src/iter.rs b/dotenvy/src/iter.rs index 051e8567..922ddb8b 100644 --- a/dotenvy/src/iter.rs +++ b/dotenvy/src/iter.rs @@ -1,12 +1,8 @@ -use crate::{ - err::{Error, Result}, - parse, EnvMap, -}; +use crate::{parse, EnvMap}; use std::{ collections::HashMap, env::{self}, io::{self, BufRead}, - result::Result as StdResult, }; pub struct Iter { @@ -22,7 +18,7 @@ impl Iter { } } - fn internal_load(mut self, mut load_fn: F) -> Result + fn internal_load(mut self, mut load_fn: F) -> Result where F: FnMut(String, String, &mut EnvMap), { @@ -35,13 +31,13 @@ impl Iter { Ok(map) } - pub fn load(self) -> Result { - self.internal_load(|k, v, map| { + pub fn load(self) -> Result { + self.internal_load(|k, v: String, map| { map.insert(k, v); }) } - pub unsafe fn load_and_modify(self) -> Result { + pub unsafe fn load_and_modify(self) -> Result { self.internal_load(|k, v, map| { if env::var(&k).is_err() { unsafe { env::set_var(&k, &v) }; @@ -50,17 +46,17 @@ impl Iter { }) } - pub unsafe fn load_and_modify_override(self) -> Result { + pub unsafe fn load_and_modify_override(self) -> Result { self.internal_load(|k, v, map| { unsafe { env::set_var(&k, &v) }; map.insert(k, v); }) } - /// Removes the BOM from the reader if it exists. + /// Removes the BOM if it exists. /// - /// For more details, see the [Unicode BOM character](https://www.compart.com/en/unicode/U+FEFF). - fn remove_bom(&mut self) -> StdResult<(), io::Error> { + /// For more info, see the [Unicode BOM character](https://www.compart.com/en/unicode/U+FEFF). + fn remove_bom(&mut self) -> Result<(), io::Error> { let buf = self.lines.0.fill_buf()?; if buf.starts_with(&[0xEF, 0xBB, 0xBF]) { @@ -127,14 +123,14 @@ impl ParseState { } impl Iterator for Lines { - type Item = Result; + type Item = Result; - fn next(&mut self) -> Option> { + fn next(&mut self) -> Option { let mut buf = String::new(); let mut cur_state = ParseState::Complete; let mut buf_pos; let mut cur_pos; - loop { + loop { buf_pos = buf.len(); match self.0.read_line(&mut buf) { Ok(0) => { @@ -142,7 +138,7 @@ impl Iterator for Lines { return None; } let len = buf.len(); - return Some(Err(Error::LineParse(buf, len))); + return Some(Err(ParseBufError::LineParse(buf, len))); } Ok(_n) => { // Skip lines which start with a `#` before iteration @@ -176,14 +172,14 @@ impl Iterator for Lines { } } } - Err(e) => return Some(Err(Error::Io(e))), + Err(e) => return Some(Err(ParseBufError::Io(e))), } } } } impl Iterator for Iter { - type Item = Result<(String, String)>; + type Item = Result<(String, String), ParseBufError>; fn next(&mut self) -> Option { loop { @@ -201,3 +197,18 @@ impl Iterator for Iter { } } } + +/// An internal error type +/// +/// This is necessary so we can handle IO errors without knowing the path. +#[derive(Debug)] +pub enum ParseBufError { + LineParse(String, usize), + Io(io::Error), +} + +impl From for ParseBufError { + fn from(e: io::Error) -> Self { + Self::Io(e) + } +} diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 6a090db1..4e0258f1 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -60,18 +60,19 @@ impl IntoIterator for EnvMap { } impl EnvMap { + #[must_use] pub fn new() -> Self { Self(HashMap::new()) } - pub fn var(&self, key: &str) -> Result { + pub fn var(&self, key: &str) -> Result { self.get(key) .cloned() .ok_or_else(|| Error::NotPresent(key.to_owned())) } } -pub use crate::err::{Error, Result}; +pub use crate::err::Error; #[cfg(feature = "macros")] pub use dotenvy_macros::*; @@ -100,7 +101,7 @@ pub use dotenvy_macros::*; /// Err(e) => println!("couldn't interpret {key}: {e}"), /// } /// ``` -pub fn var(key: &str) -> Result { +pub fn var(key: &str) -> Result { env::var(key).map_err(|e| match e { VarError::NotPresent => Error::NotPresent(key.to_owned()), VarError::NotUnicode(os_str) => Error::NotUnicode(os_str, key.to_owned()), @@ -164,9 +165,10 @@ impl<'a> EnvLoader<'a> { self } - fn buf(self) -> Result>> { + fn buf(self) -> Result>, crate::Error> { let rdr = if let Some(path) = self.path { - Box::new(File::open(path)?) + let file = File::open(&path).map_err(|io_err| crate::Error::from((io_err, path)))?; + Box::new(file) } else if let Some(rdr) = self.reader { rdr } else { @@ -176,25 +178,28 @@ impl<'a> EnvLoader<'a> { Ok(BufReader::new(rdr)) } - fn load_input(self) -> Result { + fn load_input(self) -> Result { + let path = self.path.clone(); let iter = Iter::new(self.buf()?); - iter.load() + iter.load().map_err(|e| ((e, path).into())) } - unsafe fn load_input_and_modify(self) -> Result { + unsafe fn load_input_and_modify(self) -> Result { + let path = self.path.clone(); let iter = Iter::new(self.buf()?); - unsafe { iter.load_and_modify() } + unsafe { iter.load_and_modify() }.map_err(|e| ((e, path).into())) } - unsafe fn load_input_and_modify_override(self) -> Result { + unsafe fn load_input_and_modify_override(self) -> Result { + let path = self.path.clone(); let iter = Iter::new(self.buf()?); - unsafe { iter.load_and_modify_override() } + unsafe { iter.load_and_modify_override() }.map_err(|e| ((e, path).into())) } /// Loads environment variables into a hash map. /// /// This is the primary method for loading environment variables. - pub fn load(self) -> Result { + pub fn load(self) -> Result { match self.sequence { EnvSequence::EnvOnly => Ok(env::vars().collect()), EnvSequence::EnvThenInput => { @@ -215,7 +220,7 @@ impl<'a> EnvLoader<'a> { /// Loads environment variables into a hash map, modifying the existing environment. /// /// This calls `std::env::set_var` internally and is not thread-safe. - pub unsafe fn load_and_modify(self) -> Result { + pub unsafe fn load_and_modify(self) -> Result { match self.sequence { // nothing to modify EnvSequence::EnvOnly => Err(Error::InvalidOp), diff --git a/dotenvy/src/parse.rs b/dotenvy/src/parse.rs index 970866fb..043fb1f8 100644 --- a/dotenvy/src/parse.rs +++ b/dotenvy/src/parse.rs @@ -1,12 +1,13 @@ #![allow(clippy::module_name_repetitions)] -use crate::{Error, Result}; use std::{collections::HashMap, env}; +use crate::iter::ParseBufError; + pub fn parse_line( line: &str, substitution_data: &mut HashMap>, -) -> Result> { +) -> Result, ParseBufError> { let mut parser = LineParser::new(line, substitution_data); parser.parse_line() } @@ -28,11 +29,11 @@ impl<'a> LineParser<'a> { } } - fn err(&self) -> Error { - Error::LineParse(self.original_line.into(), self.pos) + fn err(&self) -> ParseBufError { + ParseBufError::LineParse(self.original_line.into(), self.pos) } - fn parse_line(&mut self) -> Result> { + fn parse_line(&mut self) -> Result, ParseBufError> { self.skip_whitespace(); // if its an empty line or a comment, skip it if self.line.is_empty() || self.line.starts_with('#') { @@ -67,7 +68,7 @@ impl<'a> LineParser<'a> { Ok(Some((key, parsed_value))) } - fn parse_key(&mut self) -> Result { + fn parse_key(&mut self) -> Result { if !self .line .starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') @@ -87,7 +88,7 @@ impl<'a> LineParser<'a> { Ok(key) } - fn expect_equal(&mut self) -> Result<()> { + fn expect_equal(&mut self) -> Result<(), ParseBufError> { if !self.line.starts_with('=') { return Err(self.err()); } @@ -114,7 +115,10 @@ enum SubstitutionMode { EscapedBlock, } -fn parse_value(input: &str, substitution_data: &HashMap>) -> Result { +fn parse_value( + input: &str, + substitution_data: &HashMap>, +) -> Result { let mut strong_quote = false; // ' let mut weak_quote = false; // " let mut escaped = false; @@ -137,7 +141,7 @@ fn parse_value(input: &str, substitution_data: &HashMap>) } else if c == '#' { break; } - return Err(Error::LineParse(input.to_owned(), index)); + return Err(ParseBufError::LineParse(input.to_owned(), index)); } else if escaped { //TODO I tried handling literal \r but various issues //imo not worth worrying about until there's a use case @@ -147,7 +151,7 @@ fn parse_value(input: &str, substitution_data: &HashMap>) '\\' | '\'' | '"' | '$' | ' ' => output.push(c), 'n' => output.push('\n'), // handle \n case _ => { - return Err(Error::LineParse(input.to_owned(), index)); + return Err(ParseBufError::LineParse(input.to_owned(), index)); } } @@ -229,7 +233,7 @@ fn parse_value(input: &str, substitution_data: &HashMap>) //XXX also fail if escaped? or... if substitution_mode == SubstitutionMode::EscapedBlock || strong_quote || weak_quote { let value_length = input.len(); - Err(Error::LineParse( + Err(ParseBufError::LineParse( input.to_owned(), if value_length == 0 { 0 @@ -265,7 +269,7 @@ fn apply_substitution( #[cfg(test)] mod test { - use crate::{iter::Iter, Result}; + use crate::iter::{Iter, ParseBufError}; #[test] fn test_parse_line_env() { @@ -322,7 +326,7 @@ export SHELL_LOVER=1 let input = br" # foo=bar # "; - let result: Result> = Iter::new(&input[..]).collect(); + let result: Result, ParseBufError> = Iter::new(&input[..]).collect(); assert!(result.unwrap().is_empty()); } @@ -555,8 +559,7 @@ mod variable_substitution_tests { #[cfg(test)] mod error_tests { - use crate::err::Error::LineParse; - use crate::iter::Iter; + use crate::iter::{Iter, ParseBufError}; #[test] fn should_not_parse_unfinished_substitutions() { @@ -581,7 +584,7 @@ mod error_tests { panic!("Expected the first value to be parsed"); } - if let Err(LineParse(second_value, index)) = &parsed_values[1] { + if let Err(ParseBufError::LineParse(second_value, index)) = &parsed_values[1] { assert_eq!(second_value, wrong_value); assert_eq!(*index, wrong_value.len() - 1); } else { @@ -597,7 +600,7 @@ mod error_tests { assert_eq!(parsed_values.len(), 1); - if let Err(LineParse(second_value, index)) = &parsed_values[0] { + if let Err(ParseBufError::LineParse(second_value, index)) = &parsed_values[0] { assert_eq!(second_value, wrong_key_value); assert_eq!(*index, 0); } else { @@ -612,7 +615,7 @@ mod error_tests { assert_eq!(parsed_values.len(), 1); - if let Err(LineParse(wrong_value, index)) = &parsed_values[0] { + if let Err(ParseBufError::LineParse(wrong_value, index)) = &parsed_values[0] { assert_eq!(wrong_value, wrong_format); assert_eq!(*index, 0); } else { @@ -627,7 +630,7 @@ mod error_tests { assert_eq!(parsed_values.len(), 1); - if let Err(LineParse(wrong_value, index)) = &parsed_values[0] { + if let Err(ParseBufError::LineParse(wrong_value, index)) = &parsed_values[0] { assert_eq!(wrong_value, wrong_escape); assert_eq!(*index, wrong_escape.find('\\').unwrap() + 1); } else { From 312fac600dc5cd8d298887c5d92654537bda3466 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:48:36 -0400 Subject: [PATCH 13/39] Fix lowercase error message test --- dotenvy/src/err.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index cbc6cd6c..4612a1fe 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -124,7 +124,7 @@ mod test { fn test_lineparse_error_display() { let err = Error::LineParse("test line".to_owned(), 2); assert_eq!( - "Error parsing line: 'test line', error at line index: 2", + "error parsing line: 'test line', error at line index: 2", err.to_string() ); } From 9b3e9ad7f7f842bf8694c2a16f95f36cc9853fa5 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 13:57:11 -0400 Subject: [PATCH 14/39] Add `path` setter for reader error context `EnvLoader::path` is a setter that can be used to specify the path. If `with_reader` is used, the path can be specified for error context. If both reader and path are present, load will use the reader as input. --- dotenvy/src/bin/dotenvy.rs | 2 +- dotenvy/src/lib.rs | 24 +++++++++++++++++------- examples/optional/src/main.rs | 11 +++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index 8c0a68ee..a4ba9c3a 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -69,7 +69,7 @@ fn main() -> Result<(), Box> { }; // load the file - let loader = EnvLoader::with_reader(file).sequence(seq); + let loader = EnvLoader::with_reader(file).path(&cli.file).sequence(seq); if let Err(e) = unsafe { loader.load_and_modify() } { die!("Failed to load {path}: {e}", path = cli.file.display()); } diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 4e0258f1..c251923b 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -133,12 +133,12 @@ pub struct EnvLoader<'a> { impl<'a> EnvLoader<'a> { #[must_use] - /// Creates a new `EnvLoader` with the path set to `env` in the current directory. + /// Creates a new `EnvLoader` with the path set to `./env` pub fn new() -> Self { Self::with_path(".env") } - /// Creates a new `EnvLoader` with the specified path. + /// Creates a new `EnvLoader` with the path as input. /// /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. pub fn with_path>(path: P) -> Self { @@ -148,7 +148,7 @@ impl<'a> EnvLoader<'a> { } } - /// Creates a new `EnvLoader` with the specified reader. + /// Creates a new `EnvLoader` with the reader as input. /// /// This operation is infallible. IO is deferred until `load` or `load_and_modify` is called. pub fn with_reader(rdr: R) -> Self { @@ -158,6 +158,16 @@ impl<'a> EnvLoader<'a> { } } + /// Sets the path to the specified path. + /// + /// This is useful when constructing with a reader, but still desiring a path to be used in the error message context. + /// + /// If a reader exists and a path is specified, loading will be done using the reader. + pub fn path>(mut self, path: P) -> Self { + self.path = Some(path.as_ref().to_owned()); + self + } + /// Sets the sequence in which to load environment variables. #[must_use] pub const fn sequence(mut self, sequence: EnvSequence) -> Self { @@ -166,13 +176,13 @@ impl<'a> EnvLoader<'a> { } fn buf(self) -> Result>, crate::Error> { - let rdr = if let Some(path) = self.path { + let rdr = if let Some(rdr) = self.reader { + rdr + } else if let Some(path) = self.path { let file = File::open(&path).map_err(|io_err| crate::Error::from((io_err, path)))?; Box::new(file) - } else if let Some(rdr) = self.reader { - rdr } else { - // only `EnvLoader::default` would have no path or reader + // only `EnvLoader::default` would have no reader or path return Err(Error::NoInput); }; Ok(BufReader::new(rdr)) diff --git a/examples/optional/src/main.rs b/examples/optional/src/main.rs index f0f6a8c4..545d2877 100644 --- a/examples/optional/src/main.rs +++ b/examples/optional/src/main.rs @@ -2,16 +2,19 @@ //! //! `HOST=abc cargo run` use dotenvy::{EnvLoader, EnvSequence}; -use std::{error, fs::File, path::Path}; +use std::{error, fs::File, io, path::Path}; fn main() -> Result<(), Box> { let path = Path::new("non-existent-env"); - // rather than checking with `Path::exists` and then opening the file handle, we call `File::open` directly to avoid a race condition where the file is inaccessible between the exist check and open + // Rather than checking with `Path::exists` and then opening the file handle, we call `File::open` directly to avoid a race condition where the file is inaccessible between the exist check and open + // Even though we pass the file handle as input, we specify the path so it can be used as context in the error message let loader = match File::open(path) { - Ok(file) => EnvLoader::with_reader(file).sequence(EnvSequence::InputThenEnv), + Ok(file) => EnvLoader::with_reader(file) + .path(path) + .sequence(EnvSequence::InputThenEnv), Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { + if e.kind() == io::ErrorKind::NotFound { EnvLoader::default().sequence(EnvSequence::EnvOnly) } else { return Err(e.into()); From d7d0128826bf0e57a6e492963b97dbb55c804341 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:38:35 -0400 Subject: [PATCH 15/39] Test BOM removal at lower level --- dotenvy/src/iter.rs | 33 ++++++++++++++++++++++++++++++-- dotenvy/tests/test-ignore-bom.rs | 24 ----------------------- 2 files changed, 31 insertions(+), 26 deletions(-) delete mode 100644 dotenvy/tests/test-ignore-bom.rs diff --git a/dotenvy/src/iter.rs b/dotenvy/src/iter.rs index 922ddb8b..189a5315 100644 --- a/dotenvy/src/iter.rs +++ b/dotenvy/src/iter.rs @@ -56,7 +56,7 @@ impl Iter { /// Removes the BOM if it exists. /// /// For more info, see the [Unicode BOM character](https://www.compart.com/en/unicode/U+FEFF). - fn remove_bom(&mut self) -> Result<(), io::Error> { + fn remove_bom(&mut self) -> io::Result<()> { let buf = self.lines.0.fill_buf()?; if buf.starts_with(&[0xEF, 0xBB, 0xBF]) { @@ -130,7 +130,7 @@ impl Iterator for Lines { let mut cur_state = ParseState::Complete; let mut buf_pos; let mut cur_pos; - loop { + loop { buf_pos = buf.len(); match self.0.read_line(&mut buf) { Ok(0) => { @@ -212,3 +212,32 @@ impl From for ParseBufError { Self::Io(e) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::BufReader; + use std::io::Cursor; + + #[test] + fn test_remove_bom() { + // BOM present + let b = b"\xEF\xBB\xBFkey=value\n"; + let rdr = BufReader::new(Cursor::new(b)); + let mut iter = Iter::new(rdr); + iter.remove_bom().unwrap(); + let first_line = iter.lines.next().unwrap().unwrap(); + assert_eq!(first_line, "key=value"); + } + + #[test] + fn test_remove_bom_no_bom() { + // no BOM + let b = b"key=value\n"; + let reader = BufReader::new(Cursor::new(b)); + let mut iter = Iter::new(reader); + iter.remove_bom().unwrap(); + let first_line = iter.lines.next().unwrap().unwrap(); + assert_eq!(first_line, "key=value"); + } +} diff --git a/dotenvy/tests/test-ignore-bom.rs b/dotenvy/tests/test-ignore-bom.rs deleted file mode 100644 index b16ff7bf..00000000 --- a/dotenvy/tests/test-ignore-bom.rs +++ /dev/null @@ -1,24 +0,0 @@ -mod common; - -use dotenvy::EnvLoader; - -use crate::common::tempdir_with_dotenv; -use std::{env, error}; - -#[test] -fn test_ignore_bom() -> Result<(), Box> { - let txt = format!("\u{feff}TESTKEY=test_val"); - let dir = unsafe { tempdir_with_dotenv(&txt) }?; - - let mut path = env::current_dir()?; - path.push(".env"); - - let config = EnvLoader::with_path(path); - unsafe { config.load_and_modify() }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} From c788494344aa43d000c357b803eeb4a78718d09e Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:48:25 -0400 Subject: [PATCH 16/39] Remove IO from load substitution test --- dotenvy/src/lib.rs | 67 +++++++++++++++++++- dotenvy/tests/test-variable-substitution.rs | 70 --------------------- 2 files changed, 66 insertions(+), 71 deletions(-) delete mode 100644 dotenvy/tests/test-variable-substitution.rs diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index c251923b..7a9a3b98 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -133,7 +133,7 @@ pub struct EnvLoader<'a> { impl<'a> EnvLoader<'a> { #[must_use] - /// Creates a new `EnvLoader` with the path set to `./env` + /// Creates a new `EnvLoader` with the path set to `./env` in the current directory. pub fn new() -> Self { Self::with_path(".env") } @@ -260,3 +260,68 @@ impl<'a> EnvLoader<'a> { } } } + +#[cfg(test)] +mod tests { + use std::{env, io::Cursor}; + + use crate::EnvLoader; + + #[test] + fn test_substitution() -> Result<(), crate::Error> { + unsafe { + env::set_var("KEY", "value"); + env::set_var("KEY1", "value1"); + } + + let subs = [ + "$ZZZ", "$KEY", "$KEY1", "${KEY}1", "$KEY_U", "${KEY_U}", "\\$KEY", + ]; + + let common_string = subs.join(">>"); + let s = format!( + r#" + KEY1=new_value1 + KEY_U=$KEY+valueU + + STRONG_QUOTES='{common_string}' + WEAK_QUOTES="{common_string}" + NO_QUOTES={common_string} + "#, + ); + let cursor = Cursor::new(s); + let env_map = EnvLoader::with_reader(cursor).load()?; + + assert_eq!(env_map.var("KEY")?, "value"); + assert_eq!(env_map.var("KEY1")?, "value1"); + assert_eq!(env_map.var("KEY_U")?, "value+valueU"); + assert_eq!(env_map.var("STRONG_QUOTES")?, common_string); + assert_eq!( + env_map.var("WEAK_QUOTES")?, + [ + "", + "value", + "value1", + "value1", + "value_U", + "value+valueU", + "$KEY" + ] + .join(">>") + ); + assert_eq!( + env_map.var("NO_QUOTES")?, + [ + "", + "value", + "value1", + "value1", + "value_U", + "value+valueU", + "$KEY" + ] + .join(">>") + ); + Ok(()) + } +} diff --git a/dotenvy/tests/test-variable-substitution.rs b/dotenvy/tests/test-variable-substitution.rs deleted file mode 100644 index f5ffc39a..00000000 --- a/dotenvy/tests/test-variable-substitution.rs +++ /dev/null @@ -1,70 +0,0 @@ -#![deny(clippy::uninlined_format_args)] - -mod common; - -use dotenvy::EnvLoader; - -use crate::common::tempdir_with_dotenv; -use std::{env, error}; - -#[test] -fn test_variable_substitutions() -> Result<(), Box> { - unsafe { - env::set_var("KEY", "value"); - env::set_var("KEY1", "value1"); - } - - let substitutions_to_test = [ - "$ZZZ", "$KEY", "$KEY1", "${KEY}1", "$KEY_U", "${KEY_U}", "\\$KEY", - ]; - - let common_string = substitutions_to_test.join(">>"); - let txt = format!( - r#" -KEY1=new_value1 -KEY_U=$KEY+valueU - -SUBSTITUTION_FOR_STRONG_QUOTES='{common_string}' -SUBSTITUTION_FOR_WEAK_QUOTES="{common_string}" -SUBSTITUTION_WITHOUT_QUOTES={common_string} -"#, - ); - let dir = unsafe { tempdir_with_dotenv(&txt) }?; - - unsafe { EnvLoader::new().load_and_modify() }?; - - assert_eq!(env::var("KEY")?, "value"); - assert_eq!(env::var("KEY1")?, "value1"); - assert_eq!(env::var("KEY_U")?, "value+valueU"); - assert_eq!(env::var("SUBSTITUTION_FOR_STRONG_QUOTES")?, common_string); - assert_eq!( - env::var("SUBSTITUTION_FOR_WEAK_QUOTES")?, - [ - "", - "value", - "value1", - "value1", - "value_U", - "value+valueU", - "$KEY" - ] - .join(">>") - ); - assert_eq!( - env::var("SUBSTITUTION_WITHOUT_QUOTES")?, - [ - "", - "value", - "value1", - "value1", - "value_U", - "value+valueU", - "$KEY" - ] - .join(">>") - ); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} From fe46d1d0b66209eb69eefc64fd89f9145f0da06a Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:49:32 -0400 Subject: [PATCH 17/39] Remove default location tests Now that we only use paths and there is also reader input, we don't need to test with temp file creating and deleting. --- dotenvy/tests/test-child-dir.rs | 20 ------------------- .../tests/test-default-location-override.rs | 20 ------------------- dotenvy/tests/test-default-location.rs | 20 ------------------- 3 files changed, 60 deletions(-) delete mode 100644 dotenvy/tests/test-child-dir.rs delete mode 100644 dotenvy/tests/test-default-location-override.rs delete mode 100644 dotenvy/tests/test-default-location.rs diff --git a/dotenvy/tests/test-child-dir.rs b/dotenvy/tests/test-child-dir.rs deleted file mode 100644 index 5709f4c7..00000000 --- a/dotenvy/tests/test-child-dir.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod common; - -use dotenvy::EnvLoader; - -use crate::common::make_test_dotenv; -use std::{env, error, fs}; - -#[test] -fn test_child_dir() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - fs::create_dir("child")?; - env::set_current_dir("child")?; - - unsafe { EnvLoader::with_path("../.env").load_and_modify() }?; - assert_eq!(env::var("TESTKEY")?, "test_val"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenvy/tests/test-default-location-override.rs b/dotenvy/tests/test-default-location-override.rs deleted file mode 100644 index 3a04ea64..00000000 --- a/dotenvy/tests/test-default-location-override.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use dotenvy::{EnvLoader, EnvSequence}; -use std::{env, error}; - -#[test] -fn test_default_location_override() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - let loader = EnvLoader::new().sequence(EnvSequence::EnvThenInput); - unsafe { loader.load_and_modify() }?; - - assert_eq!(env::var("TESTKEY")?, "test_val_overridden"); - assert_eq!(env::var("EXISTING")?, "from_file"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} diff --git a/dotenvy/tests/test-default-location.rs b/dotenvy/tests/test-default-location.rs deleted file mode 100644 index 0a22721f..00000000 --- a/dotenvy/tests/test-default-location.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod common; - -use dotenvy::EnvLoader; - -use crate::common::make_test_dotenv; -use std::{env, error}; - -#[test] -fn test_default_location() -> Result<(), Box> { - let dir = unsafe { make_test_dotenv() }?; - - unsafe { EnvLoader::with_path("./.env").load_and_modify() }?; - - assert_eq!(env::var("TESTKEY")?, "test_val"); - assert_eq!(env::var("EXISTING")?, "from_env"); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} From b426cbf330f5d0bb748f29e72ef88a82186a9cf3 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:19:46 -0400 Subject: [PATCH 18/39] Remove panics from parse unit tests The tests are also rewritten for clarity. --- dotenvy/src/parse.rs | 204 ++++++++++++++++++++----------------------- 1 file changed, 94 insertions(+), 110 deletions(-) diff --git a/dotenvy/src/parse.rs b/dotenvy/src/parse.rs index 043fb1f8..15828a3a 100644 --- a/dotenvy/src/parse.rs +++ b/dotenvy/src/parse.rs @@ -272,7 +272,7 @@ mod test { use crate::iter::{Iter, ParseBufError}; #[test] - fn test_parse_line_env() { + fn test_parse_line_env() -> Result<(), ParseBufError> { // Note 5 spaces after 'KEY8=' below let actual_iter = Iter::new( r#" @@ -309,16 +309,15 @@ export SHELL_LOVER=1 ("SHELL_LOVER", "1"), ] .into_iter() - .map(|(key, value)| (key.to_string(), value.to_string())); + .map(|(key, value)| (key.to_owned(), value.to_owned())); let mut count = 0; for (expected, actual) in expected_iter.zip(actual_iter) { - assert!(actual.is_ok()); - assert_eq!(expected, actual.unwrap()); + assert_eq!(expected, actual?); count += 1; } - assert_eq!(count, 13); + Ok(()) } #[test] @@ -350,7 +349,7 @@ very bacon = yes indeed } #[test] - fn test_parse_value_escapes() { + fn test_parse_value_escapes() -> Result<(), ParseBufError> { let actual_iter = Iter::new( r#" KEY=my\ cool\ value @@ -378,9 +377,9 @@ KEY7="line 1\nline 2" .map(|(key, value)| (key.to_string(), value.to_string())); for (expected, actual) in expected_iter.zip(actual_iter) { - assert!(actual.is_ok()); - assert_eq!(expected, actual.unwrap()); + assert_eq!(expected, actual?); } + Ok(()) } #[test] @@ -402,100 +401,100 @@ KEY4=h\8u } #[cfg(test)] -mod variable_substitution_tests { - use crate::iter::Iter; +mod substitution_tests { + use crate::iter::{Iter, ParseBufError}; use std::env; - fn assert_parsed_string(input_string: &str, expected_parse_result: Vec<(&str, &str)>) { - let actual_iter = Iter::new(input_string.as_bytes()); - let expected_count = &expected_parse_result.len(); + /// Asserts the parsed string is equal to the expected string. + fn assert_string(input: &str, expected: Vec<(&str, &str)>) -> Result<(), ParseBufError> { + let actual_iter = Iter::new(input.as_bytes()); + let expected_count = expected.len(); - let expected_iter = expected_parse_result + let expected_iter = expected .into_iter() - .map(|(key, value)| (key.to_string(), value.to_string())); + .map(|(k, v)| (k.to_owned(), v.to_owned())); let mut count = 0; for (expected, actual) in expected_iter.zip(actual_iter) { - assert!(actual.is_ok()); - assert_eq!(expected, actual.unwrap()); + assert_eq!(expected, actual?); count += 1; } - - assert_eq!(count, *expected_count); + assert_eq!(count, expected_count); + Ok(()) } #[test] - fn variable_in_parenthesis_surrounded_by_quotes() { - assert_parsed_string( + fn variable_in_parenthesis_surrounded_by_quotes() -> Result<(), ParseBufError> { + assert_string( r#" KEY=test KEY1="${KEY}" "#, vec![("KEY", "test"), ("KEY1", "test")], - ); + ) } #[test] - fn substitute_undefined_variables_to_empty_string() { - assert_parsed_string(r#"KEY=">$KEY1<>${KEY2}<""#, vec![("KEY", "><><")]); + fn sub_undefined_variables_to_empty_string() -> Result<(), ParseBufError> { + assert_string(r#"KEY=">$KEY1<>${KEY2}<""#, vec![("KEY", "><><")]) } #[test] - fn do_not_substitute_variables_with_dollar_escaped() { - assert_parsed_string( + fn do_not_sub_with_dollar_escaped() -> Result<(), ParseBufError> { + assert_string( "KEY=>\\$KEY1<>\\${KEY2}<", vec![("KEY", ">$KEY1<>${KEY2}<")], - ); + ) } #[test] - fn do_not_substitute_variables_in_weak_quotes_with_dollar_escaped() { - assert_parsed_string( + fn do_not_sub_in_weak_quotes_with_dollar_escaped() -> Result<(), ParseBufError> { + assert_string( r#"KEY=">\$KEY1<>\${KEY2}<""#, vec![("KEY", ">$KEY1<>${KEY2}<")], - ); + ) } #[test] - fn do_not_substitute_variables_in_strong_quotes() { - assert_parsed_string("KEY='>${KEY1}<>$KEY2<'", vec![("KEY", ">${KEY1}<>$KEY2<")]); + fn do_not_sub_in_strong_quotes() -> Result<(), ParseBufError> { + assert_string("KEY='>${KEY1}<>$KEY2<'", vec![("KEY", ">${KEY1}<>$KEY2<")]) } #[test] - fn same_variable_reused() { - assert_parsed_string( + fn same_variable_reused() -> Result<(), ParseBufError> { + assert_string( r" KEY=VALUE KEY1=$KEY$KEY ", vec![("KEY", "VALUE"), ("KEY1", "VALUEVALUE")], - ); + ) } #[test] - fn with_dot() { - assert_parsed_string( + fn with_dot() -> Result<(), ParseBufError> { + assert_string( r" KEY.Value=VALUE ", vec![("KEY.Value", "VALUE")], - ); + ) } #[test] - fn recursive_substitution() { - assert_parsed_string( + fn recursive_substitution() -> Result<(), ParseBufError> { + assert_string( r" KEY=${KEY1}+KEY_VALUE KEY1=${KEY}+KEY1_VALUE ", vec![("KEY", "+KEY_VALUE"), ("KEY1", "+KEY_VALUE+KEY1_VALUE")], - ); + ) } #[test] - fn variable_without_parenthesis_is_substituted_before_separators() { - assert_parsed_string( + fn var_without_paranthesis_subbed_before_separators() -> Result<(), ParseBufError> { + assert_string( r#" KEY1=test_user KEY1_1=test_user_with_separator @@ -506,32 +505,33 @@ mod variable_substitution_tests { ("KEY1_1", "test_user_with_separator"), ("KEY", ">test_user_1<>test_user}<>test_user{<"), ], - ); + ) } #[test] - fn substitute_variable_from_env_variable() { + fn sub_var_from_env_var() -> Result<(), ParseBufError> { unsafe { env::set_var("KEY11", "test_user_env") }; - assert_parsed_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]); + assert_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]) } #[test] - fn substitute_variable_env_variable_overrides_dotenv_in_substitution() { + fn substitute_variable_env_variable_overrides_dotenv_in_substitution( + ) -> Result<(), ParseBufError> { unsafe { env::set_var("KEY11", "test_user_env") }; - assert_parsed_string( + assert_string( r#" KEY11=test_user KEY=">${KEY11}<" "#, vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")], - ); + ) } #[test] - fn consequent_substitutions() { - assert_parsed_string( + fn consequent_substitutions() -> Result<(), ParseBufError> { + assert_string( r" KEY1=test_user KEY2=$KEY1_2 @@ -542,18 +542,18 @@ mod variable_substitution_tests { ("KEY2", "test_user_2"), ("KEY", ">test_user<>test_user_2<"), ], - ); + ) } #[test] - fn consequent_substitutions_with_one_missing() { - assert_parsed_string( + fn consequent_substitutions_with_one_missing() -> Result<(), ParseBufError> { + assert_string( r" KEY2=$KEY1_2 KEY=>${KEY1}<>${KEY2}< ", vec![("KEY2", "_2"), ("KEY", "><>_2<")], - ); + ) } } @@ -562,79 +562,63 @@ mod error_tests { use crate::iter::{Iter, ParseBufError}; #[test] - fn should_not_parse_unfinished_substitutions() { - let wrong_value = ">${KEY{<"; + fn should_not_parse_unfinished_subs() { + let invalid_value = ">${baz{<"; - let parsed_values: Vec<_> = Iter::new( + let iter = Iter::new( format!( r#" - KEY=VALUE - KEY1={wrong_value} + FOO=bar + BAR={invalid_value} "# ) .as_bytes(), ) - .collect(); - - assert_eq!(parsed_values.len(), 2); + .collect::>(); - if let Ok(first_line) = &parsed_values[0] { - assert_eq!(first_line, &(String::from("KEY"), String::from("VALUE"))); - } else { - panic!("Expected the first value to be parsed"); - } - - if let Err(ParseBufError::LineParse(second_value, index)) = &parsed_values[1] { - assert_eq!(second_value, wrong_value); - assert_eq!(*index, wrong_value.len() - 1); - } else { - panic!("Expected the second value not to be parsed") - } + // first line works + assert_eq!( + iter[0].as_ref().unwrap(), + &("FOO".to_owned(), "bar".to_owned()) + ); + // second line error + assert!(matches!( + iter[1], + Err(ParseBufError::LineParse(ref v, idx)) if v == invalid_value && idx == invalid_value.len() - 1 + )); } #[test] - fn should_not_allow_dot_as_first_character_of_key() { - let wrong_key_value = ".Key=VALUE"; + fn should_not_allow_dot_as_first_char_of_key() { + let invalid_key = ".KEY=value"; - let parsed_values: Vec<_> = Iter::new(wrong_key_value.as_bytes()).collect(); + let iter = Iter::new(invalid_key.as_bytes()).collect::>(); - assert_eq!(parsed_values.len(), 1); - - if let Err(ParseBufError::LineParse(second_value, index)) = &parsed_values[0] { - assert_eq!(second_value, wrong_key_value); - assert_eq!(*index, 0); - } else { - panic!("Expected the second value not to be parsed") - } + assert!(matches!( + iter[0], + Err(ParseBufError::LineParse(ref v, idx)) if v == invalid_key && idx == 0 + )); } #[test] - fn should_not_parse_illegal_format() { - let wrong_format = r"<><><>"; - let parsed_values: Vec<_> = Iter::new(wrong_format.as_bytes()).collect(); - - assert_eq!(parsed_values.len(), 1); - - if let Err(ParseBufError::LineParse(wrong_value, index)) = &parsed_values[0] { - assert_eq!(wrong_value, wrong_format); - assert_eq!(*index, 0); - } else { - panic!("Expected the second value not to be parsed") - } + fn should_not_parse_invalid_format() { + let invalid_fmt = r"<><><>"; + let iter = Iter::new(invalid_fmt.as_bytes()).collect::>(); + + assert!(matches!( + iter[0], + Err(ParseBufError::LineParse(ref v, idx)) if v == invalid_fmt && idx == 0 + )); } #[test] - fn should_not_parse_illegal_escape() { - let wrong_escape = r">\f<"; - let parsed_values: Vec<_> = Iter::new(format!("VALUE={wrong_escape}").as_bytes()).collect(); - - assert_eq!(parsed_values.len(), 1); - - if let Err(ParseBufError::LineParse(wrong_value, index)) = &parsed_values[0] { - assert_eq!(wrong_value, wrong_escape); - assert_eq!(*index, wrong_escape.find('\\').unwrap() + 1); - } else { - panic!("Expected the second value not to be parsed") - } + fn should_not_parse_invalid_escape() { + let invalid_esc = r">\f<"; + let iter = Iter::new(format!("VALUE={invalid_esc}").as_bytes()).collect::>(); + + assert!(matches!( + iter[0], + Err(ParseBufError::LineParse(ref v, idx)) if v == invalid_esc && idx == invalid_esc.find('\\').unwrap() + 1 + )); } } From 3b05ea8b8f603fe470f362a3c515aac24defa789 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:20:39 -0400 Subject: [PATCH 19/39] Remove deprecated test --- dotenvy/tests/test-vars.rs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 dotenvy/tests/test-vars.rs diff --git a/dotenvy/tests/test-vars.rs b/dotenvy/tests/test-vars.rs deleted file mode 100644 index 684694e6..00000000 --- a/dotenvy/tests/test-vars.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod common; - -use crate::common::make_test_dotenv; -use std::{collections::HashMap, env, error}; - -// #[test] -// fn test_vars() -> Result<(), Box> { -// let dir = unsafe { make_test_dotenv() }?; - -// let vars: HashMap = unsafe { dotenvy::modify::vars() }.collect(); - -// assert_eq!(vars["TESTKEY"], "test_val"); - -// env::set_current_dir(dir.path().parent().unwrap())?; -// dir.close()?; -// Ok(()) -// } From ec0b6a35ad67ed6a78ba9e1e7b6764547c114e83 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:30:10 -0400 Subject: [PATCH 20/39] Use safe API in mutiline tests Tests are also moved from separate files to the `EnvLoader` module. --- dotenvy/src/lib.rs | 90 ++++++++++++++++++++++++- dotenvy/tests/test-multiline-comment.rs | 55 --------------- dotenvy/tests/test-multiline.rs | 49 -------------- 3 files changed, 88 insertions(+), 106 deletions(-) delete mode 100644 dotenvy/tests/test-multiline-comment.rs delete mode 100644 dotenvy/tests/test-multiline.rs diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 7a9a3b98..2ae10e0b 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -265,7 +265,7 @@ impl<'a> EnvLoader<'a> { mod tests { use std::{env, io::Cursor}; - use crate::EnvLoader; + use crate::{EnvLoader, EnvSequence}; #[test] fn test_substitution() -> Result<(), crate::Error> { @@ -290,7 +290,9 @@ mod tests { "#, ); let cursor = Cursor::new(s); - let env_map = EnvLoader::with_reader(cursor).load()?; + let env_map = EnvLoader::with_reader(cursor) + .sequence(EnvSequence::InputOnly) + .load()?; assert_eq!(env_map.var("KEY")?, "value"); assert_eq!(env_map.var("KEY1")?, "value1"); @@ -324,4 +326,88 @@ mod tests { ); Ok(()) } + + #[test] + fn test_multiline() -> Result<(), crate::Error> { + let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\""; + let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\""; + + let s = format!( + r#" + KEY=my\ cool\ value + KEY3="awesome \"stuff\" + more + on other + lines" + KEY4='hello '\''world'" + good ' \'morning" + WEAK="{value}" + STRONG='{value}' + "# + ); + + let cursor = Cursor::new(s); + let env_map = EnvLoader::with_reader(cursor) + .sequence(EnvSequence::InputOnly) + .load()?; + assert_eq!(env_map.var("KEY")?, r#"my cool value"#); + assert_eq!( + env_map.var("KEY3")?, + r#"awesome "stuff" + more + on other + lines"# + ); + assert_eq!( + env_map.var("KEY4")?, + r#"hello 'world + good ' 'morning"# + ); + assert_eq!(env_map.var("WEAK")?, weak); + assert_eq!(env_map.var("STRONG")?, value); + Ok(()) + } + + #[test] + fn test_multiline_comment() -> Result<(), crate::Error> { + let s = r#" +# Start of .env file +# Comment line with single ' quote +# Comment line with double " quote + # Comment line with double " quote and starts with a space +TESTKEY1=test_val # 1 '" comment +TESTKEY2=test_val_with_#_hash # 2 '" comment +TESTKEY3="test_val quoted with # hash" # 3 '" comment +TESTKEY4="Line 1 +# Line 2 +Line 3" # 4 Multiline "' comment +TESTKEY5="Line 4 +# Line 5 +Line 6 +" # 5 Multiline "' comment +# End of .env file +"#; + + let cursor = Cursor::new(s); + let env_map = EnvLoader::with_reader(cursor) + .sequence(EnvSequence::InputOnly) + .load()?; + assert_eq!(env_map.var("TESTKEY1")?, "test_val"); + assert_eq!(env_map.var("TESTKEY2")?, "test_val_with_#_hash"); + assert_eq!(env_map.var("TESTKEY3")?, "test_val quoted with # hash"); + assert_eq!( + env_map.var("TESTKEY4")?, + r#"Line 1 +# Line 2 +Line 3"# + ); + assert_eq!( + env_map.var("TESTKEY5")?, + r#"Line 4 +# Line 5 +Line 6 +"# + ); + Ok(()) + } } diff --git a/dotenvy/tests/test-multiline-comment.rs b/dotenvy/tests/test-multiline-comment.rs deleted file mode 100644 index 76bc3384..00000000 --- a/dotenvy/tests/test-multiline-comment.rs +++ /dev/null @@ -1,55 +0,0 @@ -mod common; -use std::env; - -use common::tempdir_with_dotenv; -use dotenvy::EnvLoader; - -#[test] -fn test_issue_12() { - let txt = r#" -# Start of .env file -# Comment line with single ' quote -# Comment line with double " quote - # Comment line with double " quote and starts with a space -TESTKEY1=test_val # 1 '" comment -TESTKEY2=test_val_with_#_hash # 2 '" comment -TESTKEY3="test_val quoted with # hash" # 3 '" comment -TESTKEY4="Line 1 -# Line 2 -Line 3" # 4 Multiline "' comment -TESTKEY5="Line 4 -# Line 5 -Line 6 -" # 5 Multiline "' comment -# End of .env file -"#; - - let _f = unsafe { tempdir_with_dotenv(txt) }.expect("should write test env"); - - unsafe { EnvLoader::new().load_and_modify() }.expect("should succeed"); - assert_eq!( - env::var("TESTKEY1").expect("testkey1 env key not set"), - "test_val" - ); - assert_eq!( - env::var("TESTKEY2").expect("testkey2 env key not set"), - "test_val_with_#_hash" - ); - assert_eq!( - env::var("TESTKEY3").expect("testkey3 env key not set"), - "test_val quoted with # hash" - ); - assert_eq!( - env::var("TESTKEY4").expect("testkey4 env key not set"), - r#"Line 1 -# Line 2 -Line 3"# - ); - assert_eq!( - env::var("TESTKEY5").expect("testkey5 env key not set"), - r#"Line 4 -# Line 5 -Line 6 -"# - ); -} diff --git a/dotenvy/tests/test-multiline.rs b/dotenvy/tests/test-multiline.rs deleted file mode 100644 index 91b54e4b..00000000 --- a/dotenvy/tests/test-multiline.rs +++ /dev/null @@ -1,49 +0,0 @@ -mod common; - -use dotenvy::EnvLoader; - -use crate::common::tempdir_with_dotenv; -use std::{env, error}; - -#[test] -fn test_multiline() -> Result<(), Box> { - let value = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\\n\\\"QUOTED\\\""; - let weak = "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n\"QUOTED\""; - - let txt = format!( - r#" -KEY=my\ cool\ value -KEY3="awesome \"stuff\" -more -on other -lines" -KEY4='hello '\''world'" -good ' \'morning" -WEAK="{}" -STRONG='{}' -"#, - value, value - ); - - let dir = unsafe { tempdir_with_dotenv(&txt) }?; - unsafe { EnvLoader::new().load_and_modify() }?; - assert_eq!(env::var("KEY")?, r#"my cool value"#); - assert_eq!( - env::var("KEY3")?, - r#"awesome "stuff" -more -on other -lines"# - ); - assert_eq!( - env::var("KEY4")?, - r#"hello 'world -good ' 'morning"# - ); - assert_eq!(env::var("WEAK")?, weak); - assert_eq!(env::var("STRONG")?, value); - - env::set_current_dir(dir.path().parent().unwrap())?; - dir.close()?; - Ok(()) -} From d07c421dc8dbbed24aa06ed5ac07d4d691c7b384 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:35:37 -0400 Subject: [PATCH 21/39] Remove unused test dir The integration tests are no longer being used. Testing is done at lower levels and will be stored as modules. --- dotenvy/tests/common.rs | 24 -- dotenvy/tests/integration/main.rs | 53 ----- dotenvy/tests/integration/testenv.rs | 333 --------------------------- 3 files changed, 410 deletions(-) delete mode 100644 dotenvy/tests/common.rs delete mode 100644 dotenvy/tests/integration/main.rs delete mode 100644 dotenvy/tests/integration/testenv.rs diff --git a/dotenvy/tests/common.rs b/dotenvy/tests/common.rs deleted file mode 100644 index 97f4cab5..00000000 --- a/dotenvy/tests/common.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::{ - env, - fs::File, - io::{self, Write}, -}; -use tempfile::{tempdir, TempDir}; - -pub unsafe fn tempdir_with_dotenv(text: &str) -> io::Result { - unsafe { env::set_var("EXISTING", "from_env") }; - let dir = tempdir()?; - env::set_current_dir(dir.path())?; - let path = dir.path().join(".env"); - let mut file = File::create(path)?; - file.write_all(text.as_bytes())?; - file.sync_all()?; - Ok(dir) -} - -#[allow(dead_code)] -pub unsafe fn make_test_dotenv() -> io::Result { - unsafe { - tempdir_with_dotenv("TESTKEY=test_val\nTESTKEY=test_val_overridden\nEXISTING=from_file") - } -} diff --git a/dotenvy/tests/integration/main.rs b/dotenvy/tests/integration/main.rs deleted file mode 100644 index 81d02b33..00000000 --- a/dotenvy/tests/integration/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::env::{self, VarError}; - -mod testenv; - -/// Default key used in envfile -pub const TEST_KEY: &str = "TESTKEY"; -/// Default value used in envfile -pub const TEST_VALUE: &str = "test_val"; - -/// Default existing key set before test is run -pub const TEST_EXISTING_KEY: &str = "TEST_EXISTING_KEY"; -/// Default existing value set before test is run -pub const TEST_EXISTING_VALUE: &str = "from_env"; -/// Default overriding value in envfile -pub const TEST_OVERRIDING_VALUE: &str = "from_file"; - -#[inline(always)] -pub fn create_default_envfile() -> String { - format!("{TEST_KEY}={TEST_VALUE}\n{TEST_EXISTING_KEY}={TEST_OVERRIDING_VALUE}",) -} - -/// missing equals -#[inline(always)] -pub fn create_invalid_envfile() -> String { - format!("{TEST_KEY}{TEST_VALUE}\n{TEST_EXISTING_KEY}{TEST_OVERRIDING_VALUE}",) -} - -/// Assert that an environment variable is set and has the expected value. -pub fn assert_env_var(key: &str, expected: &str) { - match env::var(key) { - Ok(actual) => assert_eq!( - expected, actual, - "\n\nFor Environment Variable `{key}`:\n EXPECTED: `{expected}`\n ACTUAL: `{actual}`\n", - ), - Err(VarError::NotPresent) => panic!("env var `{key}` not found"), - Err(VarError::NotUnicode(val)) => panic!( - "env var `{key}` currently has invalid unicode: `{}`", - val.to_string_lossy() - ), - } -} - -/// Assert that an environment variable is not currently set. -pub fn assert_env_var_unset(key: &str) { - match env::var(key) { - Ok(actual) => panic!("env var `{key}` should not be set, currently it is: `{actual}`",), - Err(VarError::NotUnicode(val)) => panic!( - "env var `{key}` should not be set, currently has invalid unicode: `{}`", - val.to_string_lossy() - ), - _ => (), - } -} diff --git a/dotenvy/tests/integration/testenv.rs b/dotenvy/tests/integration/testenv.rs deleted file mode 100644 index a5965f85..00000000 --- a/dotenvy/tests/integration/testenv.rs +++ /dev/null @@ -1,333 +0,0 @@ -use super::{create_default_envfile, TEST_EXISTING_KEY, TEST_EXISTING_VALUE}; -use dotenvy::EnvMap; -use std::{ - env, fs, - io::{self, Write}, - path::{Path, PathBuf}, - sync::{Arc, Mutex, OnceLock, PoisonError}, -}; -use tempfile::{tempdir, TempDir}; - -/// Initialized in [`get_env_locker`] -static ENV_LOCKER: OnceLock>> = OnceLock::new(); - -/// A test environment. -/// -/// Will create a new temporary directory. Use its builder methods to configure -/// the directory structure, preset variables, env file name and contents, and -/// the working directory to run the test from. -/// -/// Creation methods: -/// - [`TestEnv::init`]: blank environment (no envfile) -/// - [`TestEnv::init_with_envfile`]: blank environment with an envfile -/// - [`TestEnv::default`]: default testing environment (1 existing var and 2 -/// set in a `.env` file) -#[derive(Debug)] -pub struct TestEnv { - temp_dir: TempDir, - work_dir: PathBuf, - env_vars: Vec, - env_file_contents: Option, - env_file_path: PathBuf, -} - -/// Simple key value struct for representing environment variables -#[derive(Debug, Clone)] -pub struct KeyVal { - key: String, - value: String, -} - -/// Run a test closure within a test environment. -/// -/// Resets the environment variables, loads the [`TestEnv`], then runs the test -/// closure. Ensures only one thread has access to the process environment. -pub unsafe fn test_in_env(test_env: TestEnv, test: F) -where - F: FnOnce(), -{ - let locker = get_env_locker(); - // ignore a poisoned mutex - // we expect some tests may panic to indicate a failure - let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner); - // we reset the environment anyway upon acquiring the lock - reset_env(&original_env); - unsafe { create_env(&test_env) }; - test(); - // drop the lock and the `TestEnv` - should delete the tempdir -} - -/// Run a test closure within the default test environment. -/// -/// Resets the environment variables, creates the default [`TestEnv`], then runs -/// the test closure. Ensures only one thread has access to the process -/// environment. -/// -/// The default testing environment sets an existing environment variable -/// `TEST_EXISTING_KEY`, which is set to `from_env`. It also creates a `.env` -/// file with the two lines: -/// -/// ```ini -/// TESTKEY=test_val -/// TEST_EXISTING_KEY=from_file -/// ``` -/// -/// Notice that file has the potential to override `TEST_EXISTING_KEY` depending -/// on the what's being tested. -pub unsafe fn test_in_default_env(test: F) -where - F: FnOnce(), -{ - let test_env = TestEnv::default(); - unsafe { test_in_env(test_env, test) }; -} - -impl TestEnv { - /// Blank testing environment in a new temporary directory. - /// - /// No envfile_contents or pre-existing variables to set. The envfile_name - /// is set to `.env` but won't be written until its content is set. The - /// working directory is the created temporary directory. - pub fn init() -> Self { - let tempdir = tempdir().expect("create tempdir"); - let work_dir = tempdir.path().to_owned(); - let envfile_path = work_dir.join(".env"); - Self { - temp_dir: tempdir, - work_dir, - env_vars: Default::default(), - env_file_contents: None, - env_file_path: envfile_path, - } - } - - /// Testing environment with custom envfile_contents. - /// - /// No pre-existing env_vars set. The envfile_name is set to `.env`. The - /// working directory is the created temporary directory. - pub fn init_with_envfile(contents: impl ToString) -> Self { - let mut test_env = Self::init(); - test_env.set_envfile_contents(contents); - test_env - } - - /// Change the name of the default `.env` file. - /// - /// It will still be placed in the root temporary directory. If you need to - /// put the env file in a different directory, use - /// [`set_envfile_path`](TestEnv::set_envfile_path) instead. - pub fn set_envfile_name(&mut self, name: impl AsRef) -> &mut Self { - self.env_file_path = self.temp_path().join(name); - self - } - - /// Change the absolute path to the envfile. - pub fn set_envfile_path(&mut self, path: PathBuf) -> &mut Self { - self.env_file_path = path; - self - } - - /// Specify the contents of the envfile. - /// - /// If this is the only change to the [`TestEnv`] being made, use - /// [`new_with_envfile`](TestEnv::new_with_envfile). - /// - /// Setting it to an empty string will cause an empty env file to be created - pub fn set_envfile_contents(&mut self, contents: impl ToString) -> &mut Self { - self.env_file_contents = Some(contents.to_string()); - self - } - - /// Set the working directory the test will run from. - /// - /// The default is the created temporary directory. This method is useful if - /// you wish to run a test from a subdirectory or somewhere else. - pub fn set_work_dir(&mut self, path: PathBuf) -> &mut Self { - self.work_dir = path; - self - } - - /// Add an individual environment variable. - /// - /// This adds more pre-existing environment variables to the process before - /// any tests are run. - pub fn add_env_var(&mut self, key: impl ToString, value: impl ToString) -> &mut Self { - self.env_vars.push(KeyVal { - key: key.to_string(), - value: value.to_string(), - }); - self - } - - /// Set the pre-existing environment variables. - /// - /// These variables will get added to the process' environment before the - /// test is run. This overrides any previous env vars added to the - /// [`TestEnv`]. - /// - /// If you wish to just use a slice of tuples, use - /// [`set_env_vars_tuple`](TestEnv::set_env_vars_tuple) instead. - pub fn set_env_vars(&mut self, env_vars: Vec) -> &mut Self { - self.env_vars = env_vars; - self - } - - /// Set the pre-existing environment variables using [`str`] tuples. - /// - /// These variables will get added to the process' environment before the - /// test is run. This overrides any previous env vars added to the - /// [`TestEnv`]. - /// - /// If you wish to add an owned `Vec` instead of `str` tuples, use - /// [`set_env_vars`](TestEnv::set_env_vars) instead. - pub fn set_env_vars_tuple(&mut self, env_vars: &[(&str, &str)]) -> &mut Self { - self.env_vars = env_vars - .iter() - .map(|(key, value)| KeyVal { - key: key.to_string(), - value: value.to_string(), - }) - .collect(); - - self - } - - /// Create a child folder within the temporary directory. - /// - /// This will not change the working directory the test is run in, or where - /// the env file is created. - /// - /// Will create parent directories if they are missing. - pub fn add_child_dir_all(&self, rel_path: impl AsRef) -> PathBuf { - let rel_path = rel_path.as_ref(); - let child_dir = self.temp_path().join(rel_path); - if let Err(err) = fs::create_dir_all(&child_dir) { - panic!( - "unable to create child directory: `{}` in `{}`: {}", - self.temp_path().display(), - rel_path.display(), - err - ); - } - child_dir - } - - /// Get a reference to the path of the temporary directory. - pub fn temp_path(&self) -> &Path { - self.temp_dir.path() - } - - /// Get a reference to the working directory the test will be run from. - pub fn work_dir(&self) -> &Path { - &self.work_dir - } - - /// Get a reference to environment variables that will be set **before** - /// the test. - pub fn env_vars(&self) -> &[KeyVal] { - &self.env_vars - } - - /// Get a reference to the string that will be placed in the envfile. - /// - /// If `None` is returned, an env file will not be created - pub fn envfile_contents(&self) -> Option<&str> { - self.env_file_contents.as_deref() - } - - /// Get a reference to the path of the envfile. - pub fn envfile_path(&self) -> &Path { - &self.env_file_path - } -} - -impl Default for TestEnv { - fn default() -> Self { - let temp_dir = tempdir().expect("create tempdir"); - let work_dir = temp_dir.path().to_owned(); - let env_vars = vec![KeyVal { - key: TEST_EXISTING_KEY.into(), - value: TEST_EXISTING_VALUE.into(), - }]; - let envfile_contents = Some(create_default_envfile()); - let envfile_path = work_dir.join(".env"); - Self { - temp_dir, - work_dir, - env_vars, - env_file_contents: envfile_contents, - env_file_path: envfile_path, - } - } -} - -impl From<(&str, &str)> for KeyVal { - fn from(kv: (&str, &str)) -> Self { - let (key, value) = kv; - Self { - key: key.to_string(), - value: value.to_string(), - } - } -} - -impl From<(String, String)> for KeyVal { - fn from(kv: (String, String)) -> Self { - let (key, value) = kv; - Self { key, value } - } -} - -/// Get a guarded copy of the original process' env vars. -fn get_env_locker() -> Arc> { - Arc::clone(ENV_LOCKER.get_or_init(|| { - let map: EnvMap = env::vars().collect(); - Arc::new(Mutex::new(map)) - })) -} - -/// Reset the process' env vars back to what was in `original_env`. -fn reset_env(original_env: &EnvMap) { - // remove keys if they weren't in the original environment - env::vars() - .filter(|(key, _)| !original_env.contains_key(key)) - .for_each(|(key, _)| unsafe { env::remove_var(key) }); - // ensure original keys have their original values - original_env - .iter() - .for_each(|(key, value)| unsafe { env::set_var(key, value) }); -} - -/// Create an environment to run tests in. -/// -/// Writes the envfile, sets the working directory, and sets environment vars. -unsafe fn create_env(test_env: &TestEnv) { - // only create the env file if its contents has been set - if let Some(contents) = test_env.envfile_contents() { - create_env_filefile(&test_env.env_file_path, contents); - } - - env::set_current_dir(&test_env.work_dir).expect("setting working directory"); - - for KeyVal { key, value } in &test_env.env_vars { - unsafe { env::set_var(key, value) } - } -} - -/// Create an env file for use in tests. -fn create_env_filefile(path: &Path, contents: &str) { - if path.exists() { - panic!("env file `{}` already exists", path.display()) - } - // inner function to group together io::Results - fn create_env_file_inner(path: &Path, contents: &str) -> io::Result<()> { - let mut file = fs::File::create(path)?; - file.write_all(contents.as_bytes())?; - file.sync_all() - } - // call inner function - if let Err(err) = create_env_file_inner(path, contents) { - // handle any io::Result::Err - panic!("error creating env file `{}`: {}", path.display(), err); - } -} From f72df37f2c6bb1876d70c4b096ad1df653b25363 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:37:03 -0400 Subject: [PATCH 22/39] Load env for substitution test --- dotenvy/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 2ae10e0b..a5bdbcbc 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -163,6 +163,7 @@ impl<'a> EnvLoader<'a> { /// This is useful when constructing with a reader, but still desiring a path to be used in the error message context. /// /// If a reader exists and a path is specified, loading will be done using the reader. + #[must_use] pub fn path>(mut self, path: P) -> Self { self.path = Some(path.as_ref().to_owned()); self @@ -263,9 +264,8 @@ impl<'a> EnvLoader<'a> { #[cfg(test)] mod tests { - use std::{env, io::Cursor}; - use crate::{EnvLoader, EnvSequence}; + use std::{env, io::Cursor}; #[test] fn test_substitution() -> Result<(), crate::Error> { @@ -291,7 +291,7 @@ mod tests { ); let cursor = Cursor::new(s); let env_map = EnvLoader::with_reader(cursor) - .sequence(EnvSequence::InputOnly) + .sequence(EnvSequence::InputThenEnv) .load()?; assert_eq!(env_map.var("KEY")?, "value"); From aa2a0e1622e766fc041010da8ac7cc53b8889356 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:42:17 -0400 Subject: [PATCH 23/39] Remove unmeaningful `dotenvy::Error` tests --- dotenvy/src/err.rs | 47 ---------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/dotenvy/src/err.rs b/dotenvy/src/err.rs index 4612a1fe..54a8fd41 100644 --- a/dotenvy/src/err.rs +++ b/dotenvy/src/err.rs @@ -82,50 +82,3 @@ impl From<(ParseBufError, Option)> for Error { } } } - -#[cfg(test)] -mod test { - use super::Error; - use std::{error::Error as StdError, io}; - - #[test] - fn test_io_error_source() { - let err = Error::Io(io::ErrorKind::PermissionDenied.into(), None); - let io_err = err.source().unwrap().downcast_ref::().unwrap(); - assert_eq!(io::ErrorKind::PermissionDenied, io_err.kind()); - } - - #[test] - fn test_line_parse_error_source() { - let e = Error::LineParse("test line".to_string(), 2); - assert!(e.source().is_none()); - } - - #[test] - fn test_error_not_found_true() { - let e = Error::Io(io::ErrorKind::NotFound.into(), None); - assert!(e.not_found()); - } - - #[test] - fn test_error_not_found_false() { - let e = Error::Io(io::ErrorKind::PermissionDenied.into(), None); - assert!(!e.not_found()); - } - - #[test] - fn test_io_error_display() { - let err = Error::Io(io::ErrorKind::PermissionDenied.into(), None); - let io_err: io::Error = io::ErrorKind::PermissionDenied.into(); - assert_eq!(err.to_string(), io_err.to_string()); - } - - #[test] - fn test_lineparse_error_display() { - let err = Error::LineParse("test line".to_owned(), 2); - assert_eq!( - "error parsing line: 'test line', error at line index: 2", - err.to_string() - ); - } -} From 20847a46c4ddb6b5a7b27140564873ae3a42af3b Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:43:06 -0400 Subject: [PATCH 24/39] Minor signature syntax --- examples/find/src/main.rs | 2 +- examples/modify/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/find/src/main.rs b/examples/find/src/main.rs index 003099eb..90b0e1ea 100644 --- a/examples/find/src/main.rs +++ b/examples/find/src/main.rs @@ -20,7 +20,7 @@ fn main() -> Result<(), Box> { } /// Searches for the filename in the directory and parent directories until the file is found or the filesystem root is reached. -pub fn find(mut dir: &Path, filename: &str) -> Result { +pub fn find(mut dir: &Path, filename: &str) -> io::Result { loop { let candidate = dir.join(filename); diff --git a/examples/modify/src/main.rs b/examples/modify/src/main.rs index 4a8cf833..78ff39f9 100644 --- a/examples/modify/src/main.rs +++ b/examples/modify/src/main.rs @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { Ok(()) } -fn print_host_py() -> Result<(), io::Error> { +fn print_host_py() -> io::Result<()> { let script = fs::read_to_string("print_host.py")?; let output = Command::new("python3").arg("-c").arg(script).output()?; print!("{}", String::from_utf8_lossy(&output.stdout)); From befc817cf5c9700e16d743872036076fcc368ed4 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:19:47 -0400 Subject: [PATCH 25/39] Add `load` tests, use temp-env Unit tests run in parallel and may clobber each other. temp-env is now a dev dependency to ensure that unit tests run in isolated enviornments. Some tests are added for `load` and `load_and_modify`. --- dotenvy/Cargo.toml | 2 +- dotenvy/src/lib.rs | 52 ++++++++++++++++++++++++++++++++++++++------ dotenvy/src/parse.rs | 19 ++++++++-------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/dotenvy/Cargo.toml b/dotenvy/Cargo.toml index 2b80b69f..1b18c499 100644 --- a/dotenvy/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -32,7 +32,7 @@ clap = { version = "4.5.16", features = ["derive"], optional = true } dotenvy-macros = { path = "../dotenvy-macros", optional = true } [dev-dependencies] -tempfile = "3.12.0" +temp-env = "0.3.6" [features] default = ["cli", "macros"] diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index a5bdbcbc..af190365 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -265,7 +265,7 @@ impl<'a> EnvLoader<'a> { #[cfg(test)] mod tests { use crate::{EnvLoader, EnvSequence}; - use std::{env, io::Cursor}; + use std::{env, error, io::Cursor}; #[test] fn test_substitution() -> Result<(), crate::Error> { @@ -289,8 +289,7 @@ mod tests { NO_QUOTES={common_string} "#, ); - let cursor = Cursor::new(s); - let env_map = EnvLoader::with_reader(cursor) + let env_map = EnvLoader::with_reader(Cursor::new(s)) .sequence(EnvSequence::InputThenEnv) .load()?; @@ -346,8 +345,7 @@ mod tests { "# ); - let cursor = Cursor::new(s); - let env_map = EnvLoader::with_reader(cursor) + let env_map = EnvLoader::with_reader(Cursor::new(s)) .sequence(EnvSequence::InputOnly) .load()?; assert_eq!(env_map.var("KEY")?, r#"my cool value"#); @@ -388,8 +386,7 @@ Line 6 # End of .env file "#; - let cursor = Cursor::new(s); - let env_map = EnvLoader::with_reader(cursor) + let env_map = EnvLoader::with_reader(Cursor::new(s)) .sequence(EnvSequence::InputOnly) .load()?; assert_eq!(env_map.var("TESTKEY1")?, "test_val"); @@ -410,4 +407,45 @@ Line 6 ); Ok(()) } + + #[test] + fn test_non_modify() -> Result<(), crate::Error> { + temp_env::with_var("SRC", Some("env"), || { + let s = "SRC=envfile\nFOO=bar"; + let env_map = EnvLoader::with_reader(Cursor::new(s)) + .sequence(EnvSequence::EnvThenInput) + .load()?; + assert_eq!("envfile", env_map.var("SRC")?); + assert_eq!("bar", env_map.var("FOO")?); + + let env_map = EnvLoader::with_reader(Cursor::new(s)) + .sequence(EnvSequence::InputThenEnv) + .load()?; + assert_eq!("env", env_map.var("SRC")?); + Ok(()) + }) + } + + #[test] + fn test_modify() -> Result<(), Box> { + let s = "SRC=envfile\nFOO=bar"; + let cursor = Cursor::new(s); + + temp_env::with_var("SRC", Some("env"), || { + let loader = EnvLoader::with_reader(cursor.clone()).sequence(EnvSequence::InputThenEnv); + unsafe { loader.load_and_modify() }?; + assert_eq!("env", env::var("SRC")?); + assert_eq!("bar", env::var("FOO")?); + Ok::<_, Box>(()) + })?; + + // override + temp_env::with_var("SRC", Some("env"), || { + let loader = EnvLoader::with_reader(cursor).sequence(EnvSequence::EnvThenInput); + unsafe { loader.load_and_modify() }?; + assert_eq!("envfile", env::var("SRC")?); + assert_eq!("bar", env::var("FOO")?); + Ok(()) + }) + } } diff --git a/dotenvy/src/parse.rs b/dotenvy/src/parse.rs index 15828a3a..f8d59417 100644 --- a/dotenvy/src/parse.rs +++ b/dotenvy/src/parse.rs @@ -403,7 +403,6 @@ KEY4=h\8u #[cfg(test)] mod substitution_tests { use crate::iter::{Iter, ParseBufError}; - use std::env; /// Asserts the parsed string is equal to the expected string. fn assert_string(input: &str, expected: Vec<(&str, &str)>) -> Result<(), ParseBufError> { @@ -510,23 +509,23 @@ mod substitution_tests { #[test] fn sub_var_from_env_var() -> Result<(), ParseBufError> { - unsafe { env::set_var("KEY11", "test_user_env") }; - - assert_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]) + temp_env::with_var("KEY11", Some("test_user_env"), || { + assert_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]) + }) } #[test] fn substitute_variable_env_variable_overrides_dotenv_in_substitution( ) -> Result<(), ParseBufError> { - unsafe { env::set_var("KEY11", "test_user_env") }; - - assert_string( - r#" + temp_env::with_var("KEY11", Some("test_user_env"), || { + assert_string( + r#" KEY11=test_user KEY=">${KEY11}<" "#, - vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")], - ) + vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")], + ) + }) } #[test] From faeba1b6b2a5bc454a059690007b84b13a8ab271 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:28:10 -0400 Subject: [PATCH 26/39] Revert edition to 2021 --- dotenv_codegen/Cargo.toml | 6 ++---- dotenvy-macros/Cargo.toml | 6 ++---- dotenvy-macros/src/lib.rs | 4 ++++ dotenvy/Cargo.toml | 8 +++----- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/dotenv_codegen/Cargo.toml b/dotenv_codegen/Cargo.toml index a6de4e6f..a2289dcd 100644 --- a/dotenv_codegen/Cargo.toml +++ b/dotenv_codegen/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["edition2024"] - [package] name = "dotenvy_macro" version = "0.15.7" @@ -18,8 +16,8 @@ license = "MIT" homepage = "https://github.com/allan2/dotenvy" repository = "https://github.com/allan2/dotenvy" description = "A macro for compile time dotenv inspection" -edition = "2024" -#rust-version = "1.72.0" +edition = "2021" +rust-version = "1.72.0" [lib] proc-macro = true diff --git a/dotenvy-macros/Cargo.toml b/dotenvy-macros/Cargo.toml index d5281222..fb6ee329 100644 --- a/dotenvy-macros/Cargo.toml +++ b/dotenvy-macros/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["edition2024"] - [package] name = "dotenvy-macros" version = "0.1.0" @@ -11,8 +9,8 @@ license = "MIT" homepage = "https://github.com/allan2/dotenvy" repository = "https://github.com/allan2/dotenvy" description = "Runtime macros for dotenvy" -edition = "2024" -#rust-version = "1.72.0" +edition = "2021" +rust-version = "1.72.0" [lib] proc-macro = true diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs index f508a53e..3df93c8f 100644 --- a/dotenvy-macros/src/lib.rs +++ b/dotenvy-macros/src/lib.rs @@ -1,6 +1,10 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #![deny(clippy::uninlined_format_args, clippy::wildcard_imports)] +extern crate proc_macro; +extern crate quote; +extern crate syn; + use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta}; diff --git a/dotenvy/Cargo.toml b/dotenvy/Cargo.toml index 1b18c499..d745afc0 100644 --- a/dotenvy/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -1,5 +1,3 @@ -cargo-features = ["edition2024"] - [package] name = "dotenvy" version = "0.15.7" @@ -20,8 +18,8 @@ keywords = ["dotenv", "env", "environment", "settings", "config"] categories = ["configuration"] license = "MIT" repository = "https://github.com/allan2/dotenvy" -edition = "2024" -#rust-version = "1.72.0" +edition = "2021" +rust-version = "1.72.0" [[bin]] name = "dotenvy" @@ -35,6 +33,6 @@ dotenvy-macros = { path = "../dotenvy-macros", optional = true } temp-env = "0.3.6" [features] -default = ["cli", "macros"] +default = ["cli"] cli = ["dep:clap"] macros = ["dep:dotenvy-macros"] From 92158d988c5f36f257d7e85599fff35845179fb2 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:31:43 -0400 Subject: [PATCH 27/39] Update MSRV to 1.74.0 This is to build with clap_builder v4.5.17, which requires 17.4.0. 1.74.0 came out November 2023, so this is less than one year ago. --- CHANGELOG.md | 2 +- README.md | 4 +--- dotenv_codegen/Cargo.toml | 2 +- dotenvy-macros/Cargo.toml | 2 +- dotenvy/Cargo.toml | 2 +- dotenvy/README.md | 4 +--- scripts/cicheck.sh | 2 +- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad344987..29cf77b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - update to 2021 edition -- update MSRV to 1.72.0 +- update MSRV to 1.74.0 - **breaking**: `dotenvy::Error` now includes IO file path info and variable name info - **breaking**: `dotenvy::var` no longer calls `load` internally diff --git a/README.md b/README.md index 4cf8a994..a863713c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/dotenvy.svg)](https://crates.io/crates/dotenvy) [![msrv -1.72.0](https://img.shields.io/badge/msrv-1.72.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.72.0) +1.74.0](https://img.shields.io/badge/msrv-1.74.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.74.0) [![ci](https://github.com/allan2/dotenvy/actions/workflows/ci.yml/badge.svg)](https://github.com/allan2/dotenvy/actions/workflows/ci.yml) [![docs](https://img.shields.io/docsrs/dotenvy?logo=docs.rs)](https://docs.rs/dotenvy/) @@ -40,8 +40,6 @@ The `dotenv!` macro provided by `dotenvy_macro` crate can be used. ## Minimum supported Rust version -Currently: **1.72.0** - We aim to support the latest 8 rustc versions - approximately 1 year. Increasing MSRV is _not_ considered a semver-breaking change. diff --git a/dotenv_codegen/Cargo.toml b/dotenv_codegen/Cargo.toml index a2289dcd..283b22ec 100644 --- a/dotenv_codegen/Cargo.toml +++ b/dotenv_codegen/Cargo.toml @@ -17,7 +17,7 @@ homepage = "https://github.com/allan2/dotenvy" repository = "https://github.com/allan2/dotenvy" description = "A macro for compile time dotenv inspection" edition = "2021" -rust-version = "1.72.0" +rust-version = "1.74.0" [lib] proc-macro = true diff --git a/dotenvy-macros/Cargo.toml b/dotenvy-macros/Cargo.toml index fb6ee329..a57cb177 100644 --- a/dotenvy-macros/Cargo.toml +++ b/dotenvy-macros/Cargo.toml @@ -10,7 +10,7 @@ homepage = "https://github.com/allan2/dotenvy" repository = "https://github.com/allan2/dotenvy" description = "Runtime macros for dotenvy" edition = "2021" -rust-version = "1.72.0" +rust-version = "1.74.0" [lib] proc-macro = true diff --git a/dotenvy/Cargo.toml b/dotenvy/Cargo.toml index d745afc0..ac02f7f6 100644 --- a/dotenvy/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -19,7 +19,7 @@ categories = ["configuration"] license = "MIT" repository = "https://github.com/allan2/dotenvy" edition = "2021" -rust-version = "1.72.0" +rust-version = "1.74.0" [[bin]] name = "dotenvy" diff --git a/dotenvy/README.md b/dotenvy/README.md index 94e3b16b..6f1a13d2 100644 --- a/dotenvy/README.md +++ b/dotenvy/README.md @@ -2,7 +2,7 @@ [![Crates.io](https://img.shields.io/crates/v/dotenvy.svg)](https://crates.io/crates/dotenvy) [![msrv -1.72.0](https://img.shields.io/badge/msrv-1.72.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.72.0) +1.74.0](https://img.shields.io/badge/msrv-1.74.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.74.0) [![ci](https://github.com/allan2/dotenvy/actions/workflows/ci.yml/badge.svg)](https://github.com/allan2/dotenvy/actions/workflows/ci.yml) [![docs](https://img.shields.io/docsrs/dotenvy?logo=docs.rs)](https://docs.rs/dotenvy/) @@ -60,8 +60,6 @@ The `dotenv!` macro provided by `dotenvy_macro` crate can be used. ## Minimum supported Rust version -Currently: **1.72.0** - We aim to support the latest 8 rustc versions - approximately 1 year. Increasing MSRV is _not_ considered a semver-breaking change. diff --git a/scripts/cicheck.sh b/scripts/cicheck.sh index 19e279df..4fed8853 100755 --- a/scripts/cicheck.sh +++ b/scripts/cicheck.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -e -MSRV="1.72.0" +MSRV="1.74.0" echo "fmt check" cargo fmt --all --check From 5b4313af156a2761e0e72c0f958188fdbe037505 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:39:31 -0400 Subject: [PATCH 28/39] Remove unnecessary `extern` --- dotenvy-macros/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs index 3df93c8f..f508a53e 100644 --- a/dotenvy-macros/src/lib.rs +++ b/dotenvy-macros/src/lib.rs @@ -1,10 +1,6 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #![deny(clippy::uninlined_format_args, clippy::wildcard_imports)] -extern crate proc_macro; -extern crate quote; -extern crate syn; - use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta}; From 23aade4f4edbb4c7a380a094ad7b334e78565088 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:40:04 -0400 Subject: [PATCH 29/39] Conditional Unix `use` --- dotenvy/src/bin/dotenvy.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index a4ba9c3a..be0a351f 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -11,7 +11,7 @@ //! will output `bar`. use clap::{Parser, Subcommand}; use dotenvy::{EnvLoader, EnvSequence}; -use std::{error, fs::File, io::ErrorKind, os::unix::process::CommandExt, path::PathBuf, process}; +use std::{error, fs::File, io::ErrorKind, path::PathBuf, process}; macro_rules! die { ($fmt:expr) => ({ @@ -93,6 +93,7 @@ fn main() -> Result<(), Box> { Err(e) => die!("fatal: {e}"), }; } else { + use std::os::unix::process::CommandExt; die!("fatal: {}", cmd.exec()); }; } From f4dec21185ea8843583dbb9e3f135f89b1670279 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 17:05:18 -0400 Subject: [PATCH 30/39] Remove die macro --- dotenvy/src/bin/dotenvy.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index be0a351f..15b6630b 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -11,19 +11,9 @@ //! will output `bar`. use clap::{Parser, Subcommand}; use dotenvy::{EnvLoader, EnvSequence}; +#[cfg(unix)] use std::{error, fs::File, io::ErrorKind, path::PathBuf, process}; -macro_rules! die { - ($fmt:expr) => ({ - eprintln!($fmt); - process::exit(1); - }); - ($fmt:expr, $($arg:tt)*) => ({ - eprintln!($fmt, $($arg)*); - process::exit(1); - }); -} - fn mk_cmd(program: &str, args: &[String]) -> process::Command { let mut cmd = process::Command::new(program); for arg in args { @@ -70,14 +60,13 @@ fn main() -> Result<(), Box> { // load the file let loader = EnvLoader::with_reader(file).path(&cli.file).sequence(seq); - if let Err(e) = unsafe { loader.load_and_modify() } { - die!("Failed to load {path}: {e}", path = cli.file.display()); - } + unsafe { loader.load_and_modify() }?; } Err(e) => { if cli.required && e.kind() == ErrorKind::NotFound { - die!("Failed to load {path}: {e}", path = cli.file.display()); + eprintln!("Failed to load {path}: {e}", path = cli.file.display()); } + process::exit(1); } }; @@ -87,13 +76,19 @@ fn main() -> Result<(), Box> { let mut cmd = mk_cmd(program, args); // run the command - if cfg!(target_os = "windows") { + if cfg!(windows) { match cmd.spawn().and_then(|mut child| child.wait()) { Ok(status) => process::exit(status.code().unwrap_or(1)), - Err(e) => die!("fatal: {e}"), + Err(e) => { + eprintln!("fatal: {e}"); + process::exit(1); + } }; - } else { + } + if cfg!(unix) { use std::os::unix::process::CommandExt; - die!("fatal: {}", cmd.exec()); + eprintln!("fatal: {}", cmd.exec()); + process::exit(1); }; + Ok(()) } From 6822a04a42d96a2815a890b39d94584247772d2e Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 17:10:21 -0400 Subject: [PATCH 31/39] More useful comment for CLI file arg --- dotenvy/src/bin/dotenvy.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index 15b6630b..18185f1a 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -11,7 +11,6 @@ //! will output `bar`. use clap::{Parser, Subcommand}; use dotenvy::{EnvLoader, EnvSequence}; -#[cfg(unix)] use std::{error, fs::File, io::ErrorKind, path::PathBuf, process}; fn mk_cmd(program: &str, args: &[String]) -> process::Command { @@ -31,7 +30,8 @@ fn mk_cmd(program: &str, args: &[String]) -> process::Command { allow_external_subcommands = true )] struct Cli { - #[arg(short, long, default_value = ".env")] + #[arg(short, long, default_value = "./.env")] + /// Path to the env file file: PathBuf, #[clap(subcommand)] subcmd: Subcmd, @@ -76,19 +76,17 @@ fn main() -> Result<(), Box> { let mut cmd = mk_cmd(program, args); // run the command - if cfg!(windows) { - match cmd.spawn().and_then(|mut child| child.wait()) { - Ok(status) => process::exit(status.code().unwrap_or(1)), - Err(e) => { - eprintln!("fatal: {e}"); - process::exit(1); - } - }; - } - if cfg!(unix) { - use std::os::unix::process::CommandExt; - eprintln!("fatal: {}", cmd.exec()); - process::exit(1); + #[cfg(windows)] + match cmd.spawn().and_then(|mut child| child.wait()) { + Ok(status) => process::exit(status.code().unwrap_or(1)), + Err(e) => { + eprintln!("fatal: {e}"); + process::exit(1); + } }; - Ok(()) + + #[cfg(unix)] + use std::os::unix::process::CommandExt; + eprintln!("fatal: {}", cmd.exec()); + process::exit(1); } From 9663c8aacd81023b41f0a1744a8f4a1ba8f22c89 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Sun, 8 Sep 2024 17:14:06 -0400 Subject: [PATCH 32/39] Attempt to fix Windows build `cmd.exec` was accessible to the Windows code path, while also being unreachable do to `exit(1)`. --- dotenvy/src/bin/dotenvy.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index 18185f1a..105c7b39 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -86,7 +86,9 @@ fn main() -> Result<(), Box> { }; #[cfg(unix)] - use std::os::unix::process::CommandExt; - eprintln!("fatal: {}", cmd.exec()); - process::exit(1); + { + use std::os::unix::process::CommandExt; + eprintln!("fatal: {}", cmd.exec()); + process::exit(1); + } } From 43a167e34813eb9ee243f4858217c249d7432a51 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 00:38:44 -0400 Subject: [PATCH 33/39] Fix env file spelling We consistenly use "env file" across the repo now. --- dotenvy/src/bin/dotenvy.rs | 2 +- dotenvy/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotenvy/src/bin/dotenvy.rs b/dotenvy/src/bin/dotenvy.rs index 105c7b39..909e4af1 100644 --- a/dotenvy/src/bin/dotenvy.rs +++ b/dotenvy/src/bin/dotenvy.rs @@ -25,7 +25,7 @@ fn mk_cmd(program: &str, args: &[String]) -> process::Command { #[command( name = "dotenvy", version, - about = "Run a command using an environment loaded from a .env file", + about = "Run a command using an environment loaded from an env file", arg_required_else_help = true, allow_external_subcommands = true )] diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index af190365..c9b31f9c 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -369,7 +369,7 @@ mod tests { #[test] fn test_multiline_comment() -> Result<(), crate::Error> { let s = r#" -# Start of .env file +# Start of env file # Comment line with single ' quote # Comment line with double " quote # Comment line with double " quote and starts with a space @@ -383,7 +383,7 @@ TESTKEY5="Line 4 # Line 5 Line 6 " # 5 Multiline "' comment -# End of .env file +# End of env file "#; let env_map = EnvLoader::with_reader(Cursor::new(s)) From ab5b7541f6ad155ef338c53d9f3f9ca50b13be5c Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 00:40:08 -0400 Subject: [PATCH 34/39] Use root README for dotenvy crate --- dotenvy/Cargo.toml | 2 +- dotenvy/README.md | 99 ---------------------------------------------- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 dotenvy/README.md diff --git a/dotenvy/Cargo.toml b/dotenvy/Cargo.toml index ac02f7f6..3e65d843 100644 --- a/dotenvy/Cargo.toml +++ b/dotenvy/Cargo.toml @@ -13,7 +13,7 @@ authors = [ ] description = "A well-maintained fork of the dotenv crate" homepage = "https://github.com/allan2/dotenvy" -readme = "README.md" +readme = "../README.md" keywords = ["dotenv", "env", "environment", "settings", "config"] categories = ["configuration"] license = "MIT" diff --git a/dotenvy/README.md b/dotenvy/README.md deleted file mode 100644 index 6f1a13d2..00000000 --- a/dotenvy/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# dotenvy - -[![Crates.io](https://img.shields.io/crates/v/dotenvy.svg)](https://crates.io/crates/dotenvy) -[![msrv -1.74.0](https://img.shields.io/badge/msrv-1.74.0-dea584.svg?logo=rust)](https://github.com/rust-lang/rust/releases/tag/1.74.0) -[![ci](https://github.com/allan2/dotenvy/actions/workflows/ci.yml/badge.svg)](https://github.com/allan2/dotenvy/actions/workflows/ci.yml) -[![docs](https://img.shields.io/docsrs/dotenvy?logo=docs.rs)](https://docs.rs/dotenvy/) - -A well-maintained fork of the [dotenv](https://github.com/dotenv-rs/dotenv) crate. - -This crate is the suggested alternative for `dotenv` in security advisory [RUSTSEC-2021-0141](https://rustsec.org/advisories/RUSTSEC-2021-0141.html). - -This library loads environment variables from a _.env_ file. This is convenient for dev environments. - -## Components - -1. [`dotenvy`](https://crates.io/crates/dotenvy) crate - A well-maintained fork of the `dotenv` crate. -2. [`dotenvy_macro`](https://crates.io/crates/dotenvy_macro) crate - A macro for compile time dotenv inspection. This is a fork of `dotenv_codegen`. -3. `dotenvy` CLI tool for running a command using the environment from a _.env_ file (currently Unix only) - -## Usage - -## - -Safe API: - -```rs -use dotenvy::EnvLoader; -use std::env; - -fn main() { - let env_map = EnvLoader::new().load()?; - - for (key, value) in env { - println!("{key}: {value}"); - } -} -``` - -Modify API: - -```rs - -use std::{error, env}; - -fn main() -> Result<(), Box> { - let is_dev_mode = env::var("APP_ENV")? == "dev"; - // loads the.env file from the currenty directory - let env = dotenvy::modify::Config().required(is_dev_mode).load()?; - - for (key, value) in env { - println!("{key}: {value}"); - } -} - - -### Loading at compile time - -The `dotenv!` macro provided by `dotenvy_macro` crate can be used. - -## Minimum supported Rust version - -We aim to support the latest 8 rustc versions - approximately 1 year. Increasing -MSRV is _not_ considered a semver-breaking change. - -## Why does this fork exist? - -The original dotenv crate has not been updated since June 26, 2020. Attempts to reach the authors and present maintainer were not successful ([dotenv-rs/dotenv #74](https://github.com/dotenv-rs/dotenv/issues/74)). - -This fork intends to serve as the development home for the dotenv implementation in Rust. - -## What are the differences from the original? - -This repo fixes: - -- home directory works correctly (no longer using the deprecated `std::env::home_dir`) -- more helpful errors for `dotenv!` ([dotenv-rs/dotenv #57](https://github.com/dotenv-rs/dotenv/pull/57)) - -It also adds: - -- multiline support for environment variable values -- `io::Read` support via [`from_read`](https://docs.rs/dotenvy/latest/dotenvy/fn.from_read.html) and [`from_read_iter`](https://docs.rs/dotenvy/latest/dotenvy/fn.from_read_iter.html) -- improved docs - -For a full list of changes, refer to the [changelog](./CHANGELOG.md). - -## Contributing - -Thank you very much for considering to contribute to this project! See -[CONTRIBUTING.md](./CONTRIBUTING.md) for details. - -**Note**: Before you take the time to open a pull request, please open an issue first. - -## The legend - -Legend has it that the Lost Maintainer will return, merging changes from `dotenvy` into `dotenv` with such thrust that all `Cargo.toml`s will lose one keystroke. Only then shall the Rust dotenv crateverse be united in true harmony. - -Until then, this repo dutifully carries on the dotenv torch. It is actively maintained. -``` From a8ff4142e016bfca4657430b73afd42b6e488e2f Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:16:54 -0400 Subject: [PATCH 35/39] Upgrade syn 1 to syn 2 `override` argument named to `override_` because it is a keyword. --- dotenvy-macros/Cargo.toml | 2 +- dotenvy-macros/src/lib.rs | 85 ++++++++++++++++++++----------- examples/modify-macro/src/main.rs | 2 +- 3 files changed, 57 insertions(+), 32 deletions(-) diff --git a/dotenvy-macros/Cargo.toml b/dotenvy-macros/Cargo.toml index a57cb177..2129a166 100644 --- a/dotenvy-macros/Cargo.toml +++ b/dotenvy-macros/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true [dependencies] proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full"] } +syn = { version = "2", features = ["parsing"] } [dev-dependencies] dotenvy = { path = "../dotenvy" } \ No newline at end of file diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs index f508a53e..b5ddd705 100644 --- a/dotenvy-macros/src/lib.rs +++ b/dotenvy-macros/src/lib.rs @@ -3,7 +3,10 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, ItemFn, LitBool, LitStr, +}; /// Loads environment variables from a file and modifies the environment. /// @@ -13,30 +16,12 @@ use syn::{parse_macro_input, AttributeArgs, ItemFn, Lit, Meta, NestedMeta}; /// The default path is ".env". The default sequence is `EnvSequence::InputThenEnv`. #[proc_macro_attribute] pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as AttributeArgs); - let input = parse_macro_input!(item as ItemFn); + let attrs = parse_macro_input!(attr as LoadInput); + let item = parse_macro_input!(item as ItemFn); - let mut path = ".env".to_owned(); - let mut required = true; - let mut override_ = false; - - for arg in args { - if let NestedMeta::Meta(Meta::NameValue(v)) = arg { - if v.path.is_ident("path") { - if let Lit::Str(lit_str) = v.lit { - path = lit_str.value(); - } - } else if v.path.is_ident("required") { - if let Lit::Bool(lit_bool) = v.lit { - required = lit_bool.value(); - } - } else if v.path.is_ident("override") { - if let Lit::Bool(lit_bool) = v.lit { - override_ = lit_bool.value(); - } - } - } - } + let path = attrs.path; + let required = attrs.required; + let override_ = attrs.override_; let load_env = quote! { use dotenvy::{EnvLoader, EnvSequence}; @@ -59,12 +44,12 @@ pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { } }; - let attrs = &input.attrs; - let block = &input.block; - let sig = &input.sig; - let vis = &input.vis; - let fn_name = &input.sig.ident; - let output = &input.sig.output; + let attrs = &item.attrs; + let block = &item.block; + let sig = &item.sig; + let vis = &item.vis; + let fn_name = &item.sig.ident; + let output = &item.sig.output; let new_fn_name = format_ident!("{fn_name}_inner"); let expanded = if sig.asyncness.is_some() { @@ -95,3 +80,43 @@ pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { TokenStream::from(expanded) } + +struct LoadInput { + path: String, + required: bool, + override_: bool, +} + +impl Parse for LoadInput { + fn parse(input: ParseStream) -> syn::Result { + let mut path = "./.env".to_owned(); + let mut required = true; + let mut override_ = false; + + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + input.parse::()?; + match ident.to_string().as_str() { + "path" => { + path = input.parse::()?.value(); + } + "required" => { + required = input.parse::()?.value(); + } + "override_" => { + override_ = input.parse::()?.value(); + } + _ => return Err(syn::Error::new(ident.span(), "unknown attribute")), + } + if !input.is_empty() { + input.parse::()?; + } + } + + Ok(Self { + path, + required, + override_, + }) + } +} diff --git a/examples/modify-macro/src/main.rs b/examples/modify-macro/src/main.rs index fb449340..ef1479ca 100644 --- a/examples/modify-macro/src/main.rs +++ b/examples/modify-macro/src/main.rs @@ -1,6 +1,6 @@ use std::{env, error}; -#[dotenvy::load(path = "../env-example", required = true, override = true)] +#[dotenvy::load(path = "../env-example", required = true, override_ = true)] fn main() -> Result<(), Box> { println!("HOST={}", env::var("HOST")?); Ok(()) From 9b3db892b07a448f84fc075ab6801496b3a1f6b4 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:18:49 -0400 Subject: [PATCH 36/39] Fix default path --- dotenvy-macros/src/lib.rs | 6 +++--- dotenvy/src/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs index b5ddd705..79e6147f 100644 --- a/dotenvy-macros/src/lib.rs +++ b/dotenvy-macros/src/lib.rs @@ -10,10 +10,10 @@ use syn::{ /// Loads environment variables from a file and modifies the environment. /// -/// Three optional arguments are supported: `path`, `required`, and `override`. -/// Usage is like `#[dotenvy::load(path = ".env", required = true, override = true)]`. +/// Three optional arguments are supported: `path`, `required`, and `override_`. +/// Usage is like `#[dotenvy::load(path = ".env", required = true, override_ = true)]`. /// -/// The default path is ".env". The default sequence is `EnvSequence::InputThenEnv`. +/// The default path is "./env". The default sequence is `EnvSequence::InputThenEnv`. #[proc_macro_attribute] pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { let attrs = parse_macro_input!(attr as LoadInput); diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index c9b31f9c..09fbad95 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -133,9 +133,9 @@ pub struct EnvLoader<'a> { impl<'a> EnvLoader<'a> { #[must_use] - /// Creates a new `EnvLoader` with the path set to `./env` in the current directory. + /// Creates a new `EnvLoader` with the path set to `./.env` in the current directory. pub fn new() -> Self { - Self::with_path(".env") + Self::with_path("./.env") } /// Creates a new `EnvLoader` with the path as input. From deb2b49a34a1eca589d43056aeb19d7d609ab545 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:30:18 -0400 Subject: [PATCH 37/39] clippy needless_raw_string_hashes --- dotenvy/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 09fbad95..99cbccaf 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -1,4 +1,4 @@ -#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![deny(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #![allow( clippy::missing_errors_doc, clippy::too_many_lines, @@ -133,9 +133,9 @@ pub struct EnvLoader<'a> { impl<'a> EnvLoader<'a> { #[must_use] - /// Creates a new `EnvLoader` with the path set to `./.env` in the current directory. + /// Creates a new `EnvLoader` with the path set to `./env` in the current directory. pub fn new() -> Self { - Self::with_path("./.env") + Self::with_path("./env") } /// Creates a new `EnvLoader` with the path as input. @@ -348,7 +348,7 @@ mod tests { let env_map = EnvLoader::with_reader(Cursor::new(s)) .sequence(EnvSequence::InputOnly) .load()?; - assert_eq!(env_map.var("KEY")?, r#"my cool value"#); + assert_eq!(env_map.var("KEY")?, "my cool value"); assert_eq!( env_map.var("KEY3")?, r#"awesome "stuff" @@ -358,8 +358,8 @@ mod tests { ); assert_eq!( env_map.var("KEY4")?, - r#"hello 'world - good ' 'morning"# + "hello 'world + good ' 'morning" ); assert_eq!(env_map.var("WEAK")?, weak); assert_eq!(env_map.var("STRONG")?, value); @@ -394,16 +394,16 @@ Line 6 assert_eq!(env_map.var("TESTKEY3")?, "test_val quoted with # hash"); assert_eq!( env_map.var("TESTKEY4")?, - r#"Line 1 + "Line 1 # Line 2 -Line 3"# +Line 3" ); assert_eq!( env_map.var("TESTKEY5")?, - r#"Line 4 + "Line 4 # Line 5 Line 6 -"# +" ); Ok(()) } From 169a60e7334355e32e69265be883d55c99c145e1 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:41:22 -0400 Subject: [PATCH 38/39] Inline fmt args, default error message --- dotenv_codegen/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dotenv_codegen/src/lib.rs b/dotenv_codegen/src/lib.rs index a4baee45..18798920 100644 --- a/dotenv_codegen/src/lib.rs +++ b/dotenv_codegen/src/lib.rs @@ -13,8 +13,9 @@ pub fn dotenv(input: TokenStream) -> TokenStream { } unsafe fn dotenv_inner(input: TokenStream2) -> TokenStream2 { - if let Err(e) = unsafe { EnvLoader::new().load_and_modify() } { - let msg = format!("Error loading .env file: {}", e); + let loader = EnvLoader::new(); + if let Err(e) = unsafe { loader.load_and_modify() } { + let msg = e.to_string(); return quote! { compile_error!(#msg); }; @@ -53,13 +54,12 @@ fn expand_env(input_raw: TokenStream2) -> syn::Result { err_msg.map_or_else( || match e { VarError::NotPresent => { - format!("environment variable `{}` not defined", var_name) + format!("environment variable `{var_name}` not defined") } - VarError::NotUnicode(s) => format!( - "environment variable `{}` was not valid unicode: {:?}", - var_name, s - ), + VarError::NotUnicode(s) => { + format!("environment variable `{var_name}` was not valid Unicode: {s:?}",) + } }, |lit| lit.value(), ), From c2070b01f38812cc2723b2b5b01bf7891048f164 Mon Sep 17 00:00:00 2001 From: Allan <6740989+allan2@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:41:53 -0400 Subject: [PATCH 39/39] Fix default construction path --- dotenvy-macros/src/lib.rs | 2 +- dotenvy/src/lib.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dotenvy-macros/src/lib.rs b/dotenvy-macros/src/lib.rs index 79e6147f..cae16ff4 100644 --- a/dotenvy-macros/src/lib.rs +++ b/dotenvy-macros/src/lib.rs @@ -13,7 +13,7 @@ use syn::{ /// Three optional arguments are supported: `path`, `required`, and `override_`. /// Usage is like `#[dotenvy::load(path = ".env", required = true, override_ = true)]`. /// -/// The default path is "./env". The default sequence is `EnvSequence::InputThenEnv`. +/// The default path is "./.env". The default sequence is `EnvSequence::InputThenEnv`. #[proc_macro_attribute] pub fn load(attr: TokenStream, item: TokenStream) -> TokenStream { let attrs = parse_macro_input!(attr as LoadInput); diff --git a/dotenvy/src/lib.rs b/dotenvy/src/lib.rs index 99cbccaf..a41351d8 100644 --- a/dotenvy/src/lib.rs +++ b/dotenvy/src/lib.rs @@ -2,7 +2,8 @@ #![allow( clippy::missing_errors_doc, clippy::too_many_lines, - clippy::missing_safety_doc + clippy::missing_safety_doc, + unused_unsafe, // until Rust 2024 )] #![deny(clippy::uninlined_format_args, clippy::wildcard_imports)] @@ -133,9 +134,9 @@ pub struct EnvLoader<'a> { impl<'a> EnvLoader<'a> { #[must_use] - /// Creates a new `EnvLoader` with the path set to `./env` in the current directory. + /// Creates a new `EnvLoader` with the path set to `./.env` in the current directory. pub fn new() -> Self { - Self::with_path("./env") + Self::with_path("./.env") } /// Creates a new `EnvLoader` with the path as input.