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    ///
112    /// # Errors
113    ///
114    /// * [`error::ErrorKind::MissingScope`] if there are no scopes to pop.
115    /// * [`error::ErrorKind::UnexpectedScopeType`] if the innermost scope is
116    ///   not of `expected_scope_type`; the scope is left in place.
117    pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> {
118        // TODO(env): Should we panic instead on failure? It's effectively a
119        // broken invariant. Either way, don't pop a scope that isn't
120        // the one expected.
121        match self.scopes.last() {
122            Some((actual_scope_type, _)) if *actual_scope_type == expected_scope_type => {
123                self.scopes.pop();
124                Ok(())
125            }
126            Some((actual_scope_type, _)) => Err(error::ErrorKind::UnexpectedScopeType {
127                expected: expected_scope_type,
128                actual: *actual_scope_type,
129            }
130            .into()),
131            None => Err(error::ErrorKind::MissingScope.into()),
132        }
133    }
134
135    //
136    // Iterators/Getters
137    //
138
139    /// Returns an iterator over all exported variables defined in the variable.
140    pub fn iter_exported(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
141        // We won't actually need to store all entries, but we expect it should
142        // be within the same order.
143        let mut visible_vars: HashMap<&String, &ShellVariable> =
144            HashMap::with_capacity(self.entry_count);
145
146        for (_, var_map) in self.scopes.iter().rev() {
147            for (name, var) in var_map.iter().filter(|(_, v)| v.is_exported()) {
148                // Only insert the variable if it hasn't been seen yet.
149                if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
150                    entry.insert(var);
151                }
152            }
153        }
154
155        visible_vars.into_iter()
156    }
157
158    /// Returns an iterator over all the variables defined in the environment.
159    pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
160        self.iter_using_policy(EnvironmentLookup::Anywhere)
161    }
162
163    /// Returns an iterator over all the variables defined in the environment,
164    /// using the given lookup policy.
165    ///
166    /// # Arguments
167    ///
168    /// * `lookup_policy` - The policy to use when looking up variables.
169    pub fn iter_using_policy(
170        &self,
171        lookup_policy: EnvironmentLookup,
172    ) -> impl Iterator<Item = (&String, &ShellVariable)> {
173        // We won't actually need to store all entries, but we expect it should
174        // be within the same order.
175        let mut visible_vars: HashMap<&String, &ShellVariable> =
176            HashMap::with_capacity(self.entry_count);
177
178        let mut looks_in = lookup_policy.scope_filter();
179        for (_, var_map) in self.scopes.iter().rev().filter(|(t, _)| looks_in(*t)) {
180            for (name, var) in var_map.iter() {
181                // Only insert the variable if it hasn't been seen yet.
182                if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
183                    entry.insert(var);
184                }
185            }
186        }
187
188        visible_vars.into_iter()
189    }
190
191    /// Tries to retrieve an immutable reference to the variable with the given
192    /// name in the environment.
193    ///
194    /// # Arguments
195    ///
196    /// * `name` - The name of the variable to retrieve.
197    pub fn get<S: AsRef<str>>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> {
198        // Look through scopes, from the top of the stack on down.
199        for (scope_type, map) in self.scopes.iter().rev() {
200            if let Some(var) = map.get(name.as_ref()) {
201                return Some((*scope_type, var));
202            }
203        }
204
205        None
206    }
207
208    /// Tries to retrieve a mutable reference to the variable with the given
209    /// name in the environment.
210    ///
211    /// # Arguments
212    ///
213    /// * `name` - The name of the variable to retrieve.
214    pub fn get_mut<S: AsRef<str>>(
215        &mut self,
216        name: S,
217    ) -> Option<(EnvironmentScope, &mut ShellVariable)> {
218        // Look through scopes, from the top of the stack on down.
219        for (scope_type, map) in self.scopes.iter_mut().rev() {
220            if let Some(var) = map.get_mut(name.as_ref()) {
221                return Some((*scope_type, var));
222            }
223        }
224
225        None
226    }
227
228    /// Tries to retrieve the string value of the variable with the given name
229    /// in the environment.
230    ///
231    /// # Arguments
232    ///
233    /// * `name` - The name of the variable to retrieve.
234    /// * `shell` - The shell owning the environment.
235    pub fn get_str<S: AsRef<str>, SE: extensions::ShellExtensions>(
236        &self,
237        name: S,
238        shell: &Shell<SE>,
239    ) -> Option<Cow<'_, str>> {
240        self.get(name.as_ref())
241            .map(|(_, v)| v.value().to_cow_str(shell))
242    }
243
244    /// Checks if a variable of the given name is set in the environment.
245    ///
246    /// # Arguments
247    ///
248    /// * `name` - The name of the variable to check.
249    pub fn is_set<S: AsRef<str>>(&self, name: S) -> bool {
250        if let Some((_, var)) = self.get(name) {
251            !matches!(var.value(), ShellValue::Unset(_))
252        } else {
253            false
254        }
255    }
256
257    //
258    // Setters
259    //
260
261    /// Tries to unset the variable with the given name in the environment,
262    /// returning whether or not such a variable existed.
263    ///
264    /// # Arguments
265    ///
266    /// * `name` - The name of the variable to unset.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`error::ErrorKind::ReadonlyVariable`] if a matching variable is
271    /// read-only.
272    pub fn unset(&mut self, name: &str) -> Result<Option<ShellVariable>, error::Error> {
273        let mut local_count = 0;
274        for (scope_type, map) in self.scopes.iter_mut().rev() {
275            if matches!(scope_type, EnvironmentScope::Local) {
276                local_count += 1;
277            }
278
279            let unset_result = Self::try_unset_in_map(map, name)?;
280
281            if unset_result.is_some() {
282                // If we end up finding a local in the top-most local frame,
283                // then we replace it with a placeholder.
284                if matches!(scope_type, EnvironmentScope::Local) && local_count == 1 {
285                    map.set(
286                        name,
287                        ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)),
288                    );
289                } else if self.entry_count > 0 {
290                    // Entry count should never be 0 here, but we're being
291                    // defensive.
292                    self.entry_count -= 1;
293                }
294
295                return Ok(unset_result);
296            }
297        }
298
299        Ok(None)
300    }
301
302    /// Tries to unset an array element from the environment, using the given
303    /// name and element index for lookup. Returns whether or not an element
304    /// was unset.
305    ///
306    /// # Arguments
307    ///
308    /// * `name` - The name of the array variable to unset an element from.
309    /// * `index` - The index of the element to unset.
310    ///
311    /// # Errors
312    ///
313    /// Returns the errors of [`ShellVariable::unset_index`], such as
314    /// [`error::ErrorKind::NotArray`] when the variable is not an array.
315    pub fn unset_index(&mut self, name: &str, index: &str) -> Result<bool, error::Error> {
316        if let Some((_, var)) = self.get_mut(name) {
317            var.unset_index(index)
318        } else {
319            Ok(false)
320        }
321    }
322
323    fn try_unset_in_map(
324        map: &mut ShellVariableMap,
325        name: &str,
326    ) -> Result<Option<ShellVariable>, error::Error> {
327        match map.get(name).map(|v| v.is_readonly()) {
328            Some(true) => Err(error::ErrorKind::ReadonlyVariable.into()),
329            Some(false) => Ok(map.unset(name)),
330            None => Ok(None),
331        }
332    }
333
334    /// Tries to retrieve an immutable reference to a variable from the
335    /// environment, using the given name and lookup policy.
336    ///
337    /// # Arguments
338    ///
339    /// * `name` - The name of the variable to retrieve.
340    /// * `lookup_policy` - The policy to use when looking up the variable.
341    pub fn get_using_policy<N: AsRef<str>>(
342        &self,
343        name: N,
344        lookup_policy: EnvironmentLookup,
345    ) -> Option<&ShellVariable> {
346        let mut looks_in = lookup_policy.scope_filter();
347        self.scopes
348            .iter()
349            .rev()
350            .filter(|(t, _)| looks_in(*t))
351            .find_map(|(_, var_map)| var_map.get(name.as_ref()))
352    }
353
354    /// Tries to retrieve a mutable reference to a variable from the
355    /// environment, using the given name and lookup policy.
356    ///
357    /// # Arguments
358    ///
359    /// * `name` - The name of the variable to retrieve.
360    /// * `lookup_policy` - The policy to use when looking up the variable.
361    pub fn get_mut_using_policy<N: AsRef<str>>(
362        &mut self,
363        name: N,
364        lookup_policy: EnvironmentLookup,
365    ) -> Option<&mut ShellVariable> {
366        let mut looks_in = lookup_policy.scope_filter();
367        self.scopes
368            .iter_mut()
369            .rev()
370            .filter(|(t, _)| looks_in(*t))
371            .find_map(|(_, var_map)| var_map.get_mut(name.as_ref()))
372    }
373
374    /// Update a variable in the environment, or add it if it doesn't already
375    /// exist.
376    ///
377    /// # Arguments
378    ///
379    /// * `name` - The name of the variable to update or add.
380    /// * `value` - The value to assign to the variable.
381    /// * `updater` - A function to call to update the variable after assigning
382    ///   the value.
383    /// * `lookup_policy` - The policy to use when looking up the variable.
384    /// * `scope_if_creating` - The scope to create the variable in if it
385    ///   doesn't already exist.
386    ///
387    /// # Errors
388    ///
389    /// Returns any error from [`ShellVariable::assign`], such as
390    /// [`error::ErrorKind::ReadonlyVariable`], or any error returned by
391    /// `updater`.
392    pub fn update_or_add<N: Into<String>>(
393        &mut self,
394        name: N,
395        value: variables::ShellValueLiteral,
396        updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
397        lookup_policy: EnvironmentLookup,
398        scope_if_creating: EnvironmentScope,
399    ) -> Result<(), error::Error> {
400        let name = name.into();
401
402        let auto_export = self.export_variables_on_modification;
403        if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
404            var.assign(value, false)?;
405            if auto_export {
406                var.export();
407            }
408            updater(var)
409        } else {
410            let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
411            var.assign(value, false)?;
412            if auto_export {
413                var.export();
414            }
415            updater(&mut var)?;
416
417            self.add(name, var, scope_if_creating)
418        }
419    }
420
421    /// Update an array element in the environment, or add it if it doesn't
422    /// already exist.
423    ///
424    /// # Arguments
425    ///
426    /// * `name` - The name of the variable to update or add.
427    /// * `index` - The index of the element to update or add.
428    /// * `value` - The value to assign to the variable.
429    /// * `updater` - A function to call to update the variable after assigning
430    ///   the value.
431    /// * `lookup_policy` - The policy to use when looking up the variable.
432    /// * `scope_if_creating` - The scope to create the variable in if it
433    ///   doesn't already exist.
434    ///
435    /// # Errors
436    ///
437    /// Returns any error from [`ShellVariable::assign_at_index`] or
438    /// [`ShellVariable::assign`], such as
439    /// [`error::ErrorKind::ReadonlyVariable`], or any error returned by
440    /// `updater`.
441    pub fn update_or_add_array_element<N: Into<String>>(
442        &mut self,
443        name: N,
444        index: String,
445        value: String,
446        updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
447        lookup_policy: EnvironmentLookup,
448        scope_if_creating: EnvironmentScope,
449    ) -> Result<(), error::Error> {
450        let name = name.into();
451
452        if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
453            var.assign_at_index(index, value, false)?;
454            updater(var)
455        } else {
456            let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
457            var.assign(
458                variables::ShellValueLiteral::Array(variables::ArrayLiteral(vec![(
459                    Some(index),
460                    value,
461                )])),
462                false,
463            )?;
464            updater(&mut var)?;
465
466            self.add(name, var, scope_if_creating)
467        }
468    }
469
470    /// Adds a variable to the environment.
471    ///
472    /// # Arguments
473    ///
474    /// * `name` - The name of the variable to add.
475    /// * `var` - The variable to add.
476    /// * `target_scope` - The scope to add the variable to.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`error::ErrorKind::MissingScopeForNewVariable`] if no scope of
481    /// type `target_scope` exists.
482    pub fn add<N: Into<String>>(
483        &mut self,
484        name: N,
485        mut var: ShellVariable,
486        target_scope: EnvironmentScope,
487    ) -> Result<(), error::Error> {
488        if self.export_variables_on_modification {
489            var.export();
490        }
491
492        for (scope_type, map) in self.scopes.iter_mut().rev() {
493            if *scope_type == target_scope {
494                let prev_var = map.set(name, var);
495                if prev_var.is_none() {
496                    self.entry_count += 1;
497                }
498
499                return Ok(());
500            }
501        }
502
503        Err(error::ErrorKind::MissingScopeForNewVariable.into())
504    }
505
506    /// Sets a global variable in the environment.
507    ///
508    /// # Arguments
509    ///
510    /// * `name` - The name of the variable to set.
511    /// * `var` - The variable to set.
512    ///
513    /// # Errors
514    ///
515    /// Returns [`error::ErrorKind::MissingScopeForNewVariable`] if there is no
516    /// global scope.
517    pub fn set_global<N: Into<String>>(
518        &mut self,
519        name: N,
520        var: ShellVariable,
521    ) -> Result<(), error::Error> {
522        self.add(name, var, EnvironmentScope::Global)
523    }
524}
525
526/// Represents a map from names to shell variables.
527#[derive(Clone, Debug, Default)]
528#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
529pub struct ShellVariableMap {
530    variables: HashMap<String, ShellVariable>,
531}
532
533impl ShellVariableMap {
534    //
535    // Iterators/Getters
536    //
537
538    /// Returns an iterator over all the variables in the map.
539    pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
540        self.variables.iter()
541    }
542
543    /// Tries to retrieve an immutable reference to the variable with the given
544    /// name.
545    ///
546    /// # Arguments
547    ///
548    /// * `name` - The name of the variable to retrieve.
549    pub fn get(&self, name: &str) -> Option<&ShellVariable> {
550        self.variables.get(name)
551    }
552
553    /// Tries to retrieve a mutable reference to the variable with the given
554    /// name.
555    ///
556    /// # Arguments
557    ///
558    /// * `name` - The name of the variable to retrieve.
559    pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> {
560        self.variables.get_mut(name)
561    }
562
563    //
564    // Setters
565    //
566
567    /// Tries to unset the variable with the given name, returning the removed
568    /// variable or None if it was not already set.
569    ///
570    /// # Arguments
571    ///
572    /// * `name` - The name of the variable to unset.
573    pub fn unset(&mut self, name: &str) -> Option<ShellVariable> {
574        self.variables.remove(name)
575    }
576
577    /// Sets a variable in the map.
578    ///
579    /// # Arguments
580    ///
581    /// * `name` - The name of the variable to set.
582    /// * `var` - The variable to set.
583    pub fn set<N: Into<String>>(&mut self, name: N, var: ShellVariable) -> Option<ShellVariable> {
584        self.variables.insert(name.into(), var)
585    }
586}
587
588/// Checks if the given name is a valid variable name.
589pub fn valid_variable_name(s: &str) -> bool {
590    let mut cs = s.chars();
591    match cs.next() {
592        Some(c) if c.is_ascii_alphabetic() || c == '_' => {
593            cs.all(|c| c.is_ascii_alphanumeric() || c == '_')
594        }
595        Some(_) | None => false,
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn test_valid_variable_name() {
605        assert!(!valid_variable_name(""));
606        assert!(!valid_variable_name("1"));
607        assert!(!valid_variable_name(" a"));
608        assert!(!valid_variable_name(" "));
609
610        assert!(valid_variable_name("_"));
611        assert!(valid_variable_name("_a"));
612        assert!(valid_variable_name("_1"));
613        assert!(valid_variable_name("_a1"));
614        assert!(valid_variable_name("a"));
615        assert!(valid_variable_name("A"));
616        assert!(valid_variable_name("a1"));
617        assert!(valid_variable_name("A1"));
618    }
619}