use crate::extensions::emulation_startup::{personality, Personality};
use crate::ported::builtin::{bin_enable, bin_trap, traps_table, BUILTINS_DISABLED};
use crate::ported::exec::hashcmd;
use crate::ported::hashtable::{cmdnamtab_lock, emptycmdnamtable};
use crate::ported::hashtable_h::BIN_DISABLE;
use crate::ported::params::getsparam;
use crate::ported::utils::zwarnnam;
use crate::ported::zsh_h::{cmdnam, hashnode, options, HASHED, MAX_OPS, OPT_ISSET};
use crate::ported::builtin::LASTVAL;
use std::sync::atomic::Ordering;
pub fn times_field(ticks: i64, clktck: i64) -> String {
let clktck = if clktck <= 0 { 100 } else { clktck };
let mins = ticks / (60 * clktck);
let p = personality();
let secs = if p == Personality::Zsh {
(ticks / clktck) % clktck
} else {
(ticks / clktck) % 60
};
match p {
Personality::Bash => {
let ms = (ticks * 1000 / clktck) % 1000;
format!("{mins}m{secs}.{ms:03}s")
}
Personality::Dash | Personality::Sh => {
let us = (ticks * 1_000_000 / clktck) % 1_000_000;
format!("{mins}m{secs}.{us:06}s")
}
Personality::Ksh93 => {
let ms = (ticks * 1000 / clktck) % 1000;
format!("{mins}m{secs:02}.{ms:03}s")
}
Personality::Mksh | Personality::Pdksh => {
let cs = (ticks * 100 / clktck) % 100;
format!("{mins}m{secs:02}.{cs:02}s")
}
_ => {
let cs = (ticks * 100 / clktck) % 100;
format!("{mins}m{secs}.{cs:02}s")
}
}
}
pub fn kill_l_signals() -> Vec<(i32, String)> {
use crate::ported::signals_h::{sigs_name, SIGCOUNT};
#[allow(unused_mut)]
let mut sigs: Vec<(i32, String)> = (1..=SIGCOUNT)
.filter_map(|s| sigs_name(s).map(|n| (s, n.to_string())))
.collect();
#[cfg(target_os = "linux")]
for s in libc::SIGRTMIN()..=libc::SIGRTMAX() {
sigs.push((s, crate::ported::signals::rtsigname(s))); }
sigs
}
pub fn kill_list(sigs: &[(i32, String)]) -> Option<String> {
let p = personality();
let sigs: Vec<(i32, &str)> = sigs
.iter()
.map(|(num, name)| {
let io = name == "POLL" && p != Personality::Ksh93;
(*num, if io { "IO" } else { name.as_str() })
})
.collect();
let nsig = sigs.last().map_or(1, |(num, _)| num + 1);
let mut out = String::new();
match p {
Personality::Zsh => return None,
Personality::Bash => {
for (i, (num, name)) in sigs.iter().enumerate() {
out.push_str(&format!("{num:2}) SIG{name}"));
if i % 5 == 4 {
out.push('\n');
} else {
out.push('\t');
}
}
if !sigs.len().is_multiple_of(5) {
out.push('\n');
}
}
Personality::Dash | Personality::Sh => {
out.push_str("0\n");
for slot in 1..nsig {
match sigs.iter().find(|(num, name)| *num == slot && *name != "STKFLT") {
Some((_, name)) => out.push_str(name),
None => out.push_str(&slot.to_string()),
}
out.push('\n');
}
}
Personality::Ksh93 => {
for slot in 1..nsig {
match sigs.iter().find(|(num, _)| *num == slot) {
Some(_) if slot == 6 => out.push_str("IOT"),
Some((_, name)) => out.push_str(name),
None => out.push_str(&format!("SIG{slot}")),
}
out.push('\n');
}
}
Personality::Csh => {
for (_, name) in &sigs {
out.push_str(name);
out.push('\n');
}
}
Personality::Mksh | Personality::Pdksh => {
let half = sigs.len().div_ceil(2);
let left_width = 40usize;
for i in 0..half {
let (num, name) = &sigs[i];
let cell = format!("{num:2} {name:>6} {}", signal_description(*num));
match sigs.get(i + half) {
Some((rnum, rname)) => out.push_str(&format!(
"{cell:<left_width$}{rnum:2} {rname:>6} {}\n",
signal_description(*rnum)
)),
None => {
out.push_str(cell.trim_end());
out.push('\n');
}
}
}
}
}
Some(out)
}
fn signal_description(num: i32) -> String {
let p = unsafe { libc::strsignal(num) };
if p.is_null() {
return String::new();
}
let text = unsafe { std::ffi::CStr::from_ptr(p) }
.to_string_lossy()
.into_owned();
match text.rsplit_once(": ") {
Some((head, tail)) if !tail.is_empty() && tail.bytes().all(|b| b.is_ascii_digit()) => {
head.to_string()
}
_ => text,
}
}
pub fn hash_header() -> Option<&'static str> {
match personality() {
Personality::Bash => Some("hits\tcommand"),
_ => None,
}
}
static BASH_HASH_HITS: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<String, u32>>> =
std::sync::LazyLock::new(Default::default);
pub fn bash_hash_hit(name: &str) {
if personality() == Personality::Bash {
if let Ok(mut m) = BASH_HASH_HITS.lock() {
*m.entry(name.to_string()).or_insert(0) += 1;
}
}
}
pub fn bash_hash_forget(name: Option<&str>) {
if let Ok(mut m) = BASH_HASH_HITS.lock() {
match name {
Some(n) => {
m.remove(n);
}
None => m.clear(),
}
}
}
fn bash_hash_hits(name: &str) -> u32 {
BASH_HASH_HITS
.lock()
.ok()
.and_then(|m| m.get(name).copied())
.unwrap_or(0)
}
pub fn hash_entry(name: &str, path: &str) -> Option<String> {
match personality() {
Personality::Bash => Some(format!("{:>4}\t{path}", bash_hash_hits(name))),
Personality::Dash | Personality::Sh => Some(path.to_string()),
_ => None,
}
}
pub const BASH_BUILTINS: &[(&str, bool)] = &[
(".", true), (":", true), ("[", false), ("alias", false), ("bg", false),
("bind", false), ("break", true), ("builtin", false), ("caller", false),
("cd", false), ("command", false), ("compgen", false), ("complete", false),
("compopt", false), ("continue", true), ("declare", false), ("dirs", false),
("disown", false), ("echo", false), ("enable", false), ("eval", true),
("exec", true), ("exit", true), ("export", true), ("false", false),
("fc", false), ("fg", false), ("getopts", false), ("hash", false),
("help", false), ("history", false), ("jobs", false), ("kill", false),
("let", false), ("local", false), ("logout", false), ("mapfile", false),
("popd", false), ("printf", false), ("pushd", false), ("pwd", false),
("read", false), ("readarray", false), ("readonly", true), ("return", true),
("set", true), ("shift", true), ("shopt", false), ("source", true),
("suspend", false), ("test", false), ("times", true), ("trap", true),
("true", false), ("type", false), ("typeset", false), ("ulimit", false),
("umask", false), ("unalias", false), ("unset", true), ("wait", false),
];
pub fn bash_hash(argv: &[String], ops: &options) -> i32 {
use crate::extensions::emulation_output as eo;
let path_dirs = || -> Vec<String> {
getsparam("PATH").unwrap_or_default().split(':').map(String::from).collect()
};
let lookup = |n: &str| -> Option<String> {
cmdnamtab_lock()
.read()
.ok()
.and_then(|t| t.get_full_path(n))
.map(|p| p.display().to_string())
};
if OPT_ISSET(ops, b'r') {
emptycmdnamtable();
eo::bash_hash_forget(None);
if argv.is_empty() {
return 0;
}
}
if OPT_ISSET(ops, b'p') {
let (Some(path), Some(n)) = (argv.first(), argv.get(1)) else {
zwarnnam("hash", "-p: option requires an argument");
return 1;
};
if let Ok(mut t) = cmdnamtab_lock().write() {
t.add(cmdnam {
node: hashnode { next: None, nam: n.clone(), flags: HASHED as i32 },
name: None,
cmd: Some(path.clone()),
});
}
eo::bash_hash_forget(Some(n));
return 0;
}
if OPT_ISSET(ops, b'd') {
let mut rc = 0;
for n in argv {
let gone = cmdnamtab_lock().write().ok().map_or(false, |mut t| t.remove(n).is_some());
if gone {
eo::bash_hash_forget(Some(n));
} else {
zwarnnam("hash", &format!("{n}: not found"));
rc = 1;
}
}
return rc;
}
if OPT_ISSET(ops, b't') {
let mut rc = 0;
for n in argv {
match lookup(n) {
Some(p) => {
eo::bash_hash_hit(n);
if argv.len() > 1 {
println!("{n}\t{p}");
} else {
println!("{p}");
}
}
None => {
zwarnnam("hash", &format!("{n}: not found"));
rc = 1;
}
}
}
return rc;
}
if argv.is_empty() {
let mut entries: Vec<(String, String)> = cmdnamtab_lock()
.read()
.map(|t| t.iter().map(|(n, _)| n.clone()).collect::<Vec<_>>())
.unwrap_or_default()
.into_iter()
.filter_map(|n| lookup(&n).map(|p| (n, p)))
.collect();
entries.sort();
if entries.is_empty() {
if !OPT_ISSET(ops, b'l') {
println!("hash: hash table empty");
}
return 0;
}
if OPT_ISSET(ops, b'l') {
for (n, p) in entries {
println!("builtin hash -p {p} {n}");
}
} else {
println!("hits\tcommand");
for (n, p) in entries {
if let Some(row) = eo::hash_entry(&n, &p) {
println!("{row}");
}
}
}
return 0;
}
let mut rc = 0;
for n in argv {
if n.contains('/') {
continue;
}
if hashcmd(n, &path_dirs()).is_none() {
zwarnnam("hash", &format!("{n}: not found"));
rc = 1;
}
}
rc
}
pub fn bash_enable(argv: &[String], ops: &options, func: i32) -> i32 {
let disabled = |n: &str| BUILTINS_DISABLED.lock().map_or(false, |s| s.contains(n));
if argv.is_empty() {
let (all, only_off, special) = (OPT_ISSET(ops, b'a'), OPT_ISSET(ops, b'n'), OPT_ISSET(ops, b's'));
for (n, is_special) in crate::extensions::emulation_output::BASH_BUILTINS {
if (special && !is_special) || (!all && only_off != disabled(n)) {
continue;
}
println!("enable {}{n}", if disabled(n) { "-n " } else { "" });
}
return 0;
}
let mut plain = ops.clone();
for f in [b'n', b'a', b'p', b's'] {
plain.ind[f as usize] = 0;
}
let func = if OPT_ISSET(ops, b'n') { BIN_DISABLE } else { func };
let rc = bin_enable("enable", argv, &plain, func);
if rc != 0 {
for n in argv {
eprintln!("enable: {n}: not a shell builtin");
}
}
rc
}
static BASH_RETURN_SAVED: std::sync::Mutex<Vec<Option<String>>> = std::sync::Mutex::new(Vec::new());
pub fn bash_return_trap_enter() {
if !crate::dash_mode::bash_mode() {
return;
}
let outer = traps_table().lock().ok().and_then(|mut t| t.remove("RETURN"));
if let Ok(mut st) = BASH_RETURN_SAVED.lock() {
st.push(outer);
}
}
pub fn bash_return_trap_exit() {
if !crate::dash_mode::bash_mode() {
return;
}
let outer = BASH_RETURN_SAVED.lock().ok().and_then(|mut st| st.pop()).flatten();
let own = traps_table().lock().ok().and_then(|t| t.get("RETURN").cloned());
match own {
Some(body) if !body.is_empty() => {
if let Ok(mut t) = traps_table().lock() {
t.remove("RETURN");
}
let olastval = LASTVAL.load(Ordering::SeqCst);
let _ = crate::ported::exec::execute_script(&body);
LASTVAL.store(olastval, Ordering::SeqCst);
if let Ok(mut t) = traps_table().lock() {
t.insert("RETURN".to_string(), body);
}
}
Some(_) => {}
None => {
if let (Some(body), Ok(mut t)) = (outer, traps_table().lock()) {
t.insert("RETURN".to_string(), body);
}
}
}
}
pub fn bash_trap_return(argv: &[String]) -> Option<i32> {
let args: Vec<&String> = argv.iter().skip_while(|a| *a == "--").collect();
if args.len() < 2 || args[0] == "-p" || !args[1..].iter().any(|a| *a == "RETURN") {
return None;
}
let body = args[0].clone();
let others: Vec<String> = args[1..].iter().filter(|a| **a != "RETURN").map(|a| a.to_string()).collect();
if let Ok(mut t) = traps_table().lock() {
if body == "-" {
t.remove("RETURN");
} else {
t.insert("RETURN".to_string(), body.clone());
}
}
if others.is_empty() {
return Some(0);
}
let mut rest = vec![body];
rest.extend(others);
Some(bin_trap("trap", &rest, &options { ind: [0u8; MAX_OPS], args: Vec::new(), argscount: 0, argsalloc: 0 }, 0))
}
struct SetOpt {
name: &'static str,
zsh: Option<&'static str>,
invert: bool,
default_on: bool,
}
const fn o(name: &'static str, zsh: &'static str) -> SetOpt {
SetOpt {
name,
zsh: Some(zsh),
invert: false,
default_on: false,
}
}
const fn inv(name: &'static str, zsh: &'static str) -> SetOpt {
SetOpt {
name,
zsh: Some(zsh),
invert: true,
default_on: false,
}
}
const fn fixed(name: &'static str, default_on: bool) -> SetOpt {
SetOpt {
name,
zsh: None,
invert: false,
default_on,
}
}
const DASH_SET_O: &[SetOpt] = &[
o("errexit", "errexit"),
o("noglob", "noglob"),
o("ignoreeof", "ignoreeof"),
o("interactive", "interactive"),
o("monitor", "monitor"),
o("noexec", "noexec"),
o("stdin", "stdin"),
o("xtrace", "xtrace"),
o("verbose", "verbose"),
o("vi", "vi"),
o("emacs", "emacs"),
o("noclobber", "noclobber"),
o("allexport", "allexport"),
o("notify", "notify"),
o("nounset", "nounset"),
#[cfg(target_os = "linux")]
o("privileged", "privileged"),
o("nolog", "nolog"),
fixed("debug", false),
];
const KSH93_SET_O: &[SetOpt] = &[
o("allexport", "allexport"),
fixed("backslashctrl", true),
o("bgnice", "bgnice"),
o("braceexpand", "braceexpand"),
inv("clobber", "noclobber"),
o("emacs", "emacs"),
o("errexit", "errexit"),
inv("exec", "noexec"),
fixed("functrace", false),
inv("glob", "noglob"),
fixed("globcasedetect", false),
fixed("globstar", false),
fixed("gmacs", false),
fixed("histexpand", false),
fixed("histreedit", false),
fixed("histverify", false),
o("ignoreeof", "ignoreeof"),
o("interactive", "interactive"),
fixed("keyword", false),
fixed("letoctal", false),
inv("log", "nolog"),
o("login_shell", "loginshell"),
o("markdirs", "markdirs"),
o("monitor", "monitor"),
fixed("multiline", true),
o("notify", "notify"),
o("pipefail", "pipefail"),
fixed("posix", false),
o("privileged", "privileged"),
fixed("rc", false),
o("restricted", "restricted"),
fixed("showme", false),
o("trackall", "trackall"),
inv("unset", "nounset"),
o("verbose", "verbose"),
o("vi", "vi"),
fixed("viraw", true),
o("xtrace", "xtrace"),
];
const MKSH_SET_O: &[SetOpt] = &[
o("allexport", "allexport"),
o("bgnice", "bgnice"),
o("braceexpand", "braceexpand"),
fixed("emacs", true),
o("errexit", "errexit"),
fixed("gmacs", false),
o("ignoreeof", "ignoreeof"),
fixed("inherit-xtrace", true),
o("interactive", "interactive"),
fixed("keyword", false),
o("login", "login"),
o("markdirs", "markdirs"),
o("monitor", "monitor"),
o("noclobber", "noclobber"),
o("noexec", "noexec"),
o("noglob", "noglob"),
fixed("nohup", true),
o("nolog", "nolog"),
o("notify", "notify"),
o("nounset", "nounset"),
o("physical", "physical"),
o("pipefail", "pipefail"),
fixed("posix", false),
o("privileged", "privileged"),
o("restricted", "restricted"),
fixed("sh", false),
o("stdin", "stdin"),
fixed("trackall", true),
fixed("utf8-mode", false),
o("verbose", "verbose"),
o("vi", "vi"),
fixed("vi-esccomplete", false),
fixed("vi-tabcomplete", true),
fixed("viraw", false),
o("xtrace", "xtrace"),
];
impl SetOpt {
fn state(&self) -> bool {
match self.zsh {
Some(name) => {
let on = crate::ported::options::opt_state_get(name).unwrap_or(self.default_on);
if self.invert {
!on
} else {
on
}
}
None => self.default_on,
}
}
}
pub fn special_builtin_error_aborts() -> bool {
matches!(
personality(),
Personality::Ksh93
| Personality::Mksh
| Personality::Pdksh
| Personality::Dash
| Personality::Sh
)
}
pub fn special_builtin_abort_status(usage_error: bool, builtin_status: i32) -> i32 {
match personality() {
Personality::Dash | Personality::Sh => 2,
Personality::Mksh | Personality::Pdksh => 1,
Personality::Ksh93 if usage_error => 2,
_ => builtin_status,
}
}
pub fn alias_always_quotes() -> bool {
matches!(personality(), Personality::Dash | Personality::Sh)
}
pub fn set_o_listing() -> Option<String> {
set_o_render(false)
}
pub fn set_plus_o_listing() -> Option<String> {
match personality() {
Personality::Dash | Personality::Sh => set_o_render(true),
Personality::Ksh93 => Some(compact_set_plus_o(
KSH93_SET_O,
KSH93_DEFAULT_ON,
"set --default",
"--",
)),
Personality::Mksh | Personality::Pdksh => Some(compact_set_plus_o(
MKSH_SET_O,
MKSH_DEFAULT_ON,
"set -o .reset",
"-o ",
)),
_ => None,
}
}
const KSH93_DEFAULT_ON: &[&str] = &["backslashctrl", "clobber", "exec", "glob", "log", "unset"];
const MKSH_DEFAULT_ON: &[&str] = &[
"emacs",
"inherit-xtrace",
"nohup",
"trackall",
"vi-tabcomplete",
];
fn compact_set_plus_o(table: &[SetOpt], defaults: &[&str], reset: &str, prefix: &str) -> String {
let mut out = String::from(reset);
for e in table {
if e.state() && !defaults.contains(&e.name) {
out.push(' ');
out.push_str(prefix);
out.push_str(e.name);
}
}
out.push('\n');
out
}
fn set_o_render(reusable: bool) -> Option<String> {
let (table, width) = match personality() {
Personality::Dash | Personality::Sh => (DASH_SET_O, 15usize),
Personality::Ksh93 => (KSH93_SET_O, 24),
Personality::Mksh | Personality::Pdksh => (MKSH_SET_O, 14),
_ => return None,
};
let on = |b: bool| if b { "on" } else { "off" };
if reusable {
let mut out = String::new();
for e in table {
out.push_str(&format!(
"set {}o {}\n",
if e.state() { "-" } else { "+" },
e.name
));
}
return Some(out);
}
let mut out = String::from("Current option settings\n");
if matches!(personality(), Personality::Mksh | Personality::Pdksh) {
let rows = table.len().div_ceil(4);
for r in 0..rows {
let mut line = String::new();
for c in 0..4 {
let Some(e) = table.get(c * rows + r) else {
continue;
};
line.push_str(&format!("{:<width$} {:<5}", e.name, on(e.state())));
}
out.push_str(line.trim_end());
out.push('\n');
}
return Some(out);
}
for e in table {
out.push_str(&format!("{:<width$} {}\n", e.name, on(e.state())));
}
Some(out)
}