Skip to main content

uncurses/terminal/
env.rs

1//! Environment variable snapshots for terminal configuration.
2//!
3//! [`Env`] wraps a captured set of `(key, value)` pairs so that code which
4//! depends on the process environment can be exercised with deterministic
5//! inputs in tests, and so that callers can build synthetic environments
6//! (for example, from a configuration file) without mutating the
7//! process-global state managed by [`std::env`](mod@std::env).
8
9/// A snapshot of environment variables.
10///
11/// `Env` stores an ordered list of `(key, value)` pairs. Duplicate keys are
12/// allowed; lookups return the **last** matching value. This makes it useful
13/// both for capturing the process environment and for constructing
14/// deterministic terminal environments in tests.
15#[derive(Debug, Clone, Default)]
16pub struct Env {
17    vars: Vec<(String, String)>,
18}
19
20impl Env {
21    /// Capture the current process environment.
22    ///
23    /// # Returns
24    ///
25    /// An [`Env`] containing all variables yielded by [`std::env::vars`] at the
26    /// time of the call.
27    ///
28    /// # Errors and panics
29    ///
30    /// This method does not return errors. It has the same panic behavior as
31    /// [`std::env::vars`] if the process environment contains invalid data.
32    pub fn from_process() -> Self {
33        Self {
34            vars: std::env::vars().collect(),
35        }
36    }
37
38    /// Build an empty environment.
39    ///
40    /// # Returns
41    ///
42    /// An [`Env`] with no variables.
43    ///
44    /// # Errors and panics
45    ///
46    /// This method does not fail or intentionally panic.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Build an environment from `(key, value)` pairs.
52    ///
53    /// Pair order and duplicate keys are preserved. Later duplicates shadow
54    /// earlier values for [`get`](Self::get), [`has`](Self::has), and
55    /// [`is_truthy`](Self::is_truthy).
56    ///
57    /// # Parameters
58    ///
59    /// * `iter` — variables to store.
60    ///
61    /// # Returns
62    ///
63    /// An [`Env`] containing the supplied variables.
64    ///
65    /// # Errors and panics
66    ///
67    /// This method does not return errors. It may panic only if allocation for
68    /// the stored strings fails.
69    pub fn from_pairs<I, K, V>(iter: I) -> Self
70    where
71        I: IntoIterator<Item = (K, V)>,
72        K: Into<String>,
73        V: Into<String>,
74    {
75        Self {
76            vars: iter
77                .into_iter()
78                .map(|(k, v)| (k.into(), v.into()))
79                .collect(),
80        }
81    }
82
83    /// Append a variable to the snapshot.
84    ///
85    /// If `key` already exists, the new value shadows earlier values for
86    /// [`get`](Self::get), [`is_truthy`](Self::is_truthy), and [`has`](Self::has)
87    /// lookups. Existing entries are not removed.
88    ///
89    /// # Parameters
90    ///
91    /// * `key` — variable name.
92    /// * `value` — variable value.
93    ///
94    /// # Returns
95    ///
96    /// `self`, for chaining.
97    ///
98    /// # Errors and panics
99    ///
100    /// This method does not return errors. It may panic only if allocation for
101    /// the stored strings fails.
102    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
103        self.vars.push((key.into(), value.into()));
104        self
105    }
106
107    /// Return whether a variable is present with a non-empty value.
108    ///
109    /// # Parameters
110    ///
111    /// * `key` — variable name.
112    ///
113    /// # Returns
114    ///
115    /// `true` if the last value for `key` exists and is not empty.
116    ///
117    /// # Errors and panics
118    ///
119    /// This method does not fail or intentionally panic.
120    pub fn has(&self, key: &str) -> bool {
121        self.lookup(key).is_some_and(|v| !v.is_empty())
122    }
123
124    /// Return a variable's value.
125    ///
126    /// If duplicate keys are present, the last value is returned.
127    ///
128    /// # Parameters
129    ///
130    /// * `key` — variable name.
131    ///
132    /// # Returns
133    ///
134    /// A newly allocated copy of the value, or `None` if `key` is absent.
135    ///
136    /// # Errors and panics
137    ///
138    /// This method does not return errors. It may panic only if allocation for
139    /// the returned string fails.
140    pub fn get(&self, key: &str) -> Option<String> {
141        self.lookup(key).map(str::to_owned)
142    }
143
144    /// Return whether a variable parses as a truthy boolean.
145    ///
146    /// Accepts `1`, `t`, `T`, `TRUE`, `true`, and `True`. Anything else,
147    /// including an empty or absent value, is false.
148    ///
149    /// # Parameters
150    ///
151    /// * `key` — variable name.
152    ///
153    /// # Returns
154    ///
155    /// `true` for the accepted truthy values.
156    ///
157    /// # Errors and panics
158    ///
159    /// This method does not fail or intentionally panic.
160    pub fn is_truthy(&self, key: &str) -> bool {
161        matches!(
162            self.lookup(key).unwrap_or_default(),
163            "1" | "t" | "T" | "TRUE" | "true" | "True"
164        )
165    }
166
167    fn lookup(&self, key: &str) -> Option<&str> {
168        self.vars
169            .iter()
170            .rev()
171            .find(|(k, _)| k == key)
172            .map(|(_, v)| v.as_str())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn duplicate_keys_last_wins() {
182        let e = Env::from_pairs([("FOO", "a"), ("BAR", "x"), ("FOO", "b")]);
183        assert_eq!(e.get("FOO").as_deref(), Some("b"));
184        assert_eq!(e.get("BAR").as_deref(), Some("x"));
185    }
186
187    #[test]
188    fn set_shadows_earlier_value() {
189        let mut e = Env::from_pairs([("K", "first")]);
190        e.set("K", "second");
191        assert_eq!(e.get("K").as_deref(), Some("second"));
192    }
193
194    #[test]
195    fn has_requires_non_empty() {
196        let e = Env::from_pairs([("EMPTY", ""), ("SET", "v")]);
197        assert!(!e.has("EMPTY"));
198        assert!(e.has("SET"));
199        assert!(!e.has("MISSING"));
200    }
201
202    #[test]
203    fn bool_truthy_values() {
204        let e = Env::from_pairs([
205            ("A", "1"),
206            ("B", "true"),
207            ("C", "TRUE"),
208            ("D", "True"),
209            ("E", "t"),
210            ("F", "T"),
211        ]);
212        for k in ["A", "B", "C", "D", "E", "F"] {
213            assert!(e.is_truthy(k), "{k} should be truthy");
214        }
215    }
216
217    #[test]
218    fn bool_falsy_values() {
219        let e = Env::from_pairs([("A", "0"), ("B", "false"), ("C", ""), ("D", "yes")]);
220        for k in ["A", "B", "C", "D"] {
221            assert!(!e.is_truthy(k), "{k} should be falsy");
222        }
223        assert!(!e.is_truthy("MISSING"));
224    }
225}