Skip to main content

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}