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::{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 parse_line(line: String) -> ParsedLine {
let line_regex = 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]*$")).unwrap();
line_regex.captures(&line).map_or(
Err(DotenvError::Parsing {line: line.clone()}),
|captures| {
let key = captures.name("key");
let value = captures.name("value");
if key.is_some() && value.is_some() {
Ok(Some((key.unwrap().to_string(), value.unwrap().to_string())))
} 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(())
}
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> {
match env::current_exe() {
Ok(path) => from_path(path.with_file_name(filename).as_path()),
Err(_) => Err(DotenvError::ExecutableNotFound)
}
}
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());
}
}