From 2c259918bc7546b939b24865fc6f2ff99cd1d824 Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Tue, 27 Aug 2019 16:55:17 +0200 Subject: [PATCH] Replace line-parsing RegEx by hand written parser This avoids pulling in `regex` and `lazy_static` and makes this a true zero-dependency crate. --- dotenv/Cargo.toml | 12 +++- dotenv/src/parse.rs | 155 +++++++++++++++++++++++++++++--------------- 2 files changed, 113 insertions(+), 54 deletions(-) diff --git a/dotenv/Cargo.toml b/dotenv/Cargo.toml index 57a31c1..4c2fb5a 100644 --- a/dotenv/Cargo.toml +++ b/dotenv/Cargo.toml @@ -1,7 +1,15 @@ [package] name = "dotenv" version = "0.15.0" -authors = ["Noemi Lapresta ", "Craig Hills ", "Mike Piccolo ", "Alice Maz ", "Sean Griffin ", "Adam Sharp "] +authors = [ + "Noemi Lapresta ", + "Craig Hills ", + "Mike Piccolo ", + "Alice Maz ", + "Sean Griffin ", + "Adam Sharp ", + "Arpad Borsos ", +] description = "A `dotenv` implementation for Rust" homepage = "https://github.com/dotenv-rs/dotenv" readme = "../README.md" @@ -15,8 +23,6 @@ name = "dotenv" required-features = ["cli"] [dependencies] -lazy_static = "1.0.0" -regex = "1.0" clap = { version = "2", optional = true } [dev-dependencies] diff --git a/dotenv/src/parse.rs b/dotenv/src/parse.rs index 15e12f1..5fa3deb 100644 --- a/dotenv/src/parse.rs +++ b/dotenv/src/parse.rs @@ -1,63 +1,112 @@ use std::collections::HashMap; -use lazy_static::lazy_static; -use regex::{Captures, Regex}; - use crate::errors::*; // for readability's sake pub type ParsedLine = Result>; -pub fn parse_line(line: &str, mut substitution_data: &mut HashMap>) -> ParsedLine { - lazy_static! { - static ref LINE_REGEX: Regex = Regex::new(r#"(?x) - ^( - \s* - ( - \#.*| # A comment, or... - \s*| # ...an empty string, or... - (export\s+)? # ...(optionally preceded by "export")... - (?P[A-Za-z_][A-Za-z0-9_.]*) # ...a key,... - = # ...then an equal sign,... - (?P.+?)? # ...and then its corresponding value. - )\s* - ) - [\r\n]* - $ - "#).unwrap(); +pub fn parse_line(line: &str, substitution_data: &mut HashMap>) -> ParsedLine { + let mut parser = LineParser::new(line, substitution_data); + parser.parse_line() +} + +struct LineParser<'a> { + original_line: &'a str, + substitution_data: &'a mut HashMap>, + line: &'a str, + pos: usize, +} + +impl<'a> LineParser<'a> { + fn new( + line: &'a str, + substitution_data: &'a mut HashMap>, + ) -> LineParser<'a> { + LineParser { + original_line: line, + substitution_data, + line: line.trim_end(), // we don’t want trailing whitespace + pos: 0, + } } - LINE_REGEX - .captures(line) - .map_or(Err(Error::LineParse(line.into(), 0)), |captures| { - let key = named_string(&captures, "key"); - let value = named_string(&captures, "value"); + fn err(&self) -> Error { + return Error::LineParse(self.original_line.into(), self.pos); + } - match (key, value) { - (Some(k), Some(v)) => { - let parsed_value = parse_value(&v, &mut substitution_data)?; - substitution_data.insert(k.to_owned(), Some(parsed_value.to_owned())); + fn parse_line(&mut self) -> ParsedLine { + self.skip_whitespace(); + // if its an empty line or a comment, skip it + if self.line.is_empty() || self.line.starts_with('#') { + return Ok(None); + } - Ok(Some((k, parsed_value))) - } - (Some(k), None) => { - substitution_data.insert(k.to_owned(), 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) - } + let mut key = self.parse_key()?; + self.skip_whitespace(); + + // export can be either an optional prefix or a key itself + if key == "export" { + // here we check for an optional `=`, below we throw directly when it’s not found. + if self.expect_equal().is_err() { + key = self.parse_key()?; + self.skip_whitespace(); + self.expect_equal()?; } - }) -} + } else { + self.expect_equal()?; + } + self.skip_whitespace(); + + if self.line.is_empty() || self.line.starts_with('#') { + self.substitution_data.insert(key.clone(), None); + return Ok(Some((key, String::new()))); + } + + let parsed_value = parse_value(self.line, &mut self.substitution_data)?; + self.substitution_data + .insert(key.clone(), Some(parsed_value.clone())); -fn named_string(captures: &Captures<'_>, name: &str) -> Option { - captures - .name(name) - .and_then(|v| Some(v.as_str().to_owned())) + return Ok(Some((key, parsed_value))); + } + + fn parse_key(&mut self) -> Result { + if !self + .line + .starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') + { + return Err(self.err()); + } + let index = match self + .line + .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.')) + { + Some(index) => index, + None => self.line.len(), + }; + self.pos += index; + let key = String::from(&self.line[..index]); + self.line = &self.line[index..]; + Ok(key) + } + + fn expect_equal(&mut self) -> Result<()> { + if !self.line.starts_with("=") { + return Err(self.err()); + } + self.line = &self.line[1..]; + self.pos += 1; + Ok(()) + } + + fn skip_whitespace(&mut self) { + if let Some(index) = self.line.find(|c: char| !c.is_whitespace()) { + self.pos += index; + self.line = &self.line[index..]; + } else { + self.pos += self.line.len(); + self.line = ""; + } + } } #[derive(Eq, PartialEq)] @@ -210,6 +259,9 @@ KEY6=s\ ix KEY7= KEY8= KEY9= # foo +KEY10 ="whitespace before =" +KEY11= "whitespace after =" +export="export as key" export SHELL_LOVER=1 "#.as_bytes()); @@ -223,6 +275,9 @@ export SHELL_LOVER=1 ("KEY7", ""), ("KEY8", ""), ("KEY9", ""), + ("KEY10", "whitespace before ="), + ("KEY11", "whitespace after ="), + ("export", "export as key"), ("SHELL_LOVER", "1"), ].into_iter() .map(|(key, value)| (key.to_string(), value.to_string())); @@ -234,7 +289,7 @@ export SHELL_LOVER=1 count += 1; } - assert_eq!(count, 10); + assert_eq!(count, 13); } #[test] @@ -250,8 +305,6 @@ export SHELL_LOVER=1 // Note 4 spaces after 'invalid' below let actual_iter = Iter::new(r#" invalid -KEY =val -KEY2= val very bacon = yes indeed =value"#.as_bytes()); @@ -260,7 +313,7 @@ very bacon = yes indeed assert!(actual.is_err()); count += 1; } - assert_eq!(count, 5); + assert_eq!(count, 3); } #[test]