use regex::{Match, Regex};
use std::collections::HashMap;
enum ValueMatch<'h> {
SingleQuoted(Match<'h>),
DoubleQuoted(Match<'h>),
Backticks(Match<'h>),
Unquoted(Match<'h>),
}
pub fn parse_dotenv_vars(text: &str) -> HashMap<String, String> {
let pattern = concat!(
r#"(?m)^\s*"#, r#"(?:export\s+)?"#, r#"([\w.-]+)"#, r#"(?:\s*=\s*?|:\s+?)"#, r#"(?:"#, r#"'((?:\\'|[^'])*)'|"#, r#""((?:\\"|[^"])*)"|"#, r#"`((?:\\`|[^`])*)`|"#, r#"([^#\r\n]*)"#, r#")?"#, r#"\s*(?:#.*)?$"# );
let re = Regex::new(pattern).unwrap();
let mut vars = HashMap::new();
for caps in re.captures_iter(text) {
if let Some(key) = caps.get(1) {
let value_match = if let Some(val) = caps.get(2) {
ValueMatch::SingleQuoted(val)
} else if let Some(val) = caps.get(3) {
ValueMatch::DoubleQuoted(val)
} else if let Some(val) = caps.get(4) {
ValueMatch::Backticks(val)
} else {
ValueMatch::Unquoted(caps.get(5).unwrap())
};
let value = match value_match {
ValueMatch::SingleQuoted(value) => value.as_str().into(),
ValueMatch::DoubleQuoted(value) => value.as_str().replace("\\n", "\n"),
ValueMatch::Backticks(value) => value.as_str().into(),
ValueMatch::Unquoted(value) => value.as_str().trim().into(),
};
vars.insert(key.as_str().into(), value);
}
}
vars
}
#[cfg(test)]
mod tests {
use super::*;
static DOTENV_FILE_CONTENT: &str = concat!(
"BASIC=basic\n",
"\n",
"# previous line intentionally left blank\n",
"AFTER_LINE=after_line\n",
"EMPTY=\n",
"EMPTY_SINGLE_QUOTES=''\n",
"EMPTY_DOUBLE_QUOTES=\"\"\n",
"EMPTY_BACKTICKS=``\n",
"EMPTY_INLINE_COMMENTS= # comment\n",
"SINGLE_QUOTES='single_quotes'\n",
"SINGLE_QUOTES_SPACED=' single quotes '\n",
"DOUBLE_QUOTES=\"double_quotes\"\n",
"DOUBLE_QUOTES_SPACED=\" double quotes \"\n",
"DOUBLE_QUOTES_INSIDE_SINGLE='double \"quotes\" work inside single quotes'\n",
"DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET=\"{ port: $MONGOLAB_PORT}\"\n",
"SINGLE_QUOTES_INSIDE_DOUBLE=\"single 'quotes' work inside double quotes\"\n",
"BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes'\n",
"BACKTICKS_INSIDE_DOUBLE=\"`backticks` work inside double quotes\"\n",
"BACKTICKS=`backticks`\n",
"BACKTICKS_SPACED=` backticks `\n",
"DOUBLE_QUOTES_INSIDE_BACKTICKS=`double \"quotes\" work inside backticks`\n",
"SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`\n",
"DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double \"quotes\" and single 'quotes' work inside backticks`\n",
"EXPAND_NEWLINES=\"expand\\nnew\\nlines\"\n",
"DONT_EXPAND_UNQUOTED=dontexpand\\nnewlines\n",
"DONT_EXPAND_SQUOTED='dontexpand\\nnewlines'\n",
"# COMMENTS=work\n",
"INLINE_COMMENTS=inline comments # work #very #well\n",
"INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work\n",
"INLINE_COMMENTS_DOUBLE_QUOTES=\"inline comments outside of #doublequotes\" # work\n",
"INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work\n",
"INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required.\n",
"EQUAL_SIGNS=equals==\n",
"RETAIN_INNER_QUOTES={\"foo\": \"bar\"}\n",
"RETAIN_INNER_QUOTES_AS_STRING='{\"foo\": \"bar\"}'\n",
"RETAIN_INNER_QUOTES_AS_BACKTICKS=`{\"foo\": \"bar's\"}`\n",
"TRIM_SPACE_FROM_UNQUOTED= some spaced out string\n",
"USERNAME=therealnerdybeast@example.tld\n",
" SPACED_KEY = parsed\n",
" EMPTY_SPACED_KEY =\n",
"\n",
"MULTI_DOUBLE_QUOTED=\"THIS\n",
"IS\n",
"A\n",
"MULTILINE\n",
"STRING\"\n",
"\n",
"MULTI_SINGLE_QUOTED='THIS\n",
"IS\n",
"A\n",
"MULTILINE\n",
"STRING'\n",
"\n",
"MULTI_BACKTICKED=`THIS\n",
"IS\n",
"A\n",
"\"MULTILINE'S\"\n",
"STRING`\n",
"\n",
"MULTI_PEM_DOUBLE_QUOTED=\"-----BEGIN PUBLIC KEY-----\n",
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\n",
"LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\n",
"bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\n",
"kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\n",
"u4QuUoobAgMBAAE=\n",
"-----END PUBLIC KEY-----\"\n",
);
#[test]
fn test_parse_dotenv_vars() {
let result = parse_dotenv_vars(DOTENV_FILE_CONTENT);
assert_eq!(result["BASIC"], "basic");
assert_eq!(result["AFTER_LINE"], "after_line");
assert_eq!(result["EMPTY"], "");
assert_eq!(result["EMPTY_SINGLE_QUOTES"], "");
assert_eq!(result["EMPTY_DOUBLE_QUOTES"], "");
assert_eq!(result["EMPTY_BACKTICKS"], "");
assert_eq!(result["EMPTY_INLINE_COMMENTS"], "");
assert_eq!(result["SINGLE_QUOTES"], "single_quotes");
assert_eq!(result["SINGLE_QUOTES_SPACED"], " single quotes ");
assert_eq!(result["DOUBLE_QUOTES"], "double_quotes");
assert_eq!(result["DOUBLE_QUOTES_SPACED"], " double quotes ");
assert_eq!(
result["DOUBLE_QUOTES_INSIDE_SINGLE"],
"double \"quotes\" work inside single quotes"
);
assert_eq!(
result["DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET"],
"{ port: $MONGOLAB_PORT}"
);
assert_eq!(
result["SINGLE_QUOTES_INSIDE_DOUBLE"],
"single 'quotes' work inside double quotes"
);
assert_eq!(
result["BACKTICKS_INSIDE_SINGLE"],
"`backticks` work inside single quotes"
);
assert_eq!(
result["BACKTICKS_INSIDE_DOUBLE"],
"`backticks` work inside double quotes"
);
assert_eq!(result["BACKTICKS"], "backticks");
assert_eq!(result["BACKTICKS_SPACED"], " backticks ");
assert_eq!(
result["DOUBLE_QUOTES_INSIDE_BACKTICKS"],
"double \"quotes\" work inside backticks"
);
assert_eq!(
result["SINGLE_QUOTES_INSIDE_BACKTICKS"],
"single 'quotes' work inside backticks"
);
assert_eq!(
result["DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS"],
"double \"quotes\" and single 'quotes' work inside backticks"
);
assert_eq!(result["EXPAND_NEWLINES"], "expand\nnew\nlines");
assert_eq!(result["DONT_EXPAND_UNQUOTED"], "dontexpand\\nnewlines");
assert_eq!(result["DONT_EXPAND_SQUOTED"], "dontexpand\\nnewlines");
assert_eq!(result["INLINE_COMMENTS"], "inline comments");
assert_eq!(
result["INLINE_COMMENTS_SINGLE_QUOTES"],
"inline comments outside of #singlequotes"
);
assert_eq!(
result["INLINE_COMMENTS_DOUBLE_QUOTES"],
"inline comments outside of #doublequotes"
);
assert_eq!(
result["INLINE_COMMENTS_BACKTICKS"],
"inline comments outside of #backticks"
);
assert_eq!(
result["INLINE_COMMENTS_SPACE"],
"inline comments start with a"
);
assert_eq!(result["EQUAL_SIGNS"], "equals==");
assert_eq!(result["RETAIN_INNER_QUOTES"], "{\"foo\": \"bar\"}");
assert_eq!(
result["RETAIN_INNER_QUOTES_AS_STRING"],
"{\"foo\": \"bar\"}"
);
assert_eq!(
result["RETAIN_INNER_QUOTES_AS_BACKTICKS"],
"{\"foo\": \"bar's\"}"
);
assert_eq!(result["TRIM_SPACE_FROM_UNQUOTED"], "some spaced out string");
assert_eq!(result["USERNAME"], "therealnerdybeast@example.tld");
assert_eq!(result["SPACED_KEY"], "parsed");
assert_eq!(result["EMPTY_SPACED_KEY"], "");
assert_eq!(
result["MULTI_DOUBLE_QUOTED"],
"THIS\nIS\nA\nMULTILINE\nSTRING"
);
assert_eq!(
result["MULTI_SINGLE_QUOTED"],
"THIS\nIS\nA\nMULTILINE\nSTRING"
);
assert_eq!(
result["MULTI_BACKTICKED"],
"THIS\nIS\nA\n\"MULTILINE'S\"\nSTRING"
);
assert_eq!(
result["MULTI_PEM_DOUBLE_QUOTED"],
concat!(
"-----BEGIN PUBLIC KEY-----\n",
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\n",
"LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\n",
"bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\n",
"kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\n",
"u4QuUoobAgMBAAE=\n",
"-----END PUBLIC KEY-----",
)
);
assert_eq!(result.len(), 41);
}
}