use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicUsize, Ordering};
use std::sync::Mutex;
use crate::ported::zsh_h::histent;
pub use crate::zsh_h::{CASMOD_CAPS, CASMOD_LOWER, CASMOD_NONE, CASMOD_UPPER};
use crate::zsh_h::{
isset,
BANGHIST, HFILE_FAST, HFILE_USE_OPTIONS,
HISTIGNOREALLDUPS, HISTIGNOREDUPS, HISTIGNORESPACE,
HISTNOFUNCTIONS, HISTNOSTORE, HISTREDUCEBLANKS,
INCAPPENDHISTORY, INCAPPENDHISTORYTIME, INTERACTIVE,
SHAREHISTORY, SHINSTDIN,
};
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::path::Component::*;
pub const HA_ACTIVE: u32 = 1 << 0; pub const HA_NOINC: u32 = 1 << 1; pub const HA_INWORD: u32 = 1 << 2; pub const HA_UNGET: u32 = 1 << 3;
static defev: AtomicI64 = AtomicI64::new(0);
static hist_keep_comment: AtomicI32 = AtomicI32::new(0);
static histsave_stack_size: AtomicI32 = AtomicI32::new(0);
static histsave_stack_pos: AtomicI32 = AtomicI32::new(0);
static histfile_linect: AtomicI64 = AtomicI64::new(0);
pub fn hist_context_save(hs: &mut crate::ported::zsh_h::hist_stack, toplevel: i32) { if toplevel != 0 { *zle_chline.lock().unwrap() = Some(chline.lock().unwrap().clone()); }
hs.histactive = histactive.load(Ordering::SeqCst) as i32; hs.histdone = histdone.load(Ordering::SeqCst); hs.stophist = stophist.load(Ordering::SeqCst); hs.hline = Some(chline.lock().unwrap().clone()); hs.hptr = Some(hptr.load(Ordering::SeqCst).to_string()); hs.chwords = chwords.lock().unwrap().clone(); hs.chwordlen = chwordlen.load(Ordering::SeqCst); hs.chwordpos = chwordpos.load(Ordering::SeqCst); hs.hlinesz = hlinesz.load(Ordering::SeqCst); hs.defev = defev.load(Ordering::SeqCst); hs.hist_keep_comment = hist_keep_comment.load(Ordering::SeqCst); hs.csp = 0;
stophist.store(0, Ordering::SeqCst); chline.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst); }
pub fn hist_context_restore(hs: &crate::ported::zsh_h::hist_stack, toplevel: i32) { if toplevel != 0 { *zle_chline.lock().unwrap() = None; }
histactive.store(hs.histactive as u32, Ordering::SeqCst); histdone.store(hs.histdone, Ordering::SeqCst); stophist.store(hs.stophist, Ordering::SeqCst); *chline.lock().unwrap() = hs.hline.clone().unwrap_or_default(); hptr.store(hs.hptr.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0), Ordering::SeqCst);
*chwords.lock().unwrap() = hs.chwords.clone(); chwordlen.store(hs.chwordlen, Ordering::SeqCst); chwordpos.store(hs.chwordpos, Ordering::SeqCst); hlinesz.store(hs.hlinesz, Ordering::SeqCst); defev.store(hs.defev, Ordering::SeqCst); hist_keep_comment.store(hs.hist_keep_comment, Ordering::SeqCst); }
pub fn hist_in_word(yesno: i32) {
if yesno != 0 {
histactive.fetch_or(HA_INWORD, Ordering::SeqCst);
} else {
histactive.fetch_and(!HA_INWORD, Ordering::SeqCst);
}
}
pub fn hist_is_in_word() -> i32 {
if (histactive.load(Ordering::SeqCst) & HA_INWORD) != 0 { 1 } else { 0 }
}
pub fn ihwaddc(c: i32) { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
if crate::ported::utils::errflag.load(Ordering::SeqCst) != 0 || lexstop.load(Ordering::SeqCst) {
return;
}
let inbufflags = crate::ported::input::inbufflags.with(|f| f.get());
if (inbufflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS {
return;
}
let chline_empty = chline.lock().unwrap().is_empty();
if chline_empty {
return;
}
let bc = bangchar.load(Ordering::SeqCst);
let qbang_active =
c == bc && stophist.load(Ordering::SeqCst) < 2 && qbang.load(Ordering::SeqCst);
{
let mut buf = chline.lock().expect("chline poisoned");
let bytes = unsafe { buf.as_mut_vec() };
let mut pos = hptr.load(Ordering::SeqCst);
if qbang_active {
if pos < bytes.len() { bytes[pos] = b'\\'; } else { while bytes.len() < pos { bytes.push(0); } bytes.push(b'\\'); }
pos += 1;
}
if pos < bytes.len() { bytes[pos] = c as u8; }
else { while bytes.len() < pos { bytes.push(0); } bytes.push(c as u8); }
pos += 1;
hptr.store(pos, Ordering::SeqCst);
}
let cur_off = hptr.load(Ordering::SeqCst) as i32; let sz = hlinesz.load(Ordering::SeqCst);
if cur_off >= sz { let new_sz = sz + 64;
hlinesz.store(new_sz, Ordering::SeqCst); }
}
pub fn iaddtoline(c: i32) { use crate::ported::ztype_h::itok;
if expanding.load(Ordering::SeqCst) == 0
|| lexstop.load(Ordering::SeqCst)
{
return;
}
let bc = bangchar.load(Ordering::SeqCst);
if qbang.load(Ordering::SeqCst)
&& c == bc
&& stophist.load(Ordering::SeqCst) < 2
{
exlast.fetch_sub(1, Ordering::SeqCst); chline.lock().unwrap().push('\\'); }
let zlemetacs_v =
crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst);
let excs_v = excs.load(Ordering::SeqCst);
if excs_v > zlemetacs_v { let inbufct_now = crate::ported::input::inbufct.with(|c| c.get());
let exlast_v = exlast.load(Ordering::SeqCst);
let mut new_excs = excs_v + 1 + inbufct_now - exlast_v; if new_excs < zlemetacs_v { new_excs = zlemetacs_v; }
excs.store(new_excs, Ordering::SeqCst);
}
let inbufct_v = crate::ported::input::inbufct.with(|cnt| cnt.get());
exlast.store(inbufct_v, Ordering::SeqCst); let push_byte: u8 = if c >= 0 && c <= 0xff && itok(c as u8) {
let idx = (c as u8).wrapping_sub(crate::ported::zsh_h::Pound as u8) as usize;
crate::ported::lex::ztokens
.bytes()
.nth(idx)
.unwrap_or(c as u8)
} else {
c as u8
};
chline.lock().unwrap().push(push_byte as char); }
pub fn safeinungetc(c: i32) { if lexstop.load(Ordering::SeqCst) { lexstop.store(false, Ordering::SeqCst); } else { if let Some(ch) = char::from_u32(c as u32) { crate::ported::input::inungetc(ch);
}
}
}
pub fn ihgetc() -> i32 { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
let mut c: i32 = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if exit_pending.load(Ordering::SeqCst) { lexstop.store(true, Ordering::SeqCst); crate::ported::utils::errflag.fetch_or( crate::ported::utils::ERRFLAG_ERROR,
Ordering::SeqCst,
);
return b' ' as i32; }
qbang.store(false, Ordering::SeqCst); let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
if stophist.load(Ordering::SeqCst) == 0 && (inbufflags_v & INP_ALIAS) == 0 {
c = histsubchar(c); if c < 0 { lexstop.store(true, Ordering::SeqCst); crate::ported::utils::errflag.fetch_or( crate::ported::utils::ERRFLAG_ERROR,
Ordering::SeqCst,
);
return b' ' as i32; }
}
let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
let bc = bangchar.load(Ordering::SeqCst);
if (inbufflags_v & INP_HIST) != 0 && stophist.load(Ordering::SeqCst) == 0 { qbang.store(false, Ordering::SeqCst);
if c == b'\\' as i32 { let g = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if g == bc { qbang.store(true, Ordering::SeqCst);
c = g;
} else {
safeinungetc(g);
c = b'\\' as i32;
}
}
} else if stophist.load(Ordering::SeqCst) != 0 || (inbufflags_v & INP_ALIAS) != 0 {
let v = c == bc && stophist.load(Ordering::SeqCst) < 2;
qbang.store(v, Ordering::SeqCst);
}
ihwaddc(c); iaddtoline(c); c }
pub static hatchar: AtomicI32 = AtomicI32::new(b'^' as i32);
pub static hashchar: AtomicI32 = AtomicI32::new(b'#' as i32);
pub static marg: AtomicI32 = AtomicI32::new(-1);
pub static mev: AtomicI64 = AtomicI64::new(-1);
pub fn histsubchar(c_in: i32) -> i32 { use crate::ported::zsh_h::{CSHJUNKIEHISTORY, HISTVERIFY};
let mut c: i32 = c_in;
let mut farg: i32; let mut evset: i32 = -1; let mut larg: i32;
let mut argc: i32; let mut cflag: i32 = 0; let mut bflag: i32 = 0; let mut ev: i64; let mut buf: String; let mut sline: String; let lexraw_mark: i32 = 0;
let hat = hatchar.load(Ordering::SeqCst);
if crate::ported::lex::LEX_ISFIRSTCH.with(|f| f.get()) && c == hat { let mut gbal: i32 = 0; crate::ported::lex::LEX_ISFIRSTCH.with(|f| f.set(false)); if let Some(ch) = char::from_u32(hat as u32) {
crate::ported::input::inungetc(ch); }
let ehist = match gethist(defev.load(Ordering::SeqCst)) { Some(h) => h,
None => return -1, };
let argc_local = getargc(&ehist) as usize;
sline = match getargs(&ehist, 0, argc_local.saturating_sub(0)) { Some(s) => s,
None => return -1, };
if getsubsargs(&sline, &mut gbal, &mut cflag) != 0 { return substfailed(); }
if hsubl.lock().unwrap().is_none() { return -1; }
let in_pat = hsubl.lock().unwrap().clone().unwrap_or_default();
let out_pat = hsubr.lock().unwrap().clone().unwrap_or_default();
let new = subst(&sline, &in_pat, &out_pat, gbal != 0); if new == sline { return substfailed(); }
sline = new;
} else {
if c != b' ' as i32 { crate::ported::lex::LEX_ISFIRSTCH.with(|f| f.set(false)); }
let bc = bangchar.load(Ordering::SeqCst);
if c == b'\\' as i32 { let g = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if g != bc { safeinungetc(g); } else { qbang.store(true, Ordering::SeqCst); return bc; }
}
if c != bc { return c; }
let pos = hptr.load(Ordering::SeqCst); {
let mut cl = chline.lock().unwrap();
if pos < cl.len() {
cl.truncate(pos);
}
}
c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'{' as i32 { bflag = 1; cflag = 1;
c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
if c == b'"' as i32 { stophist.store(1, Ordering::SeqCst); return crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
let is_blank = (c as u8 as char).is_ascii_whitespace();
if (cflag == 0 && is_blank)
|| c == b'=' as i32
|| c == b'(' as i32
|| lexstop.load(Ordering::SeqCst) {
safeinungetc(c); return bc; }
cflag = 0; let mut buflen: usize = 265; buf = String::with_capacity(buflen);
crate::ported::signals::queue_signals(); if c == b'?' as i32 { loop { c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'?' as i32 || c == b'\n' as i32 || lexstop.load(Ordering::SeqCst) { break; } else {
buf.push(c as u8 as char); if buf.len() >= buflen { buflen *= 2; buf.reserve(buflen);
}
}
}
if c != b'\n' as i32 && !lexstop.load(Ordering::SeqCst) { c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
*hsubl.lock().unwrap() = Some(buf.clone()); let (ev_val, marg_val) = match hconsearch(&buf) { Some((e, m)) => (e, m),
None => (-1, -1),
};
ev = ev_val;
mev.store(ev, Ordering::SeqCst); marg.store(marg_val, Ordering::SeqCst); evset = 0; if ev == -1 { herrflush(); crate::ported::signals::unqueue_signals(); crate::ported::utils::zerr(&format!("no such event: {}", buf)); return -1; }
} else { loop {
let is_term = (c as u8 as char).is_ascii_whitespace()
|| c == b';' as i32 || c == b':' as i32 || c == b'^' as i32
|| c == b'$' as i32 || c == b'*' as i32 || c == b'%' as i32
|| c == b'}' as i32 || c == b'\'' as i32 || c == b'"' as i32
|| c == b'`' as i32 || lexstop.load(Ordering::SeqCst); if is_term { break; }
if !buf.is_empty() { if c == b'-' as i32 { break; } let first = buf.as_bytes()[0];
if (first.is_ascii_digit() || first == b'-') && !(c as u8).is_ascii_digit() {
break; }
}
buf.push(c as u8 as char); if buf.len() >= buflen { buflen *= 2; buf.reserve(buflen);
}
if c == b'#' as i32 || c == bc { c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
break; }
c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
if buf.is_empty() && (c == b'}' as i32 || c == b';' as i32 || c == b'\'' as i32
|| c == b'"' as i32 || c == b'`' as i32)
{
safeinungetc(c); crate::ported::signals::unqueue_signals(); return bc; }
if buf.is_empty() { if c != b'%' as i32 { if isset(CSHJUNKIEHISTORY) { ev = addhistnum(curhist.load(Ordering::SeqCst), -1, HIST_FOREIGN as i32);
} else { ev = defev.load(Ordering::SeqCst); }
if c == b':' as i32 && evset == -1 { evset = 0; } else {
evset = 1; }
} else { if marg.load(Ordering::SeqCst) != -1 { ev = mev.load(Ordering::SeqCst); } else { ev = defev.load(Ordering::SeqCst); }
evset = 0; }
} else if let Ok(t0) = buf.trim().parse::<i64>() { if t0 != 0 {
ev = if t0 < 0 { addhistnum(curhist.load(Ordering::SeqCst), t0 as i32, HIST_FOREIGN as i32)
} else {
t0
};
evset = 1; } else if buf.as_bytes()[0] == bc as u8 { ev = addhistnum(curhist.load(Ordering::SeqCst), -1, HIST_FOREIGN as i32); evset = 1; } else if buf.as_bytes()[0] == b'#' { ev = curhist.load(Ordering::SeqCst); evset = 1; } else { match hcomsearch(&buf) { Some(e) => { ev = e; evset = 1; }
None => {
herrflush(); crate::ported::signals::unqueue_signals(); crate::ported::utils::zerr(&format!("event not found: {}", buf)); return -1; }
}
}
} else if buf.as_bytes()[0] == bc as u8 { ev = addhistnum(curhist.load(Ordering::SeqCst), -1, HIST_FOREIGN as i32);
evset = 1;
} else if buf.as_bytes()[0] == b'#' { ev = curhist.load(Ordering::SeqCst);
evset = 1;
} else { match hcomsearch(&buf) {
Some(e) => { ev = e; evset = 1; }
None => {
herrflush();
crate::ported::signals::unqueue_signals();
crate::ported::utils::zerr(&format!("event not found: {}", buf));
return -1;
}
}
}
}
defev.store(ev, Ordering::SeqCst); let mut ehist = match gethist(ev) { Some(h) => h,
None => {
crate::ported::signals::unqueue_signals(); return -1; }
};
argc = getargc(&ehist) as i32;
if c == b':' as i32 { cflag = 1; c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'%' as i32 && marg.load(Ordering::SeqCst) != -1 { if evset == 0 { ehist = match gethist(mev.load(Ordering::SeqCst)) { Some(h) => { defev.store(mev.load(Ordering::SeqCst), Ordering::SeqCst); h }
None => {
crate::ported::signals::unqueue_signals();
return -1;
}
};
argc = getargc(&ehist) as i32; } else { herrflush(); crate::ported::signals::unqueue_signals(); crate::ported::utils::zerr("ambiguous history reference"); return -1; }
}
}
if c == b'*' as i32 { farg = 1; larg = argc; cflag = 0; } else { if let Some(ch) = char::from_u32(c as u32) {
crate::ported::input::inungetc(ch); }
let r = getargspec(argc, marg.load(Ordering::SeqCst), evset); larg = r; farg = r;
if larg == -2 { crate::ported::signals::unqueue_signals(); return -1; }
if farg != -1 { cflag = 0; }
c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'*' as i32 { cflag = 0; larg = argc; } else if c == b'-' as i32 { cflag = 0; larg = getargspec(argc, marg.load(Ordering::SeqCst), evset); if larg == -2 { crate::ported::signals::unqueue_signals(); return -1; }
if larg == -1 { larg = argc - 1; }
} else { if let Some(ch) = char::from_u32(c as u32) {
crate::ported::input::inungetc(ch); }
}
}
if farg == -1 { farg = 0; }
if larg == -1 { larg = argc; }
sline = match getargs(&ehist, farg as usize, larg as usize) { Some(s) => s,
None => {
crate::ported::signals::unqueue_signals(); return -1; }
};
crate::ported::signals::unqueue_signals(); }
loop {
c = if cflag != 0 { b':' as i32 } else { crate::ported::input::ingetc()
.map(|ch| ch as i32)
.unwrap_or(-1)
};
cflag = 0; if c == b':' as i32 { let mut gbal: i32 = 0; c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'g' as i32 { gbal = 1; c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c != b's' as i32 && c != b'S' as i32 && c != b'&' as i32 { crate::ported::utils::zerr("'s' or '&' modifier expected after 'g'"); return -1; }
}
match c as u8 {
b'p' => { histdone.store(HISTFLAG_DONE | HISTFLAG_NOEXEC, Ordering::SeqCst); }
b'a' => { match chabspath(&sline) { Some(new) => sline = new,
None => {
herrflush(); crate::ported::utils::zerr("modifier failed: a"); return -1; }
}
}
b'A' => { match chrealpath(&sline) { Some(new) => sline = new,
None => {
herrflush(); crate::ported::utils::zerr("modifier failed: A"); return -1; }
}
}
b'c' => { match crate::ported::subst::equalsubstr(&sline, false, false) { Some(new) => sline = new,
None => {
herrflush(); crate::ported::utils::zerr("modifier failed: c"); return -1; }
}
}
b'h' => { let count = digitcount(); sline = remtpath(&sline, count);
}
b'e' => { sline = rembutext(&sline); }
b'r' => { sline = remtext(&sline); }
b't' => { let count = digitcount(); sline = remlpaths(&sline, count);
}
b's' | b'S' => { hsubpatopt.store((c == b'S' as i32) as i32, Ordering::SeqCst); if getsubsargs(&sline, &mut gbal, &mut cflag) != 0 { return -1; }
let (in_pat, out_pat) = (
hsubl.lock().unwrap().clone(),
hsubr.lock().unwrap().clone(),
);
if let (Some(ip), Some(op)) = (in_pat, out_pat) { let new = subst(&sline, &ip, &op, gbal != 0); if new == sline { return substfailed(); }
sline = new;
} else { herrflush(); crate::ported::utils::zerr("no previous substitution"); return -1; }
}
b'&' => { let (in_pat, out_pat) = (
hsubl.lock().unwrap().clone(),
hsubr.lock().unwrap().clone(),
);
if let (Some(ip), Some(op)) = (in_pat, out_pat) {
let new = subst(&sline, &ip, &op, gbal != 0);
if new == sline { return substfailed(); }
sline = new;
} else {
herrflush();
crate::ported::utils::zerr("no previous substitution");
return -1;
}
}
b'q' => { sline = quote(&sline); }
b'Q' => { let oef = crate::ported::utils::errflag.load(Ordering::SeqCst);
let _ = crate::ported::lex::parse_subst_string(&sline); crate::ported::utils::errflag.store(
oef | (crate::ported::utils::errflag.load(Ordering::SeqCst)
& crate::ported::zsh_h::ERRFLAG_INT),
Ordering::SeqCst,
); let mut s = sline.clone();
crate::ported::glob::remnulargs(&mut s); sline = crate::ported::lex::untokenize(&s); }
b'x' => { sline = quotebreak(&sline); }
b'l' => { sline = casemodify(&sline, CASMOD_LOWER); }
b'u' => { sline = casemodify(&sline, CASMOD_UPPER); }
b'P' => { if !sline.starts_with('/') { if let Some(here) = crate::ported::utils::zgetcwd() { sline = if here.ends_with('/') {
crate::ported::utils::dyncat(&here, &sline) } else {
format!("{}/{}", here, sline)
};
}
}
match crate::ported::utils::xsymlink(&sline) { Some(new) => sline = new,
None => {} }
}
_ => { herrflush(); crate::ported::utils::zerr(&format!("illegal modifier: {}", c as u8 as char)); return -1; }
}
} else { if c != b'}' as i32 || bflag == 0 { if let Some(ch) = char::from_u32(c as u32) {
crate::ported::input::inungetc(ch); }
}
if c != b'}' as i32 && bflag != 0 { crate::ported::utils::zerr("'}' expected"); return -1; }
break; }
}
let _ = lexraw_mark;
lexstop.store(false, Ordering::SeqCst); crate::ported::input::inpush(&sline, crate::ported::zsh_h::INP_HIST, None); histdone.fetch_or(HISTFLAG_DONE, Ordering::SeqCst); if isset(HISTVERIFY) { histdone.fetch_or(HISTFLAG_NOEXEC | HISTFLAG_RECALL, Ordering::SeqCst); }
crate::ported::input::ingetc()
.map(|ch| ch as i32)
.unwrap_or(-1)
}
pub fn herrflush() { crate::ported::input::inpopalias();
if crate::ported::lex::LEX_LEXSTOP.with(|f| f.get()) {
return;
}
loop {
let inbufct = crate::ported::input::inbufct.with(|c| c.get());
if inbufct <= 0 {
break;
}
let strin_v = strin.load(Ordering::SeqCst);
let lex_add_raw = crate::ported::lex::LEX_LEX_ADD_RAW.get();
if !(strin_v == 0 || lex_add_raw != 0) { break;
}
let c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if !crate::ported::lex::LEX_LEXSTOP.with(|f| f.get()) { ihwaddc(c); iaddtoline(c); }
}
}
pub fn getargc(entry: &histent) -> usize {
entry.nwords as usize
}
pub fn substfailed() -> i32 { herrflush(); crate::ported::utils::zerr("substitution failed"); -1 }
pub fn digitcount() -> i32 { let mut c: i32 = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
let mut count: i32;
if c >= 0 && (c as u8 as char).is_ascii_digit() { count = 0; loop {
count = 10 * count + (c - b'0' as i32); c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c < 0 || !(c as u8 as char).is_ascii_digit() { break;
}
}
} else {
count = 0; }
if c >= 0 {
if let Some(ch) = char::from_u32(c as u32) { crate::ported::input::inungetc(ch);
}
}
count }
pub fn strinbeg(dohist: i32) { strin.fetch_add(1, Ordering::SeqCst); hbegin(dohist); crate::ported::lex::lexinit(); crate::ported::parse::init_parse_status(); }
pub fn strinend() { hend(None); strin.fetch_sub(1, Ordering::SeqCst); crate::ported::lex::LEX_ISFIRSTCH.with(|f| f.set(true)); histdone.store(0, Ordering::SeqCst); }
pub fn nohw(_c: i32) { }
pub fn nohwabort() { }
pub fn nohwe() { }
pub fn ihwbegin(offset: i32) { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
let hptr_val = hptr.load(Ordering::SeqCst); let stop = stophist.load(Ordering::SeqCst);
let active = histactive.load(Ordering::SeqCst);
let inflags = crate::ported::input::inbufflags.with(|f| f.get());
if stop == 2
|| (active & HA_INWORD) != 0
|| (inflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS {
return;
}
let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 != 0 { chwordpos.fetch_sub(1, Ordering::SeqCst); }
let start = ((hptr_val as i32) + offset).max(0) as i16; let mut words = chwords.lock().unwrap();
let idx = chwordpos.load(Ordering::SeqCst) as usize;
if words.len() <= idx {
words.resize(idx + 1, 0);
}
words[idx] = start; chwordpos.fetch_add(1, Ordering::SeqCst); }
pub fn linkcurline() { let new_hist = curhist.fetch_add(1, Ordering::SeqCst) + 1; let mut cur = curline.lock().unwrap();
*cur = Some(make_histent(new_hist, String::new())); }
pub fn unlinkcurline() { *curline.lock().unwrap() = None; curhist.fetch_sub(1, Ordering::SeqCst); }
pub fn hbegin(dohist: i32) {
crate::ported::utils::errflag.fetch_and( !crate::ported::utils::ERRFLAG_ERROR,
Ordering::Relaxed,
);
histdone.store(0, Ordering::SeqCst); let interact = isset(INTERACTIVE);
let shinstdin = isset(SHINSTDIN);
if dohist == 0 { stophist.store(2, Ordering::SeqCst); } else if dohist != 2 { stophist.store(if !interact || !shinstdin { 2 } else { 0 }, Ordering::SeqCst);
} else { stophist.store(0, Ordering::SeqCst); }
if stophist.load(Ordering::SeqCst) == 2 { chline.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); hlinesz.store(0, Ordering::SeqCst); chwords.lock().unwrap().clear(); chwordlen.store(0, Ordering::SeqCst); } else { let mut buf = chline.lock().unwrap(); buf.clear();
buf.reserve(64);
hlinesz.store(64, Ordering::SeqCst); drop(buf);
let mut w = chwords.lock().unwrap(); w.clear();
w.reserve(64);
chwordlen.store(64, Ordering::SeqCst);
drop(w);
if !isset(BANGHIST) { stophist.store(4, Ordering::SeqCst); }
}
chwordpos.store(0, Ordering::SeqCst);
{ let mut ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first_mut() {
if top.ftim == 0 && strin.load(Ordering::SeqCst) == 0 {
top.ftim = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0); }
}
}
if (dohist == 2 || (interact && shinstdin)) && strin.load(Ordering::SeqCst) == 0
{
histactive.store(HA_ACTIVE, Ordering::SeqCst); let mypgrp = *crate::ported::jobs::MYPGRP
.get_or_init(|| std::sync::Mutex::new(0))
.lock()
.expect("mypgrp poisoned");
crate::ported::utils::attachtty(mypgrp); linkcurline(); defev.store(addhistnum(curhist.load(Ordering::SeqCst), -1, HIST_FOREIGN as i32),
Ordering::SeqCst);
} else {
histactive.store(HA_ACTIVE | HA_NOINC, Ordering::SeqCst); }
if isset(INCAPPENDHISTORYTIME) && !isset(SHAREHISTORY)
&& !isset(INCAPPENDHISTORY)
&& (histactive.load(Ordering::SeqCst) & HA_NOINC) == 0
&& strin.load(Ordering::SeqCst) == 0
&& histsave_stack_pos.load(Ordering::SeqCst) == 0
{
let hf = resolve_histfile(); savehistfile(hf.as_deref(), 0 | HFILE_USE_OPTIONS as i32
| HFILE_FAST as i32);
}
}
pub fn histreduceblanks(text: &str) -> String {
#[inline]
fn is_inblank_narrow(c: char) -> bool {
c == ' ' || c == '\t'
}
let mut result = String::with_capacity(text.len());
let mut prev_space = false;
for c in text.chars() {
if is_inblank_narrow(c) {
if !prev_space { result.push(' '); prev_space = true; }
} else {
result.push(c); prev_space = false;
}
}
let mut s = result;
while s.ends_with(' ') { s.pop(); }
while s.starts_with(' ') { s.remove(0); }
s
}
pub fn histremovedups() { let mut ring = hist_ring.lock().unwrap();
ring.retain(|h| (h.node.flags as u32 & HIST_DUP) == 0); let new_ct = ring.len() as i64;
drop(ring);
histlinect.store(new_ct, Ordering::SeqCst);
}
pub fn addhistnum(hl: i64, mut n: i32, xflags: i32) -> i64 { let dir: i32 = if n < 0 { -1 } else if n > 0 { 1 } else { 0 }; let he = gethistent(hl, dir); let he = match he {
None => return 0, Some(h) => h,
};
if he != hl { n -= dir; }
let final_he = if n != 0 { movehistent(he, n, xflags as u32) } else {
Some(he)
};
match final_he { None => {
if dir < 0 { firsthist() - 1
} else {
curhist.load(Ordering::SeqCst) + 1
}
}
Some(h) => h, }
}
pub fn movehistent(start: i64, mut n: i32, xflags: u32) -> Option<i64> { let mut cur = start;
while n < 0 { cur = up_histent(cur)?; if let Some(e) = ring_get(cur) {
if (e.node.flags as u32 & xflags) == 0 { n += 1; }
}
}
while n > 0 { cur = down_histent(cur)?; if let Some(e) = ring_get(cur) {
if (e.node.flags as u32 & xflags) == 0 { n -= 1; }
}
}
if let Some(e) = ring_get(cur) {
checkcurline(&e); }
Some(cur) }
pub fn up_histent(current: i64) -> Option<i64> { let pos = ring_position(current)?; (pos + 1 < ring_len()).then(|| ring_at(pos + 1)) }
pub fn down_histent(current: i64) -> Option<i64> { let pos = ring_position(current)?;
(pos > 0).then(|| ring_at(pos - 1)) }
pub fn gethistent(ev: i64, nearmatch: i32) -> Option<i64> { if ring_len() == 0 {
return None;
}
if ring_get(ev).is_some() {
return Some(ev);
}
if nearmatch == 0 {
return None;
}
let mut best_older: Option<i64> = None;
let mut best_newer: Option<i64> = None;
for i in 0..ring_len() {
let n = ring_at(i);
if n < ev && best_older.map_or(true, |b| n > b) {
best_older = Some(n);
} else if n > ev && best_newer.map_or(true, |b| n < b) {
best_newer = Some(n);
}
}
if nearmatch < 0 { best_older } else { best_newer }
}
pub fn putoldhistentryontop(_keep_going: i32) -> i32 {
let mut ring = hist_ring.lock().unwrap();
if let Some(oldest) = ring.last().map(|h| h.histnum) {
let pos = ring.iter().position(|h| h.histnum == oldest).unwrap();
let entry = ring.remove(pos);
ring.insert(0, entry);
return 1;
}
0
}
pub fn prepnexthistent() -> i64 { let cap = histsiz.load(Ordering::SeqCst);
if histlinect.load(Ordering::SeqCst) >= cap {
if let Some(oldest) = ring_oldest() {
let mut ring = hist_ring.lock().unwrap();
ring.retain(|h| h.histnum != oldest);
histlinect.fetch_sub(1, Ordering::SeqCst);
}
}
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
n
}
fn should_ignore_line(prog: Option<&[u8]>) -> i32 { let line = chline.lock().unwrap().clone();
if isset(HISTIGNORESPACE) { if line.starts_with(' ') { return 1; }
}
if prog.is_none() { return 0; }
if isset(HISTNOFUNCTIONS) { return 0;
}
if isset(HISTNOSTORE) { let mut b: &str = &line;
let mut saw_builtin = false;
if let Some(rest) = b.strip_prefix("builtin ") { b = rest;
saw_builtin = true;
}
if (b == "history" || b.starts_with("history ")) && (saw_builtin )
{
return 1; }
if (b == "r" || b.starts_with("r ")) && (saw_builtin )
{
return 1;
}
if let Some(rest) = b.strip_prefix("fc -") { if (saw_builtin )
&& rest.chars().take_while(|c| c.is_ascii_alphabetic())
.any(|c| c == 'l')
{
return 1; }
}
}
0 }
pub fn hend(prog: Option<&[u8]>) -> i32 { let stack_pos = histsave_stack_pos.load(Ordering::SeqCst); let mut save: i32 = 1; let mut hookret: i32 = 0;
crate::ported::signals::queue_signals(); if (histdone.load(Ordering::SeqCst) & HISTFLAG_SETTY) != 0 { }
let active = histactive.load(Ordering::SeqCst);
if (active & HA_NOINC) == 0 { unlinkcurline(); }
if (active & HA_NOINC) != 0 { chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst); crate::ported::signals::unqueue_signals(); return 1; }
let cur_ignore_all = if isset(HISTIGNOREALLDUPS) { 1 } else { 0 }; let prev_ignore_all = hist_ignore_all_dups.load(Ordering::SeqCst);
if prev_ignore_all != cur_ignore_all && {
hist_ignore_all_dups.store(cur_ignore_all, Ordering::SeqCst); cur_ignore_all != 0
}
{
histremovedups(); }
let chline_text = chline.lock().unwrap().clone();
if !chline_text.is_empty() { let save_errflag = crate::ported::utils::errflag .load(Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed); let args = vec!["zshaddhistory".to_string(), chline_text.clone()]; hookret = crate::ported::utils::callhookfunc( "zshaddhistory", Some(&args), true);
let new_errflag = (crate::ported::utils::errflag .load(Ordering::Relaxed)
& !crate::ported::utils::ERRFLAG_ERROR) | save_errflag;
crate::ported::utils::errflag.store(new_errflag, Ordering::Relaxed);
}
let hf = resolve_histfile(); if isset(SHAREHISTORY) && lockhistfile(hf.as_deref(), 0) == 0
{
readhistfile(hf.as_deref(), 0, HFILE_USE_OPTIONS as i32 | HFILE_FAST as i32);
}
let flag = histdone.load(Ordering::SeqCst); histdone.store(0, Ordering::SeqCst); let hptr_pos = hptr.load(Ordering::SeqCst);
let mut text = chline_text;
if hptr_pos < 1 { save = 0; } else {
if text.ends_with('\n') { if text.len() > 1 { text.pop(); if hptr.load(Ordering::SeqCst) > 0 {
hptr.fetch_sub(1, Ordering::SeqCst);
}
} else {
save = 0; }
}
if chwordpos.load(Ordering::SeqCst) <= 2 && hist_keep_comment.load(Ordering::SeqCst) == 0
{
save = 0; } else if should_ignore_line(prog) != 0 { save = -1; } else if hookret == 2 { save = -2; } else if hookret != 0 { save = -1; }
}
if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) != 0 { let ptr = text.clone(); if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) == HISTFLAG_DONE { print!("{}\n", ptr);
let _ = std::io::stdout().flush();
}
if (flag & HISTFLAG_RECALL) != 0 { save = 0; }
}
if save != 0 || text.starts_with(' ') { let mut ring = hist_ring.lock().unwrap();
let mut idx: usize = 0;
while idx < ring.len()
&& (ring[idx].node.flags as u32 & HIST_FOREIGN) != 0
{
idx += 1;
}
if idx < ring.len()
&& (ring[idx].node.flags as u32 & HIST_TMPSTORE) != 0
{
if idx == 0 { curhist.fetch_sub(1, Ordering::SeqCst); }
ring.remove(idx); histlinect.fetch_sub(1, Ordering::SeqCst);
}
}
if save != 0 { if chwordpos.load(Ordering::SeqCst) % 2 != 0 {
ihwend();
}
let cwp = chwordpos.load(Ordering::SeqCst);
if cwp > 1 {
let words = chwords.lock().unwrap();
let last = words.get((cwp - 2) as usize).copied().unwrap_or(0);
if (last as usize) >= text.len() { drop(words);
chwordpos.fetch_sub(2, Ordering::SeqCst);
} else {
drop(words);
}
if isset(HISTREDUCEBLANKS) { text = histreduceblanks(&text); }
}
let newflags: u32 = if save == -1 { HIST_TMPSTORE } else if save == -2 { HIST_NOWRITE }
else { 0 };
let mut he_idx: Option<usize> = None;
let mut overwrite_old: u32 = 0;
if (isset(HISTIGNOREDUPS) || isset(HISTIGNOREALLDUPS)) && save > 0
{
let ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first() {
if top.node.nam == text { overwrite_old = top.node.flags as u32 & HIST_OLD; he_idx = Some(0);
}
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let cwp = chwordpos.load(Ordering::SeqCst);
let chwords_snapshot: Vec<i16> = chwords.lock().unwrap().clone();
let nwords = (cwp / 2) as i32;
if let Some(0) = he_idx { let mut ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first_mut() {
top.node.nam = text.clone(); top.stim = now; top.ftim = 0; top.node.flags = (newflags | overwrite_old) as i32; top.nwords = nwords; top.words = if cwp > 0 {
chwords_snapshot[..cwp as usize].to_vec() } else {
Vec::new()
};
}
} else {
let n = prepnexthistent(); let mut he = make_histent(n, text.clone());
he.stim = now;
he.ftim = 0;
he.node.flags = newflags as i32;
he.nwords = nwords;
if cwp > 0 {
he.words = chwords_snapshot[..cwp as usize].to_vec();
}
let mut ring = hist_ring.lock().unwrap();
ring.insert(0, he);
histlinect.fetch_add(1, Ordering::SeqCst);
if (newflags & HIST_TMPSTORE) == 0 { crate::ported::hashtable::addhistnode(&text, n as i32);
}
}
}
chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst);
let share = isset(SHAREHISTORY);
let do_inc = if share {
histfileIsLocked() != 0 } else {
isset(INCAPPENDHISTORY) || (isset(INCAPPENDHISTORYTIME) && histsave_stack_pos.load(Ordering::SeqCst) != 0) };
if do_inc {
savehistfile(hf.as_deref(), 0 | HFILE_USE_OPTIONS as i32
| HFILE_FAST as i32);
}
unlockhistfile(hf.as_deref().unwrap_or(""));
while histsave_stack_pos.load(Ordering::SeqCst) > stack_pos { pophiststack(); }
hist_keep_comment.store(0, Ordering::SeqCst); crate::ported::signals::unqueue_signals(); if (flag & HISTFLAG_NOEXEC) != 0
|| crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
0 } else {
1
}
}
pub fn ihwabort() { let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 != 0 {
chwordpos.fetch_sub(1, Ordering::SeqCst);
}
hist_keep_comment.store(1, Ordering::SeqCst);
}
pub fn ihwend() { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
let stop = stophist.load(Ordering::SeqCst);
let active = histactive.load(Ordering::SeqCst);
let inflags = crate::ported::input::inbufflags.with(|f| f.get());
if stop == 2
|| (active & HA_INWORD) != 0
|| (inflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS {
return;
}
let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 == 0 {
return;
}
let cur = hptr.load(Ordering::SeqCst) as i16; let mut words = chwords.lock().unwrap();
let start_idx = (pos - 1) as usize;
if cur > words[start_idx] { let end_idx = pos as usize;
if words.len() <= end_idx {
words.resize(end_idx + 1, 0);
}
words[end_idx] = cur; chwordpos.fetch_add(1, Ordering::SeqCst);
} else {
chwordpos.fetch_sub(1, Ordering::SeqCst);
}
}
pub fn histbackword() { let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 == 0 && pos != 0 { let words = chwords.lock().unwrap();
let idx = (pos - 1) as usize;
if idx < words.len() {
let off = (words[idx] as i32).max(0) as usize;
hptr.store(off, Ordering::SeqCst); }
}
}
pub fn hwget() -> Option<(i32, String)> {
let pos = chwordpos.load(Ordering::SeqCst);
if pos == 0 || pos % 2 != 0 { return None; }
let words = chwords.lock().unwrap();
let start_idx = (pos - 2) as usize;
let end_idx = (pos - 1) as usize;
if end_idx >= words.len() { return None; }
let start = words[start_idx];
let end = words[end_idx];
let line = chline.lock().unwrap();
let s = (start.max(0)) as usize;
let e = (end.max(0) as usize).min(line.len());
if s > e || s >= line.len() { return None; }
Some((start as i32, line[s..e].to_string()))
}
pub fn hwrep(rep: &str) { let (start_off, start_text) = match hwget() {
Some(v) => v,
None => return,
};
if rep == start_text {
return;
}
hptr.store(start_off.max(0) as usize, Ordering::SeqCst); chwordpos.fetch_sub(2, Ordering::SeqCst); ihwbegin(0);
qbang.store(true, Ordering::SeqCst); for b in rep.bytes() {
ihwaddc(b as i32);
}
ihwend();
}
pub fn hgetline() -> Option<String> { let hp = hptr.load(Ordering::SeqCst);
let line = chline.lock().unwrap();
if line.is_empty() || hp == 0 {
return None;
}
let truncated = if hp <= line.len() {
line[..hp].to_string()
} else {
line.clone()
};
drop(line);
hptr.store(0, Ordering::SeqCst);
chwordpos.store(0, Ordering::SeqCst);
Some(truncated) }
pub fn getargspec(argc: i32, marg_arg: i32, evset: i32) -> i32 { let mut c: i32 = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
let mut ret: i32 = -1; if c == b'0' as i32 { return 0; }
if (c as u8 as char).is_ascii_digit() { ret = 0; while (c as u8 as char).is_ascii_digit() { ret = ret * 10 + c - b'0' as i32; if ret < 0 { herrflush(); crate::ported::utils::zerr("no such word in event"); return -2; }
c = crate::ported::input::ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
if let Some(ch) = char::from_u32(c as u32) { crate::ported::input::inungetc(ch);
}
} else if c == b'^' as i32 { ret = 1; } else if c == b'$' as i32 { ret = argc; } else if c == b'%' as i32 { if evset != 0 { herrflush(); crate::ported::utils::zerr("Ambiguous history reference"); return -2; }
if marg_arg == -1 { herrflush(); crate::ported::utils::zerr("%% with no previous word matched"); return -2; }
ret = marg_arg; } else { if let Some(ch) = char::from_u32(c as u32) { crate::ported::input::inungetc(ch);
}
}
ret }
pub fn hconsearch(needle: &str) -> Option<(i64, i32)> { let ring = hist_ring.lock().expect("hist_ring poisoned");
for entry in ring.iter() {
if (entry.node.flags as u32 & HIST_FOREIGN) != 0 { continue; }
if let Some(pos) = entry.node.nam.find(needle) { let mut t1: i32 = 0; while t1 < entry.nwords { let slot_pos = entry.words.get((2 * t1) as usize)
.copied()
.unwrap_or(0) as usize;
if slot_pos > pos { break;
}
t1 += 1; }
return Some((entry.histnum, t1 - 1)); }
}
None }
pub fn hcomsearch(prefix: &str) -> Option<i64> {
let mut cur = curhist.load(Ordering::SeqCst);
while let Some(prev) = up_histent(cur) {
cur = prev;
if let Some(entry) = ring_get(cur) {
if (entry.node.flags as u32 & HIST_FOREIGN) != 0 {
continue;
}
if entry.node.nam.starts_with(prefix) {
return Some(cur);
}
}
}
None
}
pub fn chabspath(input: &str) -> Option<String> {
if input.is_empty() { return Some(String::new()); }
let mut path = if !input.starts_with('/') {
let cwd = std::env::current_dir().ok()?;
let cwd_s = cwd.to_string_lossy().into_owned();
if cwd_s.ends_with('/') { format!("{}{}", cwd_s, input) }
else { format!("{}/{}", cwd_s, input) }
} else {
input.to_string()
};
let chars: Vec<char> = path.chars().collect();
let mut out: Vec<char> = Vec::with_capacity(chars.len());
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '/' {
out.push('/');
i += 1;
while i < chars.len() && chars[i] == '/' { i += 1; }
} else if c == '.' && i + 1 < chars.len() && chars[i + 1] == '.'
&& (i + 2 == chars.len() || chars[i + 2] == '/')
{
if out.len() <= 1 {
if out.is_empty() || out == ['/'] { return None; }
out.push('.'); out.push('.');
} else if out.len() >= 3 && &out[out.len() - 3..] == &['.', '.', '/'] {
out.push('.'); out.push('.');
} else {
if out.last() == Some(&'/') && out.len() > 1 { out.pop(); }
while out.last().map(|c| *c != '/').unwrap_or(false) { out.pop(); }
}
i += 2;
if i < chars.len() && chars[i] == '/' { i += 1; }
} else if c == '.' && (i + 1 == chars.len() || chars[i + 1] == '/') {
i += 1;
while i < chars.len() && chars[i] == '/' { i += 1; }
} else {
out.push(c); i += 1;
}
}
while out.len() > 1 && out.last() == Some(&'/') { out.pop(); }
path = out.into_iter().collect();
if path.is_empty() { Some("/".to_string()) } else { Some(path) }
}
pub fn chrealpath(path: &str) -> Option<String> {
std::fs::canonicalize(path).ok().map(|p| p.to_string_lossy().into_owned())
}
pub fn remtpath(s: &str, count: i32) -> String { let s = s.trim_end_matches('/');
if s.is_empty() { return "/".to_string(); }
if count == 0 {
if let Some(pos) = s.rfind('/') {
if pos == 0 { return "/".to_string(); }
return s[..pos].trim_end_matches('/').to_string();
}
return ".".to_string();
}
let bytes = s.as_bytes();
let mut remaining = count;
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'/' {
remaining -= 1;
if remaining <= 0 {
if i == 0 { return "/".to_string(); }
return s[..i].to_string();
}
while i + 1 < bytes.len() && bytes[i + 1] == b'/' { i += 1; }
}
i += 1;
}
s.to_string()
}
pub fn remtext(s: &str) -> String { let (prefix, basename) = match s.rfind('/') {
Some(i) => (&s[..=i], &s[i + 1..]),
None => ("", s),
};
if let Some(dot_pos) = basename.rfind('.') { return format!("{}{}", prefix, &basename[..dot_pos]); }
s.to_string() }
pub fn rembutext(s: &str) -> String { if let Some(slash_pos) = s.rfind('/') {
let after_slash = &s[slash_pos + 1..];
if let Some(dot_pos) = after_slash.rfind('.') {
return after_slash[dot_pos + 1..].to_string();
}
return String::new();
}
if let Some(dot_pos) = s.rfind('.') {
return s[dot_pos + 1..].to_string();
}
String::new()
}
pub fn remlpaths(s: &str, count: i32) -> String { let trimmed = s.trim_end_matches('/');
if trimmed.is_empty() { return String::new(); }
let parts: Vec<&str> = trimmed.split('/').filter(|p| !p.is_empty()).collect();
let n = if count == 0 { 1 } else { count as usize };
if n > parts.len() {
return s.to_string();
}
parts.iter().rev().take(n).rev().copied().collect::<Vec<&str>>().join("/")
}
pub fn casemodify(s: &str, how: i32) -> String { let mut result = String::with_capacity(s.len());
let mut nextupper = true;
for c in s.chars() {
let modified = match how {
x if x == CASMOD_LOWER => c.to_lowercase().collect::<String>(),
x if x == CASMOD_UPPER => c.to_uppercase().collect::<String>(),
x if x == CASMOD_CAPS => {
if !c.is_alphanumeric() {
nextupper = true;
c.to_string()
} else if nextupper {
nextupper = false;
c.to_uppercase().collect::<String>()
} else {
c.to_lowercase().collect::<String>()
}
}
_ => c.to_string(),
};
let _ = CASMOD_NONE; result.push_str(&modified);
}
result
}
pub fn subst(s: &str, in_pattern: &str, out_pattern: &str, global: bool) -> String {
if in_pattern.is_empty() { return s.to_string(); }
let mut anchor_start = false;
let mut anchor_end = false;
let mut pat = in_pattern;
if let Some(rest) = pat.strip_prefix('#') { anchor_start = true; pat = rest; }
if let Some(rest) = pat.strip_prefix('%') { anchor_end = true; pat = rest; }
if pat.is_empty() { return s.to_string(); }
let out_expanded = convamps(out_pattern, pat);
if anchor_start && anchor_end {
if s == pat { return out_expanded; }
return s.to_string();
}
if anchor_start {
if let Some(rest) = s.strip_prefix(pat) {
return format!("{}{}", out_expanded, rest);
}
return s.to_string();
}
if anchor_end {
if s.ends_with(pat) {
let prefix_len = s.len() - pat.len();
return format!("{}{}", &s[..prefix_len], out_expanded);
}
return s.to_string();
}
if global { s.replace(pat, &out_expanded) } else { s.replacen(pat, &out_expanded, 1) }
}
fn convamps(out: &str, in_pattern: &str) -> String {
let mut result = String::with_capacity(out.len());
let mut chars = out.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next) = chars.peek() { result.push(next); chars.next(); }
} else if c == '&' {
result.push_str(in_pattern);
} else {
result.push(c);
}
}
result
}
pub fn checkcurline(he: &histent) { let curhist_val = curhist.load(Ordering::SeqCst); let active = histactive.load(Ordering::SeqCst); if he.histnum == curhist_val && (active & HA_ACTIVE) != 0 { let chline_val = chline.lock().expect("chline poisoned").clone(); let chwordpos_val = chwordpos.load(Ordering::SeqCst); let chwords_val = chwords.lock().expect("chwords poisoned").clone(); let mut cl = curline.lock().expect("curline poisoned");
*cl = Some(histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: chline_val, flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words: chwords_val, nwords: chwordpos_val / 2, histnum: he.histnum,
});
}
}
pub fn quietgethist(ev: i64) -> Option<histent> { ring_get(ev)
}
pub fn gethist(ev: i64) -> Option<histent> { let ret = quietgethist(ev);
if ret.is_none() {
herrflush();
crate::ported::utils::zerr(&format!("no such event: {}", ev));
}
ret
}
pub fn getargs(entry: &histent, arg1: usize, arg2: usize) -> Option<String> { let nwords = entry.nwords as usize; if arg2 < arg1 || arg1 >= nwords || arg2 >= nwords { herrflush(); crate::ported::utils::zerr("no such word in event"); return None; }
if arg1 == 0 && arg2 == nwords - 1 {
return Some(entry.node.nam.clone()); }
let pos1_raw = entry.words.get(2 * arg1).copied().unwrap_or(-1); let pos2_raw = entry.words.get(2 * arg2 + 1).copied().unwrap_or(-1); if pos1_raw < 0
|| (pos1_raw as i64) < (arg1 as i64)
|| pos2_raw < 0
|| (pos2_raw as i64) < (arg2 as i64)
{ herrflush(); crate::ported::utils::zerr(
"history event too long, can't index requested words" );
return None; }
let pos1 = pos1_raw as usize;
let pos2 = pos2_raw as usize;
entry.node.nam.get(pos1..pos2).map(|s| s.to_string()) }
pub fn quote(s: &str) -> String { let bytes: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(bytes.len() + 3);
out.push('\'');
let mut inquotes = false;
let mut prev: char = '\0';
for &c in bytes.iter() {
let is_inblank = matches!(c, ' ' | '\t' | '\n');
if c == '\'' {
inquotes = !inquotes;
out.push('\''); out.push('\\'); out.push('\''); out.push('\'');
} else if is_inblank && !inquotes && prev != '\\' { out.push('\''); out.push(c); out.push('\'');
} else {
out.push(c);
}
prev = c;
}
out.push('\'');
out
}
pub fn quotebreak(s: &str) -> String { let mut result = String::with_capacity(s.len() + 10);
result.push('\'');
for c in s.chars() {
let is_inblank = matches!(c, ' ' | '\t' | '\n');
if c == '\'' { result.push_str("'\\''"); }
else if is_inblank {
result.push('\''); result.push(c); result.push('\'');
} else {
result.push(c);
}
}
result.push('\'');
result
}
pub fn hdynread(stop: i32) -> Option<String> { use std::sync::atomic::Ordering::SeqCst;
let stop_c = stop as u8 as char; let mut buf = String::with_capacity(256); let mut c: Option<char>; loop {
c = crate::ported::input::ingetc(); match c {
None => break,
Some(ch) if ch == stop_c => break, Some('\n') => break, Some(ch) => {
if crate::ported::hist::lexstop.load(SeqCst) { break; } let mut written = ch;
if ch == '\\' { if let Some(nxt) = crate::ported::input::ingetc() { written = nxt;
} else {
break;
}
}
buf.push(written); }
}
}
if let Some('\n') = c { crate::ported::input::inungetc('\n'); crate::ported::utils::zerr("delimiter expected"); return None; }
Some(buf) }
pub fn ihungetc(c: i32) { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
use std::sync::atomic::Ordering::SeqCst;
let mut c = c as u8 as char; let mut doit = 1; while !crate::ported::hist::lexstop.load(SeqCst) && crate::ported::utils::errflag.load(SeqCst) == 0
{
let hp = crate::ported::hist::hptr.load(SeqCst);
let line = crate::ported::hist::chline.lock().unwrap().clone();
let line_b = line.as_bytes();
let stop = crate::ported::hist::stophist.load(SeqCst);
let inflags = crate::ported::input::inbufflags.with(|f| f.get());
let active = crate::ported::hist::histactive.load(SeqCst);
if hp >= 2 && hp <= line_b.len() && line_b[hp - 1] != c as u8 && stop < 4
&& line_b[hp - 1] == b'\n' && line_b[hp - 2] == b'\\'
&& (active & crate::ported::hist::HA_UNGET) == 0
&& (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS
{
crate::ported::hist::histactive.fetch_or(crate::ported::hist::HA_UNGET, SeqCst); crate::ported::input::inungetc('\n'); crate::ported::input::inungetc('\\'); crate::ported::hist::histactive.fetch_and(!crate::ported::hist::HA_UNGET, SeqCst); }
if crate::ported::hist::expanding.load(SeqCst) != 0 { crate::ported::zle::compcore::ZLEMETACS.fetch_sub(1, SeqCst); crate::ported::zle::compcore::ZLEMETALL.fetch_sub(1, SeqCst); crate::ported::hist::exlast.fetch_add(1, SeqCst); }
if (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS { let new_hp = hp.saturating_sub(1);
crate::ported::hist::hptr.store(new_hp, SeqCst); let bangchar_v = crate::ported::hist::bangchar.load(SeqCst) as u8;
let qb = c as u8 == bangchar_v && stop < 2 && new_hp > 0 && line_b.get(new_hp - 1).copied() == Some(b'\\');
crate::ported::hist::qbang.store(qb, SeqCst);
} else {
crate::ported::hist::qbang.store(false, SeqCst); }
if doit != 0 { crate::ported::input::inungetc(c); }
if !crate::ported::hist::qbang.load(SeqCst) { return; } let inflags2 = crate::ported::input::inbufflags.with(|f| f.get());
doit = if crate::ported::hist::stophist.load(SeqCst) == 0 && ((inflags2 & INP_HIST) != 0 || (inflags2 & INP_ALIAS) == 0)
{ 1 } else { 0 };
c = '\\'; }
}
pub fn getsubsargs(_subline: &str, gbalp: &mut i32, cflagp: &mut i32) -> i32 { let del = match crate::ported::input::ingetc() { Some(c) => c, None => return 1,
};
let read_until = |stop: char| -> Option<String> { let mut out = String::new();
loop {
match crate::ported::input::ingetc() {
None => return None,
Some('\n') => return Some(out),
Some(c) if c == stop => return Some(out),
Some('\\') => {
if let Some(n) = crate::ported::input::ingetc() {
if n != stop { out.push('\\'); }
out.push(n);
}
}
Some(c) => out.push(c),
}
}
};
let ptr1 = match read_until(del) { Some(p) => p, None => return 1 }; let ptr2 = read_until(del).unwrap_or_default(); if !ptr1.is_empty() { *hsubl.lock().unwrap() = Some(ptr1); } else if hsubl.lock().unwrap().is_none() { return 0; }
*hsubr.lock().unwrap() = Some(ptr2); let follow = crate::ported::input::ingetc(); if follow == Some(':') { let next = crate::ported::input::ingetc(); if next == Some('G') { *gbalp = 1; } else {
if let Some(c) = next { crate::ported::input::inungetc(c); } *cflagp = 1; }
} else if let Some(c) = follow {
crate::ported::input::inungetc(c); }
0 }
pub fn hdynread2(stop: char, input: &str) -> (String, usize) {
let mut out = String::new();
let mut consumed = 0usize;
let mut chars = input.chars();
while let Some(c) = chars.next() {
consumed += c.len_utf8();
if c == stop || c == '\n' {
if c == '\n' { consumed -= c.len_utf8(); }
return (out, consumed);
}
if c == '\\' {
if let Some(esc) = chars.next() {
consumed += esc.len_utf8();
out.push(esc);
}
} else {
out.push(c);
}
}
(out, consumed)
}
pub fn inithist() { histsiz.store(1000, Ordering::SeqCst);
savehistsiz.store(1000, Ordering::SeqCst);
curhist.store(0, Ordering::SeqCst);
histlinect.store(0, Ordering::SeqCst);
}
pub fn resizehistents() {
let cap = histsiz.load(Ordering::SeqCst);
while histlinect.load(Ordering::SeqCst) > cap {
if let Some(oldest) = ring_oldest() {
let mut ring = hist_ring.lock().unwrap();
ring.retain(|h| h.histnum != oldest);
histlinect.fetch_sub(1, Ordering::SeqCst);
} else {
break;
}
}
}
pub fn readhistline(line: &str) -> Option<histent> {
let line = line.trim();
if line.is_empty() { return None; }
if let Some(rest) = line.strip_prefix(": ") {
if let Some(semi) = rest.find(';') {
let meta = &rest[..semi];
let cmd = &rest[semi + 1..];
let parts: Vec<&str> = meta.splitn(2, ':').collect();
let timestamp = parts.first().and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
let mut entry = make_histent(0, cmd.to_string());
entry.stim = timestamp;
return Some(entry);
}
}
Some(make_histent(0, line.to_string()))
}
pub fn readhistfile(fn_path: Option<&str>, _err: i32, _readflags: i32) { let path: String = match fn_path {
Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return,
},
};
let contents = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return,
};
if contents.is_empty() { return; }
let _ = lockhistfile(Some(&path), 1);
let mut current: Option<(i64, i64, String)> = None;
for raw_line in contents.lines() {
if let Some((stim, ftim, ref mut text)) = current {
if text.ends_with('\\') {
text.pop();
text.push('\n');
text.push_str(raw_line);
current = Some((stim, ftim, text.clone()));
continue;
}
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
let mut entry = make_histent(n, text.clone());
entry.stim = stim;
entry.ftim = ftim;
entry.node.flags |= HIST_OLD as i32;
hist_ring.lock().unwrap().insert(0, entry);
histlinect.fetch_add(1, Ordering::SeqCst);
current = None;
}
if let Some(rest) = raw_line.strip_prefix(": ") {
if let Some((meta, text)) = rest.split_once(';') {
if let Some((stim_s, dur_s)) = meta.split_once(':') {
let stim: i64 = stim_s.parse().unwrap_or(0);
let dur: i64 = dur_s.parse().unwrap_or(0);
let ftim = stim + dur;
current = Some((stim, ftim, text.to_string()));
continue;
}
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
current = Some((now, now, raw_line.to_string()));
}
if let Some((stim, ftim, text)) = current {
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
let mut entry = make_histent(n, text);
entry.stim = stim;
entry.ftim = ftim;
entry.node.flags |= HIST_OLD as i32;
hist_ring.lock().unwrap().insert(0, entry);
histlinect.fetch_add(1, Ordering::SeqCst);
}
unlockhistfile(&path);
resizehistents();
}
pub fn flockhistfile(path: &str) -> i32 {
#[cfg(unix)]
{
if let Ok(file) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(format!("{}.lock", path))
{
let fd = file.as_raw_fd();
return unsafe { if libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) == 0 { 1 } else { 0 } };
}
0
}
#[cfg(not(unix))]
{ let _ = path; 1 }
}
pub fn savehistfile(fn_path: Option<&str>, _writeflags: i32) { if !crate::ported::zsh_h::isset(crate::ported::zsh_h::INTERACTIVE) {
return;
}
let cap = savehistsiz.load(Ordering::SeqCst); if cap <= 0 { return;
}
let path: String = match fn_path { Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return,
},
};
let _ = lockhistfile(Some(&path), 1);
if let Ok(mut file) = std::fs::OpenOptions::new()
.write(true).create(true).truncate(true).open(&path)
{
let cap = cap as usize;
let ring = hist_ring.lock().unwrap();
let mut count = 0;
for entry in ring.iter().rev() {
if count >= cap { break; }
let dur = entry.ftim.saturating_sub(entry.stim);
let _ = writeln!(file, ": {}:{};{}", entry.stim, dur, entry.node.nam);
count += 1;
}
}
unlockhistfile(&path);
}
static lockhistct: AtomicI32 = AtomicI32::new(0);
pub fn checklocktime(path: &str, max_age_secs: u64) -> i32 {
let lockfile = format!("{}.lock", path);
if let Ok(meta) = std::fs::metadata(&lockfile) {
if let Ok(modified) = meta.modified() {
if let Ok(age) = modified.elapsed() {
if age.as_secs() < max_age_secs { return 1; }
}
}
}
0
}
pub fn lockhistfile(fn_path: Option<&str>, keep_trying: i32) -> i32 { let path: String = match fn_path { Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return 1, },
};
if lockhistct.fetch_add(1, Ordering::SeqCst) > 0 {
return 0;
}
let max_tries = if keep_trying != 0 { 30 } else { 1 };
for attempt in 0..max_tries {
if flockhistfile(&path) != 0 {
return 0;
}
if attempt + 1 < max_tries {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
lockhistct.fetch_sub(1, Ordering::SeqCst);
if keep_trying != 0 { 2 } else { 1 }
}
pub fn unlockhistfile(path: &str) {
let prev = lockhistct.fetch_sub(1, Ordering::SeqCst);
if prev <= 0 {
lockhistct.store(0, Ordering::SeqCst);
return;
}
if prev == 1 {
let lockpath = format!("{}.lock", path);
let _ = std::fs::remove_file(&lockpath);
}
}
#[allow(non_snake_case)]
pub fn histfileIsLocked() -> i32 {
if lockhistct.load(Ordering::SeqCst) > 0 { 1 } else { 0 }
}
pub fn bufferwords(line: &str, cursor_pos: usize) -> (Vec<String>, usize) {
let mut words: Vec<String> = Vec::new();
let mut cur = String::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
let flush = |out: &mut Vec<String>, cur: &mut String| {
if !cur.is_empty() {
out.push(std::mem::take(cur));
}
};
while i < chars.len() {
let c = chars[i];
match c {
' ' | '\t' | '\n' => {
flush(&mut words, &mut cur);
i += 1;
}
';' | '&' | '|' | '<' | '>' | '(' | ')' => {
flush(&mut words, &mut cur);
let mut tok = String::new();
tok.push(c);
while i + 1 < chars.len()
&& chars[i + 1] == c
&& matches!(c, '&' | '|' | ';' | '<' | '>')
{
tok.push(c);
i += 1;
}
words.push(tok);
i += 1;
}
'\'' => {
i += 1;
while i < chars.len() && chars[i] != '\'' {
cur.push(chars[i]);
i += 1;
}
if i < chars.len() {
i += 1;
}
}
'"' => {
i += 1;
while i < chars.len() && chars[i] != '"' {
if chars[i] == '\\' && i + 1 < chars.len() {
i += 1;
cur.push(chars[i]);
i += 1;
continue;
}
cur.push(chars[i]);
i += 1;
}
if i < chars.len() {
i += 1;
}
}
'\\' if i + 1 < chars.len() => {
cur.push(chars[i + 1]);
i += 2;
}
_ => {
cur.push(c);
i += 1;
}
}
}
flush(&mut words, &mut cur);
let mut pos = 0;
let mut word_idx = 0;
for (i, word) in line.split_whitespace().enumerate() {
if let Some(start) = line[pos..].find(word) {
let wstart = pos + start;
let wend = wstart + word.len();
if cursor_pos >= wstart && cursor_pos <= wend {
word_idx = i;
break;
}
pos = wend;
}
}
(words, word_idx)
}
pub fn histsplitwords(line: &str) -> Vec<(usize, usize)> {
let mut words = Vec::new();
let mut in_word = false;
let mut word_start = 0;
let mut in_quote = false;
let mut quote_char = '\0';
for (i, c) in line.char_indices() {
if in_quote {
if c == quote_char { in_quote = false; }
continue;
}
if c == '\'' || c == '"' {
in_quote = true;
quote_char = c;
if !in_word { word_start = i; in_word = true; }
continue;
}
if c.is_ascii_whitespace() {
if in_word { words.push((word_start, i)); in_word = false; }
} else if !in_word {
word_start = i; in_word = true;
}
}
if in_word { words.push((word_start, line.len())); }
words
}
pub fn pushhiststack(hf: Option<&str>, hs: i64, shs: i64, level: i32) { let snap = histsave { lasthist: histfile_stats {
text: None, stim: 0, mtim: 0, fpos: 0, fsiz: 0,
interrupted: 0, next_write_ev: 0,
},
histfile: hf.map(|s| s.to_string()), hist_ring: std::mem::take(&mut *hist_ring.lock().unwrap()), curhist: curhist.load(Ordering::SeqCst), histlinect: histlinect.load(Ordering::SeqCst), histsiz: histsiz.load(Ordering::SeqCst), savehistsiz: savehistsiz.load(Ordering::SeqCst), locallevel: level, };
histsave_stack.lock().unwrap().push(snap); histsave_stack_size.fetch_add(1, Ordering::SeqCst);
histsave_stack_pos.fetch_add(1, Ordering::SeqCst);
histsiz.store(hs, Ordering::SeqCst); savehistsiz.store(shs, Ordering::SeqCst); curhist.store(0, Ordering::SeqCst); histlinect.store(0, Ordering::SeqCst);
let _ = hf;
}
pub fn pophiststack() -> i32 { let snap = match histsave_stack.lock().unwrap().pop() {
Some(s) => s,
None => return 0, };
if let Some(ref hf) = snap.histfile {
if !hf.is_empty() { crate::ported::params::setsparam("HISTFILE", hf); } else { let _ = crate::ported::params::paramtab()
.write()
.unwrap()
.remove("HISTFILE"); }
}
*hist_ring.lock().unwrap() = snap.hist_ring; curhist.store(snap.curhist, Ordering::SeqCst); histlinect.store(snap.histlinect, Ordering::SeqCst); histsiz.store(snap.histsiz, Ordering::SeqCst); savehistsiz.store(snap.savehistsiz, Ordering::SeqCst); histsave_stack_size.fetch_sub(1, Ordering::SeqCst);
histsave_stack_pos.fetch_sub(1, Ordering::SeqCst);
histsave_stack_pos.load(Ordering::SeqCst) + 1
}
pub fn saveandpophiststack(mut pop_through: i32, writeflags: i32) -> i32 { use std::sync::atomic::Ordering::SeqCst;
let stack_pos = histsave_stack_pos.load(SeqCst);
if pop_through <= 0 { pop_through += stack_pos + 1; if pop_through <= 0 { pop_through = 1;
}
}
if stack_pos < pop_through { return 0;
}
loop {
savehistfile(None, writeflags);
pophiststack(); if histsave_stack_pos.load(SeqCst) < pop_through { break;
}
}
1
}
pub static histtab: Mutex<Vec<usize>> = Mutex::new(Vec::new());
pub static hist_ring: Mutex<Vec<histent>> = Mutex::new(Vec::new());
pub static curline: Mutex<Option<histent>> = Mutex::new(None);
pub static histsiz: AtomicI64 = AtomicI64::new(0);
pub static savehistsiz: AtomicI64 = AtomicI64::new(0);
pub static histdone: AtomicI32 = AtomicI32::new(0);
pub static histactive: AtomicU32 = AtomicU32::new(0);
pub static hist_ignore_all_dups: AtomicI32 = AtomicI32::new(0);
pub static hist_skip_flags: AtomicI32 = AtomicI32::new(0);
pub static chwords: Mutex<Vec<i16>> = Mutex::new(Vec::new());
pub static chwordlen: AtomicI32 = AtomicI32::new(0);
pub static chwordpos: AtomicI32 = AtomicI32::new(0);
pub static hsubl: Mutex<Option<String>> = Mutex::new(None);
pub static hsubr: Mutex<Option<String>> = Mutex::new(None);
pub static hsubpatopt: AtomicI32 = AtomicI32::new(0);
pub static hptr: AtomicUsize = AtomicUsize::new(0);
pub static chline: Mutex<String> = Mutex::new(String::new());
pub static zle_chline: Mutex<Option<String>> = Mutex::new(None);
pub static qbang: AtomicBool = AtomicBool::new(false);
pub static hlinesz: AtomicI32 = AtomicI32::new(0);
pub static expanding: AtomicI32 = AtomicI32::new(0);
pub static excs: AtomicI32 = AtomicI32::new(0);
pub static exlast: AtomicI32 = AtomicI32::new(0);
#[allow(non_camel_case_types)]
pub struct histfile_stats { pub text: Option<String>, pub stim: i64, pub mtim: i64, pub fpos: i64, pub fsiz: i64, pub interrupted: i32, pub next_write_ev: i64, }
static lasthist: Mutex<histfile_stats> = Mutex::new(histfile_stats { text: None, stim: 0, mtim: 0, fpos: 0, fsiz: 0,
interrupted: 0, next_write_ev: 0,
});
#[allow(non_camel_case_types)]
pub struct histsave { pub lasthist: histfile_stats, pub histfile: Option<String>, pub hist_ring: Vec<histent>, pub curhist: i64, pub histlinect: i64, pub histsiz: i64, pub savehistsiz: i64, pub locallevel: i32, }
#[allow(clippy::vec_init_then_push)]
static histsave_stack: Mutex<Vec<histsave>> = Mutex::new(Vec::new());
pub static stophist: AtomicI32 = AtomicI32::new(0);
pub static curhist: AtomicI64 = AtomicI64::new(0);
pub static histlinect: AtomicI64 = AtomicI64::new(0);
pub static bangchar: AtomicI32 = AtomicI32::new(b'!' as i32);
pub static lexstop: AtomicBool = AtomicBool::new(false);
pub static exit_pending: AtomicBool = AtomicBool::new(false);
static strin: AtomicI32 = AtomicI32::new(0);
pub use crate::ported::zsh_h::{
HIST_OLD, HIST_DUP, HIST_FOREIGN, HIST_TMPSTORE, HIST_NOWRITE,
};
pub use crate::ported::zsh_h::{
HISTFLAG_DONE, HISTFLAG_NOEXEC, HISTFLAG_RECALL, HISTFLAG_SETTY,
};
fn resolve_histfile() -> Option<String> {
crate::ported::params::getsparam("HISTFILE")
}
fn ring_get(ev: i64) -> Option<histent> {
let ring = hist_ring.lock().unwrap();
for h in ring.iter() {
if h.histnum == ev {
return Some(clone_histent(h));
}
}
None
}
fn clone_histent(h: &histent) -> histent {
histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: h.node.nam.clone(),
flags: h.node.flags,
},
up: None,
down: None,
zle_text: h.zle_text.clone(),
stim: h.stim,
ftim: h.ftim,
words: h.words.clone(),
nwords: h.nwords,
histnum: h.histnum,
}
}
fn ring_position(ev: i64) -> Option<usize> {
hist_ring.lock().unwrap().iter().position(|h| h.histnum == ev)
}
fn ring_at(idx: usize) -> i64 {
hist_ring.lock().unwrap()[idx].histnum
}
fn ring_len() -> usize {
hist_ring.lock().unwrap().len()
}
fn ring_oldest() -> Option<i64> {
hist_ring.lock().unwrap().last().map(|h| h.histnum)
}
fn ring_latest() -> Option<histent> {
hist_ring.lock().unwrap().first().map(clone_histent)
}
fn make_histent(num: i64, text: String) -> histent {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: text,
flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: now,
ftim: now,
words: Vec::new(),
nwords: 0,
histnum: num,
}
}
pub fn firsthist() -> i64 {
let ring = hist_ring.lock().unwrap();
ring.last().map(|h| h.histnum).unwrap_or(1)
}
pub fn apply_history_modifiers(val: &str, modifiers: &str) -> String {
let mut result = val.to_string();
let mut chars = modifiers.chars().peekable();
while let Some(c) = chars.next() {
match c {
':' => continue,
'A' => {
if let Ok(abs) = std::fs::canonicalize(&result) {
result = abs.to_string_lossy().to_string();
} else {
let joined = if result.starts_with('/') {
std::path::PathBuf::from(&result)
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(&result)
} else {
std::path::PathBuf::from(&result)
};
let mut parts: Vec<String> = Vec::new();
for comp in joined.components() {
match comp {
CurDir => {}
ParentDir => {
parts.pop();
}
Normal(s) => parts.push(s.to_string_lossy().to_string()),
RootDir => parts.insert(0, String::new()),
Prefix(p) => {
parts.insert(0, p.as_os_str().to_string_lossy().to_string())
}
}
}
result = parts.join("/");
if result.is_empty() {
result = "/".to_string();
}
}
}
'a' => {
if !result.starts_with('/') {
if let Ok(cwd) = std::env::current_dir() {
result = cwd.join(&result).to_string_lossy().to_string();
}
}
}
'h' => {
let trimmed = result.trim_end_matches('/');
if trimmed.is_empty() {
result = "/".to_string();
} else if let Some(pos) = trimmed.rfind('/') {
if pos == 0 {
result = "/".to_string();
} else {
result = trimmed[..pos].to_string();
}
} else {
result = ".".to_string();
}
}
't' => {
let trimmed = result.trim_end_matches('/');
if let Some(pos) = trimmed.rfind('/') {
result = trimmed[pos + 1..].to_string();
} else {
result = trimmed.to_string();
}
}
'r' => {
if let Some(dot_pos) = result.rfind('.') {
let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
if dot_pos > slash_pos {
result = result[..dot_pos].to_string();
}
}
}
'e' => {
if let Some(dot_pos) = result.rfind('.') {
let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
if dot_pos > slash_pos {
result = result[dot_pos + 1..].to_string();
} else {
result = String::new();
}
} else {
result = String::new();
}
}
'l' => {
result = casemodify(&result, CASMOD_LOWER);
}
'u' => {
result = casemodify(&result, CASMOD_UPPER);
}
'C' => {
result = casemodify(&result, CASMOD_CAPS);
}
'q' => {
let mut out = String::with_capacity(result.len() + 8);
for ch in result.chars() {
if " \t\n'\"\\$`;|&<>()[]{}*?#~!".contains(ch) {
out.push('\\');
}
out.push(ch);
}
result = out;
}
'x' => {
result = crate::hist::quotebreak(&result);
}
'Q' => {
let bytes: Vec<char> = result.chars().collect();
let mut out = String::with_capacity(result.len());
let mut j = 0;
let mut in_dq = false;
let mut in_sq = false;
while j < bytes.len() {
let c = bytes[j];
if in_sq {
if c == '\'' {
in_sq = false;
} else {
out.push(c);
}
j += 1;
continue;
}
if in_dq {
if c == '"' {
in_dq = false;
} else if c == '\\' && j + 1 < bytes.len() {
j += 1;
out.push(bytes[j]);
} else {
out.push(c);
}
j += 1;
continue;
}
match c {
'\'' => in_sq = true,
'"' => in_dq = true,
'\\' if j + 1 < bytes.len() => {
j += 1;
out.push(bytes[j]);
}
_ => out.push(c),
}
j += 1;
}
result = out;
}
'P' => {
if let Ok(real) = std::fs::canonicalize(&result) {
result = real.to_string_lossy().to_string();
}
}
'g' | 's' | '&' => {
let (global, do_parse) = match c {
's' => (false, true),
'&' => (false, false),
_ => { match chars.next() {
Some('s') => (true, true),
Some('&') => (true, false),
_ => break,
}
}
};
let (pat, rep) = if do_parse {
let delim = chars.next().unwrap_or('/');
let mut old = String::new();
while let Some(&ch) = chars.peek() {
if ch == delim { chars.next(); break; }
chars.next();
if ch == '\\' {
if let Some(&n) = chars.peek() {
if n == delim { chars.next(); old.push(delim); continue; }
}
}
old.push(ch);
}
let mut new = String::new();
while let Some(&ch) = chars.peek() {
if ch == delim { chars.next(); break; }
chars.next();
if ch == '\\' {
if let Some(&n) = chars.peek() {
if n == delim { chars.next(); new.push(delim); continue; }
}
}
new.push(ch);
}
if !old.is_empty() {
LAST_SUBST_OLD.with(|c| *c.borrow_mut() = old.clone());
LAST_SUBST_NEW.with(|c| *c.borrow_mut() = new.clone());
}
if old.is_empty() {
let lo = LAST_SUBST_OLD.with(|c| c.borrow().clone());
let ln = LAST_SUBST_NEW.with(|c| c.borrow().clone());
(lo, ln)
} else {
(old, new)
}
} else {
(
LAST_SUBST_OLD.with(|c| c.borrow().clone()),
LAST_SUBST_NEW.with(|c| c.borrow().clone()),
)
};
if !pat.is_empty() {
result = if global {
result.replace(&pat, &rep)
} else {
result.replacen(&pat, &rep, 1)
};
}
}
'U' | 'L' | 'V' | 'X' => {
crate::ported::utils::zerr(&format!("unrecognized modifier `{}'", c));
result = String::new();
break;
}
_ => break,
}
}
result
}
thread_local! {
static LAST_SUBST_OLD: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
static LAST_SUBST_NEW: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
}
#[cfg(test)]
mod subst_modifier_tests {
use super::*;
fn hist_test_lock() -> &'static std::sync::Mutex<()> {
static L: std::sync::OnceLock<std::sync::Mutex<()>> =
std::sync::OnceLock::new();
L.get_or_init(|| std::sync::Mutex::new(()))
}
#[test]
fn s_replaces_first_occurrence() {
assert_eq!(apply_history_modifiers("foo bar foo", ":s/foo/baz/"),
"baz bar foo");
}
#[test]
fn gs_replaces_all_occurrences() {
assert_eq!(apply_history_modifiers("foo bar foo", ":gs/foo/baz/"),
"baz bar baz");
}
#[test]
fn ampersand_repeats_last_subst() {
let first = apply_history_modifiers("xxx", ":s/x/y/");
let second = apply_history_modifiers("xxxx", ":&");
assert_eq!(first, "yxx");
assert_eq!(second, "yxxx");
}
#[test]
fn g_ampersand_repeats_last_subst_globally() {
let _ = apply_history_modifiers("init", ":s/i/X/");
assert_eq!(apply_history_modifiers("aiibii", ":g&"), "aXXbXX");
}
#[test]
fn s_alternate_delimiter() {
assert_eq!(apply_history_modifiers("a-b-c", ":s|-|+|"), "a+b-c");
}
#[test]
fn s_escaped_delimiter_in_pattern() {
assert_eq!(apply_history_modifiers("a/b", r":s/\//#/"), "a#b");
}
#[test]
fn up_histent_on_empty_ring_is_none() {
let snapshot: Vec<_> = hist_ring.lock().unwrap().drain(..).collect();
assert!(up_histent(1).is_none());
assert!(down_histent(1).is_none());
hist_ring.lock().unwrap().extend(snapshot);
}
#[test]
fn getsubsargs_returns_one_when_no_delimiter_available() {
let mut gbal = 0i32;
let mut cflag = 0i32;
let r = getsubsargs("", &mut gbal, &mut cflag);
assert_eq!(r, 1, "no delimiter byte → fail-fast 1");
assert_eq!(gbal, 0, "no :G suffix observed");
assert_eq!(cflag, 0, "no cflag set");
}
#[test]
fn histreduceblanks_collapses_internal_runs() {
assert_eq!(histreduceblanks("a b"), "a b");
assert_eq!(histreduceblanks("foo\t\tbar"), "foo bar");
assert_eq!(histreduceblanks("a b"), "a b");
}
#[test]
fn histreduceblanks_uses_narrow_inblank_only() {
assert_eq!(histreduceblanks("a b"), "a b");
assert_eq!(histreduceblanks("a\t\tb"), "a b");
assert_eq!(histreduceblanks("a \tb"), "a b",
"c:1240 — mixed space/tab run collapses to single space");
assert_eq!(histreduceblanks("a\nb"), "a\nb",
"c:50 — newline not in inblank; passes through unchanged");
assert_eq!(histreduceblanks("a\rb"), "a\rb",
"CR not in inblank class; must NOT be collapsed");
assert_eq!(histreduceblanks("a\u{A0}b"), "a\u{A0}b",
"NBSP not in inblank; must NOT be collapsed");
assert_eq!(histreduceblanks(" x"), "x");
assert_eq!(histreduceblanks("x "), "x");
assert_eq!(histreduceblanks("\nx"), "\nx");
}
#[test]
fn digitcount_streams_from_ingetc() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
crate::ported::input::inputsetline("42abc", 0);
let n = digitcount();
assert_eq!(n, 42, "c:581 — decimal digit accumulation");
let nxt = crate::ported::input::ingetc().unwrap_or('\0');
assert_eq!(nxt, 'a', "c:587 — non-digit terminator was inungetc'd");
crate::ported::input::inputsetline("xyz", 0);
let n = digitcount();
assert_eq!(n, 0, "c:586 — non-digit first char returns 0");
let nxt = crate::ported::input::ingetc().unwrap_or('\0');
assert_eq!(nxt, 'x', "c:587 — even the non-digit first char is inungetc'd");
}
#[test]
fn hist_in_word_round_trips() {
hist_in_word(1);
assert_eq!(hist_is_in_word(), 1);
hist_in_word(0);
assert_eq!(hist_is_in_word(), 0);
}
#[test]
fn remtext_strips_extension_keeping_dirname() {
assert_eq!(remtext("path/file.ext"), "path/file");
assert_eq!(remtext("file.ext"), "file");
assert_eq!(remtext("file"), "file");
}
#[test]
fn remtext_strips_leading_dot_per_zsh_doc() {
assert_eq!(remtext(".bashrc"), "",
"$.bashrc:r is an extension per Doc/Zsh/expn.yo:303");
assert_eq!(remtext("path/.bashrc"), "path/",
"extension scan stops at `/`, then strips at first `.`");
}
#[test]
fn rembutext_returns_extension_only() {
assert_eq!(rembutext("path/file.ext"), "ext");
assert_eq!(rembutext("file.tar.gz"), "gz",
"last `.` wins (extension-only is post-LAST-dot)");
assert_eq!(rembutext("file"), "");
}
#[test]
fn remtpath_count_zero_strips_last_component() {
assert_eq!(remtpath("/a/b/c", 0), "/a/b");
assert_eq!(remtpath("/a", 0), "/");
assert_eq!(remtpath("foo", 0), ".",
"no slash → returns '.'");
}
#[test]
fn remlpaths_keeps_last_n_components() {
assert_eq!(remlpaths("/a/b/c", 1), "c");
assert_eq!(remlpaths("/a/b/c", 2), "b/c");
assert_eq!(remlpaths("/a/b/c", 3), "a/b/c");
}
#[test]
fn casemodify_lower_lowercases() {
assert_eq!(casemodify("HELLO World", CASMOD_LOWER), "hello world");
}
#[test]
fn casemodify_upper_uppercases() {
assert_eq!(casemodify("hello world", CASMOD_UPPER), "HELLO WORLD");
}
#[test]
fn casemodify_caps_capitalises_word_starts() {
assert_eq!(casemodify("hello world", CASMOD_CAPS), "Hello World");
assert_eq!(casemodify("FOO BAR", CASMOD_CAPS), "Foo Bar",
"non-first letters lowercased");
}
#[test]
fn quote_breaks_only_narrow_inblank_chars() {
assert_eq!(quote("a b"), "'a' 'b'",
"c:2514 — space broken out of single-quote span");
assert_eq!(quote("a\tb"), "'a'\t'b'");
assert_eq!(quote("a\nb"), "'a'\n'b'");
assert_eq!(quote("a\rb"), "'a\rb'",
"c:2499 — CR is NOT in C's inblank set; stays inside quotes");
assert_eq!(quote("a\u{00A0}b"), "'a\u{00A0}b'",
"NBSP is not inblank; must remain inside the quote span");
}
#[test]
fn quotebreak_uses_narrow_inblank_set() {
assert_eq!(quotebreak("a b"), "'a' 'b'");
assert_eq!(quotebreak("a\tb"), "'a'\t'b'");
assert_eq!(quotebreak("a\nb"), "'a'\n'b'");
assert_eq!(quotebreak("a\rb"), "'a\rb'",
"CR not in inblank set, must not be broken out");
assert_eq!(quotebreak("a\u{00A0}b"), "'a\u{00A0}b'",
"NBSP not in inblank, must not be broken out");
assert_eq!(quotebreak("a\u{000C}b"), "'a\u{000C}b'",
"FF not in inblank, must not be broken out");
}
#[test]
fn savehistfile_short_circuits_on_non_interactive() {
use crate::ported::options::dosetopt;
use crate::ported::zsh_h::INTERACTIVE;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("hist_test");
let path_str = path.to_str().unwrap();
std::fs::write(&path, b"PRESERVED").expect("seed write");
let saved = crate::ported::zsh_h::isset(INTERACTIVE);
dosetopt(INTERACTIVE, 0, 0);
savehistfile(Some(path_str), 0);
let after = std::fs::read(&path).expect("read after");
assert_eq!(after, b"PRESERVED",
"c:2932 — !interact must skip write; original content preserved");
dosetopt(INTERACTIVE, if saved { 1 } else { 0 }, 0);
}
#[test]
fn hgetline_truncates_chline_and_resets_globals() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
use std::sync::atomic::Ordering;
let saved_chline = std::mem::take(&mut *chline.lock().unwrap());
let saved_hptr = hptr.swap(0, Ordering::SeqCst);
let saved_chwordpos = chwordpos.swap(0, Ordering::SeqCst);
assert_eq!(hgetline(), None,
"c:1777 — empty chline returns None");
*chline.lock().unwrap() = "abcdef".to_string();
hptr.store(0, Ordering::SeqCst);
assert_eq!(hgetline(), None,
"c:1777 — hptr == 0 returns None");
*chline.lock().unwrap() = "abcdef".to_string();
hptr.store(3, Ordering::SeqCst);
chwordpos.store(2, Ordering::SeqCst);
let result = hgetline();
assert_eq!(result, Some("abc".to_string()),
"c:1779 — truncate chline at hptr=3 returns 'abc'");
assert_eq!(hptr.load(Ordering::SeqCst), 0,
"c:1783 — hptr reset to 0");
assert_eq!(chwordpos.load(Ordering::SeqCst), 0,
"c:1784 — chwordpos reset to 0");
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, Ordering::SeqCst);
chwordpos.store(saved_chwordpos, Ordering::SeqCst);
}
#[test]
fn histbackword_rewinds_hptr_on_even_boundary() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
use std::sync::atomic::Ordering;
let saved_pos = chwordpos.swap(0, Ordering::SeqCst);
let saved_hptr = hptr.swap(0, Ordering::SeqCst);
let saved_words = {
let mut w = chwords.lock().unwrap();
std::mem::take(&mut *w)
};
{
let mut w = chwords.lock().unwrap();
*w = vec![0i16, 3, 4, 7];
}
chwordpos.store(4, Ordering::SeqCst);
hptr.store(999, Ordering::SeqCst);
histbackword();
assert_eq!(hptr.load(Ordering::SeqCst), 7,
"c:1715 — even chwordpos must rewind hptr to chwords[pos-1]");
chwordpos.store(0, Ordering::SeqCst);
hptr.store(123, Ordering::SeqCst);
histbackword();
assert_eq!(hptr.load(Ordering::SeqCst), 123,
"c:1714 — chwordpos == 0 means no-op (hptr untouched)");
chwordpos.store(3, Ordering::SeqCst);
hptr.store(456, Ordering::SeqCst);
histbackword();
assert_eq!(hptr.load(Ordering::SeqCst), 456,
"c:1714 — odd chwordpos means mid-word, no-op");
chwordpos.store(saved_pos, Ordering::SeqCst);
hptr.store(saved_hptr, Ordering::SeqCst);
*chwords.lock().unwrap() = saved_words;
}
#[test]
fn ihwaddc_overwrites_at_hptr_not_append() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = chline.lock().unwrap().clone();
let saved_hptr = hptr.load(Ordering::SeqCst);
let saved_errflag =
crate::ported::utils::errflag.load(Ordering::SeqCst);
let saved_lexstop = lexstop.load(Ordering::SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
let saved_qbang = qbang.load(Ordering::SeqCst);
let saved_stophist = stophist.load(Ordering::SeqCst);
let saved_hlinesz = hlinesz.load(Ordering::SeqCst);
*chline.lock().unwrap() = "echo oldword extra".to_string();
hptr.store(5, Ordering::SeqCst);
crate::ported::utils::errflag.store(0, Ordering::SeqCst);
lexstop.store(false, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(0));
qbang.store(false, Ordering::SeqCst);
stophist.store(0, Ordering::SeqCst);
hlinesz.store(64, Ordering::SeqCst);
ihwaddc(b'N' as i32);
ihwaddc(b'E' as i32);
ihwaddc(b'W' as i32);
let cl = chline.lock().unwrap().clone();
assert_eq!(cl.as_str(), "echo NEWword extra",
"c:368 — *hptr++ = c writes AT cursor (NOT appends to end)");
assert_eq!(hptr.load(Ordering::SeqCst), 8,
"c:368 — hptr advances over the three overwrites");
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, Ordering::SeqCst);
crate::ported::utils::errflag.store(saved_errflag, Ordering::SeqCst);
lexstop.store(saved_lexstop, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
qbang.store(saved_qbang, Ordering::SeqCst);
stophist.store(saved_stophist, Ordering::SeqCst);
hlinesz.store(saved_hlinesz, Ordering::SeqCst);
}
#[test]
fn ihwaddc_advances_hptr_on_each_push() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = chline.lock().unwrap().clone();
let saved_hptr = hptr.load(Ordering::SeqCst);
let saved_errflag =
crate::ported::utils::errflag.load(Ordering::SeqCst);
let saved_lexstop = lexstop.load(Ordering::SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
let saved_qbang = qbang.load(Ordering::SeqCst);
let saved_bangchar = bangchar.load(Ordering::SeqCst);
let saved_stophist = stophist.load(Ordering::SeqCst);
let saved_hlinesz = hlinesz.load(Ordering::SeqCst);
*chline.lock().unwrap() = "AB".to_string(); hptr.store(2, Ordering::SeqCst);
crate::ported::utils::errflag.store(0, Ordering::SeqCst);
lexstop.store(false, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(0));
qbang.store(false, Ordering::SeqCst);
stophist.store(0, Ordering::SeqCst);
bangchar.store(b'!' as i32, Ordering::SeqCst);
hlinesz.store(64, Ordering::SeqCst);
ihwaddc(b'x' as i32);
assert_eq!(chline.lock().unwrap().as_str(), "ABx",
"c:368 — chline grows");
assert_eq!(hptr.load(Ordering::SeqCst), 3,
"c:368 — hptr advances by 1 (CRITICAL — previous port left this stale)");
ihwaddc(b'y' as i32);
assert_eq!(hptr.load(Ordering::SeqCst), 4,
"c:368 — hptr advances on each push");
qbang.store(true, Ordering::SeqCst);
ihwaddc(b'!' as i32);
assert_eq!(chline.lock().unwrap().as_str(), "ABxy\\!",
"c:366 — qbang escape pushes '\\\\' before bangchar");
assert_eq!(hptr.load(Ordering::SeqCst), 6,
"c:366+c:368 — both pushes advance hptr (was off-by-one previously)");
crate::ported::utils::errflag.store(1, Ordering::SeqCst);
let hptr_before = hptr.load(Ordering::SeqCst);
ihwaddc(b'z' as i32);
assert_eq!(hptr.load(Ordering::SeqCst), hptr_before,
"c:359 — errflag short-circuits, hptr unchanged");
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, Ordering::SeqCst);
crate::ported::utils::errflag.store(saved_errflag, Ordering::SeqCst);
lexstop.store(saved_lexstop, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
qbang.store(saved_qbang, Ordering::SeqCst);
bangchar.store(saved_bangchar, Ordering::SeqCst);
stophist.store(saved_stophist, Ordering::SeqCst);
hlinesz.store(saved_hlinesz, Ordering::SeqCst);
}
#[test]
fn ihwend_uses_hptr_not_chline_len() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = chline.lock().unwrap().clone();
let saved_chwords = chwords.lock().unwrap().clone();
let saved_chwordpos = chwordpos.load(Ordering::SeqCst);
let saved_hptr = hptr.load(Ordering::SeqCst);
let saved_stop = stophist.load(Ordering::SeqCst);
let saved_active = histactive.load(Ordering::SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
*chline.lock().unwrap() = "ABCDEFGHIJ".to_string();
*chwords.lock().unwrap() = vec![2, 0];
chwordpos.store(1, Ordering::SeqCst);
hptr.store(7, Ordering::SeqCst);
stophist.store(0, Ordering::SeqCst);
histactive.store(0, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(0));
ihwend();
assert_eq!(chwords.lock().unwrap().get(1).copied(), Some(7),
"c:1694 — chwords[chwordpos] = hptr - chline = 7 (NOT chline.len()=10)");
assert_eq!(chwordpos.load(Ordering::SeqCst), 2,
"c:1694 — chwordpos++ on successful close");
*chwords.lock().unwrap() = vec![5, 0];
chwordpos.store(1, Ordering::SeqCst);
hptr.store(3, Ordering::SeqCst); ihwend();
assert_eq!(chwordpos.load(Ordering::SeqCst), 0,
"c:1700 — chwordpos-- when hptr <= chwords[chwordpos-1]");
chwordpos.store(2, Ordering::SeqCst);
hptr.store(9, Ordering::SeqCst);
*chwords.lock().unwrap() = vec![0, 4, 5, 8];
ihwend();
assert_eq!(chwordpos.load(Ordering::SeqCst), 2,
"c:1691 — even chwordpos short-circuits");
*chline.lock().unwrap() = saved_chline;
*chwords.lock().unwrap() = saved_chwords;
chwordpos.store(saved_chwordpos, Ordering::SeqCst);
hptr.store(saved_hptr, Ordering::SeqCst);
stophist.store(saved_stop, Ordering::SeqCst);
histactive.store(saved_active, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
}
#[test]
fn histremovedups_removes_flagged_entries_only() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_ring = {
let r = hist_ring.lock().unwrap();
r.iter().map(|h| (h.node.nam.clone(), h.histnum, h.node.flags))
.collect::<Vec<_>>()
};
let saved_histlinect = histlinect.load(Ordering::SeqCst);
assert_eq!(HIST_DUP, 0x08,
"HIST_DUP bit value must match C (0x08), got {:#x}", HIST_DUP);
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
for (nam, num, flags) in [
("entry1", 1i64, 0i32),
("entry2", 2i64, HIST_DUP as i32), ("entry3", 3i64, 0i32),
] {
ring.push(histent {
node: crate::ported::zsh_h::hashnode {
next: None, nam: nam.to_string(), flags,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0,
words: vec![], nwords: 0,
histnum: num,
});
}
}
histlinect.store(3, Ordering::SeqCst);
histremovedups();
{
let ring = hist_ring.lock().unwrap();
assert_eq!(ring.len(), 2,
"c:1259-1260 — only the HIST_DUP-flagged entry is removed");
assert!(ring.iter().any(|h| h.node.nam == "entry1"));
assert!(ring.iter().any(|h| h.node.nam == "entry3"));
assert!(!ring.iter().any(|h| h.node.nam == "entry2"));
}
assert_eq!(histlinect.load(Ordering::SeqCst), 2,
"histlinect updated after removal");
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
for (nam, num, flags) in saved_ring {
ring.push(histent {
node: crate::ported::zsh_h::hashnode {
next: None, nam, flags,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0,
words: vec![], nwords: 0,
histnum: num,
});
}
}
histlinect.store(saved_histlinect, Ordering::SeqCst);
}
#[test]
fn iaddtoline_adjusts_excs_relative_to_zlemetacs() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = chline.lock().unwrap().clone();
let saved_excs = excs.load(Ordering::SeqCst);
let saved_zlemetacs =
crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst);
let saved_expanding = expanding.load(Ordering::SeqCst);
let saved_lexstop = lexstop.load(Ordering::SeqCst);
let saved_qbang = qbang.load(Ordering::SeqCst);
let saved_exlast = exlast.load(Ordering::SeqCst);
*chline.lock().unwrap() = String::new();
expanding.store(1, Ordering::SeqCst);
lexstop.store(false, Ordering::SeqCst);
qbang.store(false, Ordering::SeqCst);
excs.store(10, Ordering::SeqCst); crate::ported::zle::compcore::ZLEMETACS.store(5, Ordering::SeqCst);
crate::ported::input::inbufct.with(|c| c.set(3));
exlast.store(2, Ordering::SeqCst);
iaddtoline(b'x' as i32);
assert_eq!(excs.load(Ordering::SeqCst), 12,
"c:406 — excs += 1 + inbufct - exlast (10+1+3-2=12)");
excs.store(6, Ordering::SeqCst);
excs.store(25, Ordering::SeqCst);
crate::ported::zle::compcore::ZLEMETACS.store(20, Ordering::SeqCst);
crate::ported::input::inbufct.with(|c| c.set(1));
exlast.store(10, Ordering::SeqCst);
iaddtoline(b'y' as i32);
assert_eq!(excs.load(Ordering::SeqCst), 20,
"c:407-410 — clamp to zlemetacs(20) when post-add excs<zlemetacs");
excs.store(3, Ordering::SeqCst);
crate::ported::zle::compcore::ZLEMETACS.store(10, Ordering::SeqCst);
crate::ported::input::inbufct.with(|c| c.set(1));
exlast.store(0, Ordering::SeqCst);
iaddtoline(b'z' as i32);
assert_eq!(excs.load(Ordering::SeqCst), 3,
"c:405 — excs<=zlemetacs leaves excs unchanged");
*chline.lock().unwrap() = saved_chline;
excs.store(saved_excs, Ordering::SeqCst);
crate::ported::zle::compcore::ZLEMETACS
.store(saved_zlemetacs, Ordering::SeqCst);
expanding.store(saved_expanding, Ordering::SeqCst);
lexstop.store(saved_lexstop, Ordering::SeqCst);
qbang.store(saved_qbang, Ordering::SeqCst);
exlast.store(saved_exlast, Ordering::SeqCst);
}
#[test]
fn ihwbegin_records_hptr_not_chline_len() {
use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = chline.lock().unwrap().clone();
let saved_chwords = chwords.lock().unwrap().clone();
let saved_chwordpos = chwordpos.load(Ordering::SeqCst);
let saved_hptr = hptr.load(Ordering::SeqCst);
let saved_stop = stophist.load(Ordering::SeqCst);
let saved_active = histactive.load(Ordering::SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
*chline.lock().unwrap() = "ABCDEFGHIJ".to_string();
chwords.lock().unwrap().clear();
chwordpos.store(0, Ordering::SeqCst);
hptr.store(4, Ordering::SeqCst);
stophist.store(0, Ordering::SeqCst);
histactive.store(0, Ordering::SeqCst); crate::ported::input::inbufflags.with(|f| f.set(0));
ihwbegin(0);
let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
assert_eq!(recorded, 4,
"c:1658 — pos = hptr - chline + offset = 4 + 0 = 4 \
(NOT chline.len()=10)");
chwords.lock().unwrap().clear();
chwordpos.store(0, Ordering::SeqCst);
hptr.store(3, Ordering::SeqCst);
ihwbegin(-10); let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
assert_eq!(recorded, 0,
"c:1666 — pos<0 clamps to 0");
chwords.lock().unwrap().clear();
chwordpos.store(0, Ordering::SeqCst);
hptr.store(5, Ordering::SeqCst);
stophist.store(2, Ordering::SeqCst);
ihwbegin(0);
assert!(chwords.lock().unwrap().is_empty(),
"c:1659 — stophist==2 short-circuits, no record");
stophist.store(0, Ordering::SeqCst);
chwords.lock().unwrap().clear();
chwordpos.store(0, Ordering::SeqCst);
hptr.store(5, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(INP_ALIAS));
ihwbegin(0);
assert!(chwords.lock().unwrap().is_empty(),
"c:1659 — alias-only (INP_ALIAS without INP_HIST) short-circuits");
chwords.lock().unwrap().clear();
chwordpos.store(0, Ordering::SeqCst);
hptr.store(7, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(INP_ALIAS | INP_HIST));
ihwbegin(0);
let recorded = chwords.lock().unwrap().first().copied().unwrap_or(-1);
assert_eq!(recorded, 7,
"c:1659 — alias+hist mixed still records");
*chline.lock().unwrap() = saved_chline;
*chwords.lock().unwrap() = saved_chwords;
chwordpos.store(saved_chwordpos, Ordering::SeqCst);
hptr.store(saved_hptr, Ordering::SeqCst);
stophist.store(saved_stop, Ordering::SeqCst);
histactive.store(saved_active, Ordering::SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
}
#[test]
fn getargs_handles_field_indexing_and_overflow() {
let he = histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "echo hello world".to_string(),
flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words: vec![0, 4, 5, 10, 11, 16],
nwords: 3, histnum: 1,
};
assert_eq!(getargs(&he, 2, 1), None,
"c:2459 — arg2 < arg1 rejects");
assert_eq!(getargs(&he, 3, 3), None,
"c:2459 — arg1 >= nwords (3>=3) rejects");
assert_eq!(getargs(&he, 0, 3), None,
"c:2459 — arg2 >= nwords (3>=3) rejects");
assert_eq!(getargs(&he, 0, 2).as_deref(), Some("echo hello world"),
"c:2467 — full-event fast path returns dupstring(nam)");
assert_eq!(getargs(&he, 0, 0).as_deref(), Some("echo"),
"c:2481 — word[0] = nam[0..4]");
assert_eq!(getargs(&he, 1, 1).as_deref(), Some("hello"),
"c:2481 — word[1] = nam[5..10]");
assert_eq!(getargs(&he, 2, 2).as_deref(), Some("world"),
"c:2481 — word[2] = nam[11..16]");
assert_eq!(getargs(&he, 1, 2).as_deref(), Some("hello world"),
"c:2481 — words[1..=2] = nam[5..16]");
let overflow = histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "ab cd".to_string(),
flags: 0,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0,
words: vec![-1, 5, 3, 5], nwords: 2,
histnum: 1,
};
assert_eq!(getargs(&overflow, 0, 0), None,
"c:2476 — pos1 < 0 (i16 overflow) rejects");
let underflow = histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "a b c d".to_string(),
flags: 0,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0,
words: vec![0, 1, 2, 3, 1, 5, 6, 7],
nwords: 4,
histnum: 1,
};
assert_eq!(getargs(&underflow, 2, 2), None,
"c:2476 — pos1 < arg1 (i16 overflow signal) rejects");
}
#[test]
fn hconsearch_returns_histnum_and_word_index() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_ring = {
let r = hist_ring.lock().unwrap();
r.iter().map(|h| (
h.node.nam.clone(),
h.histnum,
h.words.clone(),
h.nwords,
h.node.flags,
)).collect::<Vec<_>>()
};
let saved_curhist = curhist.load(Ordering::SeqCst);
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
ring.push(histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "echo hello world".to_string(),
flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words: vec![0, 4, 5, 10, 11, 16],
nwords: 3,
histnum: 7,
});
}
curhist.store(8, Ordering::SeqCst);
let got = hconsearch("hello");
assert_eq!(got, Some((7, 1)),
"c:1846-1850 — strstr at pos 5 lands in word[1] (start=5)");
let got = hconsearch("world");
assert_eq!(got, Some((7, 2)),
"c:1846-1850 — strstr at pos 11 lands in word[2] (start=11)");
let got = hconsearch("echo");
assert_eq!(got, Some((7, 0)),
"c:1846-1850 — strstr at pos 0 lands in word[0]");
let got = hconsearch("notthere");
assert_eq!(got, None, "c:1853 — miss returns -1 / None");
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
ring.push(histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "skip me".to_string(),
flags: HIST_FOREIGN as i32,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words: vec![0, 4, 5, 7],
nwords: 2,
histnum: 3,
});
}
let got = hconsearch("skip");
assert_eq!(got, None,
"c:1843-1844 — HIST_FOREIGN entries continue past, miss → None");
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
for (nam, histnum, words, nwords, flags) in saved_ring {
ring.push(histent {
node: crate::ported::zsh_h::hashnode {
next: None, nam, flags,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0, words, nwords, histnum,
});
}
}
curhist.store(saved_curhist, Ordering::SeqCst);
}
#[test]
fn checkcurline_flushes_to_curline_only_when_active_and_matching() {
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_curhist = curhist.load(Ordering::SeqCst);
let saved_active = histactive.load(Ordering::SeqCst);
let saved_chline = chline.lock().unwrap().clone();
let saved_chwordpos = chwordpos.load(Ordering::SeqCst);
let saved_chwords = chwords.lock().unwrap().clone();
let saved_curline = curline.lock().unwrap().take();
curhist.store(42, Ordering::SeqCst);
histactive.store(HA_ACTIVE, Ordering::SeqCst);
*chline.lock().unwrap() = "echo hello".to_string();
chwordpos.store(4, Ordering::SeqCst); *chwords.lock().unwrap() = vec![0, 4, 5, 10];
*curline.lock().unwrap() = None;
let he = histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: "ignored-by-checkcurline".to_string(),
flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words: vec![],
nwords: 0,
histnum: 42,
};
checkcurline(&he);
{
let cl = curline.lock().unwrap();
let snap = cl.as_ref().expect(
"c:2425-2427 — matching+active must flush a snapshot"
);
assert_eq!(snap.node.nam, "echo hello",
"c:2425 — curline.node.nam = chline");
assert_eq!(snap.nwords, 2,
"c:2426 — curline.nwords = chwordpos/2 (4/2=2)");
assert_eq!(snap.words, vec![0, 4, 5, 10],
"c:2427 — curline.words = chwords");
}
histactive.store(0, Ordering::SeqCst); *curline.lock().unwrap() = None;
checkcurline(&he);
assert!(curline.lock().unwrap().is_none(),
"c:2424 — HA_ACTIVE cleared, no flush");
histactive.store(HA_ACTIVE, Ordering::SeqCst);
let he2 = histent { histnum: 99, ..histent {
node: crate::ported::zsh_h::hashnode {
next: None, nam: String::new(), flags: 0,
},
up: None, down: None, zle_text: None,
stim: 0, ftim: 0, words: vec![], nwords: 0, histnum: 0,
}};
checkcurline(&he2);
assert!(curline.lock().unwrap().is_none(),
"c:2424 — histnum mismatch, no flush");
curhist.store(saved_curhist, Ordering::SeqCst);
histactive.store(saved_active, Ordering::SeqCst);
*chline.lock().unwrap() = saved_chline;
chwordpos.store(saved_chwordpos, Ordering::SeqCst);
*chwords.lock().unwrap() = saved_chwords;
*curline.lock().unwrap() = saved_curline;
}
#[test]
fn remlpaths_count_exceeds_components_preserves_leading_slash() {
assert_eq!(remlpaths("/a/b/c", 4), "/a/b/c",
"c:2172-2175 — count > components → preserve original (leading slash)");
assert_eq!(remlpaths("/a/b/c", 10), "/a/b/c");
assert_eq!(remlpaths("a/b/c", 99), "a/b/c");
}
#[test]
fn remlpaths_trims_trailing_slashes() {
assert_eq!(remlpaths("/a/b/c/", 1), "c",
"c:2156-2161 — trailing slash trimmed before scan");
assert_eq!(remlpaths("/a/b/c///", 1), "c");
assert_eq!(remlpaths("/", 1), "",
"all-slashes input → empty after trim → empty result");
assert_eq!(remlpaths("", 1), "");
}
#[test]
fn remlpaths_count_zero_defaults_to_one() {
assert_eq!(remlpaths("/a/b/c", 0), "c",
"c:574 — count=0 from digitcount aliases to default 1");
assert_eq!(remlpaths("/a/b/c", 0), remlpaths("/a/b/c", 1));
}
}