extern crate regex;
use std::fs::File;
use std::io::{BufReader, BufRead};
use std::env;
use std::result::Result;
use std::path::Path;
use regex::{Captures, Regex, Error};
#[derive(Debug, Clone)]
pub enum DotenvError {
Parsing {
line: String,
},
ParseFormatter,
Io,
ExecutableNotFound,
}
impl From<regex::Error> for DotenvError {
fn from(_: regex::Error) -> DotenvError {
DotenvError::ParseFormatter
}
}
impl From<std::io::Error> for DotenvError {
fn from(_: std::io::Error) -> DotenvError {
DotenvError::Io
}
}
type ParsedLine = Result<Option<(String, String)>, DotenvError>;
type ParsedLines = Result<Vec<(String, String)>, DotenvError>;
fn named_string(captures: &Captures, name: &str) -> Option<String> {
captures.name(name).and_then(|v| Some(v.to_string()))
}
fn parse_line(line: String) -> ParsedLine {
let line_regex = try!(Regex::new(concat!(r"^(\s*(",
r"#.*|", r"\s*|", r"(export\s+)?", r"(?P<key>[A-Za-z_][A-Za-z0-9_]*)", r"\s*=\s*", r"(?P<value>.+?)", r")\s*)[\r\n]*$")));
line_regex.captures(&line)
.map_or(Err(DotenvError::Parsing { line: line.clone() }),
|captures| {
let key = named_string(&captures, "key");
let value = named_string(&captures, "value");
if key.is_some() && value.is_some() {
Ok(Some((key.unwrap(), value.unwrap())))
} else {
Ok(None)
}
})
}
fn from_file(file: File) -> Result<(), DotenvError> {
let reader = BufReader::new(file);
for line in reader.lines() {
let line = try!(line);
let parsed = try!(parse_line(line));
match parsed {
Some((key, value)) => {
if env::var(&key).is_err() {
env::set_var(&key, value);
}
()
}
None => (),
}
}
Ok(())
}
fn try_parent(path: &Path, filename: &str) -> Result<(), DotenvError> {
match path.parent() {
Some(parent) => {
match from_path(&parent.join(filename)) {
Ok(file) => Ok(file),
Err(DotenvError::Io) => try_parent(parent, filename),
err => err
}
},
None => Err(DotenvError::Io)
}
}
pub fn from_path(path: &Path) -> Result<(), DotenvError> {
match File::open(path) {
Ok(file) => from_file(file),
Err(_) => Err(DotenvError::Io),
}
}
pub fn from_filename(filename: &str) -> Result<(), DotenvError> {
let path = match env::current_dir() {
Ok(path) => path,
Err(_) => return Err(DotenvError::Io)
};
match from_path(&path.join(filename)) {
Ok(file) => Ok(file),
Err(DotenvError::Io) => try_parent(&path, filename),
err => err
}
}
pub fn dotenv() -> Result<(), DotenvError> {
from_filename(&".env")
}
#[test]
fn test_parse_line_env() {
let input_iter = vec!["THIS_IS_KEY=hi this is value",
" many_spaces = wow a maze ",
"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![("THIS_IS_KEY", "hi this is value"),
("many_spaces", "wow a maze"),
("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.clone().ok().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 ", "very bacon = yes indeed", "key=", "=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());
}
}