use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
static DASH_STRICT: AtomicBool = AtomicBool::new(false);
static POSIX_FAITHFUL: AtomicBool = AtomicBool::new(false);
static BASH_MODE: AtomicBool = AtomicBool::new(false);
static ZSH_DROPIN: AtomicBool = AtomicBool::new(false);
static NOCASEMATCH: AtomicBool = AtomicBool::new(false);
static PDKSH_FAMILY: AtomicBool = AtomicBool::new(false);
static DASH_C_STRING: AtomicBool = AtomicBool::new(false);
#[inline]
pub fn dash_c_string() -> bool {
DASH_C_STRING.load(Ordering::Relaxed)
}
#[inline]
pub fn set_dash_c_string(on: bool) {
DASH_C_STRING.store(on, Ordering::Relaxed);
}
#[inline]
pub fn pdksh_family() -> bool {
PDKSH_FAMILY.load(Ordering::Relaxed)
}
#[inline]
pub fn set_pdksh_family(on: bool) {
PDKSH_FAMILY.store(on, Ordering::Relaxed);
}
#[inline]
pub fn nocasematch() -> bool {
NOCASEMATCH.load(Ordering::Relaxed)
}
#[inline]
pub fn set_nocasematch(on: bool) {
NOCASEMATCH.store(on, Ordering::Relaxed);
crate::ported::options::opt_state_set_via_alias("casematch", !on);
}
const BASH_SHOPTS_INVERTED_ZSH_OPT: &[(&str, &str)] = &[("xpg_echo", "bsdecho")];
const BASH_SHOPTS_READONLY: &[&str] = &["login_shell", "restricted_shell"];
fn bash_shopt_inverted_zsh_opt(name: &str) -> Option<&'static str> {
BASH_SHOPTS_INVERTED_ZSH_OPT
.iter()
.find(|(b, _)| *b == name)
.map(|(_, z)| *z)
}
#[inline]
pub fn dash_strict() -> bool {
DASH_STRICT.load(Ordering::Relaxed)
}
#[inline]
pub fn set_dash_strict(on: bool) {
DASH_STRICT.store(on, Ordering::Relaxed);
}
#[inline]
pub fn posix_faithful() -> bool {
POSIX_FAITHFUL.load(Ordering::Relaxed)
}
#[inline]
pub fn bash_mode() -> bool {
BASH_MODE.load(Ordering::Relaxed)
}
#[inline]
pub fn fatal_error_status() -> Option<i32> {
if posix_faithful() && dash_strict() {
Some(2)
} else {
None
}
}
#[inline]
pub fn dash_faithful() -> bool {
posix_faithful() && dash_strict()
}
pub fn dash_builtin_optstr(name: &str) -> Option<&'static [u8]> {
match name {
"read" => Some(b"rp"),
"local" => Some(b""),
"unalias" => Some(b"a"),
"type" => Some(b""),
_ => None,
}
}
pub fn dash_echo_octal_normalize(s: &str) -> String {
let b: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
if b[i] != '\\' || i + 1 >= b.len() {
out.push(b[i]);
i += 1;
} else if b[i + 1] == '0' {
let digits: String = b[i + 2..].iter().take(3).take_while(|c| c.is_digit(8)).collect();
out.push_str(&format!("\\{:0>3}", digits));
i += 2 + digits.len();
} else {
out.push('\\');
out.push(b[i + 1]);
i += 2;
}
}
out
}
pub fn dash_arith_var(raw: &str) -> Option<i64> {
let t = raw.trim();
if t.is_empty() {
return Some(0);
}
let (neg, body) = match t.as_bytes()[0] {
b'-' => (true, &t[1..]),
b'+' => (false, &t[1..]),
_ => (false, t),
};
let (radix, digits) = if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
(16, h)
} else if body.len() > 1 && body.starts_with('0') {
(8, &body[1..])
} else {
(10, body)
};
if !digits.starts_with(|c: char| c.is_ascii_alphanumeric()) {
return None;
}
let v = i64::from_str_radix(digits, radix).ok()?;
Some(if neg { -v } else { v })
}
const DASH_SPECIAL_BUILTINS: &[&str] = &[
":", ".", "break", "continue", "eval", "exec", "exit", "export",
"readonly", "return", "set", "shift", "times", "trap", "unset",
];
pub fn dash_special_builtin(name: &str) -> bool {
DASH_SPECIAL_BUILTINS.contains(&name)
}
const DASH_BUILTINS: &[&str] = &[
":", ".", "[", "alias", "bg", "break", "cd", "chdir", "command", "continue",
"echo", "eval", "exec", "exit", "export", "false", "fc", "fg", "getopts",
"hash", "jobs", "kill", "local", "printf", "pwd", "read", "readonly",
"return", "set", "shift", "test", "times", "trap", "true", "type", "ulimit",
"umask", "unalias", "unset", "wait",
];
pub fn dash_hide_foreign_builtins() {
let Ok(mut disabled) = crate::ported::builtin::BUILTINS_DISABLED.lock() else {
return;
};
for name in crate::ported::builtin::createbuiltintable().keys() {
if !DASH_BUILTINS.contains(&name.as_str()) {
disabled.insert(name.clone());
}
}
}
pub fn dash_number(s: &str) -> Option<i32> {
let a = s.trim_start();
let digits = a.strip_prefix('+').unwrap_or(a);
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
digits.parse::<i32>().ok()
}
pub fn dash_bad_function_name(name: &str) -> bool {
let mut chars = name.chars();
let first_ok = matches!(chars.next(), Some(c) if c == '_' || c.is_ascii_alphabetic());
!first_ok || !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) || dash_special_builtin(name)
}
#[inline]
pub fn korn_mode() -> bool {
posix_faithful() && crate::ported::zsh_h::EMULATION(crate::ported::zsh_h::EMULATE_KSH)
}
#[inline]
pub fn ksh93_mode() -> bool {
korn_mode() && !pdksh_family()
}
#[inline]
pub fn sparse_arrays() -> bool {
bash_mode() || korn_mode()
}
#[inline]
pub fn bash_printf_empty_numeric_error(arg: Option<&String>) -> bool {
bash_mode() && matches!(arg, Some(s) if s.is_empty())
}
#[inline]
pub fn set_bash_mode(on: bool) {
BASH_MODE.store(on, Ordering::Relaxed);
}
pub fn ksh93_range_values(from: i64, to: i64, step: Option<i64>) -> Vec<i64> {
let step = step.unwrap_or(if from <= to { 1 } else { -1 });
let mut vals = vec![from];
let mut v = from + step;
while (step > 0 && v <= to) || (step < 0 && v >= to) {
vals.push(v);
v += step;
}
vals
}
#[inline]
pub fn zsh_dropin() -> bool {
ZSH_DROPIN.load(Ordering::Relaxed)
}
#[inline]
pub fn set_zsh_dropin(on: bool) {
ZSH_DROPIN.store(on, Ordering::Relaxed);
}
#[inline]
pub fn set_posix_faithful(on: bool) {
POSIX_FAITHFUL.store(on, Ordering::Relaxed);
}
pub const BASH_VERSION_MAJOR: &str = "5";
pub const BASH_VERSION_MINOR: &str = "3";
pub const BASH_VERSION_PATCH: &str = "20";
pub fn bash_version() -> String {
format!(
"{}.{}.{}(1)-release",
BASH_VERSION_MAJOR, BASH_VERSION_MINOR, BASH_VERSION_PATCH
)
}
pub fn bash_versinfo() -> Vec<String> {
vec![
BASH_VERSION_MAJOR.to_string(),
BASH_VERSION_MINOR.to_string(),
BASH_VERSION_PATCH.to_string(),
"1".to_string(),
"release".to_string(),
format!(
"{}-{}-{}",
crate::ported::config_h::MACHTYPE,
crate::ported::config_h::VENDOR,
crate::ported::config_h::OSTYPE
),
]
}
pub fn strip_replacement_backslashes(s: &str) -> String {
if !s.contains('\\') && !s.contains('\u{e19f}') {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{e19f}' {
if chars.clone().next() == Some('&')
&& bash_shopt_get("patsub_replacement") == Some(true)
{
chars.next();
out.push(PATSUB_LITERAL_AMP);
continue;
}
out.push(c);
if let Some(next) = chars.next() {
out.push(next);
}
} else if c == '\\' {
match chars.next() {
Some('&') if bash_shopt_get("patsub_replacement") == Some(true) => {
out.push(PATSUB_LITERAL_AMP)
}
Some(next) => out.push(next),
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
fn bash_running_script() -> bool {
crate::ported::params::getsparam("ZSH_EXECUTION_STRING").map_or(true, |c| c.is_empty())
}
fn bash_script_name() -> String {
crate::ported::params::getsparam("ZSH_ARGZERO").unwrap_or_default()
}
pub fn is_funcdef_text(t: &str) -> bool {
let t = t.trim_start();
if t.starts_with("function ") {
return true;
}
let end = t
.find(|c: char| !(c.is_alphanumeric() || matches!(c, '_' | '-' | '.')))
.unwrap_or(t.len());
end > 0 && t[end..].trim_start().starts_with("()")
}
pub fn aliases_visible() -> bool {
!bash_mode() || crate::ported::zsh_h::isset(crate::ported::zsh_h::ALIASESOPT)
}
pub fn bash_special_array(name: &str) -> Option<Vec<String>> {
if !bash_mode() && !(name == "PIPESTATUS" && pdksh_family()) {
return None;
}
match name {
"PIPESTATUS" if !bash_mode() => {
use crate::ported::jobs::{NUMPIPESTATS, PIPESTATS};
let n = NUMPIPESTATS
.get()
.and_then(|m| m.lock().ok().map(|g| *g))
.unwrap_or(0);
let stats = PIPESTATS
.get()
.and_then(|m| m.lock().ok().map(|p| p[..n].to_vec()))
.unwrap_or_default();
Some(stats.iter().map(|s| s.to_string()).collect())
}
"PIPESTATUS" => Some(crate::ported::exec::array("pipestatus").unwrap_or_default()),
"FUNCNAME" => crate::ported::modules::parameter::FUNCSTACK.lock().ok().map(|f| {
if !f.iter().any(|fs| fs.tp == crate::ported::zsh_h::FS_FUNC) {
return Vec::new();
}
let mut names: Vec<String> = f
.iter()
.rev()
.map(|fs| if fs.tp == crate::ported::zsh_h::FS_SOURCE { "source".to_string() } else { fs.name.clone() })
.collect();
if bash_running_script() {
names.push("main".to_string());
}
names
}),
"BASH_SOURCE" => crate::ported::modules::parameter::FUNCSTACK.lock().ok().map(|f| {
let script = bash_script_name();
let mut files: Vec<String> = f
.iter()
.rev()
.map(|fs| fs.filename.clone().filter(|n| !n.is_empty()).unwrap_or_else(|| script.clone()))
.collect();
if bash_running_script() {
files.push(script);
}
files
}),
"BASH_LINENO" => crate::ported::modules::parameter::FUNCSTACK.lock().ok().map(|f| {
let mut lines: Vec<String> = f.iter().rev().map(|fs| fs.lineno.to_string()).collect();
if bash_running_script() {
lines.push("0".to_string());
}
lines
}),
"BASH_VERSINFO" => Some(bash_versinfo()),
_ => None,
}
}
pub const BASH_SHOPTS: &[(&str, Option<&str>, bool)] = &[
("array_expand_once", None, false),
("assoc_expand_once", None, false),
("autocd", Some("autocd"), false),
("bash_source_fullpath", None, false),
("cdable_vars", Some("cdable_vars"), false),
("cdspell", None, false),
("checkhash", None, false),
("checkjobs", Some("checkjobs"), false),
("checkwinsize", None, true),
("cmdhist", None, true),
("compat31", None, false),
("compat32", None, false),
("compat40", None, false),
("compat41", None, false),
("compat42", None, false),
("compat43", None, false),
("compat44", None, false),
("complete_fullquote", None, true),
("direxpand", None, false),
("dirspell", None, false),
("dotglob", Some("dotglob"), false),
("execfail", None, false),
("expand_aliases", Some("aliases"), false),
("extdebug", None, false),
("extglob", Some("kshglob"), false),
("extquote", None, true),
("failglob", Some("nomatch"), false),
("force_fignore", None, true),
("globasciiranges", None, true),
("globskipdots", None, true),
("globstar", None, false),
("gnu_errfmt", None, false),
("histappend", Some("histappend"), false),
("histreedit", None, false),
("histverify", Some("histverify"), false),
("hostcomplete", None, true),
("huponexit", None, false),
("inherit_errexit", None, false),
("interactive_comments", Some("interactive_comments"), true),
("lastpipe", None, false),
("lithist", None, false),
("localvar_inherit", None, false),
("localvar_unset", None, false),
("login_shell", Some("loginshell"), false),
("mailwarn", Some("mailwarn"), false),
("no_empty_cmd_completion", None, false),
("nocaseglob", Some("nocaseglob"), false),
("nocasematch", Some("nocasematch"), false),
("noexpand_translation", None, false),
("nullglob", Some("nullglob"), false),
("patsub_replacement", None, true),
("progcomp", None, true),
("progcomp_alias", None, false),
("promptvars", Some("promptvars"), true),
("restricted_shell", None, false),
("shift_verbose", None, false),
("sourcepath", None, true),
("varredir_close", None, false),
("xpg_echo", None, false),
];
thread_local! {
static BASH_ONLY_SHOPTS: RefCell<HashMap<&'static str, bool>> =
RefCell::new(HashMap::new());
}
pub fn bash_shopt_row(name: &str) -> Option<(&'static str, Option<&'static str>, bool)> {
BASH_SHOPTS.iter().copied().find(|(n, _, _)| *n == name)
}
pub fn bash_shopt_get(name: &str) -> Option<bool> {
let (canon, zsh_opt, default_on) = bash_shopt_row(name)?;
if canon == "nocasematch" {
return Some(nocasematch());
}
if let Some(zopt) = bash_shopt_inverted_zsh_opt(canon) {
return Some(!crate::ported::options::opt_state_get(zopt).unwrap_or(!default_on));
}
Some(match zsh_opt {
Some(opt) => crate::ported::options::opt_state_get(opt).unwrap_or(default_on),
None => BASH_ONLY_SHOPTS
.with(|m| m.borrow().get(canon).copied())
.unwrap_or(default_on),
})
}
pub fn bash_shopt_set(name: &str, on: bool) -> bool {
let Some((canon, zsh_opt, _)) = bash_shopt_row(name) else {
return false;
};
if BASH_SHOPTS_READONLY.contains(&canon) {
return true;
}
if canon == "nocasematch" {
set_nocasematch(on);
return true;
}
if let Some(zopt) = bash_shopt_inverted_zsh_opt(canon) {
crate::ported::options::opt_state_set_via_alias(zopt, !on);
return true;
}
match zsh_opt {
Some(opt) => {
crate::ported::options::opt_state_set_via_alias(opt, on);
}
None => BASH_ONLY_SHOPTS.with(|m| {
m.borrow_mut().insert(canon, on);
}),
}
true
}
pub fn bash_shopt_apply_defaults() {
for (name, zsh_opt, default_on) in BASH_SHOPTS {
if DERIVED_SHOPTS.contains(name) {
continue;
}
if zsh_opt.is_some() || bash_shopt_inverted_zsh_opt(name).is_some() {
let on = if *name == "expand_aliases" {
crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE)
} else {
*default_on
};
bash_shopt_set(name, on);
}
}
}
const DERIVED_SHOPTS: &[&str] = &["login_shell", "restricted_shell"];
pub fn bash_shoptsopts() -> String {
BASH_SHOPTS
.iter()
.filter(|(n, _, _)| bash_shopt_get(n).unwrap_or(false))
.map(|(n, _, _)| *n)
.collect::<Vec<_>>()
.join(":")
}
pub const BASH_SET_O: &[(&str, &str)] = &[
("allexport", "allexport"),
("braceexpand", "braceexpand"),
("emacs", "emacs"),
("errexit", "errexit"),
("errtrace", "errtrace"),
("functrace", "functrace"),
("hashall", "hashall"),
("histexpand", "histexpand"),
("history", "history"),
("ignoreeof", "ignoreeof"),
("interactive-comments", "interactivecomments"),
("keyword", "keyword"),
("monitor", "monitor"),
("noclobber", "noclobber"),
("noexec", "noexec"),
("noglob", "noglob"),
("nolog", "nolog"),
("notify", "notify"),
("nounset", "nounset"),
("onecmd", "singlecommand"),
("physical", "physical"),
("pipefail", "pipefail"),
("posix", "posix"),
("privileged", "privileged"),
("verbose", "verbose"),
("vi", "vi"),
("xtrace", "xtrace"),
];
const BASH_ONLY_OPTS: &[(&str, &AtomicBool)] = &[
("errtrace", &BO_ERRTRACE),
("functrace", &BO_FUNCTRACE),
("history", &BO_HISTORY),
("keyword", &BO_KEYWORD),
("nolog", &BO_NOLOG),
("posix", &BO_POSIX),
];
static BO_ERRTRACE: AtomicBool = AtomicBool::new(false);
static BO_FUNCTRACE: AtomicBool = AtomicBool::new(false);
static BO_HISTORY: AtomicBool = AtomicBool::new(false);
static BO_KEYWORD: AtomicBool = AtomicBool::new(false);
static BO_NOLOG: AtomicBool = AtomicBool::new(false);
static BO_POSIX: AtomicBool = AtomicBool::new(false);
pub fn bash_set_o_get(bash_name: &str) -> bool {
if let Some((_, cell)) = BASH_ONLY_OPTS.iter().find(|(n, _)| *n == bash_name) {
return cell.load(Ordering::Relaxed);
}
match BASH_SET_O.iter().find(|(b, _)| *b == bash_name) {
Some((_, zname)) => crate::ported::options::opt_state_get(zname).unwrap_or(false),
None => false,
}
}
pub fn bash_set_o(bash_name: &str, on: bool) -> Option<i32> {
if !bash_mode() {
return None;
}
if let Some((_, cell)) = BASH_ONLY_OPTS.iter().find(|(n, _)| *n == bash_name) {
cell.store(on, Ordering::Relaxed);
return Some(0);
}
let (_, zname) = BASH_SET_O.iter().find(|(b, _)| *b == bash_name)?;
crate::ported::options::opt_state_set_via_alias(zname, on);
Some(0)
}
const BASH_SET_LETTERS: &[(char, &str)] = &[
('h', "hashall"),
('B', "braceexpand"),
('E', "errtrace"),
('H', "histexpand"),
('P', "physical"),
('T', "functrace"),
('k', "keyword"),
('t', "onecmd"),
];
pub fn bash_set_letter(letter: char, on: bool) -> bool {
bash_mode()
&& BASH_SET_LETTERS
.iter()
.find(|(l, _)| *l == letter)
.is_some_and(|(_, name)| bash_set_o(name, on).is_some())
}
pub fn bash_dollar_dash() -> String {
const ORDER: &[(char, &str)] = &[
('a', "allexport"),
('b', "notify"),
('e', "errexit"),
('f', "noglob"),
('h', "hashall"),
('i', ""),
('k', "keyword"),
('m', "monitor"),
('n', "noexec"),
('p', "privileged"),
('t', "onecmd"),
('u', "nounset"),
('v', "verbose"),
('x', "xtrace"),
('B', "braceexpand"),
('C', "noclobber"),
('E', "errtrace"),
('H', "histexpand"),
('P', "physical"),
('T', "functrace"),
];
use crate::ported::zsh_h::{isset, INTERACTIVE, SHINSTDIN};
let mut out: String = ORDER
.iter()
.filter(|(l, name)| if *l == 'i' { isset(INTERACTIVE) } else { bash_set_o_get(name) })
.map(|(l, _)| *l)
.collect();
if crate::ported::params::getsparam("ZSH_EXECUTION_STRING").is_some_and(|c| !c.is_empty()) {
out.push('c');
}
if isset(SHINSTDIN) {
out.push('s');
}
out
}
pub fn bash_shellopts() -> String {
BASH_SET_O
.iter()
.filter(|(b, _)| bash_set_o_get(b))
.map(|(b, _)| *b)
.collect::<Vec<_>>()
.join(":")
}
#[inline]
pub fn umask_four_digit() -> bool {
posix_faithful() && !pdksh_family()
}
static GETOPTS_REPORTED: AtomicI32 = AtomicI32::new(-1);
static GETOPTS_INTERNAL: AtomicI32 = AtomicI32::new(-1);
pub fn getopts_optind_report(zoptind: i32, optcind: i32, lenstr: i32) -> i32 {
if !posix_faithful() {
return zoptind;
}
if optcind == 0 {
GETOPTS_REPORTED.store(zoptind, Ordering::Relaxed);
GETOPTS_INTERNAL.store(zoptind, Ordering::Relaxed);
return zoptind;
}
let eager = dash_strict() || pdksh_family();
let reported = if eager || optcind >= lenstr {
zoptind + 1
} else {
zoptind
};
GETOPTS_REPORTED.store(reported, Ordering::Relaxed);
GETOPTS_INTERNAL.store(zoptind, Ordering::Relaxed);
reported
}
pub fn getopts_optind_internal(param_value: i64) -> i64 {
if !posix_faithful() {
return param_value;
}
let reported = GETOPTS_REPORTED.load(Ordering::Relaxed);
if reported >= 0 && param_value == reported as i64 {
return GETOPTS_INTERNAL.load(Ordering::Relaxed) as i64;
}
param_value
}
pub fn getopts_optind_user_reset(raw_param: i64) -> bool {
if !posix_faithful() {
return false;
}
raw_param != GETOPTS_REPORTED.load(Ordering::Relaxed) as i64
}
pub fn getopts_end_of_options(var: &str) {
if !posix_faithful() {
return;
}
crate::ported::params::setsparam(var, "?");
if !dash_strict() {
crate::ported::params::setsparam("OPTARG", "");
}
}
pub fn trap_listing_line(signame: &str, body: &str) -> Option<String> {
if !posix_faithful() {
return None;
}
let quoted = if korn_mode() && !pdksh_family() && body.contains('\'') {
format!("$'{}'", body.replace('\\', "\\\\").replace('\'', "\\'"))
} else if korn_mode() {
crate::ported::utils::quotedzputs(body)
} else if dash_strict() {
let mut q = format!("'{}'", body.replace('\'', "'\"'\"'"));
if q.len() > 2 && q.ends_with("''") {
q.truncate(q.len() - 2);
}
q
} else {
format!("'{}'", body.replace('\'', "'\\''"))
};
let name = if bash_mode() && real_signal_name(signame) {
format!("SIG{}", signame)
} else {
signame.to_string()
};
Some(format!("trap -- {} {}", quoted, name))
}
fn real_signal_name(name: &str) -> bool {
match crate::ported::jobs::getsigidx(name) {
Some(idx) => idx >= 1 && idx <= crate::ported::signals_h::SIGCOUNT,
None => false,
}
}
#[inline]
pub fn trap_listing_descending() -> bool {
korn_mode() && !pdksh_family()
}
pub fn function_whence_verbose(name: &str) -> Option<String> {
if !posix_faithful() {
return None;
}
if dash_strict() {
Some(format!("{} is a shell function", name))
} else {
Some(format!("{} is a function", name))
}
}
#[inline]
pub fn function_whence_prints_body() -> bool {
posix_faithful() && !korn_mode() && !dash_strict()
}
const PATSUB_LITERAL_AMP: char = '\u{F8FE}';
pub fn bash_patsub(repl: String, matched: &str) -> String {
if !bash_mode() || bash_shopt_get("patsub_replacement") != Some(true) {
return repl;
}
if !repl.contains('&') && !repl.contains(PATSUB_LITERAL_AMP) {
return repl;
}
let mut out = String::with_capacity(repl.len() + matched.len());
for c in repl.chars() {
match c {
'&' => out.push_str(matched),
PATSUB_LITERAL_AMP => out.push('&'),
_ => out.push(c),
}
}
out
}
pub fn bash_function_listing(name: &str, body_src: Option<&str>) -> String {
let rendered = body_src
.filter(|b| !b.is_empty())
.and_then(|b| crate::ported::exec::parse_string(b, 1))
.map(|prog| crate::ported::text::getpermtext(Box::new(prog), None, 0))
.unwrap_or_default();
bash_function_body(name, &rendered)
}
pub fn bash_function_body(name: &str, body_lines: &str) -> String {
let mut out = format!("{} () \n{{ \n", name);
match BashDeparse::parse(body_lines) {
Some(nodes) => out.push_str(&BashDeparse::render(&nodes, 1, true)),
None => {
let lines: Vec<&str> = body_lines.lines().collect();
for (i, line) in lines.iter().enumerate() {
let depth = line.chars().take_while(|c| *c == '\t').count();
let text = line.trim_start_matches('\t');
let last = i + 1 == lines.len();
out.push_str(&" ".repeat(4 * (depth + 1)));
out.push_str(text);
out.push_str(if last { "\n" } else { ";\n" });
}
}
}
out.push_str("}\n");
out
}
pub fn bash_presplit_braces(word: &str) -> Option<Vec<String>> {
use crate::ported::zsh_h::{
Bnull, Comma, Dnull, Inbrace, Inpar, Inparmath, Outbrace, Outpar, Outparmath, Qstring,
Qtick, Snull, Stringg, Tick,
};
if !bash_mode()
|| !word.contains(Inbrace)
|| !word.contains(Comma)
|| !word.chars().any(|c| matches!(c, Stringg | Qstring | Tick | Qtick))
{
return None;
}
let ch: Vec<char> = word.chars().collect();
let mut holes: Vec<String> = Vec::new();
let mut masked = String::new();
let mut hole = |text: String, masked: &mut String| {
masked.push(char::from_u32(0xF0000 + holes.len() as u32).unwrap_or('\u{f0000}'));
holes.push(text);
};
let mut i = 0;
while i < ch.len() {
let c = ch[i];
let end = match c {
Snull | Dnull | Tick | Qtick => {
(i + 1..ch.len()).find(|&j| ch[j] == c).map_or(ch.len(), |j| j + 1)
}
Bnull => (i + 2).min(ch.len()),
Stringg | Qstring => match ch.get(i + 1) {
Some(&Inbrace) | Some(&Inpar) | Some(&Inparmath) => {
let mut depth = 0i32;
let mut j = i + 1;
while j < ch.len() {
match ch[j] {
Inbrace | Inpar => depth += 1,
Inparmath => depth += 2,
Outbrace | Outpar => depth -= 1,
Outparmath => depth -= 2,
_ => {}
}
j += 1;
if depth <= 0 {
break;
}
}
j
}
Some(n) if n.is_ascii_alphanumeric() || *n == '_' => {
let mut j = i + 1;
while j < ch.len() && (ch[j].is_ascii_alphanumeric() || ch[j] == '_') {
j += 1;
}
j
}
Some(_) => i + 2,
None => i + 1,
},
_ => i,
};
if end > i {
hole(ch[i..end].iter().collect(), &mut masked);
i = end;
} else {
masked.push(c);
i += 1;
}
}
let parts = crate::ported::glob::xpandbraces(&masked, false);
if parts.len() == 1 && parts[0] == masked {
return None;
}
Some(
parts
.into_iter()
.filter(|p| !p.is_empty())
.map(|p| {
p.chars()
.map(|c| match (c as u32).checked_sub(0xF0000) {
Some(k) if (k as usize) < holes.len() => holes[k as usize].clone(),
_ => c.to_string(),
})
.collect()
})
.collect(),
)
}
enum BashNode {
Simple(String),
If { cond: String, then: Vec<BashNode>, els: Option<Vec<BashNode>>, closer: String },
Loop { kw: &'static str, cond: String, body: Vec<BashNode>, closer: String },
For { head: String, body: Vec<BashNode>, closer: String },
Case { subject: String, arms: Vec<(String, Vec<BashNode>, String)>, closer: String },
Subshell { body: Vec<BashNode>, closer: String },
Group { body: Vec<BashNode>, closer: String },
Function { name: String, body: Vec<BashNode> },
}
impl BashNode {
fn has_heredoc(&self) -> bool {
let any = |v: &[BashNode]| v.iter().any(BashNode::has_heredoc);
match self {
BashNode::Simple(s) => s.contains(HEREDOC_BODY),
BashNode::If { then, els, .. } => any(then) || els.as_deref().is_some_and(any),
BashNode::Loop { body, .. }
| BashNode::For { body, .. }
| BashNode::Subshell { body, .. }
| BashNode::Group { body, .. }
| BashNode::Function { body, .. } => any(body),
BashNode::Case { arms, .. } => arms.iter().any(|(_, b, _)| any(b)),
}
}
}
struct BashDeparse {
lines: Vec<String>,
pos: usize,
}
const ARM_END: char = '\u{1}';
const HEREDOC_BODY: char = '\u{2}';
impl BashDeparse {
fn parse(body_lines: &str) -> Option<Vec<BashNode>> {
let mut lines: Vec<String> = Vec::new();
let raw_lines: Vec<&str> = body_lines.lines().collect();
let mut ri = 0;
while ri < raw_lines.len() {
let t = raw_lines[ri].trim();
ri += 1;
let mut heredocs = t
.match_indices("<<")
.filter(|(i, _)| !t[*i..].starts_with("<<<") && !t[..*i].ends_with('<'));
if let Some((at, _)) = heredocs.next() {
if heredocs.next().is_some() {
return None;
}
let strip_tabs = t[at + 2..].starts_with('-');
let after = t[at + 2..].trim_start_matches('-').trim_start();
let word = after.split_whitespace().next()?;
let delim: String = word.chars().filter(|c| !matches!(c, '"' | '\'' | '\\')).collect();
let mut cmd = t.to_string();
if word.starts_with('"') {
cmd = cmd.replacen(word, &format!("'{delim}'"), 1);
}
let mut body: Vec<String> = Vec::new();
loop {
let l = *raw_lines.get(ri)?;
ri += 1;
let l = if strip_tabs { l.trim_start_matches('\t') } else { l };
body.push(l.to_string());
if l.trim_end() == delim {
break;
}
}
lines.push(format!("{cmd}{HEREDOC_BODY}{}", body.join("\n")));
continue;
}
if t.ends_with('{') && t != "{" && !t.ends_with(" () {") {
return None;
}
match [" ;;&", " ;&", " ;;"].iter().find(|e| t.ends_with(**e)) {
Some(e) => {
lines.push(t[..t.len() - e.len()].to_string());
lines.push(format!("{ARM_END}{}", e.trim()));
}
None => lines.push(t.to_string()),
}
}
let mut p = BashDeparse { lines, pos: 0 };
let nodes = p.block()?;
(p.pos == p.lines.len()).then_some(nodes)
}
fn peek(&self) -> Option<&str> {
self.lines.get(self.pos).map(String::as_str)
}
fn first_word(line: &str) -> &str {
line.split_whitespace().next().unwrap_or("")
}
fn is_stop(line: &str) -> bool {
line.starts_with(ARM_END)
|| line.starts_with('}')
|| line.starts_with(')')
|| matches!(Self::first_word(line), "fi" | "done" | "esac" | "else" | "elif" | "then" | "do")
}
fn block(&mut self) -> Option<Vec<BashNode>> {
let mut out = Vec::new();
while let Some(line) = self.peek() {
if Self::is_stop(line) {
break;
}
out.push(self.node()?);
}
Some(out)
}
fn expect_word(&mut self, w: &str) -> Option<()> {
(self.peek()? == w).then(|| self.pos += 1)
}
fn closer(&mut self, word: &str) -> Option<String> {
let line = self.peek()?.to_string();
let rest = line.strip_prefix(word)?;
if !rest.is_empty() && !rest.starts_with(' ') {
return None;
}
self.pos += 1;
Some(rest.to_string())
}
fn node(&mut self) -> Option<BashNode> {
let line = self.peek()?.to_string();
self.pos += 1;
if let Some(cond) = line.strip_prefix("if ") {
return self.if_rest(cond.to_string());
}
for kw in ["while", "until"] {
if let Some(cond) = line.strip_prefix(&format!("{kw} ")) {
self.expect_word("do")?;
let body = self.block()?;
let closer = self.closer("done")?;
return Some(BashNode::Loop {
kw: if kw == "while" { "while" } else { "until" },
cond: cond.to_string(),
body,
closer,
});
}
}
if line.starts_with("for ((") && line.ends_with(" do") {
let body = self.block()?;
let closer = self.closer("done")?;
return Some(BashNode::For { head: line[..line.len() - 3].to_string(), body, closer });
}
if line.starts_with("for ") || line.starts_with("select ") {
self.expect_word("do")?;
let body = self.block()?;
let closer = self.closer("done")?;
return Some(BashNode::For { head: line, body, closer });
}
if let Some(rest) = line.strip_prefix("case ") {
let subject = rest.strip_suffix(" in")?.to_string();
let mut arms = Vec::new();
while self.peek()?.starts_with('(') {
let arm = self.peek()?.to_string();
let close = arm.find(") ").or_else(|| arm.ends_with(')').then(|| arm.len() - 1))?;
let pat = arm[1..close].to_string();
let first = arm[close + 1..].trim().to_string();
if first.is_empty() {
self.pos += 1;
} else {
self.lines[self.pos] = first;
}
let body = self.block()?;
let term = self.peek()?.strip_prefix(ARM_END)?.to_string();
self.pos += 1;
arms.push((pat, body, term));
}
let closer = self.closer("esac")?;
return Some(BashNode::Case { subject, arms, closer });
}
if let Some(name) = line.strip_suffix(" () {") {
let body = self.block()?;
self.closer("}")?;
return Some(BashNode::Function { name: name.to_string(), body });
}
if line == "(" || line == "{" {
let body = self.block()?;
let closer = self.closer(if line == "(" { ")" } else { "}" })?;
return Some(if line == "(" {
BashNode::Subshell { body, closer }
} else {
BashNode::Group { body, closer }
});
}
Some(BashNode::Simple(line))
}
fn if_rest(&mut self, cond: String) -> Option<BashNode> {
self.expect_word("then")?;
let then = self.block()?;
let mut els = None;
match self.peek() {
Some(l) if l.starts_with("elif ") => {
let nested = l["elif ".len()..].to_string();
self.pos += 1;
els = Some(vec![self.if_rest(nested)?]);
return Some(BashNode::If { cond, then, els, closer: String::new() });
}
Some("else") => {
self.pos += 1;
els = Some(self.block()?);
}
_ => {}
}
let closer = self.closer("fi")?;
Some(BashNode::If { cond, then, els, closer })
}
fn render(nodes: &[BashNode], indent: usize, bare_last: bool) -> String {
let mut out = String::new();
for (i, n) in nodes.iter().enumerate() {
let term = if (bare_last && i + 1 == nodes.len()) || n.has_heredoc() { "" } else { ";" };
out.push_str(&Self::render_one(n, indent, term));
}
out
}
fn render_one(n: &BashNode, indent: usize, term: &str) -> String {
let ind = " ".repeat(indent);
match n {
BashNode::Simple(s) if s.contains(HEREDOC_BODY) => {
let (cmd, body) = s.split_once(HEREDOC_BODY).unwrap_or((s, ""));
format!("{ind}{cmd}\n{body}\n\n")
}
BashNode::Simple(s) => {
let term = if s.ends_with('&') { "" } else { term };
format!("{ind}{s}{term}\n")
}
BashNode::If { cond, then, els, closer } => {
let mut o = format!("{ind}if {cond}; then\n{}", Self::render(then, indent + 1, false));
if let Some(e) = els {
o.push_str(&format!("{ind}else\n{}", Self::render(e, indent + 1, false)));
}
o.push_str(&format!("{ind}fi{closer}{term}\n"));
o
}
BashNode::Loop { kw, cond, body, closer } => format!(
"{ind}{kw} {cond}; do\n{}{ind}done{closer}{term}\n",
Self::render(body, indent + 1, false)
),
BashNode::For { head, body, closer } => {
let semi = if head.starts_with("for ((") { "" } else { ";" };
format!(
"{ind}{head}{semi}\n{ind}do\n{}{ind}done{closer}{term}\n",
Self::render(body, indent + 1, false)
)
}
BashNode::Case { subject, arms, closer } => {
let mut o = format!("{ind}case {subject} in \n");
for (pat, body, t) in arms {
o.push_str(&format!(
"{ind} {pat})\n{}{ind} {t}\n",
Self::render(body, indent + 2, true)
));
}
o.push_str(&format!("{ind}esac{closer}{term}\n"));
o
}
BashNode::Subshell { body, closer } => {
let inner = Self::render(body, indent, true);
let inner = inner.trim_end_matches('\n');
let inner = inner.strip_prefix(ind.as_str()).unwrap_or(inner);
format!("{ind}( {inner} ){closer}{term}\n")
}
BashNode::Function { name, body } => format!(
"{ind}function {name} () \n{ind}{{ \n{}{ind}}}{term}\n",
Self::render(body, indent + 1, true)
),
BashNode::Group { body, closer } => {
format!("{ind}{{ \n{}{ind}}}{closer}{term}\n", Self::render(body, indent + 1, true))
}
}
}
}
#[inline]
pub fn getopts_reset_on_set_positional() -> bool {
posix_faithful() && dash_strict()
}
pub fn getopts_forget_reported() {
GETOPTS_REPORTED.store(-1, Ordering::Relaxed);
GETOPTS_INTERNAL.store(-1, Ordering::Relaxed);
}
fn bash_isprint(c: char) -> bool {
let u = c as u32;
!(u < 0x20 || u == 0x7f)
}
pub fn bash_ansic_shouldquote(s: &str) -> bool {
s.chars().any(|c| !bash_isprint(c))
}
pub fn bash_ansic_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
out.push_str("$'");
for c in s.chars() {
match c {
'\u{7}' => out.push_str("\\a"),
'\u{8}' => out.push_str("\\b"),
'\u{b}' => out.push_str("\\v"),
'\u{c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{1b}' => out.push_str("\\E"),
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
_ if bash_isprint(c) => out.push(c),
_ => {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
out.push_str(&format!("\\{:03o}", b));
}
}
}
}
out.push('\'');
out
}
pub fn bash_single_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
if c == '\'' {
out.push_str("'\\''");
} else {
out.push(c);
}
}
out.push('\'');
out
}
pub fn bash_double_quote(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
if matches!(c, '"' | '\\' | '$' | '`') {
out.push('\\');
}
out.push(c);
}
out.push('"');
out
}
pub fn bash_quote_reusable(s: &str) -> String {
if bash_ansic_shouldquote(s) {
bash_ansic_quote(s)
} else {
bash_single_quote(s)
}
}
pub fn bash_quote_element(s: &str) -> String {
if bash_ansic_shouldquote(s) {
bash_ansic_quote(s)
} else {
bash_double_quote(s)
}
}
pub fn bash_contains_shell_metas(s: &str) -> bool {
let b: Vec<char> = s.chars().collect();
for (i, &c) in b.iter().enumerate() {
match c {
' ' | '\t' | '\n' => return true,
'\'' | '"' | '\\' => return true,
'|' | '&' | ';' => return true,
'(' | ')' | '<' | '>' => return true,
'!' | '{' | '}' => return true,
'*' | '[' | '?' | ']' => return true,
'^' => return true,
'$' | '`' => return true,
'~' => {
if i == 0 || b[i - 1] == '=' || b[i - 1] == ':' {
return true;
}
}
'#' => {
if i == 0 {
return true;
}
}
_ => {}
}
}
false
}
pub fn bash_quote_assoc_key(s: &str) -> String {
if bash_ansic_shouldquote(s) {
bash_ansic_quote(s)
} else if bash_contains_shell_metas(s) {
bash_double_quote(s)
} else {
s.to_string()
}
}
pub enum BashParamShape<'a> {
Unset,
Scalar(&'a str),
Indexed(&'a [(usize, String)]),
Assoc(&'a [(String, String)]),
}
pub fn bash_assignment_string(name: &str, attrs: &str, shape: &BashParamShape) -> String {
let decl = if attrs.is_empty() {
String::new()
} else {
format!("declare -{} ", attrs)
};
match shape {
BashParamShape::Unset => format!("{}{}", decl, name),
BashParamShape::Scalar(v) => format!("{}{}={}", decl, name, bash_quote_reusable(v)),
BashParamShape::Indexed(items) => {
let body = items
.iter()
.map(|(i, v)| format!("[{}]={}", i, bash_quote_element(v)))
.collect::<Vec<_>>()
.join(" ");
format!("{}{}=({})", decl, name, body)
}
BashParamShape::Assoc(items) => {
let mut body = String::new();
for (k, v) in items.iter() {
body.push('[');
body.push_str(&bash_quote_assoc_key(k));
body.push_str("]=");
body.push_str(&bash_quote_element(v));
body.push(' ');
}
format!("{}{}=({})", decl, name, body)
}
}
}
pub fn bash_kvpair_string(shape: &BashParamShape) -> String {
match shape {
BashParamShape::Unset => String::new(),
BashParamShape::Scalar(v) => bash_quote_reusable(v),
BashParamShape::Indexed(items) => items
.iter()
.map(|(i, v)| format!("{} {}", i, bash_quote_element(v)))
.collect::<Vec<_>>()
.join(" "),
BashParamShape::Assoc(items) => {
let mut out = String::new();
for (k, v) in items.iter() {
out.push_str(&bash_quote_assoc_key(k));
out.push(' ');
out.push_str(&bash_quote_element(v));
out.push(' ');
}
out
}
}
}
pub fn bash_kv_words(shape: &BashParamShape) -> Vec<String> {
match shape {
BashParamShape::Unset => Vec::new(),
BashParamShape::Scalar(v) => vec![bash_quote_reusable(v)],
BashParamShape::Indexed(items) => items
.iter()
.flat_map(|(i, v)| [i.to_string(), v.clone()])
.collect(),
BashParamShape::Assoc(items) => items
.iter()
.flat_map(|(k, v)| [k.clone(), v.clone()])
.collect(),
}
}
pub fn bash_split_ifs_quote_aware(s: &str, ifs: Option<&str>) -> Vec<String> {
let seps: Vec<char> = match ifs {
Some(v) if !v.is_empty() => v.chars().collect(),
_ => vec![' ', '\t', '\n'],
};
let is_sep = |c: char| seps.contains(&c);
let is_ws_sep = |c: char| is_sep(c) && (c == ' ' || c == '\t' || c == '\n');
let cs: Vec<char> = s.chars().collect();
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
let mut started = false;
let mut depth = 0usize; let mut i = 0usize;
while i < cs.len() {
let c = cs[i];
if c == '(' {
depth += 1;
cur.push(c);
started = true;
i += 1;
continue;
}
if c == ')' {
depth = depth.saturating_sub(1);
cur.push(c);
started = true;
i += 1;
continue;
}
if c == '\'' {
cur.push(c);
started = true;
i += 1;
while i < cs.len() && cs[i] != '\'' {
cur.push(cs[i]);
i += 1;
}
if i < cs.len() {
cur.push('\'');
i += 1;
}
continue;
}
if c == '"' {
cur.push(c);
started = true;
i += 1;
while i < cs.len() && cs[i] != '"' {
if cs[i] == '\\' && i + 1 < cs.len() {
cur.push(cs[i]);
i += 1;
}
cur.push(cs[i]);
i += 1;
}
if i < cs.len() {
cur.push('"');
i += 1;
}
continue;
}
if c == '\\' && i + 1 < cs.len() {
cur.push(c);
cur.push(cs[i + 1]);
started = true;
i += 2;
continue;
}
if is_sep(c) && depth == 0 {
if started {
out.push(std::mem::take(&mut cur));
started = false;
}
if is_ws_sep(c) {
while i + 1 < cs.len() && is_ws_sep(cs[i + 1]) {
i += 1;
}
}
i += 1;
continue;
}
cur.push(c);
started = true;
i += 1;
}
if started {
out.push(cur);
}
out
}
pub fn bash_attr_letters(flags: u32) -> String {
use crate::ported::zsh_h::{
PM_ARRAY, PM_EXPORTED, PM_HASHED, PM_INTEGER, PM_LOWER, PM_READONLY, PM_UPPER,
};
let mut attrs = String::new();
if flags & PM_HASHED != 0 {
attrs.push('A');
} else if flags & PM_ARRAY != 0 {
attrs.push('a');
}
if flags & PM_INTEGER != 0 {
attrs.push('i');
}
if flags & PM_READONLY != 0 {
attrs.push('r');
}
if flags & PM_EXPORTED != 0 {
attrs.push('x');
}
if flags & PM_UPPER != 0 {
attrs.push('u');
}
if flags & PM_LOWER != 0 {
attrs.push('l');
}
attrs
}
pub fn bash_rejoin_elems(parts: &[String], dq_collapsed: bool, sep: Option<&str>) -> String {
if dq_collapsed {
crate::ported::utils::sepjoin(parts, sep)
} else {
parts.join(" ")
}
}
pub fn bash_param_flags(name: &str) -> u32 {
crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(name).map(|p| p.node.flags as u32))
.unwrap_or(0)
}
pub fn ksh93_printf_q(arg: &str) -> String {
if arg.is_empty() {
return "''".to_string();
}
if arg.chars().any(|c| c.is_control()) || arg.contains('\'') {
let mut out = String::from("$'");
for c in arg.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
c => out.push(c),
}
}
out.push('\'');
return out;
}
const SPECIAL: &str = " \"#$&()*;<>?[\\]`{|}~";
if arg.starts_with('=') || arg.chars().any(|c| SPECIAL.contains(c)) {
return format!("'{arg}'");
}
arg.to_string()
}
pub fn ksh93_print_escapes(s: &str) -> (String, bool) {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c != '\\' || i + 1 >= chars.len() {
out.push(c);
i += 1;
continue;
}
let simple = match chars[i + 1] {
'a' => Some('\x07'),
'b' => Some('\x08'),
'E' => Some('\x1b'),
'f' => Some('\x0c'),
'n' => Some('\n'),
'r' => Some('\r'),
't' => Some('\t'),
'v' => Some('\x0b'),
'\\' => Some('\\'),
_ => None,
};
if let Some(ch) = simple {
out.push(ch);
i += 2;
} else if chars[i + 1] == 'c' {
return (out, true);
} else if chars[i + 1] == '0' {
let digits: Vec<u32> = chars[i + 2..]
.iter()
.take(3)
.map_while(|d| d.to_digit(8))
.collect();
let byte = (digits.iter().fold(0, |acc, d| acc * 8 + d) & 0xff) as u8;
if byte < 0x80 {
out.push(byte as char);
} else {
out.push('\u{83}'); out.push(char::from(byte ^ 32));
}
i += 2 + digits.len();
} else if chars[i + 1] == 'u'
&& chars.len() >= i + 6
&& chars[i + 2..i + 6].iter().all(|d| d.is_ascii_hexdigit())
{
let hex: String = chars[i + 2..i + 6].iter().collect();
out.push(char::from_u32(u32::from_str_radix(&hex, 16).unwrap_or(0xfffd)).unwrap_or('\u{fffd}'));
i += 6;
} else {
out.push('\\');
i += 1;
}
}
(out, false)
}
static KEYWORD_FUNCS: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
pub fn note_function_spelling(name: &str, keyword: bool) {
let mut names = KEYWORD_FUNCS.lock().unwrap_or_else(|e| e.into_inner());
names.retain(|n| n != name);
if keyword {
names.push(name.to_string());
}
}
pub fn in_ksh93_keyword_function() -> bool {
ksh93_mode() && innermost_function_is_keyword_spelled() == Some(true)
}
pub fn in_ksh93_posix_function() -> bool {
ksh93_mode() && innermost_function_is_keyword_spelled() == Some(false)
}
fn innermost_function_is_keyword_spelled() -> Option<bool> {
let top = crate::ported::modules::parameter::FUNCSTACK
.lock()
.ok()
.and_then(|stk| {
stk.last()
.filter(|f| f.tp == crate::ported::zsh_h::FS_FUNC)
.map(|f| f.name.clone())
});
top.map(|name| {
KEYWORD_FUNCS
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(&name)
})
}
pub struct BashCmdsubstErrexit {
armed: bool,
}
impl BashCmdsubstErrexit {
pub fn enter() -> Self {
use crate::ported::zsh_h::{isset, ERREXIT};
let armed = bash_mode()
&& isset(ERREXIT)
&& !bash_shopt_get("inherit_errexit").unwrap_or(false);
if armed {
crate::ported::options::dosetopt(ERREXIT, 0, 1);
}
Self { armed }
}
}
impl Drop for BashCmdsubstErrexit {
fn drop(&mut self) {
if self.armed {
crate::ported::options::dosetopt(crate::ported::zsh_h::ERREXIT, 1, 1);
}
}
}
static OLDPWD_GLOBAL: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
pub fn set_oldpwd_global(val: &str) {
*OLDPWD_GLOBAL.lock().unwrap_or_else(|e| e.into_inner()) = Some(val.to_string());
}
pub fn oldpwd_global() -> Option<String> {
OLDPWD_GLOBAL.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn restore_oldpwd_global(val: Option<String>) {
*OLDPWD_GLOBAL.lock().unwrap_or_else(|e| e.into_inner()) = val;
}
pub fn oldpwd_global_get() -> Option<String> {
OLDPWD_GLOBAL.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
pub fn oldpwd_global_put(val: Option<String>) {
*OLDPWD_GLOBAL.lock().unwrap_or_else(|e| e.into_inner()) = val;
}
pub fn oldpwd_cd_target() -> Option<String> {
let param = || crate::ported::params::getsparam("OLDPWD");
if posix_faithful() {
return param();
}
OLDPWD_GLOBAL
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.or_else(param)
}
pub fn korn_subscript_blank(brct: i32, incmdpos: bool, lexbuf: &str) -> bool {
if brct <= 0 || !incmdpos || !(bash_mode() || korn_mode()) {
return false;
}
let name_len = lexbuf
.find(|c: char| !(c == '_' || c.is_ascii_alphanumeric()))
.unwrap_or(lexbuf.len());
name_len > 0
&& !lexbuf.starts_with(|c: char| c.is_ascii_digit())
&& lexbuf[name_len..].starts_with(crate::ported::zsh_h::Inbrack)
}
pub fn ksh93_round_ties_away(n: f64, conv: char, prec: Option<usize>) -> f64 {
if !ksh93_mode() || n == 0.0 || !n.is_finite() {
return n;
}
let sci = matches!(conv, 'e' | 'E' | 'g' | 'G');
let p = match conv {
'g' | 'G' => prec.unwrap_or(6).max(1) - 1,
_ => prec.unwrap_or(6),
};
let exact = if sci { format!("{:.*e}", p + 40, n) } else { format!("{:.*}", p + 40, n) };
let mantissa = exact.split('e').next().unwrap_or("");
let frac = mantissa.split_once('.').map_or("", |(_, f)| f);
let is_tie = frac.as_bytes().get(p) == Some(&b'5') && frac[p + 1..].bytes().all(|b| b == b'0');
if is_tie { f64::from_bits(n.to_bits() + 1) } else { n }
}
pub fn sparse_subscript_index(key: &str) -> Option<usize> {
let key = key.trim();
if let Ok(i) = key.parse::<usize>() {
return Some(i);
}
let pure = !key.is_empty()
&& key.chars().all(|c| c.is_ascii_alphanumeric() || " _+-*/%()".contains(c))
&& !key.contains("++")
&& !key.contains("--");
if !pure {
return None;
}
let v = crate::ported::math::mathevali(key).ok()?;
usize::try_from(v).ok()
}
pub fn ksh93_char_step_range(body: &str) -> bool {
if !ksh93_mode() {
return false;
}
let norm: String = body.chars().map(|c| if c == '\u{e19b}' { '-' } else { c }).collect();
let parts: Vec<&str> = norm.split("..").collect();
parts.len() == 3
&& parts[..2].iter().all(|p| p.chars().count() == 1)
&& parts[2].parse::<i64>().is_ok()
}
pub fn mksh_quote(v: &str) -> String {
if v.chars().any(|c| c.is_ascii_control()) {
let mut out = String::from("$'");
for c in v.chars() {
match c {
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\'' => out.push_str("\\'"),
'\\' => out.push_str("\\\\"),
c if c.is_ascii_control() => out.push_str(&format!("\\{:03o}", c as u32)),
c => out.push(c),
}
}
out.push('\'');
return out;
}
let bare = |c: char| !c.is_ascii() || c.is_ascii_alphanumeric() || "_-./:,+@%^!{}~".contains(c);
if !v.is_empty() && v.chars().all(bare) {
v.to_string()
} else {
format!("'{}'", v.replace('\'', "'\\''"))
}
}
pub fn pdksh_typeset_p(
name: &str,
flags: u32,
width: i32,
value: &str,
elems: Option<Vec<(usize, String)>>,
) -> Vec<String> {
use crate::ported::zsh_h::{
PM_EXPORTED, PM_INTEGER, PM_LEFT, PM_LOWER, PM_READONLY, PM_RIGHT_B, PM_RIGHT_Z, PM_TAGGED,
PM_UNIQUE, PM_UPPER,
};
if let Some(elems) = elems {
let mut lines = vec![format!("set -A {name}")];
lines.extend(elems.into_iter().map(|(i, v)| format!("typeset {name}[{i}]={}", mksh_quote(&v))));
return lines;
}
let mut attrs: Vec<String> = Vec::new();
for (flag, word) in [(PM_INTEGER, "-i"), (PM_EXPORTED, "-x"), (PM_READONLY, "-r"), (PM_TAGGED, "-t")] {
if flags & flag != 0 {
attrs.push(word.to_string());
}
}
if flags & PM_LEFT != 0 {
attrs.push(format!("-L{width}"));
}
if flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
attrs.push(format!("-R{width}"));
}
if flags & PM_RIGHT_Z != 0 {
attrs.push("-Z".to_string());
}
for (flag, word) in [(PM_LOWER, "-l"), (PM_UPPER, "-u"), (PM_UNIQUE, "-U")] {
if flags & flag != 0 {
attrs.push(word.to_string());
}
}
let head = if attrs.is_empty() { "typeset".to_string() } else { format!("typeset {}", attrs.join(" ")) };
let int_text = |s: &str| {
let (base, digits) = s.split_once('#').unwrap_or(("", s));
let digits_ok = |d: &str| !d.is_empty() && d.trim_start_matches('-').bytes().all(|b| b.is_ascii_alphanumeric());
digits_ok(digits) && (base.is_empty() || base.trim_start_matches('-').bytes().all(|b| b.is_ascii_digit()))
};
let shown = if flags & PM_INTEGER != 0 && int_text(value) { value.to_string() } else { mksh_quote(value) };
vec![format!("{head} {name}={shown}")]
}
pub fn mksh_at_q(v: &str) -> String {
if v.chars().any(|c| c.is_ascii_control()) {
format!("$'{}'", v.replace('\'', "\\'"))
} else {
mksh_quote(v)
}
}
pub fn ksh_version_string() -> &'static str {
if pdksh_family() {
"@(#)MIRBSD KSH R59 2020/10/31"
} else {
"Version AJM 93u+m/1.0.10 2024-08-01"
}
}
#[cfg(unix)]
pub fn dash_ulimit(argv: &[String]) -> i32 {
let limits: [(&str, libc::c_int, libc::rlim_t, char); 10] = [
("time(seconds)", libc::RLIMIT_CPU as _, 1, 't'),
("file(blocks)", libc::RLIMIT_FSIZE as _, 512, 'f'),
("data(kbytes)", libc::RLIMIT_DATA as _, 1024, 'd'),
("stack(kbytes)", libc::RLIMIT_STACK as _, 1024, 's'),
("coredump(blocks)", libc::RLIMIT_CORE as _, 512, 'c'),
("memory(kbytes)", libc::RLIMIT_RSS as _, 1024, 'm'),
("locked memory(kbytes)", libc::RLIMIT_MEMLOCK as _, 1024, 'l'),
("process", libc::RLIMIT_NPROC as _, 1, 'p'),
("nofiles", libc::RLIMIT_NOFILE as _, 1, 'n'),
("vmemory(kbytes)", libc::RLIMIT_AS as _, 1024, 'v'),
];
let (mut soft, mut hard, mut all) = (true, true, false);
let mut what = 'f';
let mut i = 0;
while let Some(arg) = argv.get(i) {
if arg == "--" {
i += 1;
break;
}
if !arg.starts_with('-') || arg.len() == 1 {
break;
}
for c in arg[1..].chars() {
match c {
'H' => (soft, hard) = (false, true),
'S' => (soft, hard) = (true, false),
'a' => all = true,
c if limits.iter().any(|l| l.3 == c) => what = c,
c => {
crate::ported::utils::zwarnnam("ulimit", &format!("Illegal option -{c}"));
return 2;
}
}
}
i += 1;
}
let value = argv.get(i);
let get = |res: libc::c_int| -> libc::rlimit {
let mut r = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
unsafe { libc::getrlimit(res as _, &mut r) };
r
};
let show = |r: &libc::rlimit, factor: libc::rlim_t| -> String {
let v = if soft { r.rlim_cur } else { r.rlim_max };
if v == libc::RLIM_INFINITY {
"unlimited".to_string()
} else {
(v / factor).to_string()
}
};
if let Some(v) = value {
if all || argv.get(i + 1).is_some() {
crate::ported::utils::zwarnnam("ulimit", "too many arguments");
return 2;
}
let (_, res, factor, _) = limits.iter().find(|l| l.3 == what).copied().unwrap();
let new = if v == "unlimited" {
libc::RLIM_INFINITY
} else if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) {
match v.parse::<libc::rlim_t>().ok().and_then(|n| n.checked_mul(factor)) {
Some(n) => n,
None => {
crate::ported::utils::zwarnnam("ulimit", "bad number");
return 2;
}
}
} else {
crate::ported::utils::zwarnnam("ulimit", "bad number");
return 2;
};
let mut r = get(res);
if soft {
r.rlim_cur = new;
}
if hard {
r.rlim_max = new;
}
if unsafe { libc::setrlimit(res as _, &r) } < 0 {
crate::ported::utils::zwarnnam(
"ulimit",
&format!("error setting limit ({})", std::io::Error::last_os_error()),
);
return 2;
}
return 0;
}
if all {
for (name, res, factor, _) in &limits {
println!("{:<20} {}", name, show(&get(*res), *factor));
}
} else {
let (_, res, factor, _) = limits.iter().find(|l| l.3 == what).copied().unwrap();
println!("{}", show(&get(res), factor));
}
0
}
pub fn dash_printf_keep_backslash_c(fmt: &str) -> String {
let mut out = String::with_capacity(fmt.len() + 2);
let mut chars = fmt.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('c') => out.push_str("\\\\c"),
Some(n) => {
out.push('\\');
out.push(n);
}
None => out.push('\\'),
}
}
out
}
pub fn dash_skip_operator_separators() -> bool {
use crate::ported::lex::{set_tok, tok, zshlex, LEX_ISNEWLIN};
use crate::ported::zsh_h::{ENDINPUT, LEXERR, NEWLIN, SEPER};
let faithful = dash_faithful();
while tok() == NEWLIN
|| (tok() == SEPER && (!faithful || LEX_ISNEWLIN.with(|c| c.get()) != 0))
{
zshlex();
}
if !faithful {
return true;
}
match tok() {
SEPER => crate::ported::utils::zerr("Syntax error: \";\" unexpected"),
ENDINPUT => crate::ported::utils::zerr("Syntax error: end of file unexpected"),
_ => return true,
}
set_tok(LEXERR);
false
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DashTok {
Eoi,
Operand,
FilRd,
FilWr,
FilEx,
FilExist,
FilReg,
FilDir,
FilCdev,
FilBdev,
FilFifo,
FilSuid,
FilSgid,
FilStck,
FilGz,
FilTt,
StrEz,
StrNz,
FilSym,
FilUid,
FilGid,
FilSock,
StrEq,
StrNe,
StrLt,
StrGt,
IntEq,
IntNe,
IntGe,
IntGt,
IntLe,
IntLt,
FilNt,
FilOt,
FilEq,
Not,
And,
Or,
LParen,
RParen,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DashOpType {
Unop,
Binop,
BUnop,
BBinop,
Paren,
}
const DASH_TEST_OPS: &[(&str, DashTok, DashOpType)] = &[
("-r", DashTok::FilRd, DashOpType::Unop),
("-w", DashTok::FilWr, DashOpType::Unop),
("-x", DashTok::FilEx, DashOpType::Unop),
("-e", DashTok::FilExist, DashOpType::Unop),
("-f", DashTok::FilReg, DashOpType::Unop),
("-d", DashTok::FilDir, DashOpType::Unop),
("-c", DashTok::FilCdev, DashOpType::Unop),
("-b", DashTok::FilBdev, DashOpType::Unop),
("-p", DashTok::FilFifo, DashOpType::Unop),
("-u", DashTok::FilSuid, DashOpType::Unop),
("-g", DashTok::FilSgid, DashOpType::Unop),
("-k", DashTok::FilStck, DashOpType::Unop),
("-s", DashTok::FilGz, DashOpType::Unop),
("-t", DashTok::FilTt, DashOpType::Unop),
("-z", DashTok::StrEz, DashOpType::Unop),
("-n", DashTok::StrNz, DashOpType::Unop),
("-h", DashTok::FilSym, DashOpType::Unop),
("-O", DashTok::FilUid, DashOpType::Unop),
("-G", DashTok::FilGid, DashOpType::Unop),
("-L", DashTok::FilSym, DashOpType::Unop),
("-S", DashTok::FilSock, DashOpType::Unop),
("=", DashTok::StrEq, DashOpType::Binop),
("!=", DashTok::StrNe, DashOpType::Binop),
("<", DashTok::StrLt, DashOpType::Binop),
(">", DashTok::StrGt, DashOpType::Binop),
("-eq", DashTok::IntEq, DashOpType::Binop),
("-ne", DashTok::IntNe, DashOpType::Binop),
("-ge", DashTok::IntGe, DashOpType::Binop),
("-gt", DashTok::IntGt, DashOpType::Binop),
("-le", DashTok::IntLe, DashOpType::Binop),
("-lt", DashTok::IntLt, DashOpType::Binop),
("-nt", DashTok::FilNt, DashOpType::Binop),
("-ot", DashTok::FilOt, DashOpType::Binop),
("-ef", DashTok::FilEq, DashOpType::Binop),
("!", DashTok::Not, DashOpType::BUnop),
("-a", DashTok::And, DashOpType::BBinop),
("-o", DashTok::Or, DashOpType::BBinop),
("(", DashTok::LParen, DashOpType::Paren),
(")", DashTok::RParen, DashOpType::Paren),
];
type DashOp = (&'static str, DashTok, DashOpType);
fn dash_getop(s: &str) -> Option<&'static DashOp> {
DASH_TEST_OPS.iter().find(|o| o.0 == s)
}
fn dash_getn(s: &str) -> Result<i64, String> {
let t = s.trim_start_matches(|c: char| c.is_ascii_whitespace());
let (neg, digits) = match t.as_bytes().first() {
Some(b'-') => (true, &t[1..]),
Some(b'+') => (false, &t[1..]),
_ => (false, t),
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(format!("Illegal number: {s}"));
}
let mag = digits.bytes().try_fold(0i64, |a, b| a.checked_mul(10)?.checked_add((b - b'0') as i64));
Ok(match (mag, neg) {
(Some(m), false) => m,
(Some(m), true) => -m,
(None, false) => i64::MAX,
(None, true) => i64::MIN,
})
}
struct DashTest {
argv: Vec<String>,
wp: usize,
wp_op: Option<&'static DashOp>,
}
impl DashTest {
fn at(&self, i: usize) -> Option<&str> {
self.argv.get(i).map(String::as_str)
}
fn isoperand(&self, tp: usize) -> bool {
let Some(s) = self.at(tp + 1) else { return true };
if self.at(tp + 2).is_none() {
return false;
}
dash_getop(s).is_some_and(|o| o.2 == DashOpType::Binop)
}
fn t_lex(&mut self, tp: usize) -> DashTok {
let Some(s) = self.at(tp) else {
self.wp_op = None;
return DashTok::Eoi;
};
if let Some(op) = dash_getop(s) {
if !(op.2 == DashOpType::Unop && self.isoperand(tp))
&& !(op.1 == DashTok::LParen && self.at(tp + 1).is_none())
{
self.wp_op = Some(op);
return op.1;
}
}
self.wp_op = None;
DashTok::Operand
}
fn oexpr(&mut self, mut n: DashTok) -> Result<bool, String> {
let mut res = false;
loop {
res |= self.aexpr(n)?;
if self.at(self.wp).is_none() {
break;
}
n = self.t_lex(self.wp + 1);
if n != DashTok::Or {
break;
}
self.wp += 2;
n = self.t_lex(self.wp);
}
Ok(res)
}
fn aexpr(&mut self, mut n: DashTok) -> Result<bool, String> {
let mut res = true;
loop {
if !self.nexpr(n)? {
res = false;
}
if self.at(self.wp).is_none() {
break;
}
n = self.t_lex(self.wp + 1);
if n != DashTok::And {
break;
}
self.wp += 2;
n = self.t_lex(self.wp);
}
Ok(res)
}
fn nexpr(&mut self, n: DashTok) -> Result<bool, String> {
if n != DashTok::Not {
return self.primary(n);
}
let n = self.t_lex(self.wp + 1);
if n != DashTok::Eoi {
self.wp += 1;
}
Ok(!self.nexpr(n)?)
}
fn primary(&mut self, n: DashTok) -> Result<bool, String> {
if n == DashTok::Eoi {
return Ok(false);
}
if n == DashTok::LParen {
self.wp += 1;
let nn = self.t_lex(self.wp);
if nn == DashTok::RParen {
return Ok(false);
}
let res = self.oexpr(nn)?;
self.wp += 1;
if self.t_lex(self.wp) != DashTok::RParen {
return Err("closing paren expected".to_string());
}
return Ok(res);
}
if let Some(op) = self.wp_op.filter(|o| o.2 == DashOpType::Unop) {
self.wp += 1;
let Some(arg) = self.at(self.wp).map(str::to_owned) else {
return Err(format!("{}: argument expected", op.0));
};
return Ok(match n {
DashTok::StrEz => arg.is_empty(),
DashTok::StrNz => !arg.is_empty(),
DashTok::FilTt => unsafe { libc::isatty(dash_getn(&arg)? as libc::c_int) != 0 },
DashTok::FilRd => dash_access(&arg, libc::R_OK),
DashTok::FilWr => dash_access(&arg, libc::W_OK),
DashTok::FilEx => dash_access(&arg, libc::X_OK),
_ => dash_filstat(&arg, n),
});
}
self.t_lex(self.wp + 1);
if self.wp_op.is_some_and(|o| o.2 == DashOpType::Binop) {
return self.binop();
}
Ok(self.at(self.wp).is_some_and(|s| !s.is_empty()))
}
fn binop(&mut self) -> Result<bool, String> {
let a = self.at(self.wp).unwrap_or("").to_owned();
self.wp += 1;
self.t_lex(self.wp);
let Some(op) = self.wp_op else { return Ok(false) };
self.wp += 1;
let Some(b) = self.at(self.wp).map(str::to_owned) else {
return Err(format!("{}: argument expected", op.0));
};
Ok(match op.1 {
DashTok::StrEq => a == b,
DashTok::StrNe => a != b,
DashTok::StrLt => dash_strcoll(&a, &b) < 0,
DashTok::StrGt => dash_strcoll(&a, &b) > 0,
DashTok::IntEq => dash_getn(&a)? == dash_getn(&b)?,
DashTok::IntNe => dash_getn(&a)? != dash_getn(&b)?,
DashTok::IntGe => dash_getn(&a)? >= dash_getn(&b)?,
DashTok::IntGt => dash_getn(&a)? > dash_getn(&b)?,
DashTok::IntLe => dash_getn(&a)? <= dash_getn(&b)?,
DashTok::IntLt => dash_getn(&a)? < dash_getn(&b)?,
DashTok::FilNt => dash_newerf(&a, &b),
DashTok::FilOt => dash_olderf(&a, &b),
DashTok::FilEq => dash_equalf(&a, &b),
_ => false,
})
}
}
fn dash_strcoll(a: &str, b: &str) -> i32 {
let (Ok(a), Ok(b)) = (std::ffi::CString::new(a), std::ffi::CString::new(b)) else {
return 0;
};
unsafe { libc::strcoll(a.as_ptr(), b.as_ptr()) }
}
fn dash_access(path: &str, mode: libc::c_int) -> bool {
let Ok(p) = std::ffi::CString::new(path) else { return false };
unsafe { libc::faccessat(libc::AT_FDCWD, p.as_ptr(), mode, libc::AT_EACCESS) == 0 }
}
fn dash_filstat(path: &str, mode: DashTok) -> bool {
use std::os::unix::fs::{FileTypeExt, MetadataExt};
let md = if mode == DashTok::FilSym {
std::fs::symlink_metadata(path)
} else {
std::fs::metadata(path)
};
let Ok(md) = md else { return false };
let ft = md.file_type();
let m = md.mode();
match mode {
DashTok::FilExist => true,
DashTok::FilReg => ft.is_file(),
DashTok::FilDir => ft.is_dir(),
DashTok::FilCdev => ft.is_char_device(),
DashTok::FilBdev => ft.is_block_device(),
DashTok::FilFifo => ft.is_fifo(),
DashTok::FilSock => ft.is_socket(),
DashTok::FilSym => ft.is_symlink(),
DashTok::FilSuid => m & libc::S_ISUID as u32 != 0,
DashTok::FilSgid => m & libc::S_ISGID as u32 != 0,
DashTok::FilStck => m & libc::S_ISVTX as u32 != 0,
DashTok::FilGz => md.size() != 0,
DashTok::FilUid => md.uid() == unsafe { libc::geteuid() },
DashTok::FilGid => md.gid() == unsafe { libc::getegid() },
_ => true,
}
}
fn dash_mtime(path: &str) -> Option<(i64, i64)> {
use std::os::unix::fs::MetadataExt;
std::fs::metadata(path).ok().map(|m| (m.mtime(), m.mtime_nsec()))
}
fn dash_newerf(f1: &str, f2: &str) -> bool {
let Some(a) = dash_mtime(f1) else { return false };
dash_mtime(f2).map_or(true, |b| a > b)
}
fn dash_olderf(f1: &str, f2: &str) -> bool {
let Some(b) = dash_mtime(f2) else { return false };
dash_mtime(f1).map_or(true, |a| a < b)
}
fn dash_equalf(f1: &str, f2: &str) -> bool {
use std::os::unix::fs::MetadataExt;
match (std::fs::metadata(f1), std::fs::metadata(f2)) {
(Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
_ => false,
}
}
pub fn dash_test(name: &str, args: &[String]) -> i32 {
let mut v: Vec<String> = Vec::with_capacity(args.len() + 1);
v.push(name.to_string());
v.extend(args.iter().cloned());
let mut argc = v.len();
if name.starts_with('[') {
argc -= 1;
if v[argc] != "]" {
crate::ported::utils::zwarnnam(name, "missing ]");
return 2;
}
v.truncate(argc);
}
let mut t = DashTest { argv: v, wp: 0, wp_op: None };
let mut res = true; let mut base = 0usize;
let n;
loop {
base += 1;
argc -= 1;
if argc < 1 {
return i32::from(res);
}
if argc == 3
&& t.at(base + 1).and_then(dash_getop).is_some_and(|o| o.2 == DashOpType::Binop)
{
n = DashTok::Operand;
break;
}
if argc == 3 || argc == 4 {
if t.at(base) == Some("(") && t.at(base + argc - 1) == Some(")") {
t.argv.truncate(base + argc - 1);
base += 1;
argc -= 1;
} else if t.at(base) == Some("!") {
res = false;
continue;
}
}
n = t.t_lex(base);
break;
}
t.wp = base;
let r = match t.oexpr(n) {
Ok(r) => r,
Err(msg) => {
crate::ported::utils::zwarnnam(name, &msg);
return 2;
}
};
let status = res ^ r;
if t.at(t.wp).is_some() && t.at(t.wp + 1).is_some() {
crate::ported::utils::zwarnnam(name, &format!("{}: unexpected operator", t.argv[t.wp]));
return 2;
}
i32::from(status)
}