Skip to main content

shrub_core/
env.rs

1//! Implements a shell variable environment.
2
3use std::borrow::Cow;
4use std::collections::{HashMap, hash_map};
5
6use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable};
7use crate::{Shell, error, extensions};
8
9/// Represents the policy for looking up variables in a shell environment.
10#[derive(Clone, Copy)]
11pub enum EnvironmentLookup {
12    /// Look anywhere.
13    Anywhere,
14    /// Look only in the global scope.
15    OnlyInGlobal,
16    /// Look only in the innermost scope of the given type (e.g. the current
17    /// function's local scope, for [`EnvironmentScope::Local`]).
18    OnlyInCurrent(EnvironmentScope),
19    /// Look only in local scopes.
20    OnlyInLocal,
21}
22
23impl EnvironmentLookup {
24    /// Returns a filter that, given the environment's scopes' types from the
25    /// innermost out, says whether this policy looks in each.
26    fn scope_filter(self) -> impl FnMut(EnvironmentScope) -> bool {
27        let mut found_current = false;
28        move |scope_type| match self {
29            Self::Anywhere => true,
30            Self::OnlyInGlobal => scope_type == EnvironmentScope::Global,
31            Self::OnlyInLocal => scope_type == EnvironmentScope::Local,
32            Self::OnlyInCurrent(current) => {
33                let is_current = !found_current && scope_type == current;
34                found_current |= is_current;
35                is_current
36            }
37        }
38    }
39}
40
41/// Represents a shell environment scope.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum EnvironmentScope {
45    /// Scope local to a function instance
46    Local,
47    /// Globals
48    Global,
49    /// Transient overrides for a command invocation
50    Command,
51}
52
53impl std::fmt::Display for EnvironmentScope {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Self::Local => write!(f, "local"),
57            Self::Global => write!(f, "global"),
58            Self::Command => write!(f, "command"),
59        }
60    }
61}
62
63/// Represents the shell variable environment, composed of a stack of scopes.
64#[derive(Clone, Debug)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct ShellEnvironment {
67    /// Stack of scopes, with the top of the stack being the current scope.
68    scopes: Vec<(EnvironmentScope, ShellVariableMap)>,
69    /// Whether or not to auto-export variables on creation or modification.
70    export_variables_on_modification: bool,
71    /// Count of total entries (may include duplicates with shadowed variables).
72    entry_count: usize,
73}
74
75impl Default for ShellEnvironment {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl ShellEnvironment {
82    /// Returns a new shell environment.
83    pub fn new() -> Self {
84        Self {
85            scopes: vec![(EnvironmentScope::Global, ShellVariableMap::default())],
86            export_variables_on_modification: false,
87            entry_count: 0,
88        }
89    }
90
91    /// Returns how many scopes deep the environment is.
92    pub(crate) const fn scope_depth(&self) -> usize {
93        self.scopes.len()
94    }
95
96    /// Pushes a new scope of the given type onto the environment's scope stack.
97    ///
98    /// # Arguments
99    ///
100    /// * `scope_type` - The type of scope to push.
101    pub fn push_scope(&mut self, scope_type: EnvironmentScope) {
102        self.scopes.push((scope_type, ShellVariableMap::default()));
103    }
104
105    /// Pops the top-most scope off the environment's scope stack.
106    ///
107    /// # Arguments
108    ///
109    /// * `expected_scope_type` - The type of scope that is expected to be atop
110    ///   the stack.
111    pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> {
112        // TODO(env): Should we panic instead on failure? It's effectively a
113        // broken invariant. Either way, don't pop a scope that isn't
114        // the one expected.
115        match self.scopes.last() {
116            Some((actual_scope_type, _)) if *actual_scope_type == expected_scope_type => {
117                self.scopes.pop();
118                Ok(())
119            }
120            Some((actual_scope_type, _)) => Err(error::ErrorKind::UnexpectedScopeType {
121                expected: expected_scope_type,
122                actual: *actual_scope_type,
123            }
124            .into()),
125            None => Err(error::ErrorKind::MissingScope.into()),
126        }
127    }
128
129    //
130    // Iterators/Getters
131    //
132
133    /// Returns an iterator over all exported variables defined in the variable.
134    pub fn iter_exported(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
135        // We won't actually need to store all entries, but we expect it should
136        // be within the same order.
137        let mut visible_vars: HashMap<&String, &ShellVariable> =
138            HashMap::with_capacity(self.entry_count);
139
140        for (_, var_map) in self.scopes.iter().rev() {
141            for (name, var) in var_map.iter().filter(|(_, v)| v.is_exported()) {
142                // Only insert the variable if it hasn't been seen yet.
143                if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
144                    entry.insert(var);
145                }
146            }
147        }
148
149        visible_vars.into_iter()
150    }
151
152    /// Returns an iterator over all the variables defined in the environment.
153    pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
154        self.iter_using_policy(EnvironmentLookup::Anywhere)
155    }
156
157    /// Returns an iterator over all the variables defined in the environment,
158    /// using the given lookup policy.
159    ///
160    /// # Arguments
161    ///
162    /// * `lookup_policy` - The policy to use when looking up variables.
163    pub fn iter_using_policy(
164        &self,
165        lookup_policy: EnvironmentLookup,
166    ) -> impl Iterator<Item = (&String, &ShellVariable)> {
167        // We won't actually need to store all entries, but we expect it should
168        // be within the same order.
169        let mut visible_vars: HashMap<&String, &ShellVariable> =
170            HashMap::with_capacity(self.entry_count);
171
172        let mut looks_in = lookup_policy.scope_filter();
173        for (_, var_map) in self.scopes.iter().rev().filter(|(t, _)| looks_in(*t)) {
174            for (name, var) in var_map.iter() {
175                // Only insert the variable if it hasn't been seen yet.
176                if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
177                    entry.insert(var);
178                }
179            }
180        }
181
182        visible_vars.into_iter()
183    }
184
185    /// Tries to retrieve an immutable reference to the variable with the given
186    /// name in the environment.
187    ///
188    /// # Arguments
189    ///
190    /// * `name` - The name of the variable to retrieve.
191    pub fn get<S: AsRef<str>>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> {
192        // Look through scopes, from the top of the stack on down.
193        for (scope_type, map) in self.scopes.iter().rev() {
194            if let Some(var) = map.get(name.as_ref()) {
195                return Some((*scope_type, var));
196            }
197        }
198
199        None
200    }
201
202    /// Tries to retrieve a mutable reference to the variable with the given
203    /// name in the environment.
204    ///
205    /// # Arguments
206    ///
207    /// * `name` - The name of the variable to retrieve.
208    pub fn get_mut<S: AsRef<str>>(
209        &mut self,
210        name: S,
211    ) -> Option<(EnvironmentScope, &mut ShellVariable)> {
212        // Look through scopes, from the top of the stack on down.
213        for (scope_type, map) in self.scopes.iter_mut().rev() {
214            if let Some(var) = map.get_mut(name.as_ref()) {
215                return Some((*scope_type, var));
216            }
217        }
218
219        None
220    }
221
222    /// Tries to retrieve the string value of the variable with the given name
223    /// in the environment.
224    ///
225    /// # Arguments
226    ///
227    /// * `name` - The name of the variable to retrieve.
228    /// * `shell` - The shell owning the environment.
229    pub fn get_str<S: AsRef<str>, SE: extensions::ShellExtensions>(
230        &self,
231        name: S,
232        shell: &Shell<SE>,
233    ) -> Option<Cow<'_, str>> {
234        self.get(name.as_ref())
235            .map(|(_, v)| v.value().to_cow_str(shell))
236    }
237
238    /// Checks if a variable of the given name is set in the environment.
239    ///
240    /// # Arguments
241    ///
242    /// * `name` - The name of the variable to check.
243    pub fn is_set<S: AsRef<str>>(&self, name: S) -> bool {
244        if let Some((_, var)) = self.get(name) {
245            !matches!(var.value(), ShellValue::Unset(_))
246        } else {
247            false
248        }
249    }
250
251    //
252    // Setters
253    //
254
255    /// Tries to unset the variable with the given name in the environment,
256    /// returning whether or not such a variable existed.
257    ///
258    /// # Arguments
259    ///
260    /// * `name` - The name of the variable to unset.
261    pub fn unset(&mut self, name: &str) -> Result<Option<ShellVariable>, error::Error> {
262        let mut local_count = 0;
263        for (scope_type, map) in self.scopes.iter_mut().rev() {
264            if matches!(scope_type, EnvironmentScope::Local) {
265                local_count += 1;
266            }
267
268            let unset_result = Self::try_unset_in_map(map, name)?;
269
270            if unset_result.is_some() {
271                // If we end up finding a local in the top-most local frame,
272                // then we replace it with a placeholder.
273                if matches!(scope_type, EnvironmentScope::Local) && local_count == 1 {
274                    map.set(
275                        name,
276                        ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)),
277                    );
278                } else if self.entry_count > 0 {
279                    // Entry count should never be 0 here, but we're being
280                    // defensive.
281                    self.entry_count -= 1;
282                }
283
284                return Ok(unset_result);
285            }
286        }
287
288        Ok(None)
289    }
290
291    /// Tries to unset an array element from the environment, using the given
292    /// name and element index for lookup. Returns whether or not an element
293    /// was unset.
294    ///
295    /// # Arguments
296    ///
297    /// * `name` - The name of the array variable to unset an element from.
298    /// * `index` - The index of the element to unset.
299    pub fn unset_index(&mut self, name: &str, index: &str) -> Result<bool, error::Error> {
300        if let Some((_, var)) = self.get_mut(name) {
301            var.unset_index(index)
302        } else {
303            Ok(false)
304        }
305    }
306
307    fn try_unset_in_map(
308        map: &mut ShellVariableMap,
309        name: &str,
310    ) -> Result<Option<ShellVariable>, error::Error> {
311        match map.get(name).map(|v| v.is_readonly()) {
312            Some(true) => Err(error::ErrorKind::ReadonlyVariable.into()),
313            Some(false) => Ok(map.unset(name)),
314            None => Ok(None),
315        }
316    }
317
318    /// Tries to retrieve an immutable reference to a variable from the
319    /// environment, using the given name and lookup policy.
320    ///
321    /// # Arguments
322    ///
323    /// * `name` - The name of the variable to retrieve.
324    /// * `lookup_policy` - The policy to use when looking up the variable.
325    pub fn get_using_policy<N: AsRef<str>>(
326        &self,
327        name: N,
328        lookup_policy: EnvironmentLookup,
329    ) -> Option<&ShellVariable> {
330        let mut looks_in = lookup_policy.scope_filter();
331        self.scopes
332            .iter()
333            .rev()
334            .filter(|(t, _)| looks_in(*t))
335            .find_map(|(_, var_map)| var_map.get(name.as_ref()))
336    }
337
338    /// Tries to retrieve a mutable reference to a variable from the
339    /// environment, using the given name and lookup policy.
340    ///
341    /// # Arguments
342    ///
343    /// * `name` - The name of the variable to retrieve.
344    /// * `lookup_policy` - The policy to use when looking up the variable.
345    pub fn get_mut_using_policy<N: AsRef<str>>(
346        &mut self,
347        name: N,
348        lookup_policy: EnvironmentLookup,
349    ) -> Option<&mut ShellVariable> {
350        let mut looks_in = lookup_policy.scope_filter();
351        self.scopes
352            .iter_mut()
353            .rev()
354            .filter(|(t, _)| looks_in(*t))
355            .find_map(|(_, var_map)| var_map.get_mut(name.as_ref()))
356    }
357
358    /// Update a variable in the environment, or add it if it doesn't already
359    /// exist.
360    ///
361    /// # Arguments
362    ///
363    /// * `name` - The name of the variable to update or add.
364    /// * `value` - The value to assign to the variable.
365    /// * `updater` - A function to call to update the variable after assigning
366    ///   the value.
367    /// * `lookup_policy` - The policy to use when looking up the variable.
368    /// * `scope_if_creating` - The scope to create the variable in if it
369    ///   doesn't already exist.
370    pub fn update_or_add<N: Into<String>>(
371        &mut self,
372        name: N,
373        value: variables::ShellValueLiteral,
374        updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
375        lookup_policy: EnvironmentLookup,
376        scope_if_creating: EnvironmentScope,
377    ) -> Result<(), error::Error> {
378        let name = name.into();
379
380        let auto_export = self.export_variables_on_modification;
381        if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
382            var.assign(value, false)?;
383            if auto_export {
384                var.export();
385            }
386            updater(var)
387        } else {
388            let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
389            var.assign(value, false)?;
390            if auto_export {
391                var.export();
392            }
393            updater(&mut var)?;
394
395            self.add(name, var, scope_if_creating)
396        }
397    }
398
399    /// Update an array element in the environment, or add it if it doesn't
400    /// already exist.
401    ///
402    /// # Arguments
403    ///
404    /// * `name` - The name of the variable to update or add.
405    /// * `index` - The index of the element to update or add.
406    /// * `value` - The value to assign to the variable.
407    /// * `updater` - A function to call to update the variable after assigning
408    ///   the value.
409    /// * `lookup_policy` - The policy to use when looking up the variable.
410    /// * `scope_if_creating` - The scope to create the variable in if it
411    ///   doesn't already exist.
412    pub fn update_or_add_array_element<N: Into<String>>(
413        &mut self,
414        name: N,
415        index: String,
416        value: String,
417        updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
418        lookup_policy: EnvironmentLookup,
419        scope_if_creating: EnvironmentScope,
420    ) -> Result<(), error::Error> {
421        let name = name.into();
422
423        if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
424            var.assign_at_index(index, value, false)?;
425            updater(var)
426        } else {
427            let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
428            var.assign(
429                variables::ShellValueLiteral::Array(variables::ArrayLiteral(vec![(
430                    Some(index),
431                    value,
432                )])),
433                false,
434            )?;
435            updater(&mut var)?;
436
437            self.add(name, var, scope_if_creating)
438        }
439    }
440
441    /// Adds a variable to the environment.
442    ///
443    /// # Arguments
444    ///
445    /// * `name` - The name of the variable to add.
446    /// * `var` - The variable to add.
447    /// * `target_scope` - The scope to add the variable to.
448    pub fn add<N: Into<String>>(
449        &mut self,
450        name: N,
451        mut var: ShellVariable,
452        target_scope: EnvironmentScope,
453    ) -> Result<(), error::Error> {
454        if self.export_variables_on_modification {
455            var.export();
456        }
457
458        for (scope_type, map) in self.scopes.iter_mut().rev() {
459            if *scope_type == target_scope {
460                let prev_var = map.set(name, var);
461                if prev_var.is_none() {
462                    self.entry_count += 1;
463                }
464
465                return Ok(());
466            }
467        }
468
469        Err(error::ErrorKind::MissingScopeForNewVariable.into())
470    }
471
472    /// Sets a global variable in the environment.
473    ///
474    /// # Arguments
475    ///
476    /// * `name` - The name of the variable to set.
477    /// * `var` - The variable to set.
478    pub fn set_global<N: Into<String>>(
479        &mut self,
480        name: N,
481        var: ShellVariable,
482    ) -> Result<(), error::Error> {
483        self.add(name, var, EnvironmentScope::Global)
484    }
485}
486
487/// Represents a map from names to shell variables.
488#[derive(Clone, Debug, Default)]
489#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
490pub struct ShellVariableMap {
491    variables: HashMap<String, ShellVariable>,
492}
493
494impl ShellVariableMap {
495    //
496    // Iterators/Getters
497    //
498
499    /// Returns an iterator over all the variables in the map.
500    pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
501        self.variables.iter()
502    }
503
504    /// Tries to retrieve an immutable reference to the variable with the given
505    /// name.
506    ///
507    /// # Arguments
508    ///
509    /// * `name` - The name of the variable to retrieve.
510    pub fn get(&self, name: &str) -> Option<&ShellVariable> {
511        self.variables.get(name)
512    }
513
514    /// Tries to retrieve a mutable reference to the variable with the given
515    /// name.
516    ///
517    /// # Arguments
518    ///
519    /// * `name` - The name of the variable to retrieve.
520    pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> {
521        self.variables.get_mut(name)
522    }
523
524    //
525    // Setters
526    //
527
528    /// Tries to unset the variable with the given name, returning the removed
529    /// variable or None if it was not already set.
530    ///
531    /// # Arguments
532    ///
533    /// * `name` - The name of the variable to unset.
534    pub fn unset(&mut self, name: &str) -> Option<ShellVariable> {
535        self.variables.remove(name)
536    }
537
538    /// Sets a variable in the map.
539    ///
540    /// # Arguments
541    ///
542    /// * `name` - The name of the variable to set.
543    /// * `var` - The variable to set.
544    pub fn set<N: Into<String>>(&mut self, name: N, var: ShellVariable) -> Option<ShellVariable> {
545        self.variables.insert(name.into(), var)
546    }
547}
548
549/// Checks if the given name is a valid variable name.
550pub fn valid_variable_name(s: &str) -> bool {
551    let mut cs = s.chars();
552    match cs.next() {
553        Some(c) if c.is_ascii_alphabetic() || c == '_' => {
554            cs.all(|c| c.is_ascii_alphanumeric() || c == '_')
555        }
556        Some(_) | None => false,
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_valid_variable_name() {
566        assert!(!valid_variable_name(""));
567        assert!(!valid_variable_name("1"));
568        assert!(!valid_variable_name(" a"));
569        assert!(!valid_variable_name(" "));
570
571        assert!(valid_variable_name("_"));
572        assert!(valid_variable_name("_a"));
573        assert!(valid_variable_name("_1"));
574        assert!(valid_variable_name("_a1"));
575        assert!(valid_variable_name("a"));
576        assert!(valid_variable_name("A"));
577        assert!(valid_variable_name("a1"));
578        assert!(valid_variable_name("A1"));
579    }
580}