//! Implements a shell variable environment. use std::borrow::Cow; use std::collections::HashMap; use std::collections::hash_map; use crate::Shell; use crate::error; use crate::extensions; use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable}; /// Represents the policy for looking up variables in a shell environment. #[derive(Clone, Copy)] pub enum EnvironmentLookup { /// Look anywhere. Anywhere, /// Look only in the global scope. OnlyInGlobal, /// Look only in the current local scope. OnlyInCurrentLocal, /// Look only in local scopes. OnlyInLocal, } /// Represents a shell environment scope. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum EnvironmentScope { /// Scope local to a function instance Local, /// Globals Global, /// Transient overrides for a command invocation Command, } impl std::fmt::Display for EnvironmentScope { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Local => write!(f, "local"), Self::Global => write!(f, "global"), Self::Command => write!(f, "command"), } } } /// A guard that pushes a scope onto a shell environment and pops it when dropped. pub(crate) struct ScopeGuard<'a, SE: extensions::ShellExtensions> { scope_type: EnvironmentScope, shell: &'a mut crate::Shell, detached: bool, } impl<'a, SE: extensions::ShellExtensions> ScopeGuard<'a, SE> { /// Creates a new scope guard, pushing the given scope type onto the environment. /// /// # Arguments /// /// * `shell` - The shell whose environment to modify. /// * `scope_type` - The type of scope to push. pub fn new(shell: &'a mut crate::Shell, scope_type: EnvironmentScope) -> Self { shell.env_mut().push_scope(scope_type); Self { scope_type, shell, detached: false, } } /// Returns a mutable reference to the shell. pub const fn shell(&mut self) -> &mut crate::Shell { self.shell } /// Detaches the guard, preventing it from popping the scope on drop. pub const fn detach(&mut self) { self.detached = true; } } impl Drop for ScopeGuard<'_, SE> { fn drop(&mut self) { if !self.detached { let _ = self.shell.env_mut().pop_scope(self.scope_type); } } } /// Represents the shell variable environment, composed of a stack of scopes. #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ShellEnvironment { /// Stack of scopes, with the top of the stack being the current scope. scopes: Vec<(EnvironmentScope, ShellVariableMap)>, /// Whether or not to auto-export variables on creation or modification. export_variables_on_modification: bool, /// Count of total entries (may include duplicates with shadowed variables). entry_count: usize, } impl Default for ShellEnvironment { fn default() -> Self { Self::new() } } impl ShellEnvironment { /// Returns a new shell environment. pub fn new() -> Self { Self { scopes: vec![(EnvironmentScope::Global, ShellVariableMap::default())], export_variables_on_modification: false, entry_count: 0, } } /// Pushes a new scope of the given type onto the environment's scope stack. /// /// # Arguments /// /// * `scope_type` - The type of scope to push. pub fn push_scope(&mut self, scope_type: EnvironmentScope) { self.scopes.push((scope_type, ShellVariableMap::default())); } /// Pops the top-most scope off the environment's scope stack. /// /// # Arguments /// /// * `expected_scope_type` - The type of scope that is expected to be atop the stack. pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> { // TODO(env): Should we panic instead on failure? It's effectively a broken invariant. match self.scopes.pop() { Some((actual_scope_type, _)) if actual_scope_type == expected_scope_type => Ok(()), Some((actual_scope_type, _)) => Err(error::ErrorKind::UnexpectedScopeType { expected: expected_scope_type, actual: actual_scope_type, } .into()), None => Err(error::ErrorKind::MissingScope.into()), } } // // Iterators/Getters // /// Returns an iterator over all exported variables defined in the variable. pub fn iter_exported(&self) -> impl Iterator { // We won't actually need to store all entries, but we expect it should be // within the same order. let mut visible_vars: HashMap<&String, &ShellVariable> = HashMap::with_capacity(self.entry_count); for (_, var_map) in self.scopes.iter().rev() { for (name, var) in var_map.iter().filter(|(_, v)| v.is_exported()) { // Only insert the variable if it hasn't been seen yet. if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) { entry.insert(var); } } } visible_vars.into_iter() } /// Returns an iterator over all the variables defined in the environment. pub fn iter(&self) -> impl Iterator { self.iter_using_policy(EnvironmentLookup::Anywhere) } /// Returns an iterator over all the variables defined in the environment, /// using the given lookup policy. /// /// # Arguments /// /// * `lookup_policy` - The policy to use when looking up variables. pub fn iter_using_policy( &self, lookup_policy: EnvironmentLookup, ) -> impl Iterator { // We won't actually need to store all entries, but we expect it should be // within the same order. let mut visible_vars: HashMap<&String, &ShellVariable> = HashMap::with_capacity(self.entry_count); let mut local_count = 0; for (scope_type, var_map) in self.scopes.iter().rev() { if matches!(scope_type, EnvironmentScope::Local) { local_count += 1; } match lookup_policy { EnvironmentLookup::Anywhere => (), EnvironmentLookup::OnlyInGlobal => { if !matches!(scope_type, EnvironmentScope::Global) { continue; } } EnvironmentLookup::OnlyInCurrentLocal => { if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) { continue; } } EnvironmentLookup::OnlyInLocal => { if !matches!(scope_type, EnvironmentScope::Local) { continue; } } } for (name, var) in var_map.iter() { // Only insert the variable if it hasn't been seen yet. if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) { entry.insert(var); } } if matches!(scope_type, EnvironmentScope::Local) && matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal) { break; } } visible_vars.into_iter() } /// Tries to retrieve an immutable reference to the variable with the given name /// in the environment. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. pub fn get>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> { // Look through scopes, from the top of the stack on down. for (scope_type, map) in self.scopes.iter().rev() { if let Some(var) = map.get(name.as_ref()) { return Some((*scope_type, var)); } } None } /// Tries to retrieve a mutable reference to the variable with the given name /// in the environment. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. pub fn get_mut>( &mut self, name: S, ) -> Option<(EnvironmentScope, &mut ShellVariable)> { // Look through scopes, from the top of the stack on down. for (scope_type, map) in self.scopes.iter_mut().rev() { if let Some(var) = map.get_mut(name.as_ref()) { return Some((*scope_type, var)); } } None } /// Tries to retrieve the string value of the variable with the given name in the /// environment. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. /// * `shell` - The shell owning the environment. pub fn get_str, SE: extensions::ShellExtensions>( &self, name: S, shell: &Shell, ) -> Option> { self.get(name.as_ref()) .map(|(_, v)| v.value().to_cow_str(shell)) } /// Checks if a variable of the given name is set in the environment. /// /// # Arguments /// /// * `name` - The name of the variable to check. pub fn is_set>(&self, name: S) -> bool { if let Some((_, var)) = self.get(name) { !matches!(var.value(), ShellValue::Unset(_)) } else { false } } // // Setters // /// Tries to unset the variable with the given name in the environment, returning /// whether or not such a variable existed. /// /// # Arguments /// /// * `name` - The name of the variable to unset. pub fn unset(&mut self, name: &str) -> Result, error::Error> { let mut local_count = 0; for (scope_type, map) in self.scopes.iter_mut().rev() { if matches!(scope_type, EnvironmentScope::Local) { local_count += 1; } let unset_result = Self::try_unset_in_map(map, name)?; if unset_result.is_some() { // If we end up finding a local in the top-most local frame, then we replace // it with a placeholder. if matches!(scope_type, EnvironmentScope::Local) && local_count == 1 { map.set( name, ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)), ); } else if self.entry_count > 0 { // Entry count should never be 0 here, but we're being defensive. self.entry_count -= 1; } return Ok(unset_result); } } Ok(None) } /// Tries to unset an array element from the environment, using the given name and /// element index for lookup. Returns whether or not an element was unset. /// /// # Arguments /// /// * `name` - The name of the array variable to unset an element from. /// * `index` - The index of the element to unset. pub fn unset_index(&mut self, name: &str, index: &str) -> Result { if let Some((_, var)) = self.get_mut(name) { var.unset_index(index) } else { Ok(false) } } fn try_unset_in_map( map: &mut ShellVariableMap, name: &str, ) -> Result, error::Error> { match map.get(name).map(|v| v.is_readonly()) { Some(true) => Err(error::ErrorKind::ReadonlyVariable.into()), Some(false) => Ok(map.unset(name)), None => Ok(None), } } /// Tries to retrieve an immutable reference to a variable from the environment, /// using the given name and lookup policy. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. /// * `lookup_policy` - The policy to use when looking up the variable. pub fn get_using_policy>( &self, name: N, lookup_policy: EnvironmentLookup, ) -> Option<&ShellVariable> { let mut local_count = 0; for (scope_type, var_map) in self.scopes.iter().rev() { if matches!(scope_type, EnvironmentScope::Local) { local_count += 1; } match lookup_policy { EnvironmentLookup::Anywhere => (), EnvironmentLookup::OnlyInGlobal => { if !matches!(scope_type, EnvironmentScope::Global) { continue; } } EnvironmentLookup::OnlyInCurrentLocal => { if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) { continue; } } EnvironmentLookup::OnlyInLocal => { if !matches!(scope_type, EnvironmentScope::Local) { continue; } } } if let Some(var) = var_map.get(name.as_ref()) { return Some(var); } if matches!(scope_type, EnvironmentScope::Local) && matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal) { break; } } None } /// Tries to retrieve a mutable reference to a variable from the environment, /// using the given name and lookup policy. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. /// * `lookup_policy` - The policy to use when looking up the variable. pub fn get_mut_using_policy>( &mut self, name: N, lookup_policy: EnvironmentLookup, ) -> Option<&mut ShellVariable> { let mut local_count = 0; for (scope_type, var_map) in self.scopes.iter_mut().rev() { if matches!(scope_type, EnvironmentScope::Local) { local_count += 1; } match lookup_policy { EnvironmentLookup::Anywhere => (), EnvironmentLookup::OnlyInGlobal => { if !matches!(scope_type, EnvironmentScope::Global) { continue; } } EnvironmentLookup::OnlyInCurrentLocal => { if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) { continue; } } EnvironmentLookup::OnlyInLocal => { if !matches!(scope_type, EnvironmentScope::Local) { continue; } } } if let Some(var) = var_map.get_mut(name.as_ref()) { return Some(var); } if matches!(scope_type, EnvironmentScope::Local) && matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal) { break; } } None } /// Update a variable in the environment, or add it if it doesn't already exist. /// /// # Arguments /// /// * `name` - The name of the variable to update or add. /// * `value` - The value to assign to the variable. /// * `updater` - A function to call to update the variable after assigning the value. /// * `lookup_policy` - The policy to use when looking up the variable. /// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist. pub fn update_or_add>( &mut self, name: N, value: variables::ShellValueLiteral, updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>, lookup_policy: EnvironmentLookup, scope_if_creating: EnvironmentScope, ) -> Result<(), error::Error> { let name = name.into(); let auto_export = self.export_variables_on_modification; if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) { var.assign(value, false)?; if auto_export { var.export(); } updater(var) } else { let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)); var.assign(value, false)?; if auto_export { var.export(); } updater(&mut var)?; self.add(name, var, scope_if_creating) } } /// Update an array element in the environment, or add it if it doesn't already exist. /// /// # Arguments /// /// * `name` - The name of the variable to update or add. /// * `index` - The index of the element to update or add. /// * `value` - The value to assign to the variable. /// * `updater` - A function to call to update the variable after assigning the value. /// * `lookup_policy` - The policy to use when looking up the variable. /// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist. pub fn update_or_add_array_element>( &mut self, name: N, index: String, value: String, updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>, lookup_policy: EnvironmentLookup, scope_if_creating: EnvironmentScope, ) -> Result<(), error::Error> { let name = name.into(); if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) { var.assign_at_index(index, value, false)?; updater(var) } else { let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)); var.assign( variables::ShellValueLiteral::Array(variables::ArrayLiteral(vec![( Some(index), value, )])), false, )?; updater(&mut var)?; self.add(name, var, scope_if_creating) } } /// Adds a variable to the environment. /// /// # Arguments /// /// * `name` - The name of the variable to add. /// * `var` - The variable to add. /// * `target_scope` - The scope to add the variable to. pub fn add>( &mut self, name: N, mut var: ShellVariable, target_scope: EnvironmentScope, ) -> Result<(), error::Error> { if self.export_variables_on_modification { var.export(); } for (scope_type, map) in self.scopes.iter_mut().rev() { if *scope_type == target_scope { let prev_var = map.set(name, var); if prev_var.is_none() { self.entry_count += 1; } return Ok(()); } } Err(error::ErrorKind::MissingScopeForNewVariable.into()) } /// Sets a global variable in the environment. /// /// # Arguments /// /// * `name` - The name of the variable to set. /// * `var` - The variable to set. pub fn set_global>( &mut self, name: N, var: ShellVariable, ) -> Result<(), error::Error> { self.add(name, var, EnvironmentScope::Global) } } /// Represents a map from names to shell variables. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ShellVariableMap { variables: HashMap, } impl ShellVariableMap { // // Iterators/Getters // /// Returns an iterator over all the variables in the map. pub fn iter(&self) -> impl Iterator { self.variables.iter() } /// Tries to retrieve an immutable reference to the variable with the given name. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. pub fn get(&self, name: &str) -> Option<&ShellVariable> { self.variables.get(name) } /// Tries to retrieve a mutable reference to the variable with the given name. /// /// # Arguments /// /// * `name` - The name of the variable to retrieve. pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> { self.variables.get_mut(name) } // // Setters // /// Tries to unset the variable with the given name, returning the removed /// variable or None if it was not already set. /// /// # Arguments /// /// * `name` - The name of the variable to unset. pub fn unset(&mut self, name: &str) -> Option { self.variables.remove(name) } /// Sets a variable in the map. /// /// # Arguments /// /// * `name` - The name of the variable to set. /// * `var` - The variable to set. pub fn set>(&mut self, name: N, var: ShellVariable) -> Option { self.variables.insert(name.into(), var) } } /// Checks if the given name is a valid variable name. pub fn valid_variable_name(s: &str) -> bool { let mut cs = s.chars(); match cs.next() { Some(c) if c.is_ascii_alphabetic() || c == '_' => { cs.all(|c| c.is_ascii_alphanumeric() || c == '_') } Some(_) | None => false, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_valid_variable_name() { assert!(!valid_variable_name("")); assert!(!valid_variable_name("1")); assert!(!valid_variable_name(" a")); assert!(!valid_variable_name(" ")); assert!(valid_variable_name("_")); assert!(valid_variable_name("_a")); assert!(valid_variable_name("_1")); assert!(valid_variable_name("_a1")); assert!(valid_variable_name("a")); assert!(valid_variable_name("A")); assert!(valid_variable_name("a1")); assert!(valid_variable_name("A1")); } }