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);
#[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_bad_function_name(name: &str) -> bool {
const SPECIAL: &[&str] = &[
":", ".", "break", "continue", "eval", "exec", "exit", "export",
"readonly", "return", "set", "shift", "times", "trap", "unset",
];
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()) || SPECIAL.contains(&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);
}
#[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 = "2";
pub const BASH_VERSION_PATCH: &str = "0";
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(),
std::env::consts::ARCH.to_string(),
]
}
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 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", None, 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() {
bash_shopt_set(name, *default_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> },
}
struct BashDeparse {
lines: Vec<String>,
pos: usize,
}
const ARM_END: char = '\u{1}';
impl BashDeparse {
fn parse(body_lines: &str) -> Option<Vec<BashNode>> {
let mut lines: Vec<String> = Vec::new();
for raw in body_lines.lines() {
let t = raw.trim();
if t.contains("<<") || (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() { "" } 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) => {
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 {
if !ksh93_mode() {
return false;
}
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.is_some_and(|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_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)
}