stupid_simple_dotenv/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! Reads key-value pairs from a file, such as an .env file, and makes them
3//! easily accessible as environment variables. This crate provides a simpler and smaller
4//! alternative to dotenv, which is no longer maintained.
5//! # Example
6//! ```rust
7//! use stupid_simple_dotenv::to_env;
8//!
9//! fn main() ->Result<(), Box<dyn std::error::Error>> {
10//!    match to_env(){
11//!       Ok(_) => println!("Success reading .env"),
12//!       Err(e) if e.kind == "io" =>{
13//!         println!("IO-Error better not ignore! {}", e);
14//!         // you can return the Error: return Err(e.into());
15//!       },
16//!       Err(e) if e.kind == "LinesError" => {
17//!         println!("Errors in some lines of .env: {}", e);
18//!       },
19//!       Err(e) => {
20//!         println!("Error {}", e);
21//!         // You can return the Error return Err(e.into());
22//!       },
23//!    };
24//!    let user = std::env::var("USER")?; // if USER is not set, this will return an error
25//!    println!("USER: {}", user);
26//!    Ok(())
27//! }
28//!
29
30use std::{error::Error, fmt::Display, fs::File, io::BufRead, path::Path};
31#[derive(Debug)]
32pub struct SimpleEnvError {
33    pub kind: String,
34    message: String,
35    pub list: Option<Vec<(String, String)>>,
36}
37
38impl Display for SimpleEnvError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "{}: {}", self.kind, self.message)
41    }
42}
43impl Error for SimpleEnvError {
44    fn source(&self) -> Option<&(dyn Error + 'static)> {
45        None
46    }
47}
48impl From<std::io::Error> for SimpleEnvError {
49    fn from(error: std::io::Error) -> Self {
50        SimpleEnvError {
51            kind: String::from("io"),
52            message: error.to_string(),
53            list: None,
54        }
55    }
56}
57
58// pub struct SimpleEnvErrorWrapper(SimpleEnvError);
59
60// impl From<SimpleEnvError> for SimpleEnvErrorWrapper {
61//     fn from(error: SimpleEnvError) -> Self {
62//         SimpleEnvErrorWrapper(error)
63//     }
64// }
65
66// impl Into<Box<dyn StdError>> for SimpleEnvErrorWrapper {
67//     fn into(self) -> Box<dyn StdError> {
68//         Box::new(self.0)
69//     }
70// }
71impl From<SimpleEnvError> for std::io::Error {
72    fn from(err: SimpleEnvError) -> Self {
73        std::io::Error::other(err.to_string())
74    }
75}
76
77/// Reads .env file and stores the key value pairs as environment variables.
78///
79/// **Note:** This function does NOT override existing environment variables.
80/// This follows the standard dotenv behavior where shell exports take precedence
81/// over .env values. Use [`to_env_override`] if you need to override existing variables.
82///
83/// ```rust
84/// fn main() {
85///    let _ = stupid_simple_dotenv::to_env(); // reads .env file and stores the key value pairs as environment variables
86///    let value = std::env::var("key").expect("Key 'key' not found."); //Works if key value pair is present in .env file
87/// }
88///
89/// ```
90pub fn to_env() -> Result<(), SimpleEnvError> {
91    match read(".env") {
92        Ok(list) => {
93            iter_to_env(&list);
94            Ok(())
95        }
96        Err(e) => {
97            e.list.as_ref().map(iter_to_env);
98            Err(e)
99        }
100    }
101}
102
103/// Reads .env file and stores the key value pairs as environment variables,
104/// overriding any existing values.
105///
106/// Unlike [`to_env`], this function will override existing environment variables
107/// with values from the .env file.
108///
109/// ```rust
110/// fn main() {
111///    let _ = stupid_simple_dotenv::to_env_override(); // reads .env file and overrides existing environment variables
112///    let value = std::env::var("key").expect("Key 'key' not found.");
113/// }
114///
115/// ```
116pub fn to_env_override() -> Result<(), SimpleEnvError> {
117    match read(".env") {
118        Ok(list) => {
119            iter_to_env_override(&list);
120            Ok(())
121        }
122        Err(e) => {
123            e.list.as_ref().map(iter_to_env_override);
124            Err(e)
125        }
126    }
127}
128
129fn iter_to_env(list: &Vec<(String, String)>) {
130    for line in list {
131        let (key, value) = (&line.0, &line.1);
132        if std::env::var(key).is_err() {
133            std::env::set_var(key, value);
134        }
135    }
136}
137
138fn iter_to_env_override(list: &Vec<(String, String)>) {
139    for line in list {
140        let (key, value) = (&line.0, &line.1);
141        std::env::set_var(key, value);
142    }
143}
144/// Reads .env file to a vector of key value pairs tuples.
145/// ```rust
146/// fn main() {
147///     let list = match stupid_simple_dotenv::to_vec(){
148///         Ok(list) => list,
149///         Err(e) => {
150///             println!("Error {}", e);
151///             if let Some(list) = e.list{
152///                 list
153///             }else{
154///                 panic!("{}",e.to_string());
155///             }
156///         },
157///     }; // reads .env file to a vector of key value pairs tuples
158///     for line in list {
159///         println! ("Key:{}, Value:{}",line.0, line.1);
160///     }
161/// }
162/// ```
163pub fn to_vec() -> Result<Vec<(String, String)>, SimpleEnvError> {
164    let list = read(".env")?;
165    Ok(list)
166}
167
168/// Reads key value pairs from a file and stores them as environment variables.
169///
170/// **Note:** This function does NOT override existing environment variables.
171/// This follows the standard dotenv behavior where shell exports take precedence
172/// over file values. Use [`file_to_env_override`] if you need to override existing variables.
173pub fn file_to_env<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn std::error::Error>> {
174    let list = read(path)?;
175    for line in list {
176        let (key, value) = (line.0, line.1);
177        if std::env::var(&key).is_err() {
178            std::env::set_var(key, value);
179        }
180    }
181    Ok(())
182}
183
184/// Reads key value pairs from a file and stores them as environment variables,
185/// overriding any existing values.
186///
187/// Unlike [`file_to_env`], this function will override existing environment variables
188/// with values from the file.
189pub fn file_to_env_override<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn std::error::Error>> {
190    let list = read(path)?;
191    for line in list {
192        let (key, value) = (line.0, line.1);
193        std::env::set_var(key, value);
194    }
195    Ok(())
196}
197
198/// Reads key value pairs from a file and returns a vector of tuples.
199/// ```rust
200/// fn main() {
201///     let list = stupid_simple_dotenv::file_to_vec("other.env").unwrap(); // reads other.env file and stores the key value pairs as environment variables
202///     for item in list{
203///         println!("Key:{}, Value:{}", item.0, item.1);
204///     }
205/// }
206pub fn file_to_vec<P: AsRef<Path>>(
207    path: P,
208) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
209    let list = read(path)?;
210    Ok(list)
211}
212
213/// Try to get the value of an environment variable.
214/// If the variable is not present in the environment, `default` is returned.
215/// ```rust
216/// fn main() {
217///     let value = stupid_simple_dotenv::get_or("key_not_here", "default_key");
218///     assert_eq!("default_key", &value);
219/// }
220pub fn get_or(key: &str, default: &str) -> String {
221    std::env::var(key).unwrap_or_else(|_| default.to_owned())
222}
223
224fn read<P: AsRef<Path>>(path: P) -> Result<Vec<(String, String)>, SimpleEnvError> {
225    let f = File::open(path)?;
226    let lines = std::io::BufReader::new(f).lines();
227    parse(lines)
228}
229
230fn parse(
231    lines: impl Iterator<Item = Result<String, std::io::Error>>,
232) -> Result<Vec<(String, String)>, SimpleEnvError> {
233    let mut error_lines = Vec::new();
234    let mut num_error_lines = 0;
235    let mut list = Vec::new();
236    let lines = lines;
237    for (col, line) in lines.enumerate() {
238        let line = line?;
239        let line = line.trim();
240        if line.starts_with('#') || line.is_empty() {
241            continue;
242        }
243        let parsed = match parse_line(line) {
244            Ok(parsed) => parsed,
245            Err(e) => {
246                num_error_lines += 1;
247                if error_lines.len() < 10 {
248                    error_lines.push(format!("Error in Line {col}: {e}"));
249                }
250                continue;
251            }
252        };
253        list.push((parsed.0.to_owned(), parsed.1.to_owned()));
254    }
255    if error_lines.is_empty() {
256        Ok(list)
257    } else {
258        if num_error_lines > error_lines.len() {
259            error_lines.push(format!(
260                "And {} more errors in .env file",
261                num_error_lines - error_lines.len()
262            ));
263        }
264        Err(SimpleEnvError {
265            kind: "LinesError".to_string(),
266            message: error_lines.join("\n"),
267            list: Some(list),
268        })
269    }
270}
271
272fn parse_line(s: &str) -> Result<(&str, &str), Box<dyn Error>> {
273    if s.is_empty() {
274        return Err("Empty line".into());
275    }
276    if s.starts_with('#') {
277        return Err("Comment line".into());
278    }
279    let mut name_begin: usize = 0;
280    let mut name_end: usize = 0;
281    let mut value_begin: usize = 0;
282    let mut value_end: usize = 0;
283    let mut in_name = true;
284    let mut in_value = false;
285    let mut quotes = 'f';
286    let mut must_trim = false;
287    for (pos, c) in s.char_indices() {
288        match c {
289            '"' | '\'' | '`' => {
290                if quotes != 'f' {
291                    //We are in Quotes
292                    if quotes == c {
293                        //End of Quotes
294                        quotes = 'f';
295                        if in_name {
296                            name_end = pos - 1;
297                        }
298                        if in_value {
299                            value_end = pos - 1;
300                            break; //Done
301                        }
302                    }
303                } else {
304                    //We are not in Quotes, let it begin
305                    quotes = c;
306                    if in_name {
307                        name_begin = pos + 1;
308                    }
309                    if in_value {
310                        value_begin = pos + 1;
311                    }
312                }
313            }
314            '=' => {
315                if quotes != 'f' {
316                    continue;
317                }
318                if in_name {
319                    in_name = false;
320                    in_value = true;
321                    if name_end == 0 && pos > 0 {
322                        name_end = pos - 1;
323                    }
324                }
325            }
326            '#' => {
327                if quotes != 'f' {
328                    continue;
329                }
330                if in_value {
331                    value_end = pos - 1;
332                    must_trim = true;
333                    break;
334                }
335                if in_name {
336                    return Err(format!("Comment character '#' in name part of '{s}'").into());
337                }
338            }
339            _ => {
340                if in_name {
341                    if c.is_whitespace() && quotes == 'f' {
342                        continue;
343                    }
344                    name_end = pos;
345                } else if c.is_whitespace() && quotes == 'f' {
346                    continue;
347                }
348                if in_value {
349                    value_end = pos;
350                    if value_begin == 0 {
351                        value_begin = pos;
352                    }
353                }
354            }
355        }
356    }
357    if value_begin == 0 || name_end == 0 {
358        Err(format!("No name or value in '{s}'").into())
359    } else if value_begin == 0 {
360        Err("No value".into())
361    } else {
362        if must_trim {
363            {
364                let s = &s[value_begin..=value_end];
365                value_end = value_begin + s.trim_end().len() - 1;
366            }
367        }
368        Ok((&s[name_begin..=name_end], &s[value_begin..=value_end]))
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn it_works() {
378        match to_env() {
379            Ok(_) => {
380                //Error expected. Sample .env file has error
381                assert!(false)
382            }
383            Err(e) => {
384                //OK
385                assert_eq!(e.kind, "LinesError");
386                assert_eq!(
387                    e.message.starts_with("Error in Line ")
388                        && e.message.ends_with("No name or value in 'error='"),
389                    true
390                );
391            }
392        }
393    }
394
395    #[test]
396    fn test_parse_line_new() {
397        assert_eq!(parse_line("FOO=BAR").unwrap(), ("FOO", "BAR"));
398        assert_eq!(parse_line("\"FOO\"=\"BAR\"").unwrap(), ("FOO", "BAR"));
399        assert_eq!(parse_line("FOO = BAR").unwrap(), ("FOO", "BAR"));
400        assert_eq!(parse_line("FOO=\"BAR\"").unwrap(), ("FOO", "BAR"));
401        assert_eq!(parse_line("FOO='BAR'").unwrap(), ("FOO", "BAR"));
402        assert_eq!(parse_line("FOO=`BAR`").unwrap(), ("FOO", "BAR"));
403        assert_eq!(parse_line("FOO=\t `BAR`").unwrap(), ("FOO", "BAR"));
404        assert_eq!(parse_line("FOO\t=\t `BAR`").unwrap(), ("FOO", "BAR"));
405        assert_eq!(parse_line("FOO\t=\t ` BAR`").unwrap(), ("FOO", " BAR"));
406        assert_eq!(
407            parse_line("FOO\t=\t ` BAR`#comment").unwrap(),
408            ("FOO", " BAR")
409        );
410        assert_eq!(parse_line("FOO\t=\t ` BAR `").unwrap(), ("FOO", " BAR "));
411        assert_eq!(
412            parse_line("FOO\t   =   \t ` BAR `").unwrap(),
413            ("FOO", " BAR ")
414        );
415        assert_eq!(
416            parse_line(" FOO\t   =   \t ` BAR `").unwrap(),
417            (" FOO", " BAR ")
418        );
419
420        assert_eq!(true, matches!(parse_line(" FOO\t   = "), Result::Err(_)));
421        assert_eq!(true, matches!(parse_line(" FOO\t   ="), Result::Err(_)));
422        assert_eq!(true, matches!(parse_line("="), Result::Err(_)));
423        assert_eq!(true, matches!(parse_line("#value=comment"), Result::Err(_)));
424    }
425
426    #[test]
427    fn test_parse() {
428        let env_sim = r#"
429FOO=BAR
430# comment
431FOO2= BAR2
432
433FOO3="BAR3"
434FOO4='BAR4'
435FOO5=`BAR5`
436FOO6=BAR6 #comment
437Foo7=BA😀R7 #comment
438#FOO8=BAR8
439FOO9=BAR9
440"#;
441        let lines = env_sim.lines().map(|s| Ok(s.to_owned()));
442        let lines_clone = lines.clone();
443        let start = std::time::Instant::now();
444        let list = parse(lines).unwrap();
445        let time_old = start.elapsed();
446        let start = std::time::Instant::now();
447        let list2 = parse(lines_clone).unwrap();
448        println!("Old: {:?} New {:?}", time_old, start.elapsed());
449        assert_eq!(
450            list,
451            vec![
452                ("FOO".to_owned(), "BAR".to_owned()),
453                ("FOO2".to_owned(), "BAR2".to_owned()),
454                ("FOO3".to_owned(), "BAR3".to_owned()),
455                ("FOO4".to_owned(), "BAR4".to_owned()),
456                ("FOO5".to_owned(), "BAR5".to_owned()),
457                ("FOO6".to_owned(), "BAR6".to_owned()),
458                ("Foo7".to_owned(), "BA😀R7".to_owned()),
459                ("FOO9".to_owned(), "BAR9".to_owned()),
460            ]
461        );
462        assert_eq!(list, list2);
463    }
464
465    #[test]
466    fn parse_reports_each_error_once() {
467        let env_sim = "FOO=BAR\ninvalid\ninvalid2\n";
468        let lines = env_sim.lines().map(|s| Ok(s.to_owned()));
469        let err = parse(lines).unwrap_err();
470        let messages: Vec<&str> = err.message.lines().collect();
471
472        assert_eq!(messages.len(), 2);
473        assert_eq!(
474            messages
475                .iter()
476                .filter(|line| line.contains("Error in Line 1:"))
477                .count(),
478            1
479        );
480        assert_eq!(
481            messages
482                .iter()
483                .filter(|line| line.contains("Error in Line 2:"))
484                .count(),
485            1
486        );
487    }
488
489    #[test]
490    fn test_iter_to_env_does_not_override() {
491        let key = "TEST_NO_OVERRIDE_KEY";
492        let original_value = "original_value";
493        let new_value = "new_value";
494
495        // Set the environment variable first
496        std::env::set_var(key, original_value);
497
498        // Try to set it via iter_to_env (should NOT override)
499        let list = vec![(key.to_owned(), new_value.to_owned())];
500        iter_to_env(&list);
501
502        // The original value should still be there
503        assert_eq!(std::env::var(key).unwrap(), original_value);
504
505        // Cleanup
506        std::env::remove_var(key);
507    }
508
509    #[test]
510    fn test_iter_to_env_sets_new_var() {
511        let key = "TEST_NEW_VAR_KEY";
512        let value = "test_value";
513
514        // Ensure the variable doesn't exist
515        std::env::remove_var(key);
516
517        // Set it via iter_to_env
518        let list = vec![(key.to_owned(), value.to_owned())];
519        iter_to_env(&list);
520
521        // The value should be set
522        assert_eq!(std::env::var(key).unwrap(), value);
523
524        // Cleanup
525        std::env::remove_var(key);
526    }
527
528    #[test]
529    fn test_iter_to_env_override_overrides() {
530        let key = "TEST_OVERRIDE_KEY";
531        let original_value = "original_value";
532        let new_value = "new_value";
533
534        // Set the environment variable first
535        std::env::set_var(key, original_value);
536
537        // Override it via iter_to_env_override
538        let list = vec![(key.to_owned(), new_value.to_owned())];
539        iter_to_env_override(&list);
540
541        // The new value should be there
542        assert_eq!(std::env::var(key).unwrap(), new_value);
543
544        // Cleanup
545        std::env::remove_var(key);
546    }
547}