#[cfg(doc)]
use crate::Env;
use crate::semantics::Field;
use crate::source::Location;
use itertools::Itertools;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::ffi::CString;
use std::fmt::Write;
use std::hash::Hash;
use std::iter::FusedIterator;
use thiserror::Error;
mod value;
pub use self::value::QuotedValue;
pub use self::value::Value::{self, Array, Scalar};
mod quirk;
pub use self::quirk::Expansion;
pub use self::quirk::Quirk;
mod main;
pub use self::main::AssignError;
pub use self::main::Variable;
pub use self::main::VariableRefMut;
mod constants;
pub use self::constants::*;
#[derive(Clone, Debug, Eq, PartialEq)]
struct VariableInContext {
variable: Variable,
context_index: usize,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PositionalParams {
pub values: Vec<String>,
pub last_modified_location: Option<Location>,
}
impl PositionalParams {
pub fn from_fields<I>(fields: I) -> Self
where
I: IntoIterator<Item = Field>,
{
let mut fields = fields.into_iter();
let last_modified_location = fields.next().map(|field| field.origin);
let values = fields.map(|field| field.value).collect();
Self {
values,
last_modified_location,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Context {
Regular { positional_params: PositionalParams },
Volatile,
}
impl Default for Context {
fn default() -> Self {
Context::Regular {
positional_params: Default::default(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VariableSet {
all_variables: HashMap<String, Vec<VariableInContext>>,
contexts: Vec<Context>,
}
impl Default for VariableSet {
fn default() -> Self {
VariableSet {
all_variables: Default::default(),
contexts: vec![Context::default()],
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Scope {
Global,
Local,
Volatile,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("cannot unset read-only variable `{name}`")]
pub struct UnsetError<'a> {
pub name: &'a str,
pub read_only_location: &'a Location,
}
#[derive(Clone, Debug)]
pub struct Iter<'a> {
inner: std::collections::hash_map::Iter<'a, String, Vec<VariableInContext>>,
min_context_index: usize,
}
impl VariableSet {
#[must_use]
pub fn new() -> VariableSet {
Default::default()
}
#[must_use]
pub fn get<N>(&self, name: &N) -> Option<&Variable>
where
String: Borrow<N>,
N: Hash + Eq + ?Sized,
{
Some(&self.all_variables.get(name)?.last()?.variable)
}
fn index_of_topmost_regular_context(contexts: &[Context]) -> usize {
contexts
.iter()
.rposition(|context| matches!(context, Context::Regular { .. }))
.expect("the stack should have at least one regular context")
}
fn index_of_context(scope: Scope, contexts: &[Context]) -> usize {
match scope {
Scope::Global => 0,
Scope::Local => Self::index_of_topmost_regular_context(contexts),
Scope::Volatile => Self::index_of_topmost_regular_context(contexts) + 1,
}
}
#[must_use]
pub fn get_scoped<N>(&self, name: &N, scope: Scope) -> Option<&Variable>
where
String: Borrow<N>,
N: Hash + Eq + ?Sized,
{
let index = Self::index_of_context(scope, &self.contexts);
self.all_variables
.get(name)?
.last()
.filter(|vic| vic.context_index >= index)
.map(|vic| &vic.variable)
}
#[inline]
pub fn get_or_new<S: Into<String>>(&mut self, name: S, scope: Scope) -> VariableRefMut<'_> {
self.get_or_new_impl(name.into(), scope)
}
fn get_or_new_impl(&mut self, name: String, scope: Scope) -> VariableRefMut<'_> {
let stack = match self.all_variables.entry(name) {
Vacant(vacant) => vacant.insert(Vec::new()),
Occupied(occupied) => occupied.into_mut(),
};
let context_index = match scope {
Scope::Global => 0,
Scope::Local => Self::index_of_topmost_regular_context(&self.contexts),
Scope::Volatile => self.contexts.len() - 1,
};
match scope {
Scope::Global | Scope::Local => 'branch: {
let mut removed_volatile_variable = None;
while let Some(var) = stack.last_mut() {
if var.context_index < context_index {
break;
}
match self.contexts[var.context_index] {
Context::Regular { .. } => {
if let Some(removed_volatile_variable) = removed_volatile_variable {
var.variable = removed_volatile_variable;
}
break 'branch;
}
Context::Volatile => {
removed_volatile_variable.get_or_insert(stack.pop().unwrap().variable);
}
}
}
stack.push(VariableInContext {
variable: removed_volatile_variable.unwrap_or_default(),
context_index,
});
}
Scope::Volatile => {
assert_eq!(
self.contexts[context_index],
Context::Volatile,
"no volatile context to store the variable",
);
if let Some(var) = stack.last() {
if var.context_index != context_index {
stack.push(VariableInContext {
variable: var.variable.clone(),
context_index,
});
}
} else {
stack.push(VariableInContext {
variable: Variable::default(),
context_index,
});
}
}
}
VariableRefMut::from(&mut stack.last_mut().unwrap().variable)
}
#[cfg(test)]
fn assert_normalized(&self) {
for context in self.all_variables.values() {
for vars in context.windows(2) {
assert!(
vars[0].context_index < vars[1].context_index,
"invalid context index: {vars:?}",
);
}
if let Some(last) = context.last() {
assert!(
last.context_index < self.contexts.len(),
"invalid context index: {last:?}",
);
}
}
}
#[must_use]
pub fn get_scalar<N>(&self, name: &N) -> Option<&str>
where
String: Borrow<N>,
N: Hash + Eq + ?Sized,
{
fn inner(var: &Variable) -> Option<&str> {
match var.value.as_ref()? {
Scalar(value) => Some(value),
Array(_) => None,
}
}
inner(self.get(name)?)
}
pub fn unset<'a>(
&'a mut self,
name: &'a str,
scope: Scope,
) -> Result<Option<Variable>, UnsetError<'a>> {
let Some(stack) = self.all_variables.get_mut(name) else {
return Ok(None);
};
let index = Self::index_of_context(scope, &self.contexts);
if let Some(read_only_position) = stack[index..]
.iter()
.rposition(|vic| vic.variable.is_read_only())
{
let read_only_index = index + read_only_position;
let read_only_location = &stack[read_only_index].variable.read_only_location;
return Err(UnsetError {
name,
read_only_location: read_only_location.as_ref().unwrap(),
});
}
Ok(stack.drain(index..).next_back().map(|vic| vic.variable))
}
pub fn iter(&self, scope: Scope) -> Iter<'_> {
Iter {
inner: self.all_variables.iter(),
min_context_index: Self::index_of_context(scope, &self.contexts),
}
}
#[must_use]
pub fn env_c_strings(&self) -> Vec<CString> {
self.all_variables
.iter()
.filter_map(|(name, vars)| {
let var = &vars.last()?.variable;
if !var.is_exported || name.contains('=') {
return None;
}
let value = var.value.as_ref()?;
let mut result = name.clone();
result.push('=');
match value {
Scalar(value) => result.push_str(value),
Array(values) => write!(result, "{}", values.iter().format(":")).ok()?,
}
CString::new(result).ok()
})
.collect()
}
pub fn extend_env<I, K, V>(&mut self, vars: I)
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
for (name, value) in vars {
let mut var = self.get_or_new(name, Scope::Global);
if var.assign(value.into(), None).is_ok() {
var.export(true)
}
}
}
pub fn init(&mut self) {
const VARIABLES: &[(&str, &str)] = &[
(IFS, IFS_INITIAL_VALUE),
(OPTIND, OPTIND_INITIAL_VALUE),
(PS1, PS1_INITIAL_VALUE_NON_ROOT),
(PS2, PS2_INITIAL_VALUE),
(PS4, PS4_INITIAL_VALUE),
];
for &(name, value) in VARIABLES {
self.get_or_new(name, Scope::Global)
.assign(value, None)
.ok();
}
self.get_or_new(LINENO, Scope::Global)
.set_quirk(Some(Quirk::LineNumber))
}
#[must_use]
pub fn positional_params(&self) -> &PositionalParams {
self.contexts
.iter()
.rev()
.find_map(|context| match context {
Context::Regular { positional_params } => Some(positional_params),
Context::Volatile => None,
})
.expect("the stack should have at least one regular context")
}
#[must_use]
pub fn positional_params_mut(&mut self) -> &mut PositionalParams {
self.contexts
.iter_mut()
.rev()
.find_map(|context| match context {
Context::Regular { positional_params } => Some(positional_params),
Context::Volatile => None,
})
.expect("the stack should have at least one regular context")
}
fn push_context_impl(&mut self, context: Context) {
self.contexts.push(context);
}
fn pop_context_impl(&mut self) {
debug_assert!(!self.contexts.is_empty());
assert_ne!(
self.contexts.len(),
1,
"the base context should not be popped"
);
self.contexts.pop();
self.all_variables.retain(|_, stack| {
stack.pop_if(|vic| {
vic.context_index >= self.contexts.len()
});
!stack.is_empty()
})
}
}
impl<'a> Iterator for Iter<'a> {
type Item = (&'a str, &'a Variable);
fn next(&mut self) -> Option<(&'a str, &'a Variable)> {
loop {
let next = self.inner.next()?;
if let Some(variable) = next.1.last() {
if variable.context_index >= self.min_context_index {
return Some((next.0, &variable.variable));
}
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let (_min, max) = self.inner.size_hint();
(0, max)
}
}
impl FusedIterator for Iter<'_> {}
mod guard;
pub use self::guard::{ContextGuard, EnvContextGuard};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_variable_in_global_scope() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Global);
assert_eq!(*var, Variable::default());
var.assign("VALUE", None).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
assert_eq!(set.get("foo").unwrap().value, Some("VALUE".into()));
}
#[test]
fn existing_variable_in_global_scope() {
let mut set = VariableSet::new();
let mut var = set.get_or_new("foo", Scope::Global);
var.assign("ONE", None).unwrap();
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Global);
assert_eq!(var.value, Some("ONE".into()));
var.assign("TWO", Location::dummy("somewhere")).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("TWO".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("somewhere")),
);
}
#[test]
fn new_variable_in_local_scope() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
let mut var = set.get_or_new("foo", Scope::Local);
assert_eq!(*var, Variable::default());
var.assign("OUTER", None).unwrap();
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Local);
assert_eq!(*var, Variable::default());
var.assign("INNER", Location::dummy("location")).unwrap();
set.assert_normalized();
set.pop_context_impl(); assert_eq!(set.get("foo").unwrap().value, Some("INNER".into()));
set.pop_context_impl(); assert_eq!(set.get("foo").unwrap().value, Some("OUTER".into()));
set.pop_context_impl(); assert_eq!(set.get("foo"), None);
}
#[test]
fn existing_variable_in_local_scope() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
let mut var = set.get_or_new("foo", Scope::Local);
var.assign("OLD", None).unwrap();
let mut var = set.get_or_new("foo", Scope::Local);
assert_eq!(var.value, Some("OLD".into()));
var.assign("NEW", None).unwrap();
assert_eq!(set.get("foo").unwrap().value, Some("NEW".into()));
set.assert_normalized();
set.pop_context_impl();
assert_eq!(set.get("foo"), None);
}
#[test]
fn new_variable_in_volatile_scope() {
let mut set = VariableSet::new();
set.push_context_impl(Context::Volatile);
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
assert_eq!(*var, Variable::default());
var.assign("VOLATILE", None).unwrap();
assert_eq!(set.get("foo").unwrap().value, Some("VOLATILE".into()));
set.assert_normalized();
set.pop_context_impl();
assert_eq!(set.get("foo"), None);
}
#[test]
fn cloning_existing_regular_variable_to_volatile_context() {
let mut set = VariableSet::new();
let mut var = set.get_or_new("foo", Scope::Global);
var.assign("VALUE", None).unwrap();
var.make_read_only(Location::dummy("read-only location"));
let save_var = var.clone();
set.push_context_impl(Context::Volatile);
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
assert_eq!(*var, save_var);
var.export(true);
assert!(set.get("foo").unwrap().is_exported);
set.assert_normalized();
set.pop_context_impl();
assert_eq!(set.get("foo"), Some(&save_var));
}
#[test]
fn existing_variable_in_volatile_scope() {
let mut set = VariableSet::new();
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("INITIAL", None).unwrap();
let mut var = set.get_or_new("foo", Scope::Volatile);
assert_eq!(var.value, Some("INITIAL".into()));
var.assign(Value::array(["MODIFIED"]), Location::dummy("somewhere"))
.unwrap();
assert_eq!(
set.get("foo").unwrap().value,
Some(Value::array(["MODIFIED"])),
);
set.assert_normalized();
set.pop_context_impl();
assert_eq!(set.get("foo"), None);
}
#[test]
fn lowering_volatile_variable_to_base_context() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("DUMMY", None).unwrap();
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("VOLATILE", Location::dummy("anywhere")).unwrap();
var.export(true);
let mut var = set.get_or_new("foo", Scope::Global);
assert_eq!(var.value, Some("VOLATILE".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("anywhere")),
);
var.assign("NEW", Location::dummy("somewhere")).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("NEW".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("somewhere")),
);
assert!(var.is_exported);
}
#[test]
fn lowering_volatile_variable_to_middle_regular_context() {
let mut set = VariableSet::new();
let mut var = set.get_or_new("foo", Scope::Local);
var.assign("ONE", None).unwrap();
set.push_context_impl(Context::default());
let mut var = set.get_or_new("foo", Scope::Local);
var.assign("TWO", None).unwrap();
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("VOLATILE", Location::dummy("anywhere")).unwrap();
var.export(true);
let mut var = set.get_or_new("foo", Scope::Global);
assert_eq!(var.value, Some("VOLATILE".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("anywhere")),
);
var.assign("NEW", Location::dummy("somewhere")).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("NEW".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("somewhere")),
);
assert!(var.is_exported);
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("ONE".into()));
}
#[test]
fn lowering_volatile_variable_to_topmost_regular_context_without_existing_variable() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
set.push_context_impl(Context::default());
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("DUMMY", None).unwrap();
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("VOLATILE", Location::dummy("anywhere")).unwrap();
var.export(true);
let mut var = set.get_or_new("foo", Scope::Local);
assert_eq!(var.value, Some("VOLATILE".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("anywhere")),
);
var.assign("NEW", Location::dummy("somewhere")).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("NEW".into()));
assert_eq!(
var.last_assigned_location,
Some(Location::dummy("somewhere")),
);
assert!(var.is_exported);
}
#[test]
fn lowering_volatile_variable_to_topmost_regular_context_overwriting_existing_variable() {
let mut set = VariableSet::new();
set.push_context_impl(Context::default());
set.push_context_impl(Context::default());
let mut var = set.get_or_new("foo", Scope::Local);
var.assign("OLD", None).unwrap();
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("DUMMY", None).unwrap();
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Volatile);
var.assign("VOLATILE", Location::dummy("first")).unwrap();
var.export(true);
set.push_context_impl(Context::Volatile);
let mut var = set.get_or_new("foo", Scope::Local);
assert_eq!(var.value, Some("VOLATILE".into()));
assert_eq!(var.last_assigned_location, Some(Location::dummy("first")));
var.assign("NEW", Location::dummy("second")).unwrap();
set.assert_normalized();
set.pop_context_impl();
set.pop_context_impl();
set.pop_context_impl();
let var = set.get("foo").unwrap();
assert_eq!(var.value, Some("NEW".into()));
assert_eq!(var.last_assigned_location, Some(Location::dummy("second")));
assert!(var.is_exported);
}
#[test]
#[should_panic(expected = "no volatile context to store the variable")]
fn missing_volatile_context() {
let mut set = VariableSet::new();
set.get_or_new("foo", Scope::Volatile);
}
#[test]
fn getting_variables_with_scopes() {
let mut set = VariableSet::new();
set.get_or_new("global", Scope::Global)
.assign("G", None)
.unwrap();
set.push_context_impl(Context::default());
set.get_or_new("local", Scope::Local)
.assign("L", None)
.unwrap();
set.push_context_impl(Context::Volatile);
set.get_or_new("volatile", Scope::Volatile)
.assign("V", None)
.unwrap();
assert_eq!(
set.get_scoped("global", Scope::Global),
Some(&Variable::new("G")),
);
assert_eq!(set.get_scoped("global", Scope::Local), None);
assert_eq!(set.get_scoped("global", Scope::Volatile), None);
assert_eq!(
set.get_scoped("local", Scope::Global),
Some(&Variable::new("L"))
);
assert_eq!(
set.get_scoped("local", Scope::Local),
Some(&Variable::new("L"))
);
assert_eq!(set.get_scoped("local", Scope::Volatile), None);
assert_eq!(
set.get_scoped("volatile", Scope::Global),
Some(&Variable::new("V"))
);
assert_eq!(
set.get_scoped("volatile", Scope::Local),
Some(&Variable::new("V"))
);
assert_eq!(
set.get_scoped("volatile", Scope::Volatile),
Some(&Variable::new("V"))
);
}
#[test]
fn unsetting_nonexisting_variable() {
let mut variables = VariableSet::new();
let result = variables.unset("", Scope::Global).unwrap();
assert_eq!(result, None);
}
#[test]
fn unsetting_variable_with_one_context() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("X", None)
.unwrap();
let result = variables.unset("foo", Scope::Global).unwrap();
assert_eq!(result, Some(Variable::new("X")));
assert_eq!(variables.get("foo"), None);
}
#[test]
fn unsetting_variables_from_all_contexts() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("X", None)
.unwrap();
variables.push_context_impl(Context::default());
variables
.get_or_new("foo", Scope::Local)
.assign("Y", None)
.unwrap();
variables.push_context_impl(Context::Volatile);
variables
.get_or_new("foo", Scope::Volatile)
.assign("Z", None)
.unwrap();
let result = variables.unset("foo", Scope::Global).unwrap();
assert_eq!(result, Some(Variable::new("Z")));
assert_eq!(variables.get("foo"), None);
}
#[test]
fn unsetting_variable_from_local_context() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("A", None)
.unwrap();
variables.push_context_impl(Context::default());
let mut readonly_foo = variables.get_or_new("foo", Scope::Local);
readonly_foo.assign("B", None).unwrap();
readonly_foo.make_read_only(Location::dummy("dummy"));
let readonly_foo = readonly_foo.clone();
variables.push_context_impl(Context::default());
variables
.get_or_new("foo", Scope::Local)
.assign("C", None)
.unwrap();
variables.push_context_impl(Context::Volatile);
variables
.get_or_new("foo", Scope::Volatile)
.assign("D", None)
.unwrap();
let result = variables.unset("foo", Scope::Local).unwrap();
assert_eq!(result, Some(Variable::new("D")));
assert_eq!(variables.get("foo"), Some(&readonly_foo));
}
#[test]
fn unsetting_nonexisting_variable_in_local_context() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("A", None)
.unwrap();
variables.push_context_impl(Context::default());
let result = variables.unset("foo", Scope::Local).unwrap();
assert_eq!(result, None);
assert_eq!(variables.get("foo"), Some(&Variable::new("A")));
}
#[test]
fn unsetting_variable_from_volatile_context() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("A", None)
.unwrap();
variables.push_context_impl(Context::default());
variables
.get_or_new("foo", Scope::Local)
.assign("B", None)
.unwrap();
variables.push_context_impl(Context::Volatile);
variables
.get_or_new("foo", Scope::Volatile)
.assign("C", None)
.unwrap();
variables.push_context_impl(Context::Volatile);
variables
.get_or_new("foo", Scope::Volatile)
.assign("D", None)
.unwrap();
let result = variables.unset("foo", Scope::Volatile).unwrap();
assert_eq!(result, Some(Variable::new("D")));
assert_eq!(variables.get("foo"), Some(&Variable::new("B")));
}
#[test]
fn unsetting_nonexisting_variable_in_volatile_context() {
let mut variables = VariableSet::new();
variables
.get_or_new("foo", Scope::Global)
.assign("A", None)
.unwrap();
variables.push_context_impl(Context::Volatile);
let result = variables.unset("foo", Scope::Volatile).unwrap();
assert_eq!(result, None);
assert_eq!(variables.get("foo"), Some(&Variable::new("A")));
}
#[test]
fn unsetting_readonly_variable() {
let read_only_location = &Location::dummy("read-only");
let mut variables = VariableSet::new();
let mut foo = variables.get_or_new("foo", Scope::Global);
foo.assign("A", None).unwrap();
variables.push_context_impl(Context::default());
let mut foo = variables.get_or_new("foo", Scope::Local);
foo.assign("B", None).unwrap();
foo.make_read_only(Location::dummy("dummy"));
variables.push_context_impl(Context::default());
let mut foo = variables.get_or_new("foo", Scope::Local);
foo.assign("C", None).unwrap();
foo.make_read_only(read_only_location.clone());
variables.push_context_impl(Context::default());
let mut foo = variables.get_or_new("foo", Scope::Local);
foo.assign("D", None).unwrap();
let error = variables.unset("foo", Scope::Global).unwrap_err();
assert_eq!(
error,
UnsetError {
name: "foo",
read_only_location
}
);
assert_eq!(variables.get("foo"), Some(&Variable::new("D")));
}
#[test]
#[should_panic(expected = "the base context should not be popped")]
fn cannot_pop_base_context() {
let mut variables = VariableSet::new();
variables.pop_context_impl();
}
fn test_iter<F: FnOnce(&VariableSet)>(f: F) {
let mut set = VariableSet::new();
let mut var = set.get_or_new("global", Scope::Global);
var.assign("global value", None).unwrap();
var.export(true);
let mut var = set.get_or_new("local", Scope::Global);
var.assign("hidden value", None).unwrap();
let mut set = set.push_context(Context::default());
let mut var = set.get_or_new("local", Scope::Local);
var.assign("visible value", None).unwrap();
let mut var = set.get_or_new("volatile", Scope::Local);
var.assign("hidden value", None).unwrap();
let mut set = set.push_context(Context::Volatile);
let mut var = set.get_or_new("volatile", Scope::Volatile);
var.assign("volatile value", None).unwrap();
f(&mut set);
}
#[test]
fn iter_global() {
test_iter(|set| {
let mut v: Vec<_> = set.iter(Scope::Global).collect();
v.sort_unstable_by_key(|&(name, _)| name);
assert_eq!(
v,
[
("global", &Variable::new("global value").export()),
("local", &Variable::new("visible value")),
("volatile", &Variable::new("volatile value"))
]
);
})
}
#[test]
fn iter_local() {
test_iter(|set| {
let mut v: Vec<_> = set.iter(Scope::Local).collect();
v.sort_unstable_by_key(|&(name, _)| name);
assert_eq!(
v,
[
("local", &Variable::new("visible value")),
("volatile", &Variable::new("volatile value"))
]
);
})
}
#[test]
fn iter_volatile() {
test_iter(|set| {
let mut v: Vec<_> = set.iter(Scope::Volatile).collect();
v.sort_unstable_by_key(|&(name, _)| name);
assert_eq!(v, [("volatile", &Variable::new("volatile value"))]);
})
}
#[test]
fn iter_size_hint() {
test_iter(|set| {
assert_eq!(set.iter(Scope::Global).size_hint(), (0, Some(3)));
assert_eq!(set.iter(Scope::Local).size_hint(), (0, Some(3)));
assert_eq!(set.iter(Scope::Volatile).size_hint(), (0, Some(3)));
})
}
#[test]
fn env_c_strings() {
let mut variables = VariableSet::new();
assert_eq!(variables.env_c_strings(), [] as [CString; 0]);
let mut var = variables.get_or_new("foo", Scope::Global);
var.assign("FOO", None).unwrap();
var.export(true);
let mut var = variables.get_or_new("bar", Scope::Global);
var.assign(Value::array(["BAR"]), None).unwrap();
var.export(true);
let mut var = variables.get_or_new("baz", Scope::Global);
var.assign(Value::array(["1", "two", "3"]), None).unwrap();
var.export(true);
let mut var = variables.get_or_new("null", Scope::Global);
var.assign("not exported", None).unwrap();
variables.get_or_new("none", Scope::Global);
let mut ss = variables.env_c_strings();
ss.sort_unstable();
assert_eq!(
&ss,
&[
c"bar=BAR".to_owned(),
c"baz=1:two:3".to_owned(),
c"foo=FOO".to_owned()
]
);
}
#[test]
fn env_c_strings_with_equal_in_name() {
let mut variables = VariableSet::new();
let mut var = variables.get_or_new("foo", Scope::Global);
var.assign("FOO", None).unwrap();
var.export(true);
let mut var = variables.get_or_new("foo=bar", Scope::Global);
var.assign("BAR", None).unwrap();
var.export(true);
let mut ss = variables.env_c_strings();
ss.sort_unstable();
assert_eq!(&ss, &[c"foo=FOO".to_owned(),]);
}
#[test]
fn extend_env() {
let mut variables = VariableSet::new();
variables.extend_env([("foo", "FOO"), ("bar", "OK")]);
let foo = variables.get("foo").unwrap();
assert_eq!(foo.value, Some("FOO".into()));
assert!(foo.is_exported);
let bar = variables.get("bar").unwrap();
assert_eq!(bar.value, Some("OK".into()));
assert!(bar.is_exported);
}
#[test]
fn init_lineno() {
let mut variables = VariableSet::new();
variables.init();
let v = variables.get(LINENO).unwrap();
assert_eq!(v.value, None);
assert_eq!(v.quirk, Some(Quirk::LineNumber));
assert_eq!(v.last_assigned_location, None);
assert!(!v.is_exported);
assert_eq!(v.read_only_location, None);
}
#[test]
fn positional_params_in_base_context() {
let mut variables = VariableSet::new();
assert_eq!(variables.positional_params().values, [] as [String; 0]);
let params = variables.positional_params_mut();
params.values.push("foo".to_string());
params.values.push("bar".to_string());
assert_eq!(
variables.positional_params().values,
["foo".to_string(), "bar".to_string()],
);
}
#[test]
fn positional_params_in_second_regular_context() {
let mut variables = VariableSet::new();
variables.push_context_impl(Context::default());
assert_eq!(variables.positional_params().values, [] as [String; 0]);
let params = variables.positional_params_mut();
params.values.push("1".to_string());
assert_eq!(variables.positional_params().values, ["1".to_string()]);
}
#[test]
fn getting_positional_params_in_volatile_context() {
let mut variables = VariableSet::new();
let params = variables.positional_params_mut();
params.values.push("a".to_string());
params.values.push("b".to_string());
params.values.push("c".to_string());
variables.push_context_impl(Context::Volatile);
assert_eq!(
variables.positional_params().values,
["a".to_string(), "b".to_string(), "c".to_string()],
);
}
#[test]
fn setting_positional_params_in_volatile_context() {
let mut variables = VariableSet::new();
variables.push_context_impl(Context::Volatile);
let params = variables.positional_params_mut();
params.values.push("x".to_string());
variables.pop_context_impl();
assert_eq!(variables.positional_params().values, ["x".to_string()]);
}
}