uncurses/terminal/env.rs
1//! Environment variables for terminal configuration.
2//!
3//! [`Env`] is a read-only view of a set of environment variables.
4//! [`ProcessEnv`] reads the live process environment, and [`EnvList`] holds a
5//! fixed list of variables. `EnvList` covers both testing with deterministic
6//! inputs and environments that arrive from somewhere other than this process,
7//! such as the variable list an SSH client forwards.
8
9/// A read-only view of environment variables.
10///
11/// Implementations decide where the variables come from and whether they can
12/// change: [`ProcessEnv`] reads the live process environment on every lookup,
13/// while [`EnvList`] answers from a fixed list.
14///
15/// Only [`get`](Self::get) needs implementing; [`has`](Self::has) is derived
16/// from it.
17pub trait Env: Send + Sync {
18 /// Return a variable's value.
19 ///
20 /// # Parameters
21 ///
22 /// * `key` — variable name.
23 ///
24 /// # Returns
25 ///
26 /// The value, or `None` if `key` is absent.
27 ///
28 /// # Errors and panics
29 ///
30 /// Implementations should not panic for an absent or malformed variable.
31 fn get(&self, key: &str) -> Option<String>;
32
33 /// Return whether a variable is present with a non-empty value.
34 ///
35 /// Overriding this is only for taking a shortcut, not for changing the
36 /// answer: it must stay equivalent to
37 /// `get(key).is_some_and(|v| !v.is_empty())`.
38 ///
39 /// # Parameters
40 ///
41 /// * `key` — variable name.
42 ///
43 /// # Returns
44 ///
45 /// `true` if `key` is present and its value is not empty.
46 ///
47 /// # Errors and panics
48 ///
49 /// This method does not fail or intentionally panic.
50 fn has(&self, key: &str) -> bool {
51 self.get(key).is_some_and(|v| !v.is_empty())
52 }
53}
54
55impl<T: Env + ?Sized> Env for Box<T> {
56 fn get(&self, key: &str) -> Option<String> {
57 (**self).get(key)
58 }
59
60 fn has(&self, key: &str) -> bool {
61 (**self).has(key)
62 }
63}
64
65/// The live process environment.
66///
67/// Every lookup reads through to [`std::env::var`], so a variable changed after
68/// this value was created is visible to the next lookup. This is the
69/// environment a [`Terminal`](crate::terminal::Terminal) uses when built over
70/// process stdio or the controlling terminal.
71///
72/// Two things follow from going through [`std::env::var`], and neither is true
73/// of [`EnvList`]: a value that is not valid Unicode reads as absent rather
74/// than panicking, and on Windows names match case-insensitively.
75#[derive(Debug, Clone, Copy, Default)]
76pub struct ProcessEnv;
77
78impl Env for ProcessEnv {
79 fn get(&self, key: &str) -> Option<String> {
80 std::env::var(key).ok()
81 }
82}
83
84/// A fixed list of environment variables.
85///
86/// Use this for an environment that does not come from this process, such as
87/// the variables an SSH client forwards or a set read from a configuration
88/// file, and for tests that need deterministic lookups.
89///
90/// Variables are stored as an ordered list of `(key, value)` pairs, matching
91/// how an environment is passed around at the process boundary. Duplicate keys
92/// are allowed, and lookups return the last matching value.
93#[derive(Debug, Clone, Default)]
94pub struct EnvList {
95 vars: Vec<(String, String)>,
96}
97
98impl EnvList {
99 /// Build an empty environment.
100 ///
101 /// # Returns
102 ///
103 /// An [`EnvList`] with no variables.
104 ///
105 /// # Errors and panics
106 ///
107 /// This method does not fail or intentionally panic.
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 /// Capture the current process environment.
113 ///
114 /// The result is a snapshot: later changes to the process environment are
115 /// not visible to it. Use [`ProcessEnv`] to read through to the live
116 /// environment instead.
117 ///
118 /// # Returns
119 ///
120 /// An [`EnvList`] containing all variables yielded by [`std::env::vars`] at
121 /// the time of the call.
122 ///
123 /// # Errors and panics
124 ///
125 /// This method does not return errors. It has the same panic behavior as
126 /// [`std::env::vars`] if the process environment contains invalid data.
127 pub fn from_process() -> Self {
128 Self {
129 vars: std::env::vars().collect(),
130 }
131 }
132
133 /// Build an environment from `(key, value)` pairs.
134 ///
135 /// Pair order and duplicate keys are preserved. Later duplicates shadow
136 /// earlier values for [`get`](Env::get) and [`has`](Env::has).
137 ///
138 /// # Parameters
139 ///
140 /// * `iter` — variables to store.
141 ///
142 /// # Returns
143 ///
144 /// An [`EnvList`] containing the supplied variables.
145 ///
146 /// # Errors and panics
147 ///
148 /// This method does not return errors. It may panic only if allocation for
149 /// the stored strings fails.
150 pub fn from_pairs<I, K, V>(iter: I) -> Self
151 where
152 I: IntoIterator<Item = (K, V)>,
153 K: Into<String>,
154 V: Into<String>,
155 {
156 Self {
157 vars: iter
158 .into_iter()
159 .map(|(k, v)| (k.into(), v.into()))
160 .collect(),
161 }
162 }
163
164 /// Append a variable to the list.
165 ///
166 /// If `key` is already present, the new value shadows earlier values for
167 /// [`get`](Env::get) and [`has`](Env::has).
168 ///
169 /// # Parameters
170 ///
171 /// * `key` — variable name.
172 /// * `value` — variable value.
173 ///
174 /// # Returns
175 ///
176 /// `self`, for chaining.
177 ///
178 /// # Errors and panics
179 ///
180 /// This method does not return errors. It may panic only if allocation for
181 /// the stored strings fails.
182 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
183 self.vars.push((key.into(), value.into()));
184 self
185 }
186}
187
188impl Env for EnvList {
189 fn get(&self, key: &str) -> Option<String> {
190 self.vars
191 .iter()
192 .rev()
193 .find(|(k, _)| k == key)
194 .map(|(_, v)| v.clone())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn duplicate_keys_last_wins() {
204 let e = EnvList::from_pairs([("FOO", "a"), ("BAR", "x"), ("FOO", "b")]);
205 assert_eq!(e.get("FOO").as_deref(), Some("b"));
206 assert_eq!(e.get("BAR").as_deref(), Some("x"));
207 }
208
209 #[test]
210 fn set_shadows_earlier_value() {
211 let mut e = EnvList::from_pairs([("K", "first")]);
212 e.set("K", "second");
213 assert_eq!(e.get("K").as_deref(), Some("second"));
214 }
215
216 #[test]
217 fn has_requires_non_empty() {
218 let e = EnvList::from_pairs([("EMPTY", ""), ("SET", "v")]);
219 assert!(!e.has("EMPTY"));
220 assert!(e.has("SET"));
221 assert!(!e.has("MISSING"));
222 }
223
224 #[test]
225 fn boxed_env_forwards() {
226 let e: Box<dyn Env> = Box::new(EnvList::from_pairs([("TERM", "xterm")]));
227 assert_eq!(e.get("TERM").as_deref(), Some("xterm"));
228 assert!(e.has("TERM"));
229 }
230
231 #[test]
232 fn boxed_env_forwards_has_override() {
233 // An implementor that treats a set-but-empty value as present, which
234 // is the opposite of the default `has`. Boxing must not silently
235 // reinstate the default.
236 struct EmptyCounts;
237 impl Env for EmptyCounts {
238 fn get(&self, _key: &str) -> Option<String> {
239 Some(String::new())
240 }
241 fn has(&self, _key: &str) -> bool {
242 true
243 }
244 }
245
246 let e: Box<dyn Env> = Box::new(EmptyCounts);
247 assert!(e.has("ANYTHING"));
248 assert!(e.as_ref().has("ANYTHING"));
249 }
250}