Skip to main content

envvault/cli/
env_parser.rs

1//! Shared `.env` file parsing logic.
2//!
3//! Used by both `init` (for auto-import) and `import` commands.
4
5use std::collections::HashMap;
6use std::fs;
7use std::path::Path;
8
9use crate::errors::{EnvVaultError, Result};
10
11/// Parse a single `.env` line into a (key, value) pair.
12///
13/// Returns `None` for blank lines, comments, and lines without `=`.
14/// Handles: `export` prefix, double/single quotes, values with `=`.
15pub fn parse_env_line(line: &str) -> Option<(&str, &str)> {
16    let trimmed = line.trim();
17
18    // Skip empty lines and comments.
19    if trimmed.is_empty() || trimmed.starts_with('#') {
20        return None;
21    }
22
23    // Strip optional `export ` prefix.
24    let trimmed = trimmed.strip_prefix("export ").unwrap_or(trimmed);
25
26    // Split on the first '=' to get KEY and VALUE.
27    let (key, value) = trimmed.split_once('=')?;
28    let key = key.trim();
29    let value = value.trim();
30
31    // Strip optional surrounding quotes from the value.
32    let value = value
33        .strip_prefix('"')
34        .and_then(|v| v.strip_suffix('"'))
35        .or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
36        .unwrap_or(value);
37
38    if key.is_empty() {
39        return None;
40    }
41
42    Some((key, value))
43}
44
45/// Parse a `.env` file into a key-value map.
46pub fn parse_env_file(path: &Path) -> Result<HashMap<String, String>> {
47    let content = fs::read_to_string(path)
48        .map_err(|e| EnvVaultError::CommandFailed(format!("failed to read file: {e}")))?;
49
50    let mut secrets = HashMap::new();
51
52    for line in content.lines() {
53        if let Some((key, value)) = parse_env_line(line) {
54            secrets.insert(key.to_string(), value.to_string());
55        }
56    }
57
58    Ok(secrets)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn parse_simple_key_value() {
67        assert_eq!(parse_env_line("KEY=value"), Some(("KEY", "value")));
68    }
69
70    #[test]
71    fn parse_export_prefix() {
72        assert_eq!(
73            parse_env_line("export DATABASE_URL=postgres://localhost/db"),
74            Some(("DATABASE_URL", "postgres://localhost/db"))
75        );
76    }
77
78    #[test]
79    fn parse_value_with_equals() {
80        assert_eq!(parse_env_line("KEY=val=ue"), Some(("KEY", "val=ue")));
81    }
82
83    #[test]
84    fn parse_double_quoted_value() {
85        assert_eq!(
86            parse_env_line(r#"KEY="hello world""#),
87            Some(("KEY", "hello world"))
88        );
89    }
90
91    #[test]
92    fn parse_single_quoted_value() {
93        assert_eq!(
94            parse_env_line("KEY='hello world'"),
95            Some(("KEY", "hello world"))
96        );
97    }
98
99    #[test]
100    fn parse_empty_value() {
101        assert_eq!(parse_env_line("KEY="), Some(("KEY", "")));
102    }
103
104    #[test]
105    fn parse_empty_quoted_value() {
106        assert_eq!(parse_env_line(r#"KEY="""#), Some(("KEY", "")));
107    }
108
109    #[test]
110    fn parse_skips_comments() {
111        assert_eq!(parse_env_line("# this is a comment"), None);
112    }
113
114    #[test]
115    fn parse_skips_blank_lines() {
116        assert_eq!(parse_env_line(""), None);
117        assert_eq!(parse_env_line("   "), None);
118    }
119
120    #[test]
121    fn parse_skips_lines_without_equals() {
122        assert_eq!(parse_env_line("NOEQUALS"), None);
123    }
124
125    #[test]
126    fn parse_trims_whitespace() {
127        assert_eq!(parse_env_line("  KEY  =  value  "), Some(("KEY", "value")));
128    }
129}