Skip to content

Commit

Permalink
feat(env): introduce dynamic variables (#360)
Browse files Browse the repository at this point in the history
Implements infra for dynamic shell values backed by code.

Adds well-known variables: BASHOPTS, BASHPID, BASH_ALIASES, BASH_ARGV0, BASH_CMDS, BASH_SUBSHELL, DIRSTACK, EPOCHREALTIME, EPOCHSECONDS, GROUPS, HOSTNAME, HOSTTYPE, LINENO, MACHTYPE, PIPESTATUS, PPID, SECONDS, SHELLOPTS, SHLVL, SRANDOM, PS4, UID
  • Loading branch information
reubeno authored Jan 25, 2025
1 parent b4638d7 commit 670ef89
Show file tree
Hide file tree
Showing 23 changed files with 885 additions and 275 deletions.
42 changes: 21 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If you feel so inclined, we'd love contributions toward any of the above, with b

## Testing strategy

This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [525+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [550+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.

For more details, please consult the [reference documentation on integration testing](docs/reference/integration-testing.md).

Expand Down
12 changes: 7 additions & 5 deletions brush-core/src/arithmetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,19 @@ impl Evaluatable for ast::ArithmeticExpr {

fn deref_lvalue(shell: &mut Shell, lvalue: &ast::ArithmeticTarget) -> Result<i64, EvalError> {
let value_str: Cow<'_, str> = match lvalue {
ast::ArithmeticTarget::Variable(name) => shell
.env
.get(name)
.map_or_else(|| Cow::Borrowed(""), |(_, v)| v.value().to_cow_string()),
ast::ArithmeticTarget::Variable(name) => {
shell.get_env_str(name).unwrap_or(Cow::Borrowed(""))
}
ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
let index_str = index_expr.eval(shell)?.to_string();

shell
.env
.get(name)
.map_or_else(|| Ok(None), |(_, v)| v.value().get_at(index_str.as_str()))
.map_or_else(
|| Ok(None),
|(_, v)| v.value().get_at(index_str.as_str(), shell),
)
.map_err(|_err| EvalError::FailedToAccessArray)?
.unwrap_or(Cow::Borrowed(""))
}
Expand Down
4 changes: 2 additions & 2 deletions brush-core/src/builtins/cd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl builtins::Command for CdCommand {
// `cd -', equivalent to `cd $OLDPWD'
if target_dir.as_os_str() == "-" {
should_print = true;
if let Some(oldpwd) = context.shell.env.get_str("OLDPWD") {
if let Some(oldpwd) = context.shell.get_env_str("OLDPWD") {
PathBuf::from(oldpwd.to_string())
} else {
writeln!(context.stderr(), "OLDPWD not set")?;
Expand All @@ -61,7 +61,7 @@ impl builtins::Command for CdCommand {
}
// `cd' without arguments is equivalent to `cd $HOME'
} else {
if let Some(home_var) = context.shell.env.get_str("HOME") {
if let Some(home_var) = context.shell.get_env_str("HOME") {
PathBuf::from(home_var.to_string())
} else {
writeln!(context.stderr(), "HOME not set")?;
Expand Down
12 changes: 7 additions & 5 deletions brush-core/src/builtins/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ impl DeclareCommand {
Ok(false)
}
} else if let Some(variable) = context.shell.env.get_using_policy(name, lookup) {
let mut cs = variable.get_attribute_flags();
let mut cs = variable.get_attribute_flags(context.shell);
if cs.is_empty() {
cs.push('-');
}
Expand All @@ -219,7 +219,7 @@ impl DeclareCommand {
"declare -{cs} {name}{separator_str}{}",
variable
.value()
.format(variables::FormatStyle::DeclarePrint)?
.format(variables::FormatStyle::DeclarePrint, context.shell)?
)?;

Ok(true)
Expand Down Expand Up @@ -467,7 +467,7 @@ impl DeclareCommand {
.sorted_by_key(|v| v.0)
{
if self.print {
let mut cs = variable.get_attribute_flags();
let mut cs = variable.get_attribute_flags(context.shell);
if cs.is_empty() {
cs.push('-');
}
Expand All @@ -483,13 +483,15 @@ impl DeclareCommand {
"declare -{cs} {name}{separator_str}{}",
variable
.value()
.format(variables::FormatStyle::DeclarePrint)?
.format(variables::FormatStyle::DeclarePrint, context.shell)?
)?;
} else {
writeln!(
context.stdout(),
"{name}={}",
variable.value().format(variables::FormatStyle::Basic)?
variable
.value()
.format(variables::FormatStyle::Basic, context.shell)?
)?;
}
}
Expand Down
2 changes: 1 addition & 1 deletion brush-core/src/builtins/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl builtins::Command for ExportCommand {
context.stdout(),
"declare -x {}=\"{}\"",
name,
variable.value().to_cow_string()
variable.value().to_cow_str(context.shell)
)?;
}
}
Expand Down
6 changes: 2 additions & 4 deletions brush-core/src/builtins/getopts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ impl builtins::Command for GetOptsCommand {
// If unset, assume OPTIND is 1.
let mut next_index: usize = context
.shell
.env
.get_str("OPTIND")
.get_env_str("OPTIND")
.unwrap_or(Cow::Borrowed("1"))
.parse()?;

Expand All @@ -92,8 +91,7 @@ impl builtins::Command for GetOptsCommand {
const DEFAULT_NEXT_CHAR_INDEX: usize = 1;
let next_char_index = context
.shell
.env
.get_str(VAR_GETOPTS_NEXT_CHAR_INDEX)
.get_env_str(VAR_GETOPTS_NEXT_CHAR_INDEX)
.map_or(DEFAULT_NEXT_CHAR_INDEX, |s| {
s.parse().unwrap_or(DEFAULT_NEXT_CHAR_INDEX)
});
Expand Down
3 changes: 2 additions & 1 deletion brush-core/src/builtins/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@ fn display_all(context: &commands::ExecutionContext<'_>) -> Result<(), error::Er
writeln!(
context.stdout(),
"{name}={}",
var.value().format(variables::FormatStyle::Basic)?,
var.value()
.format(variables::FormatStyle::Basic, context.shell)?,
)?;
}

Expand Down
2 changes: 1 addition & 1 deletion brush-core/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ pub(crate) fn compose_std_command<S: AsRef<OsStr>>(
if !empty_env {
for (name, var) in shell.env.iter() {
if var.is_exported() {
let value_as_str = var.value().to_cow_string();
let value_as_str = var.value().to_cow_str(shell);
cmd.env(name, value_as_str.as_ref());
}
}
Expand Down
6 changes: 4 additions & 2 deletions brush-core/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::HashMap;

use crate::error;
use crate::shell;
use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable};

/// Represents the policy for looking up variables in a shell environment.
Expand Down Expand Up @@ -179,9 +180,10 @@ impl ShellEnvironment {
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn get_str<S: AsRef<str>>(&self, name: S) -> Option<Cow<'_, str>> {
/// * `shell` - The shell owning the environment.
pub fn get_str<S: AsRef<str>>(&self, name: S, shell: &shell::Shell) -> Option<Cow<'_, str>> {
self.get(name.as_ref())
.map(|(_, v)| v.value().to_cow_string())
.map(|(_, v)| v.value().to_cow_str(shell))
}

/// Checks if a variable of the given name is set in the environment.
Expand Down
22 changes: 13 additions & 9 deletions brush-core/src/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ impl<'a> WordExpander<'a> {
.try_resolve_parameter_to_variable(&parameter, indirect)
.await?
{
Ok(var.get_attribute_flags().into())
Ok(var.get_attribute_flags(self.shell).into())
} else {
Ok(String::new().into())
}
Expand All @@ -963,17 +963,19 @@ impl<'a> WordExpander<'a> {
.try_resolve_parameter_to_variable(&parameter, indirect)
.await?
{
let assignable_value_str = var.value().to_assignable_str(index.as_deref());
let assignable_value_str =
var.value().to_assignable_str(index.as_deref(), self.shell);

let mut attr_str = var.get_attribute_flags();
let mut attr_str = var.get_attribute_flags(self.shell);
if attr_str.is_empty() {
attr_str.push('-');
}

match var.value() {
ShellValue::IndexedArray(_)
| ShellValue::AssociativeArray(_)
| ShellValue::Random => {
// TODO(dynamic): confirm this
| ShellValue::Dynamic { .. } => {
let equals_or_nothing = if assignable_value_str.is_empty() {
""
} else {
Expand Down Expand Up @@ -1120,7 +1122,7 @@ impl<'a> WordExpander<'a> {
concatenate,
} => {
let keys = if let Some((_, var)) = self.shell.env.get(variable_name) {
var.value().get_element_keys()
var.value().get_element_keys(self.shell)
} else {
vec![]
};
Expand Down Expand Up @@ -1273,7 +1275,9 @@ impl<'a> WordExpander<'a> {
if matches!(var.value(), ShellValue::Unset(_)) {
Ok(Expansion::undefined())
} else {
Ok(Expansion::from(var.value().to_cow_string().to_string()))
Ok(Expansion::from(
var.value().to_cow_str(self.shell).to_string(),
))
}
} else {
Ok(Expansion::undefined())
Expand All @@ -1298,7 +1302,7 @@ impl<'a> WordExpander<'a> {

// Index into the array.
if let Some((_, var)) = self.shell.env.get(name) {
if let Some(value) = var.value().get_at(index_to_use.as_str())? {
if let Some(value) = var.value().get_at(index_to_use.as_str(), self.shell)? {
Ok(Expansion::from(value.to_string()))
} else {
Ok(Expansion::undefined())
Expand All @@ -1309,7 +1313,7 @@ impl<'a> WordExpander<'a> {
}
brush_parser::word::Parameter::NamedWithAllIndices { name, concatenate } => {
if let Some((_, var)) = self.shell.env.get(name) {
let values = var.value().get_element_values();
let values = var.value().get_element_values(self.shell);

Ok(Expansion {
fields: values
Expand Down Expand Up @@ -1374,7 +1378,7 @@ impl<'a> WordExpander<'a> {
Ok(Expansion::from(self.shell.last_exit_status.to_string()))
}
brush_parser::word::SpecialParameter::CurrentOptionFlags => {
Ok(Expansion::from(self.shell.current_option_flags()))
Ok(Expansion::from(self.shell.options.get_option_flags()))
}
brush_parser::word::SpecialParameter::ProcessId => {
Ok(Expansion::from(std::process::id().to_string()))
Expand Down
Loading

0 comments on commit 670ef89

Please sign in to comment.