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}