dotenv 0.4.0

A `dotenv` implementation for Rust
Documentation
//! 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 operative
//! system.

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
    }
}

// for readability's sake
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"#.*|", // 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"\s*=\s*", // ...then an equal sign,...
                                        r"(?P<value>.+?)", // ...and then its corresponding 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 {
                // If there's no key and value, but capturing did not fail,
                // then this means we're dealing with a comment or an empty
                // string.
                                              Ok(None)
                                          }
                                      })
}

/// Loads the specified file.
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(())
}

/// Loads the file at the specified 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<(), DotenvError> {
    match File::open(path) {
        Ok(file) => from_file(file),
        Err(_) => Err(DotenvError::Io),
    }
}

/// Loads the specified file from the same directory as the current executable.
///
/// # 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<(), DotenvError> {
    match env::current_exe() {
        Ok(path) => from_path(path.with_file_name(filename).as_path()),
        Err(_) => Err(DotenvError::ExecutableNotFound),
    }
}

/// This is usually what you want.
/// It loads the .env file located in the same directory as the current executable.
///
/// # Examples
/// ```
/// use dotenv;
/// dotenv::dotenv().ok();
/// ```
pub fn dotenv() -> Result<(), DotenvError> {
    match env::current_dir() {
       Ok(path) => from_path(path.join(".env").as_path()),
       Err(_) => Err(DotenvError::Io),
    }
}

#[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());
    }
}