├── .env ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── dotenv ├── Cargo.toml ├── examples │ └── simple.rs ├── src │ ├── bin │ │ └── dotenv.rs │ ├── errors.rs │ ├── find.rs │ ├── iter.rs │ ├── lib.rs │ └── parse.rs └── tests │ ├── common │ └── mod.rs │ ├── test-child-dir.rs │ ├── test-default-location.rs │ ├── test-dotenv-iter.rs │ ├── test-from-filename-iter.rs │ ├── test-from-filename.rs │ ├── test-from-path-iter.rs │ ├── test-from-path.rs │ ├── test-var.rs │ ├── test-variable-substitution.rs │ └── test-vars.rs ├── dotenv_codegen ├── Cargo.toml ├── src │ └── lib.rs └── tests │ └── basic_dotenv_macro.rs └── dotenv_codegen_implementation ├── Cargo.toml └── src └── lib.rs /.env: -------------------------------------------------------------------------------- 1 | CODEGEN_TEST_VAR1="hello!" 2 | CODEGEN_TEST_VAR2="'quotes within quotes'" 3 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | master 7 | pull_request: 8 | branches: 9 | master 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | rust: [stable, beta, nightly] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions-rs/toolchain@v1 22 | with: 23 | toolchain: ${{ matrix.rust }} 24 | override: true 25 | - uses: actions-rs/cargo@v1 26 | with: 27 | command: test 28 | check: 29 | runs-on: ubuntu-latest 30 | name: Check 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | override: true 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: fmt 40 | args: -- --check 41 | 42 | - uses: actions-rs/tarpaulin@v0.1 43 | with: 44 | args: --ignore-tests -- --test-threads 1 45 | - uses: codecov/codecov-action@v1 46 | - name: Archive code coverage results 47 | uses: actions/upload-artifact@v1 48 | with: 49 | name: code-coverage-report 50 | path: cobertura.xml 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.15.0] - 2019-10-21 10 | 11 | ### Changed 12 | - Undeprecate `iter` methods 13 | - Library no longer has any dependencies 14 | 15 | ### Added 16 | - Support for variables with a `.` in their name 17 | - Support `\n` in double-quoted lines 18 | - Support for variable substitution 19 | 20 | ## [0.14.1] - 2019-05-14 21 | 22 | ### Changed 23 | - Deprecate `iter` methods. 24 | 25 | ## [0.14.0] - 2019-05-07 26 | ### Changed 27 | - Switched repo to use cargo workspaces. 28 | - Renamed dotenv_codegen_impl to dotenv_codegen_implementation since we no longer own the original crate. 29 | - Update code to 2018 edition 30 | 31 | 32 | 33 | [Unreleased]: https://github.com/dotenv-rs/dotenv/compare/v0.15.0...HEAD 34 | [0.15.0]: https://github.com/dotenv-rs/dotenv/compare/v0.14.1...v0.15.0 35 | [0.14.1]: https://github.com/dotenv-rs/dotenv/compare/v0.14.0...v0.14.1 36 | [0.14.0]: https://github.com/dotenv-rs/dotenv/compare/v0.13.0...v0.14.0 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "dotenv", 5 | "dotenv_codegen", 6 | "dotenv_codegen_implementation", 7 | ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Santiago Lapresta and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-dotenv 2 | 3 | ![CI](https://github.com/dotenv-rs/dotenv/workflows/CI/badge.svg) 4 | [![codecov](https://codecov.io/gh/dotenv-rs/dotenv/branch/master/graph/badge.svg)](https://codecov.io/gh/dotenv-rs/dotenv) 5 | [![Crates.io](https://img.shields.io/crates/v/dotenv.svg)](https://crates.io/crates/dotenv) 6 | 7 | **Achtung!** This is a v0.\* version! Expect bugs and issues all around. 8 | Submitting pull requests and issues is highly encouraged! 9 | 10 | Quoting [bkeepers/dotenv][dotenv]: 11 | 12 | > Storing [configuration in the environment](http://www.12factor.net/config) 13 | > is one of the tenets of a [twelve-factor app](http://www.12factor.net/). 14 | > Anything that is likely to change between deployment environments–such as 15 | > resource handles for databases or credentials for external services–should 16 | > be extracted from the code into environment variables. 17 | 18 | This library is meant to be used on development or testing environments in 19 | which setting environment variables is not practical. It loads environment 20 | variables from a `.env` file, if available, and mashes those with the actual 21 | environment variables provided by the operative system. 22 | 23 | Usage 24 | ---- 25 | 26 | The easiest and most common usage consists on calling `dotenv::dotenv` when the 27 | application starts, which will load environment variables from a file named 28 | `.env` in the current directory or any of its parents; after that, you can just call 29 | the environment-related method you need as provided by `std::os`. 30 | 31 | If you need finer control about the name of the file or its location, you can 32 | use the `from_filename` and `from_path` methods provided by the crate. 33 | 34 | `dotenv_codegen` provides the `dotenv!` macro, which 35 | behaves identically to `env!`, but first tries to load a `.env` file at compile 36 | time. 37 | 38 | Examples 39 | ---- 40 | 41 | A `.env` file looks like this: 42 | 43 | ```sh 44 | # a comment, will be ignored 45 | REDIS_ADDRESS=localhost:6379 46 | MEANING_OF_LIFE=42 47 | ``` 48 | 49 | You can optionally prefix each line with the word `export`, which will 50 | conveniently allow you to source the whole file on your shell. 51 | 52 | A sample project using Dotenv would look like this: 53 | 54 | ```rust 55 | extern crate dotenv; 56 | 57 | use dotenv::dotenv; 58 | use std::env; 59 | 60 | fn main() { 61 | dotenv().ok(); 62 | 63 | for (key, value) in env::vars() { 64 | println!("{}: {}", key, value); 65 | } 66 | } 67 | ``` 68 | 69 | Variable substitution 70 | ---- 71 | 72 | It's possible to reuse variables in the `.env` file using `$VARIABLE` syntax. 73 | The syntax and rules are similar to bash ones, here's the example: 74 | 75 | 76 | ```sh 77 | 78 | VAR=one 79 | VAR_2=two 80 | 81 | # Non-existing values are replaced with an empty string 82 | RESULT=$NOPE #value: '' (empty string) 83 | 84 | # All the letters after $ symbol are treated as the variable name to replace 85 | RESULT=$VAR #value: 'one' 86 | 87 | # Double quotes do not affect the substitution 88 | RESULT="$VAR" #value: 'one' 89 | 90 | # Different syntax, same result 91 | RESULT=${VAR} #value: 'one' 92 | 93 | # Curly braces are useful in cases when we need to use a variable with non-alphanumeric name 94 | RESULT=$VAR_2 #value: 'one_2' since $ with no curly braces stops after first non-alphanumeric symbol 95 | RESULT=${VAR_2} #value: 'two' 96 | 97 | # The replacement can be escaped with either single quotes or a backslash: 98 | RESULT='$VAR' #value: '$VAR' 99 | RESULT=\$VAR #value: '$VAR' 100 | 101 | # Environment variables are used in the substutution and always override the local variables 102 | RESULT=$PATH #value: the contents of the $PATH environment variable 103 | PATH="My local variable value" 104 | RESULT=$PATH #value: the contents of the $PATH environment variable, even though the local variable is defined 105 | ``` 106 | 107 | Dotenv will parse the file, substituting the variables the way it's described in the comments. 108 | 109 | 110 | Using the `dotenv!` macro 111 | ------------------------------------ 112 | 113 | Add `dotenv_codegen` to your dependencies, and add the following to the top of 114 | your crate: 115 | 116 | ```rust 117 | #[macro_use] 118 | extern crate dotenv_codegen; 119 | ``` 120 | 121 | Then, in your crate: 122 | 123 | ```rust 124 | fn main() { 125 | println!("{}", dotenv!("MEANING_OF_LIFE")); 126 | } 127 | ``` 128 | 129 | [dotenv]: https://github.com/bkeepers/dotenv 130 | -------------------------------------------------------------------------------- /dotenv/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dotenv" 3 | version = "0.15.0" 4 | authors = [ 5 | "Noemi Lapresta ", 6 | "Craig Hills ", 7 | "Mike Piccolo ", 8 | "Alice Maz ", 9 | "Sean Griffin ", 10 | "Adam Sharp ", 11 | "Arpad Borsos ", 12 | ] 13 | description = "A `dotenv` implementation for Rust" 14 | homepage = "https://github.com/dotenv-rs/dotenv" 15 | readme = "../README.md" 16 | keywords = ["environment", "env", "dotenv", "settings", "config"] 17 | license = "MIT" 18 | repository = "https://github.com/dotenv-rs/dotenv" 19 | edition = "2018" 20 | 21 | [[bin]] 22 | name = "dotenv" 23 | required-features = ["cli"] 24 | 25 | [dependencies] 26 | clap = { version = "2", optional = true } 27 | 28 | [dev-dependencies] 29 | tempfile = "3.0.0" 30 | 31 | [features] 32 | cli = ["clap"] 33 | -------------------------------------------------------------------------------- /dotenv/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use std::env; 3 | 4 | fn main() { 5 | dotenv().ok(); 6 | 7 | for (key, value) in env::vars() { 8 | println!("{}: {}", key, value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dotenv/src/bin/dotenv.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate dotenv; 3 | 4 | use clap::{App, AppSettings, Arg}; 5 | use std::os::unix::process::CommandExt; 6 | use std::process::{exit, Command}; 7 | 8 | macro_rules! die { 9 | ($fmt:expr) => ({ 10 | eprintln!($fmt); 11 | exit(1); 12 | }); 13 | ($fmt:expr, $($arg:tt)*) => ({ 14 | eprintln!($fmt, $($arg)*); 15 | exit(1); 16 | }); 17 | } 18 | 19 | fn make_command(name: &str, args: Vec<&str>) -> Command { 20 | let mut command = Command::new(name); 21 | 22 | for arg in args { 23 | command.arg(arg); 24 | } 25 | 26 | return command; 27 | } 28 | 29 | fn main() { 30 | let matches = App::new("dotenv") 31 | .about("Run a command using the environment in a .env file") 32 | .usage("dotenv [ARGS]...") 33 | .setting(AppSettings::AllowExternalSubcommands) 34 | .setting(AppSettings::ArgRequiredElseHelp) 35 | .setting(AppSettings::UnifiedHelpMessage) 36 | .arg( 37 | Arg::with_name("FILE") 38 | .short("f") 39 | .long("file") 40 | .takes_value(true) 41 | .help("Use a specific .env file (defaults to .env)"), 42 | ) 43 | .get_matches(); 44 | 45 | match matches.value_of("FILE") { 46 | None => dotenv::dotenv(), 47 | Some(file) => dotenv::from_filename(file), 48 | } 49 | .unwrap_or_else(|e| die!("error: failed to load environment: {}", e)); 50 | 51 | let mut command = match matches.subcommand() { 52 | (name, Some(matches)) => { 53 | let args = matches 54 | .values_of("") 55 | .map(|v| v.collect()) 56 | .unwrap_or(Vec::new()); 57 | 58 | make_command(name, args) 59 | } 60 | _ => die!("error: missing required argument "), 61 | }; 62 | 63 | if cfg!(target_os = "windows") { 64 | match command.spawn().and_then(|mut child| child.wait()) { 65 | Ok(status) => exit(status.code().unwrap_or(1)), 66 | Err(error) => die!("fatal: {}", error), 67 | }; 68 | } else { 69 | let error = command.exec(); 70 | die!("fatal: {}", error); 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /dotenv/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | use std::io; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | LineParse(String, usize), 10 | Io(io::Error), 11 | EnvVar(std::env::VarError), 12 | #[doc(hidden)] 13 | __Nonexhaustive, 14 | } 15 | 16 | impl Error { 17 | pub fn not_found(&self) -> bool { 18 | if let Error::Io(ref io_error) = *self { 19 | return io_error.kind() == io::ErrorKind::NotFound; 20 | } 21 | false 22 | } 23 | } 24 | 25 | impl error::Error for Error { 26 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 27 | match self { 28 | Error::Io(err) => Some(err), 29 | Error::EnvVar(err) => Some(err), 30 | _ => None, 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for Error { 36 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 37 | match self { 38 | Error::Io(err) => write!(fmt, "{}", err), 39 | Error::EnvVar(err) => write!(fmt, "{}", err), 40 | Error::LineParse(line, error_index) => write!( 41 | fmt, 42 | "Error parsing line: '{}', error at line index: {}", 43 | line, error_index 44 | ), 45 | _ => unreachable!(), 46 | } 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod test { 52 | use std::error::Error as StdError; 53 | 54 | use super::*; 55 | 56 | #[test] 57 | fn test_io_error_source() { 58 | let err = Error::Io(std::io::ErrorKind::PermissionDenied.into()); 59 | let io_err = err 60 | .source() 61 | .unwrap() 62 | .downcast_ref::() 63 | .unwrap(); 64 | assert_eq!(std::io::ErrorKind::PermissionDenied, io_err.kind()); 65 | } 66 | 67 | #[test] 68 | fn test_envvar_error_source() { 69 | let err = Error::EnvVar(std::env::VarError::NotPresent); 70 | let var_err = err 71 | .source() 72 | .unwrap() 73 | .downcast_ref::() 74 | .unwrap(); 75 | assert_eq!(&std::env::VarError::NotPresent, var_err); 76 | } 77 | 78 | #[test] 79 | fn test_lineparse_error_source() { 80 | let err = Error::LineParse("test line".to_string(), 2); 81 | assert!(err.source().is_none()); 82 | } 83 | 84 | #[test] 85 | fn test_error_not_found_true() { 86 | let err = Error::Io(std::io::ErrorKind::NotFound.into()); 87 | assert!(err.not_found()); 88 | } 89 | 90 | #[test] 91 | fn test_error_not_found_false() { 92 | let err = Error::Io(std::io::ErrorKind::PermissionDenied.into()); 93 | assert!(!err.not_found()); 94 | } 95 | 96 | #[test] 97 | fn test_io_error_display() { 98 | let err = Error::Io(std::io::ErrorKind::PermissionDenied.into()); 99 | let io_err: std::io::Error = std::io::ErrorKind::PermissionDenied.into(); 100 | 101 | let err_desc = format!("{}", err); 102 | let io_err_desc = format!("{}", io_err); 103 | assert_eq!(io_err_desc, err_desc); 104 | } 105 | 106 | #[test] 107 | fn test_envvar_error_display() { 108 | let err = Error::EnvVar(std::env::VarError::NotPresent); 109 | let var_err = std::env::VarError::NotPresent; 110 | 111 | let err_desc = format!("{}", err); 112 | let var_err_desc = format!("{}", var_err); 113 | assert_eq!(var_err_desc, err_desc); 114 | } 115 | 116 | #[test] 117 | fn test_lineparse_error_display() { 118 | let err = Error::LineParse("test line".to_string(), 2); 119 | let err_desc = format!("{}", err); 120 | assert_eq!( 121 | "Error parsing line: 'test line', error at line index: 2", 122 | err_desc 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /dotenv/src/find.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::{Path, PathBuf}; 3 | use std::{env, fs, io}; 4 | 5 | use crate::errors::*; 6 | use crate::iter::Iter; 7 | 8 | pub struct Finder<'a> { 9 | filename: &'a Path, 10 | } 11 | 12 | impl<'a> Finder<'a> { 13 | pub fn new() -> Self { 14 | Finder { 15 | filename: Path::new(".env"), 16 | } 17 | } 18 | 19 | pub fn filename(mut self, filename: &'a Path) -> Self { 20 | self.filename = filename; 21 | self 22 | } 23 | 24 | pub fn find(self) -> Result<(PathBuf, Iter)> { 25 | let path = find(&env::current_dir().map_err(Error::Io)?, self.filename)?; 26 | let file = File::open(&path).map_err(Error::Io)?; 27 | let iter = Iter::new(file); 28 | Ok((path, iter)) 29 | } 30 | } 31 | 32 | /// Searches for `filename` in `directory` and parent directories until found or root is reached. 33 | pub fn find(directory: &Path, filename: &Path) -> Result { 34 | let candidate = directory.join(filename); 35 | 36 | match fs::metadata(&candidate) { 37 | Ok(metadata) => { 38 | if metadata.is_file() { 39 | return Ok(candidate); 40 | } 41 | } 42 | Err(error) => { 43 | if error.kind() != io::ErrorKind::NotFound { 44 | return Err(Error::Io(error)); 45 | } 46 | } 47 | } 48 | 49 | if let Some(parent) = directory.parent() { 50 | find(parent, filename) 51 | } else { 52 | Err(Error::Io(io::Error::new( 53 | io::ErrorKind::NotFound, 54 | "path not found", 55 | ))) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dotenv/src/iter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::io::prelude::*; 4 | use std::io::{BufReader, Lines}; 5 | 6 | use crate::errors::*; 7 | use crate::parse; 8 | 9 | pub struct Iter { 10 | lines: Lines>, 11 | substitution_data: HashMap>, 12 | } 13 | 14 | impl Iter { 15 | pub fn new(reader: R) -> Iter { 16 | Iter { 17 | lines: BufReader::new(reader).lines(), 18 | substitution_data: HashMap::new(), 19 | } 20 | } 21 | 22 | pub fn load(self) -> Result<()> { 23 | for item in self { 24 | let (key, value) = item?; 25 | if env::var(&key).is_err() { 26 | env::set_var(&key, value); 27 | } 28 | } 29 | 30 | Ok(()) 31 | } 32 | } 33 | 34 | impl Iterator for Iter { 35 | type Item = Result<(String, String)>; 36 | 37 | fn next(&mut self) -> Option { 38 | loop { 39 | let line = match self.lines.next() { 40 | Some(Ok(line)) => line, 41 | Some(Err(err)) => return Some(Err(Error::Io(err))), 42 | None => return None, 43 | }; 44 | 45 | match parse::parse_line(&line, &mut self.substitution_data) { 46 | Ok(Some(result)) => return Some(Ok(result)), 47 | Ok(None) => {} 48 | Err(err) => return Some(Err(err)), 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dotenv/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides a configuration loader in the style of the [ruby dotenv 2 | //! gem](https://github.com/bkeepers/dotenv). This library is meant to be used 3 | //! on development or testing environments in which setting environment 4 | //! variables is not practical. It loads environment variables from a .env 5 | //! file, if available, and mashes those with the actual environment variables 6 | //! provided by the operating system. 7 | 8 | mod errors; 9 | mod find; 10 | mod iter; 11 | mod parse; 12 | 13 | use std::env::{self, Vars}; 14 | use std::ffi::OsStr; 15 | use std::fs::File; 16 | use std::path::{Path, PathBuf}; 17 | use std::sync::Once; 18 | 19 | pub use crate::errors::*; 20 | use crate::find::Finder; 21 | use crate::iter::Iter; 22 | 23 | static START: Once = Once::new(); 24 | 25 | /// After loading the dotenv file, fetches the environment variable key from the current process. 26 | /// 27 | /// The returned result is Ok(s) if the environment variable is present and is valid unicode. If the 28 | /// environment variable is not present, or it is not valid unicode, then Err will be returned. 29 | /// 30 | /// Examples: 31 | /// 32 | /// ```no_run 33 | /// 34 | /// use dotenv; 35 | /// 36 | /// let key = "FOO"; 37 | /// let value= dotenv::var(key).unwrap(); 38 | /// ``` 39 | pub fn var>(key: K) -> Result { 40 | START.call_once(|| { 41 | dotenv().ok(); 42 | }); 43 | env::var(key).map_err(Error::EnvVar) 44 | } 45 | 46 | /// After loading the dotenv file, returns an iterator of (variable, value) pairs of strings, 47 | /// for all the environment variables of the current process. 48 | /// 49 | /// The returned iterator contains a snapshot of the process's environment variables at the 50 | /// time of this invocation, modifications to environment variables afterwards will not be 51 | /// reflected in the returned iterator. 52 | /// 53 | /// Examples: 54 | /// 55 | /// ```no_run 56 | /// 57 | /// use dotenv; 58 | /// use std::io; 59 | /// 60 | /// let result: Vec<(String, String)> = dotenv::vars().collect(); 61 | /// ``` 62 | pub fn vars() -> Vars { 63 | START.call_once(|| { 64 | dotenv().ok(); 65 | }); 66 | env::vars() 67 | } 68 | 69 | /// Loads the file at the specified absolute path. 70 | /// 71 | /// Examples 72 | /// 73 | /// ``` 74 | /// use dotenv; 75 | /// use std::env; 76 | /// use std::path::{Path}; 77 | /// 78 | /// let my_path = env::home_dir().and_then(|a| Some(a.join("/.env"))).unwrap(); 79 | /// dotenv::from_path(my_path.as_path()); 80 | /// ``` 81 | pub fn from_path>(path: P) -> Result<()> { 82 | let iter = Iter::new(File::open(path).map_err(Error::Io)?); 83 | iter.load() 84 | } 85 | 86 | /// Like `from_path`, but returns an iterator over variables instead of loading into environment. 87 | /// 88 | /// Examples 89 | /// 90 | /// ```no_run 91 | /// use dotenv; 92 | /// use std::env; 93 | /// use std::path::{Path}; 94 | /// 95 | /// let my_path = env::home_dir().and_then(|a| Some(a.join("/.env"))).unwrap(); 96 | /// let iter = dotenv::from_path_iter(my_path.as_path()).unwrap(); 97 | /// 98 | /// for item in iter { 99 | /// let (key, val) = item.unwrap(); 100 | /// println!("{}={}", key, val); 101 | /// } 102 | /// ``` 103 | pub fn from_path_iter>(path: P) -> Result> { 104 | Ok(Iter::new(File::open(path).map_err(Error::Io)?)) 105 | } 106 | 107 | /// Loads the specified file from the environment's current directory or its parents in sequence. 108 | /// 109 | /// # Examples 110 | /// ``` 111 | /// use dotenv; 112 | /// dotenv::from_filename("custom.env").ok(); 113 | /// ``` 114 | /// 115 | /// It is also possible to do the following, but it is equivalent to using `dotenv::dotenv()`, 116 | /// which is preferred. 117 | /// 118 | /// ``` 119 | /// use dotenv; 120 | /// dotenv::from_filename(".env").ok(); 121 | /// ``` 122 | pub fn from_filename>(filename: P) -> Result { 123 | let (path, iter) = Finder::new().filename(filename.as_ref()).find()?; 124 | iter.load()?; 125 | Ok(path) 126 | } 127 | 128 | /// Like `from_filename`, but returns an iterator over variables instead of loading into environment. 129 | /// 130 | /// # Examples 131 | /// ``` 132 | /// use dotenv; 133 | /// dotenv::from_filename("custom.env").ok(); 134 | /// ``` 135 | /// 136 | /// It is also possible to do the following, but it is equivalent to using `dotenv::dotenv()`, 137 | /// which is preferred. 138 | /// 139 | /// ```no_run 140 | /// use dotenv; 141 | /// let iter = dotenv::from_filename_iter(".env").unwrap(); 142 | /// 143 | /// for item in iter { 144 | /// let (key, val) = item.unwrap(); 145 | /// println!("{}={}", key, val); 146 | /// } 147 | /// ``` 148 | pub fn from_filename_iter>(filename: P) -> Result> { 149 | let (_, iter) = Finder::new().filename(filename.as_ref()).find()?; 150 | Ok(iter) 151 | } 152 | 153 | /// This is usually what you want. 154 | /// It loads the .env file located in the environment's current directory or its parents in sequence. 155 | /// 156 | /// # Examples 157 | /// ``` 158 | /// use dotenv; 159 | /// dotenv::dotenv().ok(); 160 | /// ``` 161 | pub fn dotenv() -> Result { 162 | let (path, iter) = Finder::new().find()?; 163 | iter.load()?; 164 | Ok(path) 165 | } 166 | 167 | /// Like `dotenv`, but returns an iterator over variables instead of loading into environment. 168 | /// 169 | /// # Examples 170 | /// ```no_run 171 | /// use dotenv; 172 | /// 173 | /// for item in dotenv::dotenv_iter().unwrap() { 174 | /// let (key, val) = item.unwrap(); 175 | /// println!("{}={}", key, val); 176 | /// } 177 | /// ``` 178 | pub fn dotenv_iter() -> Result> { 179 | let (_, iter) = Finder::new().find()?; 180 | Ok(iter) 181 | } 182 | -------------------------------------------------------------------------------- /dotenv/src/parse.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::errors::*; 4 | 5 | // for readability's sake 6 | pub type ParsedLine = Result>; 7 | 8 | pub fn parse_line( 9 | line: &str, 10 | substitution_data: &mut HashMap>, 11 | ) -> ParsedLine { 12 | let mut parser = LineParser::new(line, substitution_data); 13 | parser.parse_line() 14 | } 15 | 16 | struct LineParser<'a> { 17 | original_line: &'a str, 18 | substitution_data: &'a mut HashMap>, 19 | line: &'a str, 20 | pos: usize, 21 | } 22 | 23 | impl<'a> LineParser<'a> { 24 | fn new( 25 | line: &'a str, 26 | substitution_data: &'a mut HashMap>, 27 | ) -> LineParser<'a> { 28 | LineParser { 29 | original_line: line, 30 | substitution_data, 31 | line: line.trim_end(), // we don’t want trailing whitespace 32 | pos: 0, 33 | } 34 | } 35 | 36 | fn err(&self) -> Error { 37 | Error::LineParse(self.original_line.into(), self.pos) 38 | } 39 | 40 | fn parse_line(&mut self) -> ParsedLine { 41 | self.skip_whitespace(); 42 | // if its an empty line or a comment, skip it 43 | if self.line.is_empty() || self.line.starts_with('#') { 44 | return Ok(None); 45 | } 46 | 47 | let mut key = self.parse_key()?; 48 | self.skip_whitespace(); 49 | 50 | // export can be either an optional prefix or a key itself 51 | if key == "export" { 52 | // here we check for an optional `=`, below we throw directly when it’s not found. 53 | if self.expect_equal().is_err() { 54 | key = self.parse_key()?; 55 | self.skip_whitespace(); 56 | self.expect_equal()?; 57 | } 58 | } else { 59 | self.expect_equal()?; 60 | } 61 | self.skip_whitespace(); 62 | 63 | if self.line.is_empty() || self.line.starts_with('#') { 64 | self.substitution_data.insert(key.clone(), None); 65 | return Ok(Some((key, String::new()))); 66 | } 67 | 68 | let parsed_value = parse_value(self.line, &mut self.substitution_data)?; 69 | self.substitution_data 70 | .insert(key.clone(), Some(parsed_value.clone())); 71 | 72 | Ok(Some((key, parsed_value))) 73 | } 74 | 75 | fn parse_key(&mut self) -> Result { 76 | if !self 77 | .line 78 | .starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') 79 | { 80 | return Err(self.err()); 81 | } 82 | let index = match self 83 | .line 84 | .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.')) 85 | { 86 | Some(index) => index, 87 | None => self.line.len(), 88 | }; 89 | self.pos += index; 90 | let key = String::from(&self.line[..index]); 91 | self.line = &self.line[index..]; 92 | Ok(key) 93 | } 94 | 95 | fn expect_equal(&mut self) -> Result<()> { 96 | if !self.line.starts_with('=') { 97 | return Err(self.err()); 98 | } 99 | self.line = &self.line[1..]; 100 | self.pos += 1; 101 | Ok(()) 102 | } 103 | 104 | fn skip_whitespace(&mut self) { 105 | if let Some(index) = self.line.find(|c: char| !c.is_whitespace()) { 106 | self.pos += index; 107 | self.line = &self.line[index..]; 108 | } else { 109 | self.pos += self.line.len(); 110 | self.line = ""; 111 | } 112 | } 113 | } 114 | 115 | #[derive(Eq, PartialEq)] 116 | enum SubstitutionMode { 117 | None, 118 | Block, 119 | EscapedBlock, 120 | } 121 | 122 | fn parse_value( 123 | input: &str, 124 | substitution_data: &mut HashMap>, 125 | ) -> Result { 126 | let mut strong_quote = false; // ' 127 | let mut weak_quote = false; // " 128 | let mut escaped = false; 129 | let mut expecting_end = false; 130 | 131 | //FIXME can this be done without yet another allocation per line? 132 | let mut output = String::new(); 133 | 134 | let mut substitution_mode = SubstitutionMode::None; 135 | let mut substitution_name = String::new(); 136 | 137 | for (index, c) in input.chars().enumerate() { 138 | //the regex _should_ already trim whitespace off the end 139 | //expecting_end is meant to permit: k=v #comment 140 | //without affecting: k=v#comment 141 | //and throwing on: k=v w 142 | if expecting_end { 143 | if c == ' ' || c == '\t' { 144 | continue; 145 | } else if c == '#' { 146 | break; 147 | } else { 148 | return Err(Error::LineParse(input.to_owned(), index)); 149 | } 150 | } else if escaped { 151 | //TODO I tried handling literal \r but various issues 152 | //imo not worth worrying about until there's a use case 153 | //(actually handling backslash 0x10 would be a whole other matter) 154 | //then there's \v \f bell hex... etc 155 | match c { 156 | '\\' | '\'' | '"' | '$' | ' ' => output.push(c), 157 | 'n' => output.push('\n'), // handle \n case 158 | _ => { 159 | return Err(Error::LineParse(input.to_owned(), index)); 160 | } 161 | } 162 | 163 | escaped = false; 164 | } else if strong_quote { 165 | if c == '\'' { 166 | strong_quote = false; 167 | } else { 168 | output.push(c); 169 | } 170 | } else if substitution_mode != SubstitutionMode::None { 171 | if c.is_alphanumeric() { 172 | substitution_name.push(c); 173 | } else { 174 | match substitution_mode { 175 | SubstitutionMode::None => unreachable!(), 176 | SubstitutionMode::Block => { 177 | if c == '{' && substitution_name.is_empty() { 178 | substitution_mode = SubstitutionMode::EscapedBlock; 179 | } else { 180 | apply_substitution( 181 | substitution_data, 182 | &substitution_name.drain(..).collect::(), 183 | &mut output, 184 | ); 185 | if c == '$' { 186 | substitution_mode = if !strong_quote && !escaped { 187 | SubstitutionMode::Block 188 | } else { 189 | SubstitutionMode::None 190 | } 191 | } else { 192 | substitution_mode = SubstitutionMode::None; 193 | output.push(c); 194 | } 195 | } 196 | } 197 | SubstitutionMode::EscapedBlock => { 198 | if c == '}' { 199 | substitution_mode = SubstitutionMode::None; 200 | apply_substitution( 201 | substitution_data, 202 | &substitution_name.drain(..).collect::(), 203 | &mut output, 204 | ); 205 | } else { 206 | substitution_name.push(c); 207 | } 208 | } 209 | } 210 | } 211 | } else if c == '$' { 212 | substitution_mode = if !strong_quote && !escaped { 213 | SubstitutionMode::Block 214 | } else { 215 | SubstitutionMode::None 216 | } 217 | } else if weak_quote { 218 | if c == '"' { 219 | weak_quote = false; 220 | } else if c == '\\' { 221 | escaped = true; 222 | } else { 223 | output.push(c); 224 | } 225 | } else if c == '\'' { 226 | strong_quote = true; 227 | } else if c == '"' { 228 | weak_quote = true; 229 | } else if c == '\\' { 230 | escaped = true; 231 | } else if c == ' ' || c == '\t' { 232 | expecting_end = true; 233 | } else { 234 | output.push(c); 235 | } 236 | } 237 | 238 | //XXX also fail if escaped? or... 239 | if substitution_mode == SubstitutionMode::EscapedBlock || strong_quote || weak_quote { 240 | let value_length = input.len(); 241 | Err(Error::LineParse( 242 | input.to_owned(), 243 | if value_length == 0 { 244 | 0 245 | } else { 246 | value_length - 1 247 | }, 248 | )) 249 | } else { 250 | apply_substitution( 251 | substitution_data, 252 | &substitution_name.drain(..).collect::(), 253 | &mut output, 254 | ); 255 | Ok(output) 256 | } 257 | } 258 | 259 | fn apply_substitution( 260 | substitution_data: &mut HashMap>, 261 | substitution_name: &str, 262 | output: &mut String, 263 | ) { 264 | if let Ok(environment_value) = std::env::var(substitution_name) { 265 | output.push_str(&environment_value); 266 | } else { 267 | let stored_value = substitution_data 268 | .get(substitution_name) 269 | .unwrap_or(&None) 270 | .to_owned(); 271 | output.push_str(&stored_value.unwrap_or_else(String::new)); 272 | }; 273 | } 274 | 275 | #[cfg(test)] 276 | mod test { 277 | use crate::iter::Iter; 278 | 279 | use super::*; 280 | 281 | #[test] 282 | fn test_parse_line_env() { 283 | // Note 5 spaces after 'KEY8=' below 284 | let actual_iter = Iter::new( 285 | r#" 286 | KEY=1 287 | KEY2="2" 288 | KEY3='3' 289 | KEY4='fo ur' 290 | KEY5="fi ve" 291 | KEY6=s\ ix 292 | KEY7= 293 | KEY8= 294 | KEY9= # foo 295 | KEY10 ="whitespace before =" 296 | KEY11= "whitespace after =" 297 | export="export as key" 298 | export SHELL_LOVER=1 299 | "# 300 | .as_bytes(), 301 | ); 302 | 303 | let expected_iter = vec![ 304 | ("KEY", "1"), 305 | ("KEY2", "2"), 306 | ("KEY3", "3"), 307 | ("KEY4", "fo ur"), 308 | ("KEY5", "fi ve"), 309 | ("KEY6", "s ix"), 310 | ("KEY7", ""), 311 | ("KEY8", ""), 312 | ("KEY9", ""), 313 | ("KEY10", "whitespace before ="), 314 | ("KEY11", "whitespace after ="), 315 | ("export", "export as key"), 316 | ("SHELL_LOVER", "1"), 317 | ] 318 | .into_iter() 319 | .map(|(key, value)| (key.to_string(), value.to_string())); 320 | 321 | let mut count = 0; 322 | for (expected, actual) in expected_iter.zip(actual_iter) { 323 | assert!(actual.is_ok()); 324 | assert_eq!(expected, actual.ok().unwrap()); 325 | count += 1; 326 | } 327 | 328 | assert_eq!(count, 13); 329 | } 330 | 331 | #[test] 332 | fn test_parse_line_comment() { 333 | let result: Result> = Iter::new( 334 | r#" 335 | # foo=bar 336 | # "# 337 | .as_bytes(), 338 | ) 339 | .collect(); 340 | assert!(result.unwrap().is_empty()); 341 | } 342 | 343 | #[test] 344 | fn test_parse_line_invalid() { 345 | // Note 4 spaces after 'invalid' below 346 | let actual_iter = Iter::new( 347 | r#" 348 | invalid 349 | very bacon = yes indeed 350 | =value"# 351 | .as_bytes(), 352 | ); 353 | 354 | let mut count = 0; 355 | for actual in actual_iter { 356 | assert!(actual.is_err()); 357 | count += 1; 358 | } 359 | assert_eq!(count, 3); 360 | } 361 | 362 | #[test] 363 | fn test_parse_value_escapes() { 364 | let actual_iter = Iter::new( 365 | r#" 366 | KEY=my\ cool\ value 367 | KEY2=\$sweet 368 | KEY3="awesome stuff \"mang\"" 369 | KEY4='sweet $\fgs'\''fds' 370 | KEY5="'\"yay\\"\ "stuff" 371 | KEY6="lol" #well you see when I say lol wh 372 | KEY7="line 1\nline 2" 373 | "# 374 | .as_bytes(), 375 | ); 376 | 377 | let expected_iter = vec![ 378 | ("KEY", r#"my cool value"#), 379 | ("KEY2", r#"$sweet"#), 380 | ("KEY3", r#"awesome stuff "mang""#), 381 | ("KEY4", r#"sweet $\fgs'fds"#), 382 | ("KEY5", r#"'"yay\ stuff"#), 383 | ("KEY6", "lol"), 384 | ("KEY7", "line 1\nline 2"), 385 | ] 386 | .into_iter() 387 | .map(|(key, value)| (key.to_string(), value.to_string())); 388 | 389 | for (expected, actual) in expected_iter.zip(actual_iter) { 390 | assert!(actual.is_ok()); 391 | assert_eq!(expected, actual.unwrap()); 392 | } 393 | } 394 | 395 | #[test] 396 | fn test_parse_value_escapes_invalid() { 397 | let actual_iter = Iter::new( 398 | r#" 399 | KEY=my uncool value 400 | KEY2="why 401 | KEY3='please stop'' 402 | KEY4=h\8u 403 | "# 404 | .as_bytes(), 405 | ); 406 | 407 | for actual in actual_iter { 408 | assert!(actual.is_err()); 409 | } 410 | } 411 | } 412 | 413 | #[cfg(test)] 414 | mod variable_substitution_tests { 415 | use crate::iter::Iter; 416 | 417 | fn assert_parsed_string(input_string: &str, expected_parse_result: Vec<(&str, &str)>) { 418 | let actual_iter = Iter::new(input_string.as_bytes()); 419 | let expected_count = &expected_parse_result.len(); 420 | 421 | let expected_iter = expected_parse_result 422 | .into_iter() 423 | .map(|(key, value)| (key.to_string(), value.to_string())); 424 | 425 | let mut count = 0; 426 | for (expected, actual) in expected_iter.zip(actual_iter) { 427 | assert!(actual.is_ok()); 428 | assert_eq!(expected, actual.ok().unwrap()); 429 | count += 1; 430 | } 431 | 432 | assert_eq!(count, *expected_count); 433 | } 434 | 435 | #[test] 436 | fn variable_in_parenthesis_surrounded_by_quotes() { 437 | assert_parsed_string( 438 | r#" 439 | KEY=test 440 | KEY1="${KEY}" 441 | "#, 442 | vec![("KEY", "test"), ("KEY1", "test")], 443 | ); 444 | } 445 | 446 | #[test] 447 | fn substitute_undefined_variables_to_empty_string() { 448 | assert_parsed_string(r#"KEY=">$KEY1<>${KEY2}<""#, vec![("KEY", "><><")]); 449 | } 450 | 451 | #[test] 452 | fn do_not_substitute_variables_with_dollar_escaped() { 453 | assert_parsed_string( 454 | "KEY=>\\$KEY1<>\\${KEY2}<", 455 | vec![("KEY", ">$KEY1<>${KEY2}<")], 456 | ); 457 | } 458 | 459 | #[test] 460 | fn do_not_substitute_variables_in_weak_quotes_with_dollar_escaped() { 461 | assert_parsed_string( 462 | r#"KEY=">\$KEY1<>\${KEY2}<""#, 463 | vec![("KEY", ">$KEY1<>${KEY2}<")], 464 | ); 465 | } 466 | 467 | #[test] 468 | fn do_not_substitute_variables_in_strong_quotes() { 469 | assert_parsed_string("KEY='>${KEY1}<>$KEY2<'", vec![("KEY", ">${KEY1}<>$KEY2<")]); 470 | } 471 | 472 | #[test] 473 | fn same_variable_reused() { 474 | assert_parsed_string( 475 | r#" 476 | KEY=VALUE 477 | KEY1=$KEY$KEY 478 | "#, 479 | vec![("KEY", "VALUE"), ("KEY1", "VALUEVALUE")], 480 | ); 481 | } 482 | 483 | #[test] 484 | fn with_dot() { 485 | assert_parsed_string( 486 | r#" 487 | KEY.Value=VALUE 488 | "#, 489 | vec![("KEY.Value", "VALUE")], 490 | ); 491 | } 492 | 493 | #[test] 494 | fn recursive_substitution() { 495 | assert_parsed_string( 496 | r#" 497 | KEY=${KEY1}+KEY_VALUE 498 | KEY1=${KEY}+KEY1_VALUE 499 | "#, 500 | vec![("KEY", "+KEY_VALUE"), ("KEY1", "+KEY_VALUE+KEY1_VALUE")], 501 | ); 502 | } 503 | 504 | #[test] 505 | fn variable_without_parenthesis_is_substituted_before_separators() { 506 | assert_parsed_string( 507 | r#" 508 | KEY1=test_user 509 | KEY1_1=test_user_with_separator 510 | KEY=">$KEY1_1<>$KEY1}<>$KEY1{<" 511 | "#, 512 | vec![ 513 | ("KEY1", "test_user"), 514 | ("KEY1_1", "test_user_with_separator"), 515 | ("KEY", ">test_user_1<>test_user}<>test_user{<"), 516 | ], 517 | ); 518 | } 519 | 520 | #[test] 521 | fn substitute_variable_from_env_variable() { 522 | std::env::set_var("KEY11", "test_user_env"); 523 | 524 | assert_parsed_string(r#"KEY=">${KEY11}<""#, vec![("KEY", ">test_user_env<")]); 525 | } 526 | 527 | #[test] 528 | fn substitute_variable_env_variable_overrides_dotenv_in_substitution() { 529 | std::env::set_var("KEY11", "test_user_env"); 530 | 531 | assert_parsed_string( 532 | r#" 533 | KEY11=test_user 534 | KEY=">${KEY11}<" 535 | "#, 536 | vec![("KEY11", "test_user"), ("KEY", ">test_user_env<")], 537 | ); 538 | } 539 | 540 | #[test] 541 | fn consequent_substitutions() { 542 | assert_parsed_string( 543 | r#" 544 | KEY1=test_user 545 | KEY2=$KEY1_2 546 | KEY=>${KEY1}<>${KEY2}< 547 | "#, 548 | vec![ 549 | ("KEY1", "test_user"), 550 | ("KEY2", "test_user_2"), 551 | ("KEY", ">test_user<>test_user_2<"), 552 | ], 553 | ); 554 | } 555 | 556 | #[test] 557 | fn consequent_substitutions_with_one_missing() { 558 | assert_parsed_string( 559 | r#" 560 | KEY2=$KEY1_2 561 | KEY=>${KEY1}<>${KEY2}< 562 | "#, 563 | vec![("KEY2", "_2"), ("KEY", "><>_2<")], 564 | ); 565 | } 566 | } 567 | 568 | #[cfg(test)] 569 | mod error_tests { 570 | use crate::errors::Error::LineParse; 571 | use crate::iter::Iter; 572 | 573 | #[test] 574 | fn should_not_parse_unfinished_substitutions() { 575 | let wrong_value = ">${KEY{<"; 576 | 577 | let parsed_values: Vec<_> = Iter::new( 578 | format!( 579 | r#" 580 | KEY=VALUE 581 | KEY1={} 582 | "#, 583 | wrong_value 584 | ) 585 | .as_bytes(), 586 | ) 587 | .collect(); 588 | 589 | assert_eq!(parsed_values.len(), 2); 590 | 591 | if let Ok(first_line) = &parsed_values[0] { 592 | assert_eq!(first_line, &(String::from("KEY"), String::from("VALUE"))) 593 | } else { 594 | assert!(false, "Expected the first value to be parsed") 595 | } 596 | 597 | if let Err(LineParse(second_value, index)) = &parsed_values[1] { 598 | assert_eq!(second_value, wrong_value); 599 | assert_eq!(*index, wrong_value.len() - 1) 600 | } else { 601 | assert!(false, "Expected the second value not to be parsed") 602 | } 603 | } 604 | 605 | #[test] 606 | fn should_not_allow_dot_as_first_character_of_key() { 607 | let wrong_key_value = ".Key=VALUE"; 608 | 609 | let parsed_values: Vec<_> = Iter::new(wrong_key_value.as_bytes()).collect(); 610 | 611 | assert_eq!(parsed_values.len(), 1); 612 | 613 | if let Err(LineParse(second_value, index)) = &parsed_values[0] { 614 | assert_eq!(second_value, wrong_key_value); 615 | assert_eq!(*index, 0) 616 | } else { 617 | assert!(false, "Expected the second value not to be parsed") 618 | } 619 | } 620 | 621 | #[test] 622 | fn should_not_parse_illegal_format() { 623 | let wrong_format = r"<><><>"; 624 | let parsed_values: Vec<_> = Iter::new(wrong_format.as_bytes()).collect(); 625 | 626 | assert_eq!(parsed_values.len(), 1); 627 | 628 | if let Err(LineParse(wrong_value, index)) = &parsed_values[0] { 629 | assert_eq!(wrong_value, wrong_format); 630 | assert_eq!(*index, 0) 631 | } else { 632 | assert!(false, "Expected the second value not to be parsed") 633 | } 634 | } 635 | 636 | #[test] 637 | fn should_not_parse_illegal_escape() { 638 | let wrong_escape = r">\f<"; 639 | let parsed_values: Vec<_> = 640 | Iter::new(format!("VALUE={}", wrong_escape).as_bytes()).collect(); 641 | 642 | assert_eq!(parsed_values.len(), 1); 643 | 644 | if let Err(LineParse(wrong_value, index)) = &parsed_values[0] { 645 | assert_eq!(wrong_value, wrong_escape); 646 | assert_eq!(*index, wrong_escape.find("\\").unwrap() + 1) 647 | } else { 648 | assert!(false, "Expected the second value not to be parsed") 649 | } 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /dotenv/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::prelude::*; 3 | use std::{env, io}; 4 | use tempfile::{tempdir, TempDir}; 5 | 6 | pub fn tempdir_with_dotenv(dotenv_text: &str) -> io::Result { 7 | let dir = tempdir()?; 8 | env::set_current_dir(dir.path())?; 9 | let dotenv_path = dir.path().join(".env"); 10 | let mut dotenv_file = File::create(dotenv_path)?; 11 | dotenv_file.write_all(dotenv_text.as_bytes())?; 12 | dotenv_file.sync_all()?; 13 | Ok(dir) 14 | } 15 | 16 | pub fn make_test_dotenv() -> io::Result { 17 | tempdir_with_dotenv("TESTKEY=test_val") 18 | } 19 | -------------------------------------------------------------------------------- /dotenv/tests/test-child-dir.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::{env, fs}; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_child_dir() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | fs::create_dir("child").unwrap(); 13 | 14 | env::set_current_dir("child").unwrap(); 15 | 16 | dotenv().ok(); 17 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 18 | 19 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 20 | dir.close().unwrap(); 21 | } 22 | -------------------------------------------------------------------------------- /dotenv/tests/test-default-location.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_default_location() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | dotenv().ok(); 13 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 14 | 15 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 16 | dir.close().unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /dotenv/tests/test-dotenv-iter.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_dotenv_iter() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | let iter = dotenv_iter().unwrap(); 13 | 14 | assert!(env::var("TESTKEY").is_err()); 15 | 16 | iter.load().ok(); 17 | 18 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 19 | 20 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 21 | dir.close().unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /dotenv/tests/test-from-filename-iter.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_from_filename_iter() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | let iter = from_filename_iter(".env").unwrap(); 13 | 14 | assert!(env::var("TESTKEY").is_err()); 15 | 16 | iter.load().ok(); 17 | 18 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 19 | 20 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 21 | dir.close().unwrap(); 22 | } 23 | -------------------------------------------------------------------------------- /dotenv/tests/test-from-filename.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_from_filename() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | from_filename(".env").ok(); 13 | 14 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 15 | 16 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 17 | dir.close().unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /dotenv/tests/test-from-path-iter.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_from_path_iter() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | let mut path = env::current_dir().unwrap(); 13 | path.push(".env"); 14 | 15 | let iter = from_path_iter(&path).unwrap(); 16 | 17 | assert!(env::var("TESTKEY").is_err()); 18 | 19 | iter.load().ok(); 20 | 21 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 22 | 23 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 24 | dir.close().unwrap(); 25 | } 26 | -------------------------------------------------------------------------------- /dotenv/tests/test-from-path.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_from_path() { 10 | let dir = make_test_dotenv().unwrap(); 11 | 12 | let mut path = env::current_dir().unwrap(); 13 | path.push(".env"); 14 | 15 | from_path(&path).ok(); 16 | 17 | assert_eq!(env::var("TESTKEY").unwrap(), "test_val"); 18 | 19 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 20 | dir.close().unwrap(); 21 | } 22 | -------------------------------------------------------------------------------- /dotenv/tests/test-var.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use std::env; 4 | 5 | use dotenv::*; 6 | 7 | use crate::common::*; 8 | 9 | #[test] 10 | fn test_var() { 11 | let dir = make_test_dotenv().unwrap(); 12 | 13 | assert_eq!(var("TESTKEY").unwrap(), "test_val"); 14 | 15 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 16 | dir.close().unwrap(); 17 | } 18 | -------------------------------------------------------------------------------- /dotenv/tests/test-variable-substitution.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use dotenv::*; 4 | use std::env; 5 | 6 | use crate::common::*; 7 | 8 | #[test] 9 | fn test_variable_substitutions() { 10 | std::env::set_var("KEY", "value"); 11 | std::env::set_var("KEY1", "value1"); 12 | 13 | let substitutions_to_test = [ 14 | "$ZZZ", "$KEY", "$KEY1", "${KEY}1", "$KEY_U", "${KEY_U}", "\\$KEY", 15 | ]; 16 | 17 | let common_string = substitutions_to_test.join(">>"); 18 | let dir = tempdir_with_dotenv(&format!( 19 | r#" 20 | KEY1=new_value1 21 | KEY_U=$KEY+valueU 22 | 23 | SUBSTITUTION_FOR_STRONG_QUOTES='{}' 24 | SUBSTITUTION_FOR_WEAK_QUOTES="{}" 25 | SUBSTITUTION_WITHOUT_QUOTES={} 26 | "#, 27 | common_string, common_string, common_string 28 | )) 29 | .unwrap(); 30 | 31 | assert_eq!(var("KEY").unwrap(), "value"); 32 | assert_eq!(var("KEY1").unwrap(), "value1"); 33 | assert_eq!(var("KEY_U").unwrap(), "value+valueU"); 34 | assert_eq!( 35 | var("SUBSTITUTION_FOR_STRONG_QUOTES").unwrap(), 36 | common_string 37 | ); 38 | assert_eq!( 39 | var("SUBSTITUTION_FOR_WEAK_QUOTES").unwrap(), 40 | [ 41 | "", 42 | "value", 43 | "value1", 44 | "value1", 45 | "value_U", 46 | "value+valueU", 47 | "$KEY" 48 | ] 49 | .join(">>") 50 | ); 51 | assert_eq!( 52 | var("SUBSTITUTION_WITHOUT_QUOTES").unwrap(), 53 | [ 54 | "", 55 | "value", 56 | "value1", 57 | "value1", 58 | "value_U", 59 | "value+valueU", 60 | "$KEY" 61 | ] 62 | .join(">>") 63 | ); 64 | 65 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 66 | dir.close().unwrap(); 67 | } 68 | -------------------------------------------------------------------------------- /dotenv/tests/test-vars.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use std::collections::HashMap; 4 | use std::env; 5 | 6 | use dotenv::*; 7 | 8 | use crate::common::*; 9 | 10 | #[test] 11 | fn test_vars() { 12 | let dir = make_test_dotenv().unwrap(); 13 | 14 | let vars: HashMap = vars().collect(); 15 | 16 | assert_eq!(vars["TESTKEY"], "test_val"); 17 | 18 | env::set_current_dir(dir.path().parent().unwrap()).unwrap(); 19 | dir.close().unwrap(); 20 | } 21 | -------------------------------------------------------------------------------- /dotenv_codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "dotenv_codegen" 4 | version = "0.15.0" 5 | authors = [ 6 | "Santiago Lapresta ", 7 | "Craig Hills ", 8 | "Mike Piccolo ", 9 | "Alice Maz ", 10 | "Sean Griffin ", 11 | ] 12 | readme = "../README.md" 13 | keywords = ["environment", "env", "dotenv", "settings", "config"] 14 | license = "MIT" 15 | homepage = "https://github.com/dotenv-rs/dotenv" 16 | repository = "https://github.com/dotenv-rs/dotenv" 17 | description = "A `dotenv` implementation for Rust" 18 | edition = "2018" 19 | 20 | [dependencies] 21 | dotenv_codegen_implementation = { version = "0.15", path = "../dotenv_codegen_implementation" } 22 | proc-macro-hack = "0.5" 23 | -------------------------------------------------------------------------------- /dotenv_codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro_hack::proc_macro_hack; 2 | 3 | #[proc_macro_hack] 4 | pub use dotenv_codegen_implementation::dotenv; 5 | -------------------------------------------------------------------------------- /dotenv_codegen/tests/basic_dotenv_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate dotenv_codegen; 3 | 4 | #[test] 5 | fn dotenv_works() { 6 | assert_eq!(dotenv!("CODEGEN_TEST_VAR1"), "hello!"); 7 | } 8 | 9 | #[test] 10 | fn two_argument_form_works() { 11 | assert_eq!( 12 | dotenv!( 13 | "CODEGEN_TEST_VAR2", 14 | "err, you should be running this in the 'dotenv_codegen' \ 15 | directory to pick up the right .env file." 16 | ), 17 | "'quotes within quotes'" 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /dotenv_codegen_implementation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [lib] 2 | proc-macro = true 3 | 4 | [package] 5 | 6 | name = "dotenv_codegen_implementation" 7 | version = "0.15.0" 8 | authors = [ 9 | "Santiago Lapresta ", 10 | "Craig Hills ", 11 | "Mike Piccolo ", 12 | "Alice Maz ", 13 | "Sean Griffin ", 14 | ] 15 | readme = "../README.md" 16 | keywords = ["environment", "env", "dotenv", "settings", "config"] 17 | license = "MIT" 18 | homepage = "https://github.com/dotenv-rs/dotenv" 19 | repository = "https://github.com/dotenv-rs/dotenv" 20 | description = "A `dotenv` implementation for Rust" 21 | edition = "2018" 22 | 23 | [dependencies] 24 | proc-macro2 = "1" 25 | quote = "1" 26 | syn = "1" 27 | proc-macro-hack = "0.5" 28 | dotenv = { version = "0.15", path = "../dotenv" } 29 | -------------------------------------------------------------------------------- /dotenv_codegen_implementation/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use std::env::{self, VarError}; 4 | 5 | use proc_macro::TokenStream; 6 | use proc_macro_hack::proc_macro_hack; 7 | use quote::quote; 8 | use syn::parse::Parser; 9 | use syn::punctuated::Punctuated; 10 | use syn::Token; 11 | 12 | #[proc_macro_hack] 13 | pub fn dotenv(input: TokenStream) -> TokenStream { 14 | if let Err(err) = dotenv::dotenv() { 15 | panic!("Error loading .env file: {}", err); 16 | } 17 | 18 | // Either everything was fine, or we didn't find an .env file (which we ignore) 19 | expand_env(input) 20 | } 21 | 22 | fn expand_env(input_raw: TokenStream) -> TokenStream { 23 | let args = >::parse_terminated 24 | .parse(input_raw) 25 | .expect("expected macro to be called with a comma-separated list of string literals"); 26 | 27 | let mut iter = args.iter(); 28 | 29 | let var_name = match iter.next() { 30 | Some(s) => s.value(), 31 | None => panic!("expected 1 or 2 arguments, found none"), 32 | }; 33 | 34 | let err_msg = match iter.next() { 35 | Some(lit) => lit.value(), 36 | None => format!("environment variable `{}` not defined", var_name), 37 | }; 38 | 39 | if iter.next().is_some() { 40 | panic!("expected 1 or 2 arguments, found 3 or more"); 41 | } 42 | 43 | match env::var(var_name) { 44 | Ok(val) => quote!(#val).into(), 45 | Err(VarError::NotPresent) | Err(VarError::NotUnicode(_)) => panic!("{}", err_msg), 46 | } 47 | } 48 | --------------------------------------------------------------------------------