Skip to main content

robinpath_modules/modules/
dotenv_mod.rs

1use robinpath::{RobinPath, Value};
2use std::fs;
3
4pub fn register(rp: &mut RobinPath) {
5    rp.register_builtin("dotenv.parse", |args, _| {
6        let content = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7        Ok(Value::Object(parse_dotenv(&content)))
8    });
9
10    rp.register_builtin("dotenv.stringify", |args, _| {
11        let obj = args.first().cloned().unwrap_or(Value::Null);
12        if let Value::Object(map) = &obj {
13            let lines: Vec<String> = map
14                .iter()
15                .map(|(k, v)| {
16                    let val = v.to_display_string();
17                    if val.contains(' ') || val.contains('"') || val.contains('\'') {
18                        format!("{}=\"{}\"", k, val.replace('"', "\\\""))
19                    } else {
20                        format!("{}={}", k, val)
21                    }
22                })
23                .collect();
24            Ok(Value::String(lines.join("\n")))
25        } else {
26            Ok(Value::String(String::new()))
27        }
28    });
29
30    rp.register_builtin("dotenv.load", |args, _| {
31        let path = args
32            .first()
33            .map(|v| v.to_display_string())
34            .unwrap_or_else(|| ".env".to_string());
35        match fs::read_to_string(&path) {
36            Ok(content) => {
37                let vars = parse_dotenv(&content);
38                for (key, value) in &vars {
39                    if let Value::String(val) = value {
40                        // SAFETY: Caller is responsible for ensuring no concurrent access to env vars
41                        unsafe { std::env::set_var(key, val) };
42                    }
43                }
44                Ok(Value::Bool(true))
45            }
46            Err(e) => Err(format!("dotenv.load error: {}", e)),
47        }
48    });
49
50    rp.register_builtin("dotenv.read", |args, _| {
51        let path = args
52            .first()
53            .map(|v| v.to_display_string())
54            .unwrap_or_else(|| ".env".to_string());
55        match fs::read_to_string(&path) {
56            Ok(content) => Ok(Value::Object(parse_dotenv(&content))),
57            Err(e) => Err(format!("dotenv.read error: {}", e)),
58        }
59    });
60
61    rp.register_builtin("dotenv.get", |args, _| {
62        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
63        let key = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
64        match fs::read_to_string(&path) {
65            Ok(content) => {
66                let vars = parse_dotenv(&content);
67                Ok(vars.get(&key).cloned().unwrap_or(Value::Null))
68            }
69            Err(_) => Ok(Value::Null),
70        }
71    });
72
73    rp.register_builtin("dotenv.set", |args, _| {
74        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
75        let key = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
76        let value = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
77        let content = fs::read_to_string(&path).unwrap_or_default();
78        let mut vars = parse_dotenv(&content);
79        vars.insert(key, Value::String(value));
80        let lines: Vec<String> = vars
81            .iter()
82            .map(|(k, v)| format!("{}={}", k, v.to_display_string()))
83            .collect();
84        match fs::write(&path, lines.join("\n")) {
85            Ok(()) => Ok(Value::Bool(true)),
86            Err(e) => Err(format!("dotenv.set error: {}", e)),
87        }
88    });
89
90    rp.register_builtin("dotenv.remove", |args, _| {
91        let path = args.first().map(|v| v.to_display_string()).unwrap_or_default();
92        let key = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
93        let content = fs::read_to_string(&path).unwrap_or_default();
94        let mut vars = parse_dotenv(&content);
95        vars.shift_remove(&key);
96        let lines: Vec<String> = vars
97            .iter()
98            .map(|(k, v)| format!("{}={}", k, v.to_display_string()))
99            .collect();
100        match fs::write(&path, lines.join("\n")) {
101            Ok(()) => Ok(Value::Bool(true)),
102            Err(e) => Err(format!("dotenv.remove error: {}", e)),
103        }
104    });
105}
106
107fn parse_dotenv(content: &str) -> indexmap::IndexMap<String, Value> {
108    let mut map = indexmap::IndexMap::new();
109    for line in content.lines() {
110        let line = line.trim();
111        // Skip empty lines and comments
112        if line.is_empty() || line.starts_with('#') {
113            continue;
114        }
115        if let Some(eq_pos) = line.find('=') {
116            let key = line[..eq_pos].trim().to_string();
117            let mut value = line[eq_pos + 1..].trim().to_string();
118            // Remove surrounding quotes
119            if (value.starts_with('"') && value.ends_with('"'))
120                || (value.starts_with('\'') && value.ends_with('\''))
121            {
122                value = value[1..value.len() - 1].to_string();
123            }
124            // Handle escape sequences in double-quoted values
125            value = value.replace("\\n", "\n").replace("\\\"", "\"");
126            if !key.is_empty() {
127                map.insert(key, Value::String(value));
128            }
129        }
130    }
131    map
132}