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}