blob: 1eb715d0e120a9aa216f049ea841b9453f56e764 [file] [log] [blame] [edit]
//! This crate provides a configuration loader in the style of the [ruby dotenv
//! gem](https://github.com/bkeepers/dotenv). This library is meant to be used
//! on development or testing environments in which setting environment
//! variables is not practical. It loads environment variables from a .env
//! file, if available, and mashes those with the actual environment variables
//! provided by the operating system.
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate derive_error_chain;
extern crate regex;
use std::env::{self, Vars};
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufReader, BufRead};
use std::path::{Path, PathBuf};
use std::sync::{Once, ONCE_INIT};
use regex::{Captures, Regex};
#[derive(Debug, error_chain)]
#[cfg_attr(not(feature = "backtrace"), error_chain(backtrace = "false"))]
pub enum ErrorKind {
// generic error string, required by derive_error_chain
Msg(String),
#[error_chain(custom)]
#[error_chain(description = r#"|_| "Parsing Error""#)]
#[error_chain(display = r#"|l| write!(f, "Error parsing line: '{}'", l)"#)]
LineParse(String),
#[error_chain(foreign)]
ParseFormatter(::regex::Error),
#[error_chain(foreign)]
Io(::std::io::Error),
#[error_chain(foreign)]
EnvVar(::std::env::VarError),
}
static START: Once = ONCE_INIT;
/// After loading the dotenv file, fetches the environment variable key from the current process.
///
/// The returned result is Ok(s) if the environment variable is present and is valid unicode. If the
/// environment variable is not present, or it is not valid unicode, then Err will be returned.
pub fn var<K: AsRef<OsStr>>(key: K) -> Result<String> {
START.call_once(|| { dotenv().ok(); });
env::var(key).map_err(Error::from)
}
/// After loading the dotenv file, returns an iterator of (variable, value) pairs of strings,
/// for all the environment variables of the current process.
///
/// The returned iterator contains a snapshot of the process's environment variables at the
/// time of this invocation, modifications to environment variables afterwards will not be
/// reflected in the returned iterator.
pub fn vars() -> Vars {
START.call_once(|| { dotenv().ok(); });
env::vars()
}
// for readability's sake
type ParsedLine = Result<Option<(String, String)>>;
fn named_string(captures: &Captures, name: &str) -> Option<String> {
captures.name(name).and_then(|v| Some(v.as_str().to_owned()))
}
fn parse_value(input: &str) -> Result<String> {
let mut strong_quote = false; // '
let mut weak_quote = false; // "
let mut escaped = false;
let mut expecting_end = false;
//FIXME can this be done without yet another allocation per line?
let mut output = String::new();
for c in input.chars() {
//the regex _should_ already trim whitespace off the end
//expecting_end is meant to permit: k=v #comment
//without affecting: k=v#comment
//and throwing on: k=v w
if expecting_end {
if c == ' ' || c == '\t' {
continue;
} else if c == '#' {
break;
} else {
bail!(ErrorKind::LineParse(input.to_owned()));
}
} else if strong_quote {
if c == '\'' {
strong_quote = false;
} else {
output.push(c);
}
} else if weak_quote {
if escaped {
//TODO variable expansion perhaps
//not in this update but in the future
//$ requires escape anyway for conformance
//and so as not to make that future change breaking
//TODO I tried handling literal \n \r but various issues
//imo not worth worrying about until there's a use case
//(actually handling backslash 0x10 would be a whole other matter)
//then there's \v \f bell hex... etc
match c {
'\\' | '"' | '$' => output.push(c),
_ => bail!(ErrorKind::LineParse(input.to_owned())),
}
escaped = false;
} else if c == '"' {
weak_quote = false;
} else if c == '\\' {
escaped = true;
} else {
output.push(c);
}
} else {
if escaped {
match c {
'\\' | '\'' | '"' | '$' | ' ' => output.push(c),
_ => bail!(ErrorKind::LineParse(input.to_owned())),
}
escaped = false;
} else if c == '\'' {
strong_quote = true;
} else if c == '"' {
weak_quote = true;
} else if c == '\\' {
escaped = true;
} else if c == '$' {
//variable interpolation goes here later
bail!(ErrorKind::LineParse(input.to_owned()));
} else if c == ' ' || c == '\t' {
expecting_end = true;
} else {
output.push(c);
}
}
}
//XXX also fail if escaped? or...
if strong_quote || weak_quote {
Err(ErrorKind::LineParse(input.to_owned()).into())
} else {
Ok(output)
}
}
fn parse_line(line: String) -> ParsedLine {
let line_regex = try!(Regex::new(concat!(r"^(\s*(",
r"#.*|", // A comment, or...
r"\s*|", // ...an empty string, or...
r"(export\s+)?", // ...(optionally preceded by "export")...
r"(?P<key>[A-Za-z_][A-Za-z0-9_]*)", // ...a key,...
r"=", // ...then an equal sign,...
r"(?P<value>.+?)?", // ...and then its corresponding value.
r")\s*)[\r\n]*$")));
line_regex.captures(&line)
.map_or(Err(ErrorKind::LineParse(line.clone()).into()), |captures| {
let key = named_string(&captures, "key");
let value = named_string(&captures, "value");
match (key, value) {
(Some(k), Some(v)) => {
let parsed_value = try!(parse_value(&v));
Ok(Some((k, parsed_value)))
}
(Some(k), None) => {
// Empty string for value.
Ok(Some((k, String::from(""))))
}
_ => {
// If there's no key, but capturing did not
// fail, we're dealing with a comment
Ok(None)
}
}
})
}
/// Loads the specified file.
fn from_file(file: File) -> Result<()> {
let reader = BufReader::new(file);
for line in reader.lines() {
let line = try!(line);
let parsed = try!(parse_line(line));
if let Some((key, value)) = parsed {
if env::var(&key).is_err() {
env::set_var(&key, value);
}
}
}
Ok(())
}
/// Attempts to load from given directory and parent directories until file is found or root is reached.
fn try_dir_with_parents(mut dir: PathBuf, filename: &str) -> Result<PathBuf> {
let env_path = dir.join(filename);
match from_path(&env_path) {
Ok(()) => Ok(env_path),
Err(Error(ErrorKind::Io(io_error), err_data)) => {
match io_error.kind() {
std::io::ErrorKind::NotFound => {
// Reuse allocation of parent path directory.
if dir.pop() {
try_dir_with_parents(dir, filename)
} else {
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "path not found").into())
}
},
_ => Err(Error(ErrorKind::Io(io_error), err_data)),
}
},
Err(other) => Err(other),
}
}
/// Loads the file at the specified absolute path.
///
/// Examples
///
/// ```
/// use dotenv;
/// use std::env;
/// use std::path::{Path};
///
/// let my_path = env::home_dir().and_then(|a| Some(a.join("/.env"))).unwrap();
/// dotenv::from_path(my_path.as_path());
/// ```
pub fn from_path(path: &Path) -> Result<()> {
File::open(path).map(from_file)?
}
/// Loads the specified file from the environment's current directory or its parents in sequence.
///
/// # Examples
/// ```
/// use dotenv;
/// dotenv::from_filename("custom.env").ok();
/// ```
///
/// It is also possible to do the following, but it is equivalent to using dotenv::dotenv(), which
/// is preferred.
///
/// ```
/// use dotenv;
/// dotenv::from_filename(".env").ok();
/// ```
pub fn from_filename(filename: &str) -> Result<PathBuf> {
let path = env::current_dir()?;
try_dir_with_parents(path, filename)
}
/// This is usually what you want.
/// It loads the .env file located in the environment's current directory or its parents in sequence.
///
/// # Examples
/// ```
/// use dotenv;
/// dotenv::dotenv().ok();
/// ```
pub fn dotenv() -> Result<PathBuf> {
from_filename(&".env")
}
#[test]
fn test_parse_line_env() {
let input_iter = vec!["KEY=1",
r#"KEY2="2""#,
"KEY3='3'",
"KEY4='fo ur'",
r#"KEY5="fi ve""#,
r"KEY6=s\ ix",
"KEY7=",
"KEY8= ",
"KEY9= # foo",
"export SHELL_LOVER=1"]
.into_iter()
.map(|input| input.to_string());
let actual_iter = input_iter.map(|input| parse_line(input));
let expected_iter = vec![("KEY", "1"),
("KEY2", "2"),
("KEY3", "3"),
("KEY4", "fo ur"),
("KEY5", "fi ve"),
("KEY6", "s ix"),
("KEY7", ""),
("KEY8", ""),
("KEY9", ""),
("SHELL_LOVER", "1")]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()));
for (expected, actual) in expected_iter.zip(actual_iter) {
assert!(actual.is_ok());
assert!(actual.as_ref().unwrap().is_some());
assert_eq!(expected, actual.ok().unwrap().unwrap());
}
}
#[test]
fn test_parse_line_comment() {
let input_iter = vec!["# foo=bar", " # "]
.into_iter()
.map(|input| input.to_string());
let actual_iter = input_iter.map(|input| parse_line(input));
for actual in actual_iter {
assert!(actual.is_ok());
assert!(actual.ok().unwrap().is_none());
}
}
#[test]
fn test_parse_line_invalid() {
let input_iter =
vec![" invalid ", "KEY =val", "KEY2= val", "very bacon = yes indeed", "=value"]
.into_iter()
.map(|input| input.to_string());
let actual_iter = input_iter.map(|input| parse_line(input));
for actual in actual_iter {
assert!(actual.is_err());
}
}
#[test]
fn test_parse_value_escapes() {
let input_iter = vec![r#"KEY=my\ cool\ value"#,
r#"KEY2=\$sweet"#,
r#"KEY3="awesome stuff \"mang\"""#,
r#"KEY4='sweet $\fgs'\''fds'"#,
r#"KEY5="'\"yay\\"\ "stuff""#,
r##"KEY6="lol" #well you see when I say lol wh"##]
.into_iter()
.map(|input| input.to_string());
let actual_iter = input_iter.map(|input| parse_line(input));
let expected_iter = vec![("KEY", r#"my cool value"#),
("KEY2", r#"$sweet"#),
("KEY3", r#"awesome stuff "mang""#),
("KEY4", r#"sweet $\fgs'fds"#),
("KEY5", r#"'"yay\ stuff"#),
("KEY6", "lol")]
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()));
for (expected, actual) in expected_iter.zip(actual_iter) {
assert!(actual.is_ok());
assert!(actual.as_ref().unwrap().is_some());
assert_eq!(expected, actual.unwrap().unwrap());
}
}
#[test]
fn test_parse_value_escapes_invalid() {
let input_iter = vec![r#"KEY=my uncool value"#,
r#"KEY2=$notcool"#,
r#"KEY3="why"#,
r#"KEY4='please stop''"#,
r#"KEY5=h\8u"#]
.into_iter()
.map(|input| input.to_string());
let actual_iter = input_iter.map(|input| parse_line(input));
for actual in actual_iter {
assert!(actual.is_err());
}
}