Repository: reubeno/brush
Pull request #360 (2025-01-25 06:07:17)

feat(env): introduce dynamic vars backed by rust code
 Implements the ideas in #233, with `$RANDOM` as an initial case. The main downside here is requiring that a `&Shell` be passed into various `ShellVariable` functions

Adds various other standard variables that are now easier for us to implement and keep in sync with fields in the `Shell` object.

Notably, this lacks support for handling mutation of these dynamic values. We'll need to implement this in the future.

github-actions[bot]: ## Test Results
    2 files      9 suites   1m 33s ⏱️
  658 tests   656 ✅ 0 💤 2 ❌
1 302 runs  1 299 ✅ 0 💤 3 ❌

For more details on these failures, see [this check](https://github.com/reubeno/brush/runs/36159143829).

Results for commit 02917177.

[test-results]:data:application/gzip;base64,H4sIAGGClGcC/02Myw6DIBQFf8Ww7gK4Ebz9mYaHJqQqDY9V038vUNQuZ87JvMni1jmS+8BvA4nZpQZYwOagkvN7RShcllQ3MU4HPWI2pilxqad7FUVPsSi39v5PzCH40C8h77XJgPJOR5NxxFNdzcY9CQf/F43fNpcKEMqRSSalVVIxYHTR02isBqq5BNBUIEdpUJHPFyteZ9YGAQAA

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   16.21 μs` | `   17.93 μs` | `    1.72 μs` | `🟠 +10.61%   ` |
| `eval_arithmetic                     ` | `    0.17 μs` | `    0.17 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    1.61 μs` | `    1.60 μs` | `   -0.01 μs` | `⚪  Unchanged` |
| `for_loop                            ` | `   33.88 μs` | `   34.64 μs` | `    0.76 μs` | `🟠 +2.23%    ` |
| `function_call                       ` | `    3.44 μs` | `    2.85 μs` | `   -0.59 μs` | `🟢 -17.19%   ` |
| `instantiate_shell                   ` | `   48.87 μs` | `  339.67 μs` | `  290.80 μs` | `🟠 +595.04%  ` |
| `instantiate_shell_with_init_scripts ` | `22030.94 μs` | `22575.73 μs` | `  544.79 μs` | `🟠 +2.47%    ` |
| `parse_bash_completion               ` | ` 1659.97 μs` | ` 1668.06 μs` | `    8.09 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    1.77 μs` | `    1.73 μs` | `   -0.04 μs` | `⚪  Unchanged` |
| `run_echo_builtin_command            ` | `   16.00 μs` | `   16.57 μs` | `    0.58 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 2346.55 μs` | ` 2368.55 μs` | `   22.00 μs` | `⚪  Unchanged` |
| `tokenize_sample_script              ` | `    2.87 μs` | `    2.81 μs` | `   -0.06 μs` | `🟢 -2.19%    ` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/arithmetic.rs | 🟢 98.47% | 🟢 98.48% | 🟢 0.01% |
| brush-core/src/builtins/declare.rs | 🟢 82.5% | 🟢 83.15% | 🟢 0.65% |
| brush-core/src/builtins/getopts.rs | 🟢 95.12% | 🟢 95.06% | 🔴 -0.06% |
| brush-core/src/builtins/set.rs | 🟢 77.17% | 🟢 76.82% | 🔴 -0.35% |
| brush-core/src/expansion.rs | 🟢 96.23% | 🟢 96.24% | 🟢 0.01% |
| brush-core/src/interp.rs | 🟢 91.18% | 🟢 91.14% | 🔴 -0.04% |
| brush-core/src/options.rs | 🟢 90.38% | 🟢 93.48% | 🟢 3.1% |
| brush-core/src/shell.rs | 🟢 78.73% | 🟢 82.54% | 🟢 3.81% |
| brush-core/src/sys/unix/terminal.rs | 🔴 32.65% | 🔴 38.78% | 🟢 6.13% |
| brush-core/src/sys/unix/users.rs | 🔴 28.95% | 🔴 38.64% | 🟢 9.69% |
| brush-core/src/variables.rs | 🟢 82.3% | 🟢 83.3% | 🟢 1% |
| brush-interactive/src/interactive_shell.rs | 🟠 74.68% | 🟠 72.84% | 🔴 -1.84% |
| **Overall Coverage** | **🟢 75.16%** | **🟢 75.58%** | **🟢 0.42%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `75.58%`
<!-- Sticky Pull Request Comment -->

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f17c2ba..13041d0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -513,9 +513,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.26"
+version = "4.5.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
+checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -532,9 +532,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.26"
+version = "4.5.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
+checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
 dependencies = [
  "anstream",
  "anstyle",
@@ -741,9 +741,9 @@ dependencies = [
 
 [[package]]
 name = "crunchy"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
 
 [[package]]
 name = "darling"
@@ -1312,13 +1312,13 @@ dependencies = [
 
 [[package]]
 name = "is-terminal"
-version = "0.4.13"
+version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
 dependencies = [
  "hermit-abi",
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -1980,9 +1980,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 
 [[package]]
 name = "rustix"
-version = "0.38.43"
+version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
  "bitflags 2.8.0",
  "errno",
@@ -2040,9 +2040,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.136"
+version = "1.0.137"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "336a0c23cf42a38d9eaa7cd22c7040d04e1228a19a933890805ffd00a16437d2"
+checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
 dependencies = [
  "itoa",
  "memchr",
@@ -2180,9 +2180,9 @@ dependencies = [
 
 [[package]]
 name = "symbolic-common"
-version = "12.13.2"
+version = "12.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8150eae9699e3c73a3e6431dc1f80d87748797c0457336af23e94c1de619ed24"
+checksum = "13a4dfe4bbeef59c1f32fc7524ae7c95b9e1de5e79a43ce1604e181081d71b0c"
 dependencies = [
  "debugid",
  "memmap2",
@@ -2192,9 +2192,9 @@ dependencies = [
 
 [[package]]
 name = "symbolic-demangle"
-version = "12.13.2"
+version = "12.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95f4a9846f7a8933b6d198c022faa2c9bd89e1a970bed9d9a98d25708bf8de17"
+checksum = "98cf6a95abff97de4d7ff3473f33cacd38f1ddccad5c1feab435d6760300e3b6"
 dependencies = [
  "cpp_demangle",
  "rustc-demangle",
@@ -2472,9 +2472,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
 
 [[package]]
 name = "unicode-segmentation"
@@ -2517,9 +2517,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uuid"
-version = "1.12.0"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
+checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
 dependencies = [
  "getrandom",
 ]
diff --git a/README.md b/README.md
index 0ffa905..d94bfd5 100644
--- a/README.md
+++ b/README.md
@@ -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).
 
diff --git a/brush-core/src/arithmetic.rs b/brush-core/src/arithmetic.rs
index 6d041cc..59f51dd 100644
--- a/brush-core/src/arithmetic.rs
+++ b/brush-core/src/arithmetic.rs
@@ -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(""))
         }
diff --git a/brush-core/src/builtins/cd.rs b/brush-core/src/builtins/cd.rs
index 319b4c6..ed932e5 100644
--- a/brush-core/src/builtins/cd.rs
+++ b/brush-core/src/builtins/cd.rs
@@ -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")?;
@@ -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")?;
diff --git a/brush-core/src/builtins/declare.rs b/brush-core/src/builtins/declare.rs
index 42e9b0e..735a44a 100644
--- a/brush-core/src/builtins/declare.rs
+++ b/brush-core/src/builtins/declare.rs
@@ -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('-');
             }
@@ -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)
@@ -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('-');
                 }
@@ -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)?
                 )?;
             }
         }
diff --git a/brush-core/src/builtins/export.rs b/brush-core/src/builtins/export.rs
index 19541f4..98088e9 100644
--- a/brush-core/src/builtins/export.rs
+++ b/brush-core/src/builtins/export.rs
@@ -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)
                     )?;
                 }
             }
diff --git a/brush-core/src/builtins/getopts.rs b/brush-core/src/builtins/getopts.rs
index 925d3c4..aaa0135 100644
--- a/brush-core/src/builtins/getopts.rs
+++ b/brush-core/src/builtins/getopts.rs
@@ -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()?;
 
@@ -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)
                     });
diff --git a/brush-core/src/builtins/set.rs b/brush-core/src/builtins/set.rs
index f487e27..c584229 100644
--- a/brush-core/src/builtins/set.rs
+++ b/brush-core/src/builtins/set.rs
@@ -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)?,
         )?;
     }
 
diff --git a/brush-core/src/commands.rs b/brush-core/src/commands.rs
index 18c1349..b3464bf 100644
--- a/brush-core/src/commands.rs
+++ b/brush-core/src/commands.rs
@@ -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());
             }
         }
diff --git a/brush-core/src/env.rs b/brush-core/src/env.rs
index 9017443..739cc2f 100644
--- a/brush-core/src/env.rs
+++ b/brush-core/src/env.rs
@@ -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.
@@ -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.
diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs
index 84a2c76..2b033ab 100644
--- a/brush-core/src/expansion.rs
+++ b/brush-core/src/expansion.rs
@@ -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())
                 }
@@ -963,9 +963,10 @@ 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('-');
                     }
@@ -973,7 +974,8 @@ impl<'a> WordExpander<'a> {
                     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 {
@@ -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![]
                 };
@@ -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())
@@ -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())
@@ -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
@@ -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()))
diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs
index 45e17c7..c1205fd 100644
--- a/brush-core/src/interp.rs
+++ b/brush-core/src/interp.rs
@@ -299,14 +299,16 @@ impl Execute for ast::Pipeline {
         // as needed.
         let spawn_results = spawn_pipeline_processes(self, shell, params).await?;
 
-        // Wait for the processes.
-        let mut result = wait_for_pipeline_processes(self, spawn_results, shell).await?;
+        // Wait for the processes. This also has a side effect of updating pipeline status.
+        let mut result =
+            wait_for_pipeline_processes_and_update_status(self, spawn_results, shell).await?;
 
         // Invert the exit code if requested.
         if self.bang {
             result.exit_code = if result.exit_code == 0 { 1 } else { 0 };
         }
 
+        // Update statuses.
         shell.last_exit_status = result.exit_code;
 
         // If requested, report timing.
@@ -401,7 +403,7 @@ async fn spawn_pipeline_processes(
     Ok(spawn_results)
 }
 
-async fn wait_for_pipeline_processes(
+async fn wait_for_pipeline_processes_and_update_status(
     pipeline: &ast::Pipeline,
     mut process_spawn_results: VecDeque<CommandSpawnResult>,
     shell: &mut Shell,
@@ -409,15 +411,20 @@ async fn wait_for_pipeline_processes(
     let mut result = ExecutionResult::success();
     let mut stopped_children = vec![];
 
+    // Clear our the pipeline status so we can start filling it out.
+    shell.last_pipeline_statuses.clear();
+
     while let Some(child) = process_spawn_results.pop_front() {
         match child.wait(!stopped_children.is_empty()).await? {
             commands::CommandWaitResult::CommandCompleted(current_result) => {
                 result = current_result;
                 shell.last_exit_status = result.exit_code;
+                shell.last_pipeline_statuses.push(result.exit_code);
             }
             commands::CommandWaitResult::CommandStopped(current_result, child) => {
                 result = current_result;
                 shell.last_exit_status = result.exit_code;
+                shell.last_pipeline_statuses.push(result.exit_code);
 
                 stopped_children.push(jobs::JobTask::External(child));
             }
diff --git a/brush-core/src/options.rs b/brush-core/src/options.rs
index e6697d0..d1e6896 100644
--- a/brush-core/src/options.rs
+++ b/brush-core/src/options.rs
@@ -1,3 +1,5 @@
+use itertools::Itertools;
+
 use crate::CreateOptions;
 
 /// Runtime changeable options for a shell instance.
@@ -244,4 +246,62 @@ impl RuntimeOptions {
 
         options
     }
+
+    /// Returns a string representing the current `set`-style option flags set in the shell.
+    pub fn get_option_flags(&self) -> String {
+        let mut cs = vec![];
+
+        for (x, y) in crate::namedoptions::SET_OPTIONS.iter() {
+            if (y.getter)(self) {
+                cs.push(*x);
+            }
+        }
+
+        // Sort the flags in a way that matches what bash does.
+        cs.sort_by(|a, b| {
+            if a == b {
+                std::cmp::Ordering::Equal
+            } else if *a == 's' {
+                std::cmp::Ordering::Greater
+            } else if *b == 's' {
+                std::cmp::Ordering::Less
+            } else if a.is_ascii_lowercase() && b.is_ascii_uppercase() {
+                std::cmp::Ordering::Less
+            } else if a.is_ascii_uppercase() && b.is_ascii_lowercase() {
+                std::cmp::Ordering::Greater
+            } else {
+                a.cmp(b)
+            }
+        });
+
+        cs.into_iter().collect()
+    }
+
+    /// Returns a colon-separated list of sorted 'set -o' options enabled.
+    pub fn get_set_o_optstr(&self) -> String {
+        let mut cs = vec![];
+
+        for (x, y) in crate::namedoptions::SET_O_OPTIONS.iter() {
+            if (y.getter)(self) {
+                cs.push(*x);
+            }
+        }
+
+        cs.sort_unstable();
+        cs.into_iter().join(":")
+    }
+
+    /// Returns a colon-separated list of sorted 'shopt' options enabled.
+    pub fn get_shopt_optstr(&self) -> String {
+        let mut cs = vec![];
+
+        for (x, y) in crate::namedoptions::SHOPT_OPTIONS.iter() {
+            if (y.getter)(self) {
+                cs.push(*x);
+            }
+        }
+
+        cs.sort_unstable();
+        cs.into_iter().join(":")
+    }
 }
diff --git a/brush-core/src/pathcache.rs b/brush-core/src/pathcache.rs
index b36b357..fff2c7f 100644
--- a/brush-core/src/pathcache.rs
+++ b/brush-core/src/pathcache.rs
@@ -1,3 +1,4 @@
+use crate::{error, variables};
 use std::path::PathBuf;
 
 /// A cache of paths associated with names.
@@ -31,6 +32,17 @@ impl PathCache {
         self.cache.insert(name.as_ref().to_string(), path);
     }
 
+    /// Projects the cache into a shell value.
+    pub fn to_value(&self) -> Result<variables::ShellValue, error::Error> {
+        let pairs = self
+            .cache
+            .iter()
+            .map(|(k, v)| (Some(k.to_owned()), v.to_string_lossy().to_string()))
+            .collect::<Vec<_>>();
+
+        variables::ShellValue::associative_array_from_literals(variables::ArrayLiteral(pairs))
+    }
+
     /// Removes the path associated with the given name, if there is one.
     /// Returns whether or not an entry was removed.
     ///
diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs
index 60870c0..a08b8d2 100644
--- a/brush-core/src/shell.rs
+++ b/brush-core/src/shell.rs
@@ -5,6 +5,8 @@ use std::io::{Read, Write};
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
+use rand::Rng;
+
 use crate::arithmetic::Evaluatable;
 use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment};
 use crate::interp::{self, Execute, ExecutionParameters, ExecutionResult};
@@ -15,7 +17,14 @@ use crate::{
     builtins, commands, completion, env, error, expansion, functions, jobs, keywords, openfiles,
     patterns, prompt, sys::users, traps,
 };
-use crate::{pathcache, trace_categories};
+use crate::{pathcache, sys, trace_categories};
+
+const BASH_MAJOR: u32 = 5;
+const BASH_MINOR: u32 = 2;
+const BASH_PATCH: u32 = 15;
+const BASH_BUILD: u32 = 1;
+const BASH_RELEASE: &str = "release";
+const BASH_MACHINE: &str = "unknown";
 
 /// Represents an instance of a shell.
 pub struct Shell {
@@ -43,6 +52,9 @@ pub struct Shell {
     /// The status of the last completed command.
     pub last_exit_status: u8,
 
+    /// The status of each of the commands in the last pipeline.
+    pub last_pipeline_statuses: Vec<u8>,
+
     /// Clone depth from the original ancestor shell.
     depth: usize,
 
@@ -75,6 +87,12 @@ pub struct Shell {
 
     /// Shell program location cache.
     pub program_location_cache: pathcache::PathCache,
+
+    /// Last "SECONDS" captured time.
+    last_stopwatch_time: std::time::SystemTime,
+
+    /// Last "SECONDS" offset requested.
+    last_stopwatch_offset: u32,
 }
 
 impl Clone for Shell {
@@ -89,6 +107,7 @@ impl Clone for Shell {
             jobs: jobs::JobManager::new(),
             aliases: self.aliases.clone(),
             last_exit_status: self.last_exit_status,
+            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
             positional_parameters: self.positional_parameters.clone(),
             shell_name: self.shell_name.clone(),
             shell_product_display_str: self.shell_product_display_str.clone(),
@@ -99,6 +118,8 @@ impl Clone for Shell {
             completion_config: self.completion_config.clone(),
             builtins: self.builtins.clone(),
             program_location_cache: self.program_location_cache.clone(),
+            last_stopwatch_time: self.last_stopwatch_time,
+            last_stopwatch_offset: self.last_stopwatch_offset,
             depth: self.depth + 1,
         }
     }
@@ -189,12 +210,13 @@ impl Shell {
             traps: traps::TrapHandlerConfig::default(),
             open_files: openfiles::OpenFiles::default(),
             working_dir: std::env::current_dir()?,
-            env: Self::initialize_vars(options)?,
+            env: env::ShellEnvironment::new(),
             funcs: functions::FunctionEnv::default(),
             options: RuntimeOptions::defaults_from(options),
             jobs: jobs::JobManager::new(),
             aliases: HashMap::default(),
             last_exit_status: 0,
+            last_pipeline_statuses: vec![0],
             positional_parameters: vec![],
             shell_name: options.shell_name.clone(),
             shell_product_display_str: options.shell_product_display_str.clone(),
@@ -205,6 +227,8 @@ impl Shell {
             completion_config: completion::Config::default(),
             builtins: builtins::get_default_builtins(options),
             program_location_cache: pathcache::PathCache::default(),
+            last_stopwatch_time: std::time::SystemTime::now(),
+            last_stopwatch_offset: 0,
             depth: 0,
         };
 
@@ -212,75 +236,310 @@ impl Shell {
         // parse the entire script with the same settings.
         shell.options.extended_globbing = true;
 
+        // Initialize environment.
+        shell.initialize_vars(options)?;
+
         // Load profiles/configuration.
         shell.load_config(options).await?;
 
         Ok(shell)
     }
 
-    fn initialize_vars(options: &CreateOptions) -> Result<ShellEnvironment, error::Error> {
-        let mut env = ShellEnvironment::new();
-
+    #[allow(clippy::too_many_lines)]
+    #[allow(clippy::unwrap_in_result)]
+    fn initialize_vars(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
         // Seed parameters from environment (unless requested not to do so).
         if !options.do_not_inherit_env {
             for (k, v) in std::env::vars() {
                 let mut var = ShellVariable::new(ShellValue::String(v));
                 var.export();
-                env.set_global(k, var)?;
+                self.env.set_global(k, var)?;
             }
         }
 
-        // Set some additional ones.
+        // TODO(vars): implement $_
+
+        // BASH
+        if let Some(shell_name) = &options.shell_name {
+            self.env
+                .set_global("BASH", ShellVariable::new(shell_name.into()))?;
+        }
+
+        // BASHOPTS
+        let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic {
+            getter: |shell| shell.options.get_shopt_optstr().into(),
+            setter: |_| (),
+        });
+        bashopts_var.set_readonly();
+        self.env.set_global("BASHOPTS", bashopts_var)?;
+
+        // BASHPID
+        let mut bashpid_var =
+            ShellVariable::new(ShellValue::String(std::process::id().to_string()));
+        bashpid_var.treat_as_integer();
+        self.env.set_global("BASHPID", bashpid_var)?;
+
+        // BASH_ALIASES
+        self.env.set_global(
+            "BASH_ALIASES",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| {
+                    let values = variables::ArrayLiteral(
+                        shell
+                            .aliases
+                            .iter()
+                            .map(|(k, v)| (Some(k.to_owned()), v.to_owned()))
+                            .collect::<Vec<_>>(),
+                    );
+
+                    ShellValue::associative_array_from_literals(values).unwrap()
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // TODO(vars): implement BASH_ARGC
+        // TODO(vars): implement BASH_ARGV
+
+        // BASH_ARGV0
+        self.env.set_global(
+            "BASH_ARGV0",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| {
+                    let argv0 = shell.shell_name.as_deref().unwrap_or_default();
+                    argv0.to_string().into()
+                },
+                // TODO(vars): implement updating BASH_ARGV0
+                setter: |_| (),
+            }),
+        )?;
+
+        // TODO(vars): implement mutation of BASH_CMDS
+        self.env.set_global(
+            "BASH_CMDS",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| shell.program_location_cache.to_value().unwrap(),
+                setter: |_| (),
+            }),
+        )?;
+
+        // TODO(vars): implement BASH_COMMAND
+        // TODO(vars): implement BASH_EXECUTIION_STRING
+        // TODO(vars): implement BASH_LINENO
+
+        // BASH_SOURCE
+        self.env.set_global(
+            "BASH_SOURCE",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| shell.get_bash_source_value(),
+                setter: |_| (),
+            }),
+        )?;
+
+        // BASH_SUBSHELL
+        self.env.set_global(
+            "BASH_SUBSHELL",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| shell.depth.to_string().into(),
+                setter: |_| (),
+            }),
+        )?;
+
+        // BASH_VERSINFO
+        let mut bash_versinfo_var = ShellVariable::new(ShellValue::indexed_array_from_strs(
+            [
+                BASH_MAJOR.to_string().as_str(),
+                BASH_MINOR.to_string().as_str(),
+                BASH_PATCH.to_string().as_str(),
+                BASH_BUILD.to_string().as_str(),
+                BASH_RELEASE,
+                BASH_MACHINE,
+            ]
+            .as_slice(),
+        ));
+        bash_versinfo_var.set_readonly();
+        self.env.set_global("BASH_VERSINFO", bash_versinfo_var)?;
+
+        // BASH_VERSION
+        self.env.set_global(
+            "BASH_VERSION",
+            ShellVariable::new(
+                std::format!("{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}")
+                    .into(),
+            ),
+        )?;
+
+        // COMP_WORDBREAKS
+        self.env.set_global(
+            "COMP_WORDBREAKS",
+            ShellVariable::new(" \t\n\"\'@><=;|&(:".into()),
+        )?;
+
+        // DIRSTACK
+        self.env.set_global(
+            "DIRSTACK",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| {
+                    shell
+                        .directory_stack
+                        .iter()
+                        .map(|p| p.to_string_lossy().to_string())
+                        .collect::<Vec<_>>()
+                        .into()
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // EPOCHREALTIME
+        self.env.set_global(
+            "EPOCHREALTIME",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |_shell| {
+                    let now = std::time::SystemTime::now();
+                    let since_epoch = now
+                        .duration_since(std::time::UNIX_EPOCH)
+                        .unwrap_or_default();
+                    since_epoch.as_secs_f64().to_string().into()
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // EPOCHSECONDS
+        self.env.set_global(
+            "EPOCHSECONDS",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |_shell| {
+                    let now = std::time::SystemTime::now();
+                    let since_epoch = now
+                        .duration_since(std::time::UNIX_EPOCH)
+                        .unwrap_or_default();
+                    since_epoch.as_secs().to_string().into()
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // EUID
         #[cfg(unix)]
         {
             let mut euid_var = ShellVariable::new(ShellValue::String(format!(
                 "{}",
                 uzers::get_effective_uid()
             )));
-            euid_var.set_readonly();
-            env.set_global("EUID", euid_var)?;
+            euid_var.treat_as_integer().set_readonly();
+            self.env.set_global("EUID", euid_var)?;
         }
 
-        let mut random_var = ShellVariable::new(ShellValue::Random);
-        random_var.hide_from_enumeration();
-        random_var.treat_as_integer();
-        env.set_global("RANDOM", random_var)?;
+        // FUNCNAME
+        self.env.set_global(
+            "FUNCNAME",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| shell.get_funcname_value(),
+                setter: |_| (),
+            }),
+        )?;
 
-        // Parsing and completion vars
-        env.set_global("IFS", ShellVariable::new(" \t\n".into()))?;
-        env.set_global(
-            "COMP_WORDBREAKS",
-            ShellVariable::new(" \t\n\"\'@><=;|&(:".into()),
+        // GROUPS
+        // N.B. We could compute this up front, but we choose to make it dynamic so that we
+        // don't have to make costly system calls if the user never accesses it.
+        self.env.set_global(
+            "GROUPS",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |_shell| {
+                    let groups = sys::users::get_user_group_ids().unwrap_or_default();
+                    ShellValue::indexed_array_from_strings(
+                        groups.into_iter().map(|gid| gid.to_string()),
+                    )
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // TODO(vars): implement HISTCMD
+
+        // HISTFILE (if not already set)
+        if !self.env.is_set("HISTFILE") {
+            if let Some(home_dir) = self.get_home_dir() {
+                let histfile = home_dir.join(".brush_history");
+                self.env.set_global(
+                    "HISTFILE",
+                    ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
+                )?;
+            }
+        }
+
+        // HOSTNAME
+        self.env.set_global(
+            "HOSTNAME",
+            ShellVariable::new(
+                sys::network::get_hostname()
+                    .unwrap_or_default()
+                    .to_string_lossy()
+                    .to_string()
+                    .into(),
+            ),
         )?;
 
-        // getopts vars
-        env.set_global("OPTERR", ShellVariable::new("1".into()))?;
+        // HOSTTYPE
+        #[cfg(unix)]
+        {
+            if let Ok(info) = nix::sys::utsname::uname() {
+                self.env.set_global(
+                    "HOSTTYPE",
+                    ShellVariable::new(info.machine().to_string_lossy().to_string().into()),
+                )?;
+            }
+        }
+
+        // IFS
+        self.env
+            .set_global("IFS", ShellVariable::new(" \t\n".into()))?;
+
+        // LINENO
+        self.env.set_global(
+            "LINENO",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| shell.current_line_number.to_string().into(),
+                setter: |_| (),
+            }),
+        )?;
+
+        // MACHTYPE
+        self.env
+            .set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE.into()))?;
+
+        // OLDPWD (initialization)
+        if !self.env.is_set("OLDPWD") {
+            let mut oldpwd_var =
+                ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped));
+            oldpwd_var.export();
+            self.env.set_global("OLDPWD", oldpwd_var)?;
+        }
+
+        // OPTERR
+        self.env
+            .set_global("OPTERR", ShellVariable::new("1".into()))?;
+
+        // OPTIND
         let mut optind_var = ShellVariable::new("1".into());
         optind_var.treat_as_integer();
-        env.set_global("OPTIND", optind_var)?;
+        self.env.set_global("OPTIND", optind_var)?;
 
-        // OS info vars
+        // OSTYPE
         let os_type = match std::env::consts::OS {
             "linux" => "linux-gnu",
             "windows" => "windows",
             _ => "unknown",
         };
-        env.set_global("OSTYPE", ShellVariable::new(os_type.into()))?;
-
-        // Set some defaults (if they're not already initialized).
-        if !env.is_set("HISTFILE") {
-            if let Some(home_dir) = Self::get_home_dir_with_env(&env) {
-                let histfile = home_dir.join(".brush_history");
-                env.set_global(
-                    "HISTFILE",
-                    ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
-                )?;
-            }
-        }
+        self.env
+            .set_global("OSTYPE", ShellVariable::new(os_type.into()))?;
 
+        // PATH (if not already set)
         #[cfg(unix)]
-        if !env.is_set("PATH") {
-            env.set_global(
+        if !self.env.is_set("PATH") {
+            self.env.set_global(
                 "PATH",
                 ShellVariable::new(
                     "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
@@ -288,52 +547,113 @@ impl Shell {
             )?;
         }
 
-        // Update PWD to reflect our actual working directory. There's a chance
+        // PIPESTATUS
+        // TODO: Investigate what happens if this gets unset.
+        // TODO: Investigate if this needs to be saved/preserved across prompt display.
+        self.env.set_global(
+            "PIPESTATUS",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| {
+                    ShellValue::indexed_array_from_strings(
+                        shell.last_pipeline_statuses.iter().map(|s| s.to_string()),
+                    )
+                },
+                setter: |_| (),
+            }),
+        )?;
+
+        // PPID
+        if let Some(ppid) = sys::terminal::get_parent_process_id() {
+            let mut ppid_var = ShellVariable::new(ppid.to_string().into());
+            ppid_var.treat_as_integer().set_readonly();
+            self.env.set_global("PPID", ppid_var)?;
+        }
+
+        // RANDOM
+        let mut random_var = ShellVariable::new(ShellValue::Dynamic {
+            getter: get_random_value,
+            setter: |_| (),
+        });
+        random_var.treat_as_integer();
+        self.env.set_global("RANDOM", random_var)?;
+
+        // SECONDS
+        self.env.set_global(
+            "SECONDS",
+            ShellVariable::new(ShellValue::Dynamic {
+                getter: |shell| {
+                    let now = std::time::SystemTime::now();
+                    let since_last = now
+                        .duration_since(shell.last_stopwatch_time)
+                        .unwrap_or_default();
+                    let total_seconds =
+                        since_last.as_secs() + u64::from(shell.last_stopwatch_offset);
+                    total_seconds.to_string().into()
+                },
+                // TODO(vars): implement updating SECONDS
+                setter: |_| (),
+            }),
+        )?;
+
+        // SHELL
+        if let Ok(exe_path) = std::env::current_exe() {
+            self.env.set_global(
+                "SHELL",
+                ShellVariable::new(exe_path.to_string_lossy().to_string().into()),
+            )?;
+        }
+
+        // SHELLOPTS
+        let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic {
+            getter: |shell| shell.options.get_set_o_optstr().into(),
+            setter: |_| (),
+        });
+        shellopts_var.set_readonly();
+        self.env.set_global("SHELLOPTS", shellopts_var)?;
+
+        // SHLVL
+        let input_shlvl = self.get_env_str("SHLVL").unwrap_or("0".into());
+        let updated_shlvl = input_shlvl.as_ref().parse::<u32>().unwrap_or(0) + 1;
+        let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string().into());
+        shlvl_var.export();
+        self.env.set_global("SHLVL", shlvl_var)?;
+
+        // SRANDOM
+        let mut random_var = ShellVariable::new(ShellValue::Dynamic {
+            getter: get_srandom_value,
+            setter: |_| (),
+        });
+        random_var.treat_as_integer();
+        self.env.set_global("SRANDOM", random_var)?;
+
+        // PS4
+        if !self.env.is_set("PS4") {
+            self.env
+                .set_global("PS4", ShellVariable::new("+ ".into()))?;
+        }
+
+        //
+        // PWD
+        //
+        // Reflect our actual working directory. There's a chance
         // we inherited an out-of-sync version of the variable. Future updates
         // will be handled by set_working_dir().
+        //
         let pwd = std::env::current_dir()?.to_string_lossy().to_string();
         let mut pwd_var = ShellVariable::new(pwd.into());
         pwd_var.export();
-        env.set_global("PWD", pwd_var)?;
-
-        // Set version info.
-        if !options.sh_mode {
-            const BASH_MAJOR: u32 = 5;
-            const BASH_MINOR: u32 = 2;
-            const BASH_PATCH: u32 = 15;
-            const BASH_BUILD: u32 = 1;
-            const BASH_RELEASE: &str = "release";
-            const BASH_MACHINE: &str = "unknown";
-
-            if let Some(shell_name) = &options.shell_name {
-                env.set_global("BASH", ShellVariable::new(shell_name.into()))?;
-            }
-            env.set_global(
-                "BASH_VERSINFO",
-                ShellVariable::new(ShellValue::indexed_array_from_slice(
-                    [
-                        BASH_MAJOR.to_string().as_str(),
-                        BASH_MINOR.to_string().as_str(),
-                        BASH_PATCH.to_string().as_str(),
-                        BASH_BUILD.to_string().as_str(),
-                        BASH_RELEASE,
-                        BASH_MACHINE,
-                    ]
-                    .as_slice(),
-                )),
-            )?;
-            env.set_global(
-                "BASH_VERSION",
-                ShellVariable::new(
-                    std::format!(
-                        "{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}"
-                    )
-                    .into(),
-                ),
-            )?;
+        self.env.set_global("PWD", pwd_var)?;
+
+        // UID
+        #[cfg(unix)]
+        {
+            let mut uid_var =
+                ShellVariable::new(ShellValue::String(format!("{}", uzers::get_current_uid())));
+            uid_var.treat_as_integer().set_readonly();
+            self.env.set_global("UID", uid_var)?;
         }
 
-        Ok(env)
+        Ok(())
     }
 
     async fn load_config(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
@@ -516,14 +836,12 @@ impl Shell {
 
         self.script_call_stack
             .push_front((call_type.clone(), source_info.source.clone()));
-        self.update_bash_source_var()?;
 
         let result = self
             .run_parsed_result(parse_result, source_info, params)
             .await;
 
         self.script_call_stack.pop_front();
-        self.update_bash_source_var()?;
 
         // Restore.
         std::mem::swap(&mut self.shell_name, &mut other_shell_name);
@@ -757,32 +1075,36 @@ impl Shell {
 
     /// Composes the shell's post-input, pre-command prompt, applying all appropriate expansions.
     pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
-        self.prompt_from_var_or_default("PS0", "").await
+        self.expand_prompt_var("PS0", "").await
     }
 
     /// Composes the shell's prompt, applying all appropriate expansions.
     pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
-        self.prompt_from_var_or_default("PS1", self.default_prompt())
-            .await
+        self.expand_prompt_var("PS1", self.default_prompt()).await
     }
 
     /// Compose's the shell's alternate-side prompt, applying all appropriate expansions.
     #[allow(clippy::unused_async)]
     pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
         // This is a brush extension.
-        self.prompt_from_var_or_default("BRUSH_PS_ALT", "").await
+        self.expand_prompt_var("BRUSH_PS_ALT", "").await
     }
 
     /// Composes the shell's continuation prompt.
     pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
-        self.prompt_from_var_or_default("PS2", "> ").await
+        self.expand_prompt_var("PS2", "> ").await
     }
 
-    async fn prompt_from_var_or_default(
+    async fn expand_prompt_var(
         &mut self,
         var_name: &str,
         default: &str,
     ) -> Result<String, error::Error> {
+        //
+        // TODO(prompt): bash appears to do this in a subshell; we need to investigate
+        // if that's required.
+        //
+
         // Retrieve the spec.
         let prompt_spec = self.parameter_or_default(var_name, default);
         if prompt_spec.is_empty() {
@@ -793,9 +1115,7 @@ impl Shell {
         let formatted_prompt = prompt::expand_prompt(self, prompt_spec.into_owned())?;
 
         // Now expand.
-        let formatted_prompt = expansion::basic_expand_str(self, &formatted_prompt).await?;
-
-        Ok(formatted_prompt)
+        expansion::basic_expand_str(self, &formatted_prompt).await
     }
 
     /// Returns the exit status of the last command executed in this shell.
@@ -804,37 +1124,7 @@ impl Shell {
     }
 
     fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
-        self.env.get_str(name).unwrap_or(default.into())
-    }
-
-    /// Returns a string representing the current `set`-style option flags set in the shell.
-    pub(crate) fn current_option_flags(&self) -> String {
-        let mut cs = vec![];
-
-        for (x, y) in crate::namedoptions::SET_OPTIONS.iter() {
-            if (y.getter)(&self.options) {
-                cs.push(*x);
-            }
-        }
-
-        // Sort the flags in a way that matches what bash does.
-        cs.sort_by(|a, b| {
-            if a == b {
-                std::cmp::Ordering::Equal
-            } else if *a == 's' {
-                std::cmp::Ordering::Greater
-            } else if *b == 's' {
-                std::cmp::Ordering::Less
-            } else if a.is_ascii_lowercase() && b.is_ascii_uppercase() {
-                std::cmp::Ordering::Less
-            } else if a.is_ascii_uppercase() && b.is_ascii_lowercase() {
-                std::cmp::Ordering::Greater
-            } else {
-                a.cmp(b)
-            }
-        });
-
-        cs.into_iter().collect()
+        self.get_env_str(name).unwrap_or(default.into())
     }
 
     /// Returns the options that should be used for parsing shell programs; reflects
@@ -889,7 +1179,6 @@ impl Shell {
             function_definition: function_def.clone(),
         });
         self.env.push_scope(env::EnvironmentScope::Local);
-        self.update_funcname_var()?;
         Ok(())
     }
 
@@ -906,61 +1195,35 @@ impl Shell {
             }
         }
 
-        self.update_funcname_var()?;
         Ok(())
     }
 
-    fn update_funcname_var(&mut self) -> Result<(), error::Error> {
-        //
-        // Fill out FUNCNAME[*]
-        //
-        let funcname_values = self
-            .function_call_stack
+    fn get_funcname_value(&self) -> variables::ShellValue {
+        self.function_call_stack
             .iter()
-            .map(|s| (None, s.function_name.clone()))
-            .collect::<Vec<_>>();
-
-        self.env.update_or_add(
-            "FUNCNAME",
-            variables::ShellValueLiteral::Array(variables::ArrayLiteral(funcname_values)),
-            |_| Ok(()),
-            EnvironmentLookup::Anywhere,
-            EnvironmentScope::Global,
-        )?;
-
-        self.update_bash_source_var()
+            .map(|s| s.function_name.as_str())
+            .collect::<Vec<_>>()
+            .into()
     }
 
-    fn update_bash_source_var(&mut self) -> Result<(), error::Error> {
-        //
-        // Fill out BASH_SOURCE[*]
-        //
-        let source_values = if self.function_call_stack.is_empty() {
+    fn get_bash_source_value(&self) -> variables::ShellValue {
+        if self.function_call_stack.is_empty() {
             self.script_call_stack
                 .front()
-                .map_or_else(Vec::new, |(_call_type, s)| vec![(None, s.to_owned())])
+                .map_or_else(Vec::new, |(_call_type, s)| vec![s.as_ref()])
+                .into()
         } else {
             self.function_call_stack
                 .iter()
-                .map(|s| (None, s.function_definition.source.clone()))
+                .map(|s| s.function_definition.source.as_ref())
                 .collect::<Vec<_>>()
-        };
-
-        self.env.update_or_add(
-            "BASH_SOURCE",
-            variables::ShellValueLiteral::Array(variables::ArrayLiteral(source_values)),
-            |_| Ok(()),
-            EnvironmentLookup::Anywhere,
-            EnvironmentScope::Global,
-        )?;
-
-        Ok(())
+                .into()
+        }
     }
 
     /// Returns the path to the history file used by the shell, if one is set.
     pub fn get_history_file_path(&self) -> Option<PathBuf> {
-        self.env
-            .get_str("HISTFILE")
+        self.get_env_str("HISTFILE")
             .map(|s| PathBuf::from(s.into_owned()))
     }
 
@@ -969,9 +1232,19 @@ impl Shell {
         self.current_line_number
     }
 
+    /// Tries to retrieve a variable from the shell's environment, converting it into its
+    /// string form.
+    ///
+    /// # Arguments
+    ///
+    /// * `name` - The name of the variable to retrieve.
+    pub fn get_env_str(&self, name: &str) -> Option<Cow<'_, str>> {
+        self.env.get_str(name, self)
+    }
+
     /// Returns the current value of the IFS variable, or the default value if it is not set.
     pub(crate) fn get_ifs(&self) -> Cow<'_, str> {
-        self.env.get_str("IFS").unwrap_or_else(|| " \t\n".into())
+        self.get_env_str("IFS").unwrap_or_else(|| " \t\n".into())
     }
 
     /// Returns the first character of the IFS variable, or a space if it is not set.
@@ -1006,7 +1279,7 @@ impl Shell {
         let is_executable = |path: &Path| path.is_file() && path.executable();
 
         let mut executables = vec![];
-        for dir_str in self.env.get_str("PATH").unwrap_or_default().split(':') {
+        for dir_str in self.get_env_str("PATH").unwrap_or_default().split(':') {
             let pattern =
                 patterns::Pattern::from(std::format!("{dir_str}/{required_glob_pattern}"))
                     .set_extended_globbing(self.options.extended_globbing)
@@ -1037,7 +1310,7 @@ impl Shell {
         &self,
         candidate_name: S,
     ) -> Option<PathBuf> {
-        for dir_str in self.env.get_str("PATH").unwrap_or_default().split(':') {
+        for dir_str in self.get_env_str("PATH").unwrap_or_default().split(':') {
             let candidate_path = Path::new(dir_str).join(candidate_name.as_ref());
             if candidate_path.executable() {
                 return Some(candidate_path);
@@ -1178,11 +1451,11 @@ impl Shell {
 
     /// Returns the shell's current home directory, if available.
     pub(crate) fn get_home_dir(&self) -> Option<PathBuf> {
-        Self::get_home_dir_with_env(&self.env)
+        Self::get_home_dir_with_env(&self.env, self)
     }
 
-    fn get_home_dir_with_env(env: &ShellEnvironment) -> Option<PathBuf> {
-        if let Some(home) = env.get_str("HOME") {
+    fn get_home_dir_with_env(env: &ShellEnvironment, shell: &Shell) -> Option<PathBuf> {
+        if let Some(home) = env.get_str("HOME", shell) {
             Some(PathBuf::from(home.to_string()))
         } else {
             // HOME isn't set, so let's sort it out ourselves.
@@ -1208,10 +1481,9 @@ impl Shell {
     ///
     /// * `command` - The command to trace.
     pub(crate) fn trace_command<S: AsRef<str>>(&self, command: S) -> Result<(), std::io::Error> {
-        // TODO: get prefix from PS4
-        const DEFAULT_PREFIX: &str = "+ ";
+        let ps4 = self.get_env_str("PS4").unwrap_or_else(|| "".into());
 
-        let mut prefix = DEFAULT_PREFIX.to_owned();
+        let mut prefix = ps4.to_string();
 
         let additional_depth = self.script_call_stack.len() + self.depth;
         if let Some(c) = prefix.chars().next() {
@@ -1286,3 +1558,17 @@ fn parse_string_impl(
 fn repeated_char_str(c: char, count: usize) -> String {
     (0..count).map(|_| c).collect()
 }
+
+fn get_random_value(_shell: &Shell) -> ShellValue {
+    let mut rng = rand::thread_rng();
+    let num = rng.gen_range(0..32768);
+    let str = num.to_string();
+    str.into()
+}
+
+fn get_srandom_value(_shell: &Shell) -> ShellValue {
+    let mut rng = rand::thread_rng();
+    let num: u32 = rng.gen();
+    let str = num.to_string();
+    str.into()
+}
diff --git a/brush-core/src/sys/stubs/users.rs b/brush-core/src/sys/stubs/users.rs
index b885d76..cbd92e3 100644
--- a/brush-core/src/sys/stubs/users.rs
+++ b/brush-core/src/sys/stubs/users.rs
@@ -25,6 +25,10 @@ pub(crate) fn get_current_username() -> Result<String, error::Error> {
     error::unimp("get current username")
 }
 
+pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
+    Ok(vec![])
+}
+
 pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
     Ok(vec![])
 }
diff --git a/brush-core/src/sys/unix/users.rs b/brush-core/src/sys/unix/users.rs
index 4bf7144..61b2600 100644
--- a/brush-core/src/sys/unix/users.rs
+++ b/brush-core/src/sys/unix/users.rs
@@ -40,6 +40,13 @@ pub(crate) fn get_current_username() -> Result<String, error::Error> {
     Ok(username.to_string_lossy().to_string())
 }
 
+pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
+    let username = uzers::get_current_username().ok_or_else(|| error::Error::NoCurrentUser)?;
+    let gid = uzers::get_current_gid();
+    let groups = uzers::get_user_groups(&username, gid).unwrap_or_default();
+    Ok(groups.into_iter().map(|g| g.gid()).collect())
+}
+
 #[allow(clippy::unnecessary_wraps)]
 pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
     // TODO: uzers::all_users() is available but unsafe
diff --git a/brush-core/src/sys/windows/users.rs b/brush-core/src/sys/windows/users.rs
index 55dd8b4..84b5dcc 100644
--- a/brush-core/src/sys/windows/users.rs
+++ b/brush-core/src/sys/windows/users.rs
@@ -31,6 +31,11 @@ pub(crate) fn get_current_username() -> Result<String, error::Error> {
     Ok(username)
 }
 
+pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
+    // TODO: implement some version of this for Windows
+    Ok(vec![])
+}
+
 #[allow(clippy::unnecessary_wraps)]
 pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
     // TODO: implement some version of this for Windows
diff --git a/brush-core/src/variables.rs b/brush-core/src/variables.rs
index 3f4df71..8bd3e72 100644
--- a/brush-core/src/variables.rs
+++ b/brush-core/src/variables.rs
@@ -1,8 +1,8 @@
-use rand::Rng;
 use std::borrow::Cow;
 use std::collections::BTreeMap;
 use std::fmt::{Display, Write};
 
+use crate::shell::Shell;
 use crate::{error, escape};
 
 /// A shell variable.
@@ -76,13 +76,15 @@ impl ShellVariable {
     }
 
     /// Marks the variable as exported to child processes.
-    pub fn export(&mut self) {
+    pub fn export(&mut self) -> &mut Self {
         self.exported = true;
+        self
     }
 
     /// Marks the variable as not exported to child processes.
-    pub fn unexport(&mut self) {
+    pub fn unexport(&mut self) -> &mut Self {
         self.exported = false;
+        self
     }
 
     /// Returns whether or not the variable is read-only.
@@ -91,18 +93,19 @@ impl ShellVariable {
     }
 
     /// Marks the variable as read-only.
-    pub fn set_readonly(&mut self) {
+    pub fn set_readonly(&mut self) -> &mut Self {
         self.readonly = true;
+        self
     }
 
     /// Marks the variable as not read-only.
-    pub fn unset_readonly(&mut self) -> Result<(), error::Error> {
+    pub fn unset_readonly(&mut self) -> Result<&mut Self, error::Error> {
         if self.readonly {
             return Err(error::Error::ReadonlyVariable);
         }
 
         self.readonly = false;
-        Ok(())
+        Ok(self)
     }
 
     /// Returns whether or not the variable is traced.
@@ -111,13 +114,15 @@ impl ShellVariable {
     }
 
     /// Marks the variable as traced.
-    pub fn enable_trace(&mut self) {
+    pub fn enable_trace(&mut self) -> &mut Self {
         self.trace = true;
+        self
     }
 
     /// Marks the variable as not traced.
-    pub fn disable_trace(&mut self) {
+    pub fn disable_trace(&mut self) -> &mut Self {
         self.trace = false;
+        self
     }
 
     /// Returns whether or not the variable should be enumerated in the shell's environment.
@@ -126,8 +131,9 @@ impl ShellVariable {
     }
 
     /// Marks the variable as not enumerable in the shell's environment.
-    pub fn hide_from_enumeration(&mut self) {
+    pub fn hide_from_enumeration(&mut self) -> &mut Self {
         self.enumerable = false;
+        self
     }
 
     /// Return the update transform associated with the variable.
@@ -146,13 +152,15 @@ impl ShellVariable {
     }
 
     /// Marks the variable as being treated as an integer.
-    pub fn treat_as_integer(&mut self) {
+    pub fn treat_as_integer(&mut self) -> &mut Self {
         self.treat_as_integer = true;
+        self
     }
 
     /// Marks the variable as not being treated as an integer.
-    pub fn unset_treat_as_integer(&mut self) {
+    pub fn unset_treat_as_integer(&mut self) -> &mut Self {
         self.treat_as_integer = false;
+        self
     }
 
     /// Returns whether or not the variable should be treated as a name reference.
@@ -161,13 +169,15 @@ impl ShellVariable {
     }
 
     /// Marks the variable as being treated as a name reference.
-    pub fn treat_as_nameref(&mut self) {
+    pub fn treat_as_nameref(&mut self) -> &mut Self {
         self.treat_as_nameref = true;
+        self
     }
 
     /// Marks the variable as not being treated as a name reference.
-    pub fn unset_treat_as_nameref(&mut self) {
+    pub fn unset_treat_as_nameref(&mut self) -> &mut Self {
         self.treat_as_nameref = false;
+        self
     }
 
     /// Converts the variable to an indexed array.
@@ -179,7 +189,10 @@ impl ShellVariable {
             }
             _ => {
                 let mut new_values = BTreeMap::new();
-                new_values.insert(0, self.value.to_cow_string().to_string());
+                new_values.insert(
+                    0,
+                    self.value.to_cow_str_without_dynamic_support().to_string(),
+                );
                 self.value = ShellValue::IndexedArray(new_values);
                 Ok(())
             }
@@ -195,7 +208,10 @@ impl ShellVariable {
             }
             _ => {
                 let mut new_values: BTreeMap<String, String> = BTreeMap::new();
-                new_values.insert(String::from("0"), self.value.to_cow_string().to_string());
+                new_values.insert(
+                    String::from("0"),
+                    self.value.to_cow_str_without_dynamic_support().to_string(),
+                );
                 self.value = ShellValue::AssociativeArray(new_values);
                 Ok(())
             }
@@ -208,6 +224,7 @@ impl ShellVariable {
     ///
     /// * `value` - The value to assign to the variable.
     /// * `append` - Whether or not to append the value to the preexisting value.
+    #[allow(clippy::too_many_lines)]
     pub fn assign(&mut self, value: ShellValueLiteral, append: bool) -> Result<(), error::Error> {
         if self.is_readonly() {
             return Err(error::Error::ReadonlyVariable);
@@ -282,7 +299,8 @@ impl ShellVariable {
                     }
                 },
                 ShellValue::Unset(_) => unreachable!("covered in conversion above"),
-                ShellValue::Random => Ok(()),
+                // TODO(dynamic): implement appending to dynamic vars
+                ShellValue::Dynamic { .. } => Ok(()),
             }
         } else {
             match (&self.value, value) {
@@ -306,7 +324,7 @@ impl ShellVariable {
                         ShellValueUnsetType::IndexedArray | ShellValueUnsetType::Untyped,
                     )
                     | ShellValue::String(_)
-                    | ShellValue::Random,
+                    | ShellValue::Dynamic { .. },
                     ShellValueLiteral::Array(literal_values),
                 ) => {
                     self.value = ShellValue::indexed_array_from_literals(literal_values)?;
@@ -324,8 +342,9 @@ impl ShellVariable {
                     Ok(())
                 }
 
-                // Drop other updates to random values.
-                (ShellValue::Random, _) => Ok(()),
+                // Handle updates to dynamic values; for now we just drop them.
+                // TODO(dynamic): Allow updates to dynamic values
+                (ShellValue::Dynamic { .. }, _) => Ok(()),
 
                 // Assign a scalar value to a scalar or unset (and untyped) variable.
                 (ShellValue::String(_) | ShellValue::Unset(_), ShellValueLiteral::Scalar(s)) => {
@@ -455,27 +474,38 @@ impl ShellVariable {
                     Ok(false)
                 }
             },
-            ShellValue::String(_) | ShellValue::Random => Err(error::Error::NotArray),
+            ShellValue::String(_) => Err(error::Error::NotArray),
             ShellValue::AssociativeArray(values) => Ok(values.remove(index).is_some()),
             ShellValue::IndexedArray(values) => {
                 let key = index.parse::<u64>().unwrap_or(0);
                 Ok(values.remove(&key).is_some())
             }
+            ShellValue::Dynamic { .. } => Ok(false),
+        }
+    }
+
+    fn resolve_value(&self, shell: &Shell) -> ShellValue {
+        // N.B. We do *not* specially handle a dynamic value that resolves to a dynamic value.
+        match &self.value {
+            ShellValue::Dynamic { getter, .. } => getter(shell),
+            _ => self.value.clone(),
         }
     }
 
     /// Returns the canonical attribute flag string for this variable.
-    pub fn get_attribute_flags(&self) -> String {
+    pub fn get_attribute_flags(&self, shell: &Shell) -> String {
+        let value = self.resolve_value(shell);
+
         let mut result = String::new();
 
         if matches!(
-            self.value(),
+            value,
             ShellValue::IndexedArray(_) | ShellValue::Unset(ShellValueUnsetType::IndexedArray)
         ) {
             result.push('a');
         }
         if matches!(
-            self.value(),
+            value,
             ShellValue::AssociativeArray(_)
                 | ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
         ) {
@@ -507,6 +537,9 @@ impl ShellVariable {
     }
 }
 
+type DynamicValueGetter = fn(&Shell) -> ShellValue;
+type DynamicValueSetter = fn(&Shell) -> ();
+
 /// A shell value.
 #[derive(Clone, Debug)]
 pub enum ShellValue {
@@ -518,8 +551,13 @@ pub enum ShellValue {
     AssociativeArray(BTreeMap<String, String>),
     /// An indexed array.
     IndexedArray(BTreeMap<u64, String>),
-    /// A special value that yields a different random number each time its read.
-    Random,
+    /// A value that is dynamically computed.
+    Dynamic {
+        /// Function that can query the value.
+        getter: DynamicValueGetter,
+        /// Function that receives value update requests.
+        setter: DynamicValueSetter,
+    },
 }
 
 /// The type of an unset shell value.
@@ -622,12 +660,29 @@ impl ShellValue {
         )
     }
 
-    /// Returns a new indexed array value constructed from the given slice of strings.
+    /// Returns a new indexed array value constructed from the given slice of owned strings.
     ///
     /// # Arguments
     ///
     /// * `values` - The slice of strings to construct the indexed array from.
-    pub fn indexed_array_from_slice(values: &[&str]) -> Self {
+    pub fn indexed_array_from_strings<S>(values: S) -> Self
+    where
+        S: IntoIterator<Item = String>,
+    {
+        let mut owned_values = BTreeMap::new();
+        for (i, value) in values.into_iter().enumerate() {
+            owned_values.insert(i as u64, value);
+        }
+
+        ShellValue::IndexedArray(owned_values)
+    }
+
+    /// Returns a new indexed array value constructed from the given slice of unowned strings.
+    ///
+    /// # Arguments
+    ///
+    /// * `values` - The slice of strings to construct the indexed array from.
+    pub fn indexed_array_from_strs(values: &[&str]) -> Self {
         let mut owned_values = BTreeMap::new();
         for (i, value) in values.iter().enumerate() {
             owned_values.insert(i as u64, (*value).to_string());
@@ -716,7 +771,7 @@ impl ShellValue {
     /// # Arguments
     ///
     /// * `style` - The style to use for formatting the value.
-    pub fn format(&self, style: FormatStyle) -> Result<Cow<'_, str>, error::Error> {
+    pub fn format(&self, style: FormatStyle, shell: &Shell) -> Result<Cow<'_, str>, error::Error> {
         match self {
             ShellValue::Unset(_) => Ok("".into()),
             ShellValue::String(s) => {
@@ -757,7 +812,11 @@ impl ShellValue {
                 result.push(')');
                 Ok(result.into())
             }
-            ShellValue::Random => Ok(std::format!("\"{}\"", get_random_str()).into()),
+            ShellValue::Dynamic { getter, .. } => {
+                let dynamic_value = getter(shell);
+                let result = dynamic_value.format(style, shell)?.to_string();
+                Ok(result.into())
+            }
         }
     }
 
@@ -767,7 +826,7 @@ impl ShellValue {
     ///
     /// * `index` - The index at which to retrieve the value.
     #[allow(clippy::unnecessary_wraps)]
-    pub fn get_at(&self, index: &str) -> Result<Option<Cow<'_, str>>, error::Error> {
+    pub fn get_at(&self, index: &str, shell: &Shell) -> Result<Option<Cow<'_, str>>, error::Error> {
         match self {
             ShellValue::Unset(_) => Ok(None),
             ShellValue::String(s) => {
@@ -784,33 +843,49 @@ impl ShellValue {
                 let key = index.parse::<u64>().unwrap_or(0);
                 Ok(values.get(&key).map(|s| Cow::Borrowed(s.as_str())))
             }
-            ShellValue::Random => Ok(Some(Cow::Owned(get_random_str()))),
+            ShellValue::Dynamic { getter, .. } => {
+                let dynamic_value = getter(shell);
+                let result = dynamic_value.get_at(index, shell)?;
+                Ok(result.map(|s| s.to_string().into()))
+            }
         }
     }
 
     /// Returns the keys of the elements in this variable.
-    pub fn get_element_keys(&self) -> Vec<String> {
+    pub fn get_element_keys(&self, shell: &Shell) -> Vec<String> {
         match self {
             ShellValue::Unset(_) => vec![],
-            ShellValue::String(_) | ShellValue::Random => vec!["0".to_owned()],
+            ShellValue::String(_) => vec!["0".to_owned()],
             ShellValue::AssociativeArray(array) => array.keys().map(|k| k.to_owned()).collect(),
             ShellValue::IndexedArray(array) => array.keys().map(|k| k.to_string()).collect(),
+            ShellValue::Dynamic { getter, .. } => getter(shell).get_element_keys(shell),
         }
     }
 
     /// Returns the values of the elements in this variable.
-    pub fn get_element_values(&self) -> Vec<String> {
+    pub fn get_element_values(&self, shell: &Shell) -> Vec<String> {
         match self {
             ShellValue::Unset(_) => vec![],
             ShellValue::String(s) => vec![s.to_owned()],
             ShellValue::AssociativeArray(array) => array.values().map(|v| v.to_owned()).collect(),
             ShellValue::IndexedArray(array) => array.values().map(|v| v.to_owned()).collect(),
-            ShellValue::Random => vec![get_random_str()],
+            ShellValue::Dynamic { getter, .. } => getter(shell).get_element_values(shell),
         }
     }
 
     /// Converts this value to a string.
-    pub fn to_cow_string(&self) -> Cow<'_, str> {
+    pub fn to_cow_str(&self, shell: &Shell) -> Cow<'_, str> {
+        match self {
+            ShellValue::Dynamic { getter, .. } => {
+                let dynamic_value = getter(shell);
+                let result = dynamic_value.to_cow_str(shell).to_string();
+                result.into()
+            }
+            _ => self.to_cow_str_without_dynamic_support(),
+        }
+    }
+
+    fn to_cow_str_without_dynamic_support(&self) -> Cow<'_, str> {
         match self {
             ShellValue::Unset(_) => Cow::Borrowed(""),
             ShellValue::String(s) => Cow::Borrowed(s.as_str()),
@@ -820,7 +895,7 @@ impl ShellValue {
             ShellValue::IndexedArray(values) => values
                 .get(&0)
                 .map_or_else(|| Cow::Borrowed(""), |s| Cow::Borrowed(s.as_str())),
-            ShellValue::Random => Cow::Owned(get_random_str()),
+            ShellValue::Dynamic { .. } => "".into(),
         }
     }
 
@@ -829,22 +904,24 @@ impl ShellValue {
     /// # Arguments
     ///
     /// * `index` - The index at which to retrieve the value, if indexing is to be performed.
-    pub fn to_assignable_str(&self, index: Option<&str>) -> String {
+    pub fn to_assignable_str(&self, index: Option<&str>, shell: &Shell) -> String {
         match self {
             ShellValue::Unset(_) => String::new(),
             ShellValue::String(s) => quote_str_for_assignment(s.as_str()),
             ShellValue::AssociativeArray(_) | ShellValue::IndexedArray(_) => {
                 if let Some(index) = index {
-                    if let Ok(Some(value)) = self.get_at(index) {
+                    if let Ok(Some(value)) = self.get_at(index, shell) {
                         quote_str_for_assignment(value.as_ref())
                     } else {
                         String::new()
                     }
                 } else {
-                    self.format(FormatStyle::DeclarePrint).unwrap().into_owned()
+                    self.format(FormatStyle::DeclarePrint, shell)
+                        .unwrap()
+                        .into_owned()
                 }
             }
-            ShellValue::Random => quote_str_for_assignment(get_random_str().as_str()),
+            ShellValue::Dynamic { getter, .. } => getter(shell).to_assignable_str(index, shell),
         }
     }
 }
@@ -867,10 +944,16 @@ impl From<String> for ShellValue {
     }
 }
 
-fn get_random_str() -> String {
-    let mut rng = rand::thread_rng();
-    let value = rng.gen_range(0..32768);
-    value.to_string()
+impl From<Vec<String>> for ShellValue {
+    fn from(values: Vec<String>) -> Self {
+        ShellValue::indexed_array_from_strings(values)
+    }
+}
+
+impl From<Vec<&str>> for ShellValue {
+    fn from(values: Vec<&str>) -> Self {
+        ShellValue::indexed_array_from_strs(values.as_slice())
+    }
 }
 
 pub(crate) fn quote_str_for_assignment(s: &str) -> String {
diff --git a/brush-interactive/src/interactive_shell.rs b/brush-interactive/src/interactive_shell.rs
index 7c38c75..4bb8568 100644
--- a/brush-interactive/src/interactive_shell.rs
+++ b/brush-interactive/src/interactive_shell.rs
@@ -118,15 +118,18 @@ pub trait InteractiveShell {
             shell_mut.check_for_completed_jobs()?;
 
             // If there's a variable called PROMPT_COMMAND, then run it first.
-            if let Some(prompt_cmd) = shell_mut.env.get_str("PROMPT_COMMAND") {
+            if let Some(prompt_cmd) = shell_mut.get_env_str("PROMPT_COMMAND") {
                 // Save (and later restore) the last exit status.
                 let prev_last_result = shell_mut.last_exit_status;
+                let prev_last_pipeline_statuses = shell_mut.last_pipeline_statuses.clone();
 
                 let params = shell_mut.default_exec_params();
 
                 shell_mut
                     .run_string(prompt_cmd.into_owned(), &params)
                     .await?;
+
+                shell_mut.last_pipeline_statuses = prev_last_pipeline_statuses;
                 shell_mut.last_exit_status = prev_last_result;
             }
 


Test patch:
diff --git a/brush-shell/tests/cases/basic.yaml b/brush-shell/tests/cases/basic.yaml
index dfdcd8f..522e167 100644
--- a/brush-shell/tests/cases/basic.yaml
+++ b/brush-shell/tests/cases/basic.yaml
@@ -21,13 +21,5 @@ cases:
           exit 22
     args: ["./script.sh"]
 
-  - name: "Basic defaulted PATH var"
-    stdin: |
-      if [[ -z "$PATH" ]]; then
-        echo "PATH is not set or is empty"
-      else
-        echo "PATH is set and non-empty"
-      fi
-
   - name: "Ensure ~ is resolvable"
     stdin: "test ~"
diff --git a/brush-shell/tests/cases/status.yaml b/brush-shell/tests/cases/status.yaml
new file mode 100644
index 0000000..31a13e7
--- /dev/null
+++ b/brush-shell/tests/cases/status.yaml
@@ -0,0 +1,35 @@
+name: "Status tests"
+cases:
+  - name: "Basic status"
+    stdin: |
+      cat /non/existent >/dev/null
+      echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+  - name: "Parse error status"
+    known_failure: true # Needs investigation
+    ignore_stderr: true
+    stdin: |
+      # Generate parse error
+      for f done
+      echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+  - name: "Pipeline status"
+    stdin: |
+      /non/existent/program 2>/dev/null | cat
+      echo "Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+  - name: "Command substitution status"
+    stdin: |
+      x=$(echo hi | wc -l)
+      echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+      x=$(cat /non/existent 2>/dev/null)
+      echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+  - name: "Subshell status"
+    stdin: |
+      (echo hi | wc -l)
+      echo "[1] Status: $?; pipe status: ${PIPESTATUS[@]}"
+
+      (cat /non/existent 2>/dev/null)
+      echo "[2] Status: $?; pipe status: ${PIPESTATUS[@]}"
diff --git a/brush-shell/tests/cases/well_known_vars.yaml b/brush-shell/tests/cases/well_known_vars.yaml
new file mode 100644
index 0000000..1bbd62f
--- /dev/null
+++ b/brush-shell/tests/cases/well_known_vars.yaml
@@ -0,0 +1,107 @@
+name: "Well-known variable tests"
+cases:
+  - name: "Basic defaulted PATH var"
+    stdin: |
+      if [[ -z "$PATH" ]]; then
+        echo "PATH is not set or is empty"
+      else
+        echo "PATH is set and non-empty"
+      fi
+
+  - name: "Basic variables"
+    known_failure: true # Not fully implemented
+    stdin: |
+      declare -p > ./vars.txt
+      # Filter out variables set by test infrastructure.
+      cat vars.txt | grep '^declare ' | grep -v LLVM | sed -e 's/=.*//'
+      rm vars.txt
+
+  - name: "Static well-known variables"
+    stdin: |
+      echo "EUID: $EUID"
+      echo "UID: $UID"
+
+      [[ $BASHPID == $$ ]]
+      echo 'BASHPID == \$$: ' $?
+
+      [[ $BASH_ARGV0 == $0 ]]
+      echo 'BASH_ARGV0 == $0: ' $?
+
+      [[ $MACHTYPE == ${BASH_VERSINFO[5]} ]]
+      echo 'MACHTYPE == ${BASH_VERSINFO[5]: ' $?
+
+  - name: "BASH_ALIASES"
+    stdin: |
+      echo "Initial BASH_ALIASES: " ${BASH_ALIASES[@]}
+      alias x=y
+      echo "Updated BASH_ALIASES: " ${BASH_ALIASES[@]}
+      unalias x
+      echo "Final BASH_ALIASES: " ${BASH_ALIASES[@]}
+
+  - name: "BASHOPTS"
+    known_failure: true # Need to normalize which options are enabled in oracle
+    stdin: |
+      # Workaround for brush forcing on extglob
+      shopt -u extglob
+
+      echo "BASHOPTS: $BASHOPTS"
+
+  - name: "BASH_SUBSHELL"
+    stdin: |
+      echo "Initial BASH_SUBSHELL: $BASH_SUBSHELL"
+      (echo "Subshell BASH_SUBSHELL: $BASH_SUBSHELL")
+      ( 
+        ( echo "Nested subshell BASH_SUBSHELL: $BASH_SUBSHELL"  )
+      )
+
+      echo $(echo "Command substitution BASH_SUBSHELL: $BASH_SUBSHELL")
+
+  - name: "BASH_VERSINFO"
+    stdin: |
+      shopt -s extglob
+
+      echo "len(BASH_VERSINFO): " ${#BASH_VERSINFO[@]}
+
+      # Only inspect the first 4 elements.
+      for ((i = 0; i < 4; i++)); do
+        cleaned=${BASH_VERSINFO[i]//+([0-9])/d}
+        echo "Cleaned BASH_VERSINFO[$i]: $cleaned"
+      done
+
+  - name: "BASH_VERSION"
+    stdin: |
+      shopt -s extglob
+
+      # Replace specific numbers with placeholder text.
+      cleaned=${BASH_VERSION//+([0-9])/d}
+      echo "Cleaned BASH_VERSION: $cleaned"
+
+  - name: "EPOCHSECONDS"
+    stdin: |
+      external=$(date +%s)
+
+      es1=${EPOCHSECONDS}
+      [[ $es1 =~ ^[0-9]+$ ]] && echo "EPOCHSECONDS is a number"
+
+      es2=${EPOCHSECONDS}
+      [[ $es2 =~ ^[0-9]+$ ]] && echo "EPOCHSECONDS is still a number"
+
+      (( es1 >= external )) && echo "Time is moving forward"
+      (( es1 >= es2 )) && echo "Time is moving forward within the shell"
+
+  - name: "GROUPS"
+    skip: true # TODO: macOS failure needs investigation (passes elsewhere)
+    stdin: |
+      (for group in ${GROUPS[@]}; do
+        echo $group
+      done) | sort
+
+  - name: "SHELLOPTS"
+    stdin: |
+      echo "SHELLOPTS: $SHELLOPTS"
+
+  - name: "SHLVL"
+    stdin: |
+      echo "SHLVL: $SHLVL"
+      bash -c 'echo "bash SHLVL: $SHLVL"'
+      $0 -c 'echo "nested SHLVL: $SHLVL"'