725 lines
23 KiB
Rust
725 lines
23 KiB
Rust
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<impl extensions::ShellExtensions>,
|
||
|
|
) -> 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<impl extensions::ShellExtensions>,
|
||
|
|
) -> 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::<Vec<_>>(),
|
||
|
|
);
|
||
|
|
|
||
|
|
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::<Vec<_>>()
|
||
|
|
.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::<u32>().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<u32> {
|
||
|
|
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::<Vec<_>>()
|
||
|
|
.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::<Vec<_>>()
|
||
|
|
.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::<Vec<_>>()
|
||
|
|
.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::<Vec<_>>()
|
||
|
|
.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)
|
||
|
|
}
|