use std::collections::BTreeMap; use std::path::PathBuf; use rand::RngExt as _; use crate::shell::ShellState; use crate::{Shell, ShellValue, ShellVariable, error, extensions, sys, variables}; const BASH_MAJOR: u32 = 5; const BASH_MINOR: u32 = 2; const BASH_PATCH: u32 = 37; const BASH_BUILD: u32 = 1; const BASH_RELEASE: &str = "release"; const BASH_MACHINE: &str = "unknown"; const DEFAULT_LINENO: usize = 1; /// Inherit environment variables from the host process into the shell's environment. /// /// # Arguments /// /// * `shell` - The shell instance to inherit environment variables into. pub(crate) fn inherit_env_vars( shell: &mut Shell, ) -> Result<(), error::Error> { for (k, v) in sys::env::get_host_env_vars() { // See if it's a function exported by an ancestor process. if let Some(func_name) = k.strip_prefix("BASH_FUNC_") && let Some(func_name) = func_name.strip_suffix("%%") { // Intentionally best-effort; don't fail out of the shell if we can't // parse an incoming function. if shell.define_func_from_str(func_name, v.as_str()).is_ok() && let Some(func) = shell.func_mut(func_name) { func.export(); } continue; } // Special case OLDPWD for bash compatibility. if k == "OLDPWD" { continue; } let mut var = ShellVariable::new(ShellValue::String(v)); var.export(); shell.env_mut().set_global(k, var)?; } Ok(()) } #[expect(clippy::too_many_lines)] pub(crate) fn init_well_known_vars( shell: &mut Shell, ) -> Result<(), error::Error> { let shell_version = shell.version().map(ToString::to_string); shell.env_mut().set_global( "BRUSH_VERSION", ShellVariable::new(shell_version.unwrap_or_default()), )?; // BASH if let Some(shell_name) = shell.current_shell_name().map(|s| s.to_string()) { shell .env_mut() .set_global("BASH", ShellVariable::new(shell_name.clone()))?; // Initialize $_ to the shell name ($0). shell.update_last_arg_variable(Some(shell_name)); } // BASHOPTS let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic { getter: |shell| shell.options().shopt_optstr().into(), setter: |_| (), }); bashopts_var.set_readonly(); shell.env_mut().set_global("BASHOPTS", bashopts_var)?; // BASHPID #[cfg(not(target_family = "wasm"))] { let mut bashpid_var = ShellVariable::new(ShellValue::String(std::process::id().to_string())); bashpid_var.treat_as_integer(); shell.env_mut().set_global("BASHPID", bashpid_var)?; } // BASH_ALIASES shell.env_mut().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::>(), ); ShellValue::associative_array_from_literals(values) .unwrap_or_else(|_error| ShellValue::AssociativeArray(BTreeMap::new())) }, setter: |_| (), }), )?; // BASH_ARGC shell.env_mut().set_global( "BASH_ARGC", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_bash_argc_value(shell), setter: |_| (), }), )?; // BASH_ARGV shell.env_mut().set_global( "BASH_ARGV", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_bash_argv_value(shell), setter: |_| (), }), )?; // BASH_ARGV0 shell.env_mut().set_global( "BASH_ARGV0", ShellVariable::new(ShellValue::Dynamic { getter: |shell| { let argv0 = shell.current_shell_name().unwrap_or_default(); argv0.to_string().into() }, // TODO(vars): implement updating BASH_ARGV0 setter: |_| (), }), )?; // TODO(vars): implement mutation of BASH_CMDS shell.env_mut().set_global( "BASH_CMDS", ShellVariable::new(ShellValue::Dynamic { getter: |shell| { shell .program_location_cache() .to_value() .unwrap_or_else(|_error| ShellValue::AssociativeArray(BTreeMap::new())) }, setter: |_| (), }), )?; // TODO(vars): implement BASH_COMMAND // TODO(vars): implement BASH_EXECUTION_STRING // BASH_LINENO shell.env_mut().set_global( "BASH_LINENO", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_bash_lineno_value(shell), setter: |_| (), }), )?; // BASH_SOURCE shell.env_mut().set_global( "BASH_SOURCE", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_bash_source_value(shell), setter: |_| (), }), )?; // BASH_SUBSHELL shell.env_mut().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(); shell .env_mut() .set_global("BASH_VERSINFO", bash_versinfo_var)?; // BASH_VERSION // This is the Bash interface version. See BRUSH_VERSION for its implementation version. shell.env_mut().set_global( "BASH_VERSION", ShellVariable::new(std::format!( "{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}" )), )?; // COMP_WORDBREAKS let mut default_comp_wordbreaks = String::from(" \t\n\"\'><=;|&(:"); if shell.options().enable_hostname_completion { default_comp_wordbreaks.push('@'); } shell.env_mut().set_global( "COMP_WORDBREAKS", ShellVariable::new(default_comp_wordbreaks), )?; // DIRSTACK shell.env_mut().set_global( "DIRSTACK", ShellVariable::new(ShellValue::Dynamic { getter: |shell| { shell .directory_stack() .iter() .map(|p| p.to_string_lossy().to_string()) .collect::>() .into() }, setter: |_| (), }), )?; // EPOCHREALTIME shell.env_mut().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 shell.env_mut().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 if let Ok(euid) = sys::users::get_effective_uid() { let mut euid_var = ShellVariable::new(ShellValue::String(format!("{euid}"))); euid_var.treat_as_integer().set_readonly(); shell.env_mut().set_global("EUID", euid_var)?; } // FUNCNAME shell.env_mut().set_global( "FUNCNAME", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_funcname_value(shell), setter: |_| (), }), )?; // 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. shell.env_mut().set_global( "GROUPS", ShellVariable::new(ShellValue::Dynamic { getter: |_shell| { let groups = get_current_user_gids(); ShellValue::indexed_array_from_strings( groups.into_iter().map(|gid| gid.to_string()), ) }, setter: |_| (), }), )?; // HISTCMD let mut histcmd_var = ShellVariable::new(ShellValue::Dynamic { getter: |shell| { shell .history() .map_or_else(|| "0".into(), |h| h.count().to_string().into()) }, setter: |_| (), }); histcmd_var.treat_as_integer(); shell.env_mut().set_global("HISTCMD", histcmd_var)?; // HISTFILE (if not already set) if !shell.env().is_set("HISTFILE") && let Some(home_dir) = shell.home_dir() { let histfile = home_dir.join(".brush_history"); shell.env_mut().set_global( "HISTFILE", ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())), )?; } // HOSTNAME shell.env_mut().set_global( "HOSTNAME", ShellVariable::new( sys::network::get_hostname() .unwrap_or_default() .to_string_lossy() .to_string(), ), )?; // HOSTTYPE shell.env_mut().set_global( "HOSTTYPE", ShellVariable::new(std::env::consts::ARCH.to_string()), )?; // IFS shell .env_mut() .set_global("IFS", ShellVariable::new(" \t\n"))?; // LINENO shell.env_mut().set_global( "LINENO", ShellVariable::new(ShellValue::Dynamic { getter: |shell| get_lineno(shell).to_string().into(), setter: |_| (), }), )?; // MACHTYPE shell .env_mut() .set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE))?; // OLDPWD (initialization) if !shell.env().is_set("OLDPWD") { let mut oldpwd_var = ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped)); oldpwd_var.export(); shell.env_mut().set_global("OLDPWD", oldpwd_var)?; } // OPTERR shell .env_mut() .set_global("OPTERR", ShellVariable::new("1"))?; // OPTIND let mut optind_var = ShellVariable::new("1"); optind_var.treat_as_integer(); shell.env_mut().set_global("OPTIND", optind_var)?; // OSTYPE // Match bash's conventional OSTYPE on each platform so that shell scripts // branching on `[[ $OSTYPE == darwin* ]]` / `linux-gnu*` etc. (Homebrew // shellenv, nvm, asdf, ...) take the expected path. Real bash includes a // kernel-version suffix on macOS/BSDs (e.g. `darwin24`); we omit the // suffix for now since the common patterns all use prefix matching. let os_type = match std::env::consts::OS { "linux" => "linux-gnu", "android" => "linux-android", "macos" | "ios" | "tvos" | "watchos" | "visionos" => "darwin", "freebsd" => "freebsd", "netbsd" => "netbsd", "openbsd" => "openbsd", "dragonfly" => "dragonfly", "solaris" | "illumos" => "solaris", "windows" => "windows", _ => "unknown", }; shell .env_mut() .set_global("OSTYPE", ShellVariable::new(os_type))?; // PATH (if not already set) if !shell.env().is_set("PATH") { let default_path_str = std::env::join_paths(sys::fs::get_default_executable_search_paths()) .unwrap_or_else(|_| PathBuf::from("").into()); shell .env_mut() .set_global("PATH", ShellVariable::new(default_path_str))?; } // PIPESTATUS // TODO(well-known-vars): Investigate what happens if this gets unset. // TODO(well-known-vars): Investigate if this needs to be saved/preserved across prompt display. shell.env_mut().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()); ppid_var.treat_as_integer().set_readonly(); shell.env_mut().set_global("PPID", ppid_var)?; } // RANDOM let mut random_var = ShellVariable::new(ShellValue::Dynamic { getter: get_random_value, setter: |_| (), }); random_var.treat_as_integer(); shell.env_mut().set_global("RANDOM", random_var)?; // SECONDS shell.env_mut().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 not already set) if !shell.env().is_set("SHELL") { // Per docs, this should be the user's default login shell -- not the current shell. if let Some(default_shell) = sys::users::get_current_user_default_shell() { shell.env_mut().set_global( "SHELL", ShellVariable::new(default_shell.to_string_lossy().to_string()), )?; } } // SHELLOPTS let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic { getter: |shell| shell.options().seto_optstr().into(), setter: |_| (), }); shellopts_var.set_readonly(); shell.env_mut().set_global("SHELLOPTS", shellopts_var)?; // SHLVL let input_shlvl = shell.env_str("SHLVL").unwrap_or_else(|| "0".into()); let updated_shlvl = input_shlvl.as_ref().parse::().unwrap_or(0) + 1; let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string()); shlvl_var.export(); shell.env_mut().set_global("SHLVL", shlvl_var)?; // SRANDOM let mut random_var = ShellVariable::new(ShellValue::Dynamic { getter: get_srandom_value, setter: |_| (), }); random_var.treat_as_integer(); shell.env_mut().set_global("SRANDOM", random_var)?; // PS1 / PS2 if shell.options().interactive { if !shell.env().is_set("PS1") { shell .env_mut() .set_global("PS1", ShellVariable::new(r"\s-\v\$ "))?; } if !shell.env().is_set("PS2") { shell .env_mut() .set_global("PS2", ShellVariable::new("> "))?; } } // PS4 if !shell.env().is_set("PS4") { shell .env_mut() .set_global("PS4", ShellVariable::new("+ "))?; } // // 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 = shell.working_dir().to_string_lossy().to_string(); let mut pwd_var = ShellVariable::new(pwd); pwd_var.export(); shell.env_mut().set_global("PWD", pwd_var)?; // UID if let Ok(uid) = sys::users::get_current_uid() { let mut uid_var = ShellVariable::new(ShellValue::String(format!("{uid}"))); uid_var.treat_as_integer().set_readonly(); shell.env_mut().set_global("UID", uid_var)?; } Ok(()) } /// Returns a list of the current user's group IDs, with the effective GID at the front. fn get_current_user_gids() -> Vec { let mut groups = sys::users::get_user_group_ids().unwrap_or_default(); // If the effective GID is present but not in the first position in the list, then move // it there. if let Ok(gid) = sys::users::get_effective_gid() { if let Some(index) = groups.iter().position(|&g| g == gid) { if index > 0 { // Move it to the front. groups.remove(index); groups.insert(0, gid); } } } groups } fn get_random_value(_shell: &dyn ShellState) -> ShellValue { let mut rng = rand::rng(); let num = rng.random_range(0..32768); let str = num.to_string(); str.into() } fn get_srandom_value(_shell: &dyn ShellState) -> ShellValue { let mut rng = rand::rng(); let num: u32 = rng.random(); let str = num.to_string(); str.into() } fn get_funcname_value(shell: &dyn ShellState) -> variables::ShellValue { let stack = shell.call_stack(); if stack.iter_function_calls().next().is_none() { ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray) } else { // When in a function, include both functions and sourced scripts in the stack stack .iter() .filter_map(|frame| match &frame.frame_type { crate::callstack::FrameType::Function(func) => Some(func.function_name.as_str()), crate::callstack::FrameType::Script(script) => { // Only include sourced scripts, not run scripts if matches!(script.call_type, crate::callstack::ScriptCallType::Source) { Some("source") } else { None } } crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval | crate::callstack::FrameType::CommandString | crate::callstack::FrameType::InteractiveSession => None, }) .collect::>() .into() } } fn get_bash_lineno_value(shell: &dyn ShellState) -> variables::ShellValue { let stack = shell.call_stack(); // BASH_LINENO[$i] contains the line number where FUNCNAME[$i] was called // This is extracted from the call_site of each frame if stack.iter_function_calls().next().is_none() { ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray) } else { stack .iter() .enumerate() .filter_map(|(frame_idx, frame)| match &frame.frame_type { crate::callstack::FrameType::Function(..) | crate::callstack::FrameType::Script(..) => { let caller_idx = frame_idx + 1; if caller_idx < stack.depth() { let caller_frame = &stack[caller_idx]; Some( caller_frame .current_line() .unwrap_or(DEFAULT_LINENO) .to_string(), ) } else { None } } crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval | crate::callstack::FrameType::CommandString | crate::callstack::FrameType::InteractiveSession => None, }) .collect::>() .into() } } fn get_bash_source_value(shell: &dyn ShellState) -> variables::ShellValue { let stack = shell.call_stack(); if stack.iter_function_calls().next().is_none() { let top_frame = stack.iter_script_calls().next(); top_frame .map_or_else(Vec::new, |frame| vec![frame.source_info.source.clone()]) .into() } else { // When in a function, include both functions and sourced scripts in the stack // This mirrors the FUNCNAME array structure stack .iter() .filter_map(|frame| match &frame.frame_type { crate::callstack::FrameType::Function(func) => { Some(func.function.source().source.clone()) } crate::callstack::FrameType::Script(script) => { // Only include sourced scripts (matching the "source" in FUNCNAME) if matches!(script.call_type, crate::callstack::ScriptCallType::Source) { Some(script.source_info.source.clone()) } else { None } } crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => { None } crate::callstack::FrameType::CommandString | crate::callstack::FrameType::InteractiveSession => None, }) .collect::>() .into() } } fn get_bash_argc_value(shell: &dyn ShellState) -> variables::ShellValue { if !shell.options().enable_debugger { return ShellValue::indexed_array_from_strs(&[]); } let stack = shell.call_stack(); stack .iter() .filter_map(|frame| match &frame.frame_type { crate::callstack::FrameType::Function(..) | crate::callstack::FrameType::Script(..) | crate::callstack::FrameType::CommandString | crate::callstack::FrameType::InteractiveSession => Some(frame.args.len().to_string()), crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => None, }) .collect::>() .into() } fn get_bash_argv_value(shell: &dyn ShellState) -> variables::ShellValue { if !shell.options().enable_debugger { return ShellValue::indexed_array_from_strs(&[]); } let stack = shell.call_stack(); let mut argv = Vec::new(); for frame in stack.iter() { let include = match &frame.frame_type { crate::callstack::FrameType::Function(..) | crate::callstack::FrameType::Script(..) | crate::callstack::FrameType::CommandString | crate::callstack::FrameType::InteractiveSession => true, crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => { false } }; if include { // Push args in reverse order per frame (last arg at lowest index = top of stack) for arg in frame.args.iter().rev() { argv.push(arg.clone()); } } } argv.into() } fn get_lineno(shell: &dyn ShellState) -> usize { shell .call_stack() .current_frame() .and_then(|frame| frame.current_line()) .unwrap_or(DEFAULT_LINENO) }