Skip to main content

brush_core/
env.rs

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