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