use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicUsize, Ordering};
use std::sync::Mutex;
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::path::Component::*;
use std::sync::atomic::Ordering::SeqCst;
use crate::{DPUTS, DPUTS1};
use crate::ported::glob::remnulargs;
use crate::ported::hashtable::addhistnode;
use crate::ported::input::{ingetc, inputsetline, inungetc};
use crate::ported::lex::{lexinit, parse_subst_string, untokenize, ztokens, LEX_ISFIRSTCH, LEX_LEXSTOP};
use crate::ported::options::dosetopt;
use crate::ported::parse::init_parse_status;
use crate::ported::signals::unqueue_signals;
use crate::ported::subst::equalsubstr;
use crate::ported::utils::{errflag, zerr, ERRFLAG_ERROR};
use crate::ported::zle::compcore::ZLEMETACS;
use crate::ported::zsh_h::{
hashnode, histent, hist_stack, isset, Pound, BANGHIST, CSHJUNKIEHISTORY, ERRFLAG_INT,
HFILE_FAST, HFILE_USE_OPTIONS, HISTEXPIREDUPSFIRST, HISTIGNOREALLDUPS, HISTIGNOREDUPS,
HISTIGNORESPACE, HISTNOFUNCTIONS, HISTNOSTORE, HISTREDUCEBLANKS, HISTVERIFY, INCAPPENDHISTORY,
INCAPPENDHISTORYTIME, INP_ALIAS, INP_HIST, INTERACTIVE, SHAREHISTORY, SHINSTDIN,
CASMOD_CAPS, CASMOD_LOWER, CASMOD_NONE, CASMOD_UPPER, HISTFLAG_DONE, HISTFLAG_NOEXEC,
HISTFLAG_RECALL, HISTFLAG_SETTY, HIST_DUP, HIST_FOREIGN, HIST_NOWRITE, HIST_OLD, HIST_TMPSTORE,
};
use crate::ported::ztype_h::itok;
use crate::signals::queue_signals;
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 hist_stack, toplevel: i32) {
if toplevel != 0 {
*zle_chline.lock().unwrap() = Some(chline.lock().unwrap().clone()); }
hs.histactive = histactive.load(SeqCst) as i32; hs.histdone = histdone.load(SeqCst); hs.stophist = stophist.load(SeqCst); hs.hline = Some(chline.lock().unwrap().clone()); hs.hptr = Some(hptr.load(SeqCst).to_string()); hs.chwords = chwords.lock().unwrap().clone(); hs.chwordlen = chwordlen.load(SeqCst); hs.chwordpos = chwordpos.load(SeqCst); hs.hlinesz = hlinesz.load(SeqCst); hs.defev = defev.load(SeqCst); hs.hist_keep_comment = hist_keep_comment.load(SeqCst); hs.csp = 0;
stophist.store(0, SeqCst); chline.lock().unwrap().clear(); hptr.store(0, SeqCst); histactive.store(0, SeqCst); }
pub fn hist_context_restore(hs: &hist_stack, toplevel: i32) {
if toplevel != 0 {
DPUTS!(
hs.hline != *zle_chline.lock().unwrap(), "BUG: Ouch, wrong chline for ZLE" );
*zle_chline.lock().unwrap() = None; }
histactive.store(hs.histactive as u32, SeqCst); histdone.store(hs.histdone, SeqCst); stophist.store(hs.stophist, 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), SeqCst,
);
*chwords.lock().unwrap() = hs.chwords.clone(); chwordlen.store(hs.chwordlen, SeqCst); chwordpos.store(hs.chwordpos, SeqCst); hlinesz.store(hs.hlinesz, SeqCst); defev.store(hs.defev, SeqCst); hist_keep_comment.store(hs.hist_keep_comment, SeqCst); }
pub fn hist_in_word(yesno: i32) {
if yesno != 0 {
histactive.fetch_or(HA_INWORD, SeqCst);
} else {
histactive.fetch_and(!HA_INWORD, SeqCst);
}
}
pub fn hist_is_in_word() -> i32 {
if (histactive.load(SeqCst) & HA_INWORD) != 0 {
1
} else {
0
}
}
pub fn ihwaddc(c: i32) {
if errflag.load(SeqCst) != 0 || lexstop.load(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(SeqCst);
let qbang_active =
c == bc && stophist.load(SeqCst) < 2 && qbang.load(SeqCst);
{
let mut buf = chline.lock().expect("chline poisoned");
let bytes = unsafe { buf.as_mut_vec() };
let mut pos = hptr.load(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, SeqCst);
}
let cur_off = hptr.load(SeqCst) as i32; let sz = hlinesz.load(SeqCst);
if cur_off >= sz {
let new_sz = sz + 64;
hlinesz.store(new_sz, SeqCst); }
}
pub fn iaddtoline(c: i32) {
if expanding.load(SeqCst) == 0 || lexstop.load(SeqCst) {
return;
}
let bc = bangchar.load(SeqCst);
if qbang.load(SeqCst) && c == bc && stophist.load(SeqCst) < 2 {
exlast.fetch_sub(1, SeqCst); chline.lock().unwrap().push('\\'); }
let zlemetacs_v = ZLEMETACS.load(SeqCst);
let excs_v = excs.load(SeqCst);
if excs_v > zlemetacs_v {
let inbufct_now = crate::ported::input::inbufct.with(|c| c.get());
let exlast_v = exlast.load(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, SeqCst);
}
let inbufct_v = crate::ported::input::inbufct.with(|cnt| cnt.get());
exlast.store(inbufct_v, SeqCst); let push_byte: u8 = if c >= 0 && c <= 0xff && itok(c as u8) {
let idx = (c as u8).wrapping_sub(Pound as u8) as usize;
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(SeqCst) {
lexstop.store(false, SeqCst); } else {
if let Some(ch) = char::from_u32(c as u32) {
inungetc(ch);
}
}
}
pub fn ihgetc() -> i32 {
let mut c: i32 = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if exit_pending.load(SeqCst) {
lexstop.store(true, SeqCst); errflag.fetch_or(
ERRFLAG_ERROR,
SeqCst,
);
return b' ' as i32; }
qbang.store(false, SeqCst); let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
if stophist.load(SeqCst) == 0 && (inbufflags_v & INP_ALIAS) == 0
{
c = histsubchar(c); if c < 0 {
lexstop.store(true, SeqCst); errflag.fetch_or(
ERRFLAG_ERROR,
SeqCst,
);
return b' ' as i32; }
}
let inbufflags_v = crate::ported::input::inbufflags.with(|f| f.get());
let bc = bangchar.load(SeqCst);
if (inbufflags_v & INP_HIST) != 0 && stophist.load(SeqCst) == 0 {
qbang.store(false, SeqCst);
if c == b'\\' as i32 {
let g = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if g == bc {
qbang.store(true, SeqCst);
c = g;
} else {
safeinungetc(g);
c = b'\\' as i32;
}
}
} else if stophist.load(SeqCst) != 0 || (inbufflags_v & INP_ALIAS) != 0
{
let v = c == bc && stophist.load(SeqCst) < 2;
qbang.store(v, 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 {
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(SeqCst);
if LEX_ISFIRSTCH.with(|f| f.get()) && c == hat {
let mut gbal: i32 = 0; LEX_ISFIRSTCH.with(|f| f.set(false)); if let Some(ch) = char::from_u32(hat as u32) {
inungetc(ch); }
let ehist = match gethist(defev.load(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 {
LEX_ISFIRSTCH.with(|f| f.set(false)); }
let bc = bangchar.load(SeqCst);
if c == b'\\' as i32 {
let g = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if g != bc {
safeinungetc(g); } else {
qbang.store(true, SeqCst); return bc; }
}
if c != bc {
return c; }
let pos = hptr.load(SeqCst); {
let mut cl = chline.lock().unwrap();
if pos < cl.len() {
cl.truncate(pos);
}
}
c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'{' as i32 {
bflag = 1; cflag = 1;
c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
if c == b'"' as i32 {
stophist.store(1, SeqCst); return 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(SeqCst)
{
safeinungetc(c); return bc; }
cflag = 0; let mut buflen: usize = 265; buf = String::with_capacity(buflen);
queue_signals(); if c == b'?' as i32 {
loop {
c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'?' as i32 || c == b'\n' as i32 || lexstop.load(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(SeqCst) {
c = 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, SeqCst); marg.store(marg_val, SeqCst); evset = 0; if ev == -1 {
herrflush(); unqueue_signals(); 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(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 = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
break; }
c = 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); unqueue_signals(); return bc; }
if buf.is_empty() {
if c != b'%' as i32 {
if isset(CSHJUNKIEHISTORY) {
ev = addhistnum(curhist.load(SeqCst), -1, HIST_FOREIGN as i32);
} else {
ev = defev.load(SeqCst); }
if c == b':' as i32 && evset == -1 {
evset = 0; } else {
evset = 1; }
} else {
if marg.load(SeqCst) != -1 {
ev = mev.load(SeqCst); } else {
ev = defev.load(SeqCst); }
evset = 0; }
} else if let Ok(t0) = buf.trim().parse::<i64>() {
if t0 != 0 {
ev = if t0 < 0 {
addhistnum(
curhist.load(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(SeqCst), -1, HIST_FOREIGN as i32); evset = 1; } else if buf.as_bytes()[0] == b'#' {
ev = curhist.load(SeqCst); evset = 1; } else {
match hcomsearch(&buf) {
Some(e) => {
ev = e;
evset = 1;
}
None => {
herrflush(); unqueue_signals(); zerr(&format!("event not found: {}", buf)); return -1; }
}
}
} else if buf.as_bytes()[0] == bc as u8 {
ev = addhistnum(curhist.load(SeqCst), -1, HIST_FOREIGN as i32);
evset = 1;
} else if buf.as_bytes()[0] == b'#' {
ev = curhist.load(SeqCst);
evset = 1;
} else {
match hcomsearch(&buf) {
Some(e) => {
ev = e;
evset = 1;
}
None => {
herrflush();
unqueue_signals();
zerr(&format!("event not found: {}", buf));
return -1;
}
}
}
}
defev.store(ev, SeqCst); let mut ehist = match gethist(ev) {
Some(h) => h,
None => {
unqueue_signals(); return -1; }
};
argc = getargc(&ehist) as i32;
if c == b':' as i32 {
cflag = 1; c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'%' as i32 && marg.load(SeqCst) != -1 {
if evset == 0 {
ehist = match gethist(mev.load(SeqCst)) {
Some(h) => {
defev.store(mev.load(SeqCst), SeqCst);
h
}
None => {
unqueue_signals();
return -1;
}
};
argc = getargc(&ehist) as i32; } else {
herrflush(); unqueue_signals(); 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) {
inungetc(ch); }
let r = getargspec(argc, marg.load(SeqCst), evset); larg = r;
farg = r;
if larg == -2 {
unqueue_signals(); return -1; }
if farg != -1 {
cflag = 0; }
c = 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(SeqCst), evset); if larg == -2 {
unqueue_signals(); return -1; }
if larg == -1 {
larg = argc - 1; }
} else {
if let Some(ch) = char::from_u32(c as u32) {
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 => {
unqueue_signals(); return -1; }
};
unqueue_signals(); }
loop {
c = if cflag != 0 {
b':' as i32
} else {
ingetc().map(|ch| ch as i32).unwrap_or(-1)
};
cflag = 0; if c == b':' as i32 {
let mut gbal: i32 = 0; c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c == b'g' as i32 {
gbal = 1; c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if c != b's' as i32 && c != b'S' as i32 && c != b'&' as i32 {
zerr("'s' or '&' modifier expected after 'g'"); return -1; }
}
match c as u8 {
b'p' => {
histdone.store(HISTFLAG_DONE | HISTFLAG_NOEXEC, SeqCst);
}
b'a' => {
match chabspath(&sline) {
Some(new) => sline = new,
None => {
herrflush(); zerr("modifier failed: a"); return -1; }
}
}
b'A' => {
match chrealpath(&sline, b'A', false) {
Some(new) => sline = new,
None => {
herrflush(); zerr("modifier failed: A"); return -1; }
}
}
b'c' => {
match equalsubstr(&sline, false, false) {
Some(new) => sline = new,
None => {
herrflush(); 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, 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(); 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();
zerr("no previous substitution");
return -1;
}
}
b'q' => {
sline = quote(&sline); }
b'Q' => {
let oef = errflag.load(SeqCst);
let _ = parse_subst_string(&sline); errflag.store(
oef | (errflag.load(SeqCst) & ERRFLAG_INT),
SeqCst,
); let mut s = sline.clone();
remnulargs(&mut s); sline = 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('/') {
let here = crate::ported::compat::zgetcwd(); sline = if here.ends_with('/') {
crate::ported::string::dyncat(&here, &sline) } else {
format!("{}/{}", here, sline) }; }
match crate::ported::utils::xsymlink(&sline) {
Some(new) => sline = new,
None => {} }
}
_ => {
herrflush(); 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) {
inungetc(ch); }
}
if c != b'}' as i32 && bflag != 0 {
zerr("'}' expected"); return -1; }
break; }
}
let _ = lexraw_mark;
lexstop.store(false, SeqCst); crate::ported::input::inpush(&sline, INP_HIST, None); histdone.fetch_or(HISTFLAG_DONE, SeqCst); if isset(HISTVERIFY) {
histdone.fetch_or(HISTFLAG_NOEXEC | HISTFLAG_RECALL, SeqCst); }
ingetc().map(|ch| ch as i32).unwrap_or(-1)
}
pub fn herrflush() {
crate::ported::input::inpopalias();
if 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(SeqCst);
let lex_add_raw = crate::ported::lex::LEX_LEX_ADD_RAW.get();
if !(strin_v == 0 || lex_add_raw != 0) {
break;
}
let c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
if !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(); zerr("substitution failed"); -1 }
pub fn digitcount() -> i32 {
let mut c: i32 = 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 = 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) {
inungetc(ch);
}
}
count }
pub fn strinbeg(dohist: i32) {
strin.fetch_add(1, SeqCst); hbegin(dohist); lexinit(); init_parse_status(); }
pub fn strinend() {
hend(None); DPUTS!(
strin.load(Ordering::SeqCst) == 0, "BUG: strinend() called without strinbeg()" );
strin.fetch_sub(1, SeqCst); LEX_ISFIRSTCH.with(|f| f.set(true)); histdone.store(0, SeqCst); }
pub fn nohw(_c: i32) {
}
pub fn nohwabort() {
}
pub fn nohwe() {
}
pub fn ihwbegin(offset: i32) {
let hptr_val = hptr.load(SeqCst); let stop = stophist.load(SeqCst);
let active = histactive.load(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(SeqCst);
if pos % 2 != 0 {
chwordpos.fetch_sub(1, SeqCst); }
let word_pos = (hptr_val as i32) + offset; DPUTS1!(
word_pos < 0, "History word position < 0 in {}", {
let line = chline.lock().unwrap(); line.chars().take(hptr_val).collect::<String>() }
);
let start = word_pos.max(0) as i16; let mut words = chwords.lock().unwrap();
let idx = chwordpos.load(SeqCst) as usize;
if words.len() <= idx {
words.resize(idx + 1, 0);
}
words[idx] = start; chwordpos.fetch_add(1, SeqCst); }
pub fn linkcurline() {
let new_hist = curhist.fetch_add(1, 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, SeqCst); }
pub fn hbegin(dohist: i32) {
errflag.fetch_and(
!ERRFLAG_ERROR,
Ordering::Relaxed,
);
histdone.store(0, SeqCst); let interact = isset(INTERACTIVE);
let shinstdin = isset(SHINSTDIN);
if dohist == 0 {
stophist.store(2, SeqCst); } else if dohist != 2 {
stophist.store(
if !interact || !shinstdin { 2 } else { 0 }, SeqCst,
);
} else {
stophist.store(0, SeqCst); }
if stophist.load(SeqCst) == 2 {
chline.lock().unwrap().clear(); hptr.store(0, SeqCst); hlinesz.store(0, SeqCst); chwords.lock().unwrap().clear(); chwordlen.store(0, SeqCst); } else {
let mut buf = chline.lock().unwrap(); buf.clear();
buf.reserve(64);
hlinesz.store(64, SeqCst); drop(buf);
let mut w = chwords.lock().unwrap(); w.clear();
w.reserve(64);
chwordlen.store(64, SeqCst);
drop(w);
if !isset(BANGHIST) {
stophist.store(4, SeqCst); }
}
chwordpos.store(0, SeqCst);
{
let mut ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first_mut() {
if top.ftim == 0 && strin.load(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(SeqCst) == 0
{
histactive.store(HA_ACTIVE, SeqCst); let mypgrp = *crate::ported::jobs::MYPGRP
.get_or_init(|| Mutex::new(0))
.lock()
.expect("mypgrp poisoned");
crate::ported::utils::attachtty(mypgrp); linkcurline(); defev.store(
addhistnum(
curhist.load(SeqCst), -1,
HIST_FOREIGN as i32,
),
SeqCst,
);
} else {
histactive.store(HA_ACTIVE | HA_NOINC, SeqCst); }
if isset(INCAPPENDHISTORYTIME) && !isset(SHAREHISTORY)
&& !isset(INCAPPENDHISTORY)
&& (histactive.load(SeqCst) & HA_NOINC) == 0
&& strin.load(SeqCst) == 0
&& histsave_stack_pos.load(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, 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(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 {
thread_local! {
static NEXT_IDX: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
static MAX_UNIQUE_CT: std::cell::Cell<i64> = const { std::cell::Cell::new(0) };
}
let mut ring = hist_ring.lock().unwrap();
if ring.is_empty() {
return 0;
}
let mut idx: Option<usize> = if keep_going != 0 {
NEXT_IDX.with(|c| c.get())
} else {
Some(ring.len() - 1) };
let mut cur_idx = match idx {
Some(i) if i < ring.len() => i,
_ => return 0,
};
idx = if cur_idx == 0 {
None
} else {
Some(cur_idx - 1)
};
NEXT_IDX.with(|c| c.set(idx));
let exp_dups_first = isset(HISTEXPIREDUPSFIRST);
if exp_dups_first && (ring[cur_idx].node.flags as u32 & HIST_DUP) == 0 {
if keep_going == 0 {
MAX_UNIQUE_CT.with(|c| c.set(savehistsiz.load(SeqCst) as i64));
}
loop {
let cur = MAX_UNIQUE_CT.with(|c| {
let v = c.get();
c.set(v - 1);
v
});
if cur <= 0 {
MAX_UNIQUE_CT.with(|c| c.set(0));
cur_idx = ring.len() - 1;
NEXT_IDX.with(|c| {
c.set(if cur_idx == 0 {
None
} else {
Some(cur_idx - 1)
})
});
break;
}
cur_idx = match NEXT_IDX.with(|c| c.get()) {
Some(i) if i < ring.len() => i,
_ => return 0,
};
let nxt = if cur_idx == 0 {
None
} else {
Some(cur_idx - 1)
};
NEXT_IDX.with(|c| c.set(nxt));
if (ring[cur_idx].node.flags as u32 & HIST_DUP) != 0 {
break;
}
}
}
if cur_idx < ring.len() && cur_idx != 0 {
let entry = ring.remove(cur_idx);
ring.insert(0, entry);
}
1
}
pub fn prepnexthistent() -> i64 {
let cap = histsiz.load(SeqCst);
if histlinect.load(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, SeqCst);
}
}
let n = curhist.fetch_add(1, SeqCst) + 1;
n
}
fn should_ignore_line(prog: Option<&[u8]>) -> i32 {
let line = chline.lock().unwrap().clone();
if isset(HISTIGNORESPACE) {
let alias_space = crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.get()) != 0;
if line.starts_with(' ') || alias_space {
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(SeqCst); let mut save: i32 = 1; let mut hookret: i32 = 0;
crate::ported::signals::queue_signals(); if (histdone.load(SeqCst) & HISTFLAG_SETTY) != 0 { }
let active = histactive.load(SeqCst);
if (active & HA_NOINC) == 0 {
unlinkcurline(); }
if (active & HA_NOINC) != 0 {
chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, SeqCst); histactive.store(0, SeqCst); unqueue_signals(); return 1; }
let cur_ignore_all = if isset(HISTIGNOREALLDUPS) { 1 } else { 0 }; let prev_ignore_all = hist_ignore_all_dups.load(SeqCst);
if prev_ignore_all != cur_ignore_all && {
hist_ignore_all_dups.store(cur_ignore_all, SeqCst); cur_ignore_all != 0
}
{
histremovedups(); }
let chline_text = chline.lock().unwrap().clone();
if !chline_text.is_empty() {
let save_errflag = errflag .load(Ordering::Relaxed);
errflag.store(0, Ordering::Relaxed); let args = vec!["zshaddhistory".to_string(), chline_text.clone()]; hookret = crate::ported::utils::callhookfunc(
"zshaddhistory",
Some(&args),
1,
std::ptr::null_mut(),
);
let new_errflag = (errflag .load(Ordering::Relaxed)
& !ERRFLAG_ERROR)
| save_errflag;
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(SeqCst); histdone.store(0, SeqCst); let hptr_pos = hptr.load(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(SeqCst) > 0 {
hptr.fetch_sub(1, SeqCst);
}
} else {
save = 0; }
}
if chwordpos.load(SeqCst) <= 2 && hist_keep_comment.load(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 {
crate::ported::zle::zle_main::BUFSTACK
.lock()
.unwrap()
.insert(0, ptr.clone()); 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, SeqCst); }
ring.remove(idx); histlinect.fetch_sub(1, SeqCst);
}
}
if save != 0 {
if chwordpos.load(SeqCst) % 2 != 0 {
ihwend();
}
let cwp = chwordpos.load(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, 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(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, SeqCst);
if (newflags & HIST_TMPSTORE) == 0 {
addhistnode(&text, n as i32);
}
}
}
chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, SeqCst); histactive.store(0, SeqCst);
let share = isset(SHAREHISTORY);
let do_inc = if share {
histfileIsLocked() != 0 } else {
isset(INCAPPENDHISTORY) || (isset(INCAPPENDHISTORYTIME) && histsave_stack_pos.load(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(SeqCst) > stack_pos {
pophiststack(); }
hist_keep_comment.store(0, SeqCst); unqueue_signals(); if (flag & HISTFLAG_NOEXEC) != 0 || errflag.load(Ordering::Relaxed) != 0 {
0 } else {
1
}
}
pub fn ihwabort() {
let pos = chwordpos.load(SeqCst);
if pos % 2 != 0 {
chwordpos.fetch_sub(1, SeqCst);
}
hist_keep_comment.store(1, SeqCst);
}
pub fn ihwend() {
let stop = stophist.load(SeqCst);
let active = histactive.load(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(SeqCst);
if pos % 2 == 0 {
return;
}
let cur = hptr.load(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, SeqCst);
} else {
chwordpos.fetch_sub(1, SeqCst);
}
}
pub fn histbackword() {
let pos = chwordpos.load(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, SeqCst); }
}
}
pub fn hwget() -> Option<(i32, String)> {
let pos = chwordpos.load(SeqCst);
if pos == 0 {
DPUTS!(true, "BUG: hwget() called with no words"); return None;
}
if pos % 2 != 0 {
DPUTS!(true, "BUG: hwget() called in middle of word"); 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, SeqCst); chwordpos.fetch_sub(2, SeqCst); ihwbegin(0);
qbang.store(true, SeqCst); for b in rep.bytes() {
ihwaddc(b as i32);
}
ihwend();
}
pub fn hgetline() -> Option<String> {
let hp = hptr.load(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, SeqCst);
chwordpos.store(0, SeqCst);
Some(truncated) }
pub fn getargspec(argc: i32, marg_arg: i32, evset: i32) -> i32 {
let mut c: i32 = 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(); zerr("no such word in event"); return -2; }
c = ingetc() .map(|ch| ch as i32)
.unwrap_or(-1);
}
if let Some(ch) = char::from_u32(c as u32) {
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(); zerr("Ambiguous history reference"); return -2; }
if marg_arg == -1 {
herrflush(); zerr("%% with no previous word matched"); return -2; }
ret = marg_arg; } else {
if let Some(ch) = char::from_u32(c as u32) {
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(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, mode: u8, _use_heap: bool) -> Option<String> {
DPUTS1!(
mode != b'A' && mode != b'P', "chrealpath: mode='{}' is invalid",
mode as char );
if path.is_empty() {
return Some(String::new());
}
if !path.starts_with('/') {
return None;
}
let bytes = path.as_bytes();
let mut prefix_end = bytes.len();
let mut real: Option<String> = None;
loop {
let trial = &path[..prefix_end];
if let Ok(canonical) = std::fs::canonicalize(trial) {
real = Some(canonical.to_string_lossy().into_owned());
break;
}
let mut i = prefix_end.saturating_sub(1);
while i > 0 && bytes[i] != b'/' {
i -= 1;
}
if i == 0 {
break;
}
prefix_end = i;
}
let tail = &path[prefix_end..];
match real {
Some(r) => Some(format!("{}{}", r, tail)),
None => Some(tail.to_string()),
}
}
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 is_combining =
(c as u32) != 0 && unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) == 0;
let modified = match how {
x if x == CASMOD_LOWER => { if c.is_uppercase() {
c.to_lowercase().collect::<String>()
} else {
c.to_string()
}
}
x if x == CASMOD_UPPER => { if c.is_lowercase() {
c.to_uppercase().collect::<String>()
} else {
c.to_string()
}
}
x if x == CASMOD_CAPS => { if is_combining { c.to_string()
} else if !c.is_alphanumeric() { nextupper = true;
c.to_string()
} else if nextupper { nextupper = false;
if c.is_lowercase() { c.to_uppercase().collect::<String>()
} else {
c.to_string()
}
} else if c.is_uppercase() { c.to_lowercase().collect::<String>()
} else {
c.to_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('#').or_else(|| pat.strip_prefix(Pound)) {
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(SeqCst); let active = histactive.load(SeqCst); if he.histnum == curhist_val && (active & HA_ACTIVE) != 0 {
let chline_val = chline.lock().expect("chline poisoned").clone(); let chwordpos_val = chwordpos.load(SeqCst); let chwords_val = chwords.lock().expect("chwords poisoned").clone(); let mut cl = curline.lock().expect("curline poisoned");
*cl = Some(histent {
node: 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();
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(); 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(); 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> {
let stop_c = stop as u8 as char; let mut buf = String::with_capacity(256); let mut c: Option<char>; loop {
c = ingetc(); match c {
None => break,
Some(ch) if ch == stop_c => break, Some('\n') => break, Some(ch) => {
if lexstop.load(SeqCst) {
break;
} let mut written = ch;
if ch == '\\' {
if let Some(nxt) = ingetc() {
written = nxt;
} else {
break;
}
}
buf.push(written); }
}
}
if let Some('\n') = c {
inungetc('\n'); zerr("delimiter expected"); return None; }
Some(buf) }
pub fn ihungetc(c: i32) {
let mut c = c as u8 as char; let mut doit = 1; while !lexstop.load(SeqCst) && errflag.load(SeqCst) == 0
{
let hp = hptr.load(SeqCst);
let line = chline.lock().unwrap().clone();
let line_b = line.as_bytes();
let stop = stophist.load(SeqCst);
let inflags = crate::ported::input::inbufflags.with(|f| f.get());
let active = 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 & HA_UNGET) == 0
&& (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS
{
histactive.fetch_or(HA_UNGET, SeqCst); inungetc('\n'); inungetc('\\'); histactive.fetch_and(!HA_UNGET, SeqCst);
}
if expanding.load(SeqCst) != 0 {
ZLEMETACS.fetch_sub(1, SeqCst); crate::ported::zle::compcore::ZLEMETALL.fetch_sub(1, SeqCst); exlast.fetch_add(1, SeqCst); }
if (inflags & (INP_ALIAS | INP_HIST)) != INP_ALIAS {
DPUTS!(hp <= 0, "BUG: hungetc attempted at buffer start"); DPUTS!(
hp > 0 && line_b.get(hp - 1).copied() != Some(c as u8), "BUG: wrong character in hungetc() " );
let new_hp = hp.saturating_sub(1);
hptr.store(new_hp, SeqCst); let bangchar_v = 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'\\');
qbang.store(qb, SeqCst);
} else {
qbang.store(false, SeqCst); }
if doit != 0 {
inungetc(c); }
if !qbang.load(SeqCst) {
return;
} let inflags2 = crate::ported::input::inbufflags.with(|f| f.get());
doit = if 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 ingetc() {
Some(c) => c,
None => return 1,
};
let read_until = |stop: char| -> Option<String> {
let mut out = String::new();
loop {
match ingetc() {
None => return None,
Some('\n') => return Some(out),
Some(c) if c == stop => return Some(out),
Some('\\') => {
if let Some(n) = 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 = ingetc(); if follow == Some(':') {
let next = ingetc(); if next == Some('G') {
*gbalp = 1;
}
else {
if let Some(c) = next {
inungetc(c);
} *cflagp = 1; }
} else if let Some(c) = follow {
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, SeqCst);
savehistsiz.store(1000, SeqCst);
curhist.store(0, SeqCst);
histlinect.store(0, SeqCst);
}
pub fn resizehistents() {
let cap = histsiz.load(SeqCst);
while histlinect.load(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, 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 lock_ret = lockhistfile(Some(&path), 1);
if lock_ret != 0 {
if lock_ret == 2 {
crate::ported::utils::zwarn(&format!(
"locking failed for {}: {}: reading anyway",
path,
std::io::Error::last_os_error()
));
} else {
crate::ported::utils::zerr(&format!(
"locking failed for {}: {}",
path,
std::io::Error::last_os_error()
));
return;
}
}
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, 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, 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, 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, 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 !isset(INTERACTIVE)
{
return;
}
let cap = savehistsiz.load(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 lock_ret = lockhistfile(Some(&path), 1);
if lock_ret != 0 && lock_ret != 2 {
crate::ported::utils::zerr(&format!(
"locking failed for {}: {}",
path,
std::io::Error::last_os_error()
));
return;
}
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, 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, SeqCst);
if keep_trying != 0 {
2
} else {
1
}
}
pub fn unlockhistfile(path: &str) {
let prev = lockhistct.fetch_sub(1, SeqCst);
if prev <= 0 {
lockhistct.store(0, 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(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, uselex: bool) -> Vec<(usize, usize)> {
if uselex {
let (lexed, _) = bufferwords(line, 0);
let bytes = line.as_bytes();
let mut lptr: usize = 0;
let mut words: Vec<(usize, usize)> = Vec::with_capacity(lexed.len());
let mut bad = false;
for word in &lexed {
while lptr < bytes.len() {
let b = bytes[lptr];
if b == b' ' || b == b'\t' {
lptr += 1;
} else if b == b'\\' && lptr + 1 < bytes.len() && bytes[lptr + 1] == b'\n' {
lptr += 2;
} else {
break;
}
}
let word_start = lptr;
let wbytes = word.as_bytes();
let mut wptr: usize = 0;
loop {
if word == ";"
&& lptr + 1 < bytes.len()
&& bytes[lptr] == b';'
&& bytes[lptr + 1] == b';'
{
break;
}
if lptr + wbytes.len() - wptr <= bytes.len()
&& bytes[lptr..lptr + wbytes.len() - wptr] == wbytes[wptr..]
{
lptr += wbytes.len() - wptr;
wptr = wbytes.len();
break;
}
let mut skipping = false;
if word == ";" {
break;
}
while lptr < bytes.len() {
if wptr >= wbytes.len() {
bad = true;
break;
}
if bytes[lptr] == wbytes[wptr] || (bytes[lptr] == b'!' && wbytes[wptr] == b'|')
{
lptr += 1;
wptr += 1;
if wptr >= wbytes.len() {
break;
}
} else if bytes[lptr] == b'\\'
&& lptr + 1 < bytes.len()
&& bytes[lptr + 1] == b'\n'
{
lptr += 2;
skipping = true;
break;
} else {
bad = true;
break;
}
}
if bad || !skipping {
break;
}
}
if bad {
return histsplitwords(line, false);
}
words.push((word_start, lptr));
}
return words;
}
let mut words = Vec::new();
let bytes = line.as_bytes();
let mut lptr: usize = 0;
loop {
while lptr < bytes.len() {
let b = bytes[lptr];
if b == b' ' || b == b'\t' || b == b'\n' {
lptr += 1;
} else if b == b'\\' && lptr + 1 < bytes.len() && bytes[lptr + 1] == b'\n' {
lptr += 2;
} else {
break;
}
}
if lptr >= bytes.len() {
break;
}
let word_start = lptr; while lptr < bytes.len() {
let b = bytes[lptr];
if b == 0x83 && lptr + 1 < bytes.len() {
lptr += 2;
} else if b == b' ' || b == b'\t' || b == b'\n' {
break;
} else {
lptr += 1;
}
}
words.push((word_start, lptr)); }
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(SeqCst), histlinect: histlinect.load(SeqCst), histsiz: histsiz.load(SeqCst), savehistsiz: savehistsiz.load(SeqCst), locallevel: level, };
histsave_stack.lock().unwrap().push(snap); histsave_stack_size.fetch_add(1, SeqCst);
histsave_stack_pos.fetch_add(1, SeqCst);
histsiz.store(hs, SeqCst); savehistsiz.store(shs, SeqCst); curhist.store(0, SeqCst); histlinect.store(0, 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, SeqCst); histlinect.store(snap.histlinect, SeqCst); histsiz.store(snap.histsiz, SeqCst); savehistsiz.store(snap.savehistsiz, SeqCst); histsave_stack_size.fetch_sub(1, SeqCst);
histsave_stack_pos.fetch_sub(1, SeqCst);
histsave_stack_pos.load(SeqCst) + 1
}
pub fn saveandpophiststack(mut pop_through: i32, writeflags: i32) -> i32 {
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);
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: 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: 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 = 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' => {
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 chrealpath_tests {
use super::*;
#[test]
fn chrealpath_rejects_relative_path() {
let _g = crate::test_util::global_state_lock();
let r = chrealpath("relative/path", b'P', false); assert!(
r.is_none(),
"c:1999-2000 — relative path MUST return None; got {:?}",
r
);
}
#[test]
fn chrealpath_empty_returns_empty() {
let _g = crate::test_util::global_state_lock();
let r = chrealpath("", b'P', false); assert_eq!(
r.as_deref(),
Some(""),
"c:1985-1986 — empty input returns Some(empty), not None"
);
}
#[test]
fn chrealpath_partial_prefix_fallback() {
let _g = crate::test_util::global_state_lock();
let dir = tempfile::tempdir().expect("tempdir");
let probe = dir.path().join("nonexistent_sub/nonexistent_tail");
let probe_str = probe.to_str().unwrap();
let r = chrealpath(probe_str, b'P', false) .expect("partial-prefix walk → Some");
assert!(
r.ends_with("/nonexistent_sub/nonexistent_tail"),
"c:2046-2048 — unresolvable tail must be re-spliced; got {:?}",
r
);
assert!(r.starts_with('/'), "result must be absolute; got {:?}", r);
}
}
#[cfg(test)]
mod histsplitwords_uselex_tests {
use super::*;
#[test]
fn uselex_matches_simple_words() {
let line = "echo hi";
let words = histsplitwords(line, true);
assert_eq!(words, vec![(0, 4), (5, 7)]);
}
#[test]
fn no_uselex_matches_simple_words() {
let line = "echo hi";
let words = histsplitwords(line, false);
assert_eq!(words, vec![(0, 4), (5, 7)]);
}
#[test]
fn uselex_falls_back_on_lex_disagreement() {
let line = "a;b";
let words = histsplitwords(line, true);
assert!(!words.is_empty(), "must produce at least one word");
assert!(words.iter().all(|(s, e)| s < e && *e <= line.len()));
}
#[test]
fn uselex_handles_compound_operators() {
let line = "a && b";
let words = histsplitwords(line, true);
for (s, e) in &words {
assert!(*e <= line.len() && s < e);
}
}
#[test]
fn no_uselex_trailing_whitespace_no_phantom() {
let words = histsplitwords("hi ", false);
assert_eq!(words, vec![(0, 2)]);
}
#[test]
fn uselex_distinguishes_semicolon_from_double() {
let line = "foo ;; bar";
let words = histsplitwords(line, true);
for (s, e) in &words {
assert!(
*e <= line.len(),
"word ({},{}) overflows line len {}",
s,
e,
line.len()
);
assert!(s < e, "empty span ({},{})", s, e);
}
let last = words.last().expect("at least one word");
assert_eq!(
&line[last.0..last.1],
"bar",
"c:3715-3722 — last word must be 'bar' regardless of ;; handling, got {:?}",
&line[last.0..last.1]
);
}
}
#[cfg(test)]
mod subst_modifier_tests {
use super::*;
fn hist_test_lock() -> &'static Mutex<()> {
static L: std::sync::OnceLock<Mutex<()>> = std::sync::OnceLock::new();
L.get_or_init(|| Mutex::new(()))
}
#[test]
fn s_replaces_first_occurrence() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
apply_history_modifiers("foo bar foo", ":s/foo/baz/"),
"baz bar foo"
);
}
#[test]
fn gs_replaces_all_occurrences() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
apply_history_modifiers("foo bar foo", ":gs/foo/baz/"),
"baz bar baz"
);
}
#[test]
fn ampersand_repeats_last_subst() {
let _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
let _ = apply_history_modifiers("init", ":s/i/X/");
assert_eq!(apply_history_modifiers("aiibii", ":g&"), "aXXbXX");
}
#[test]
fn s_alternate_delimiter() {
let _g = crate::test_util::global_state_lock();
assert_eq!(apply_history_modifiers("a-b-c", ":s|-|+|"), "a+b-c");
}
#[test]
fn s_escaped_delimiter_in_pattern() {
let _g = crate::test_util::global_state_lock();
assert_eq!(apply_history_modifiers("a/b", r":s/\//#/"), "a#b");
}
#[test]
fn up_histent_on_empty_ring_is_none() {
let _g = crate::test_util::global_state_lock();
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 _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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 = crate::test_util::global_state_lock();
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
inputsetline("42abc", 0);
let n = digitcount();
assert_eq!(n, 42, "c:581 — decimal digit accumulation");
let nxt = ingetc().unwrap_or('\0');
assert_eq!(nxt, 'a', "c:587 — non-digit terminator was inungetc'd");
inputsetline("xyz", 0);
let n = digitcount();
assert_eq!(n, 0, "c:586 — non-digit first char returns 0");
let nxt = 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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("HELLO World", CASMOD_LOWER), "hello world");
}
#[test]
fn casemodify_upper_uppercases() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("hello world", CASMOD_UPPER), "HELLO WORLD");
}
#[test]
fn casemodify_caps_capitalises_word_starts() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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 = 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 = crate::test_util::global_state_lock();
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_chline = std::mem::take(&mut *chline.lock().unwrap());
let saved_hptr = hptr.swap(0, SeqCst);
let saved_chwordpos = chwordpos.swap(0, SeqCst);
assert_eq!(hgetline(), None, "c:1777 — empty chline returns None");
*chline.lock().unwrap() = "abcdef".to_string();
hptr.store(0, SeqCst);
assert_eq!(hgetline(), None, "c:1777 — hptr == 0 returns None");
*chline.lock().unwrap() = "abcdef".to_string();
hptr.store(3, SeqCst);
chwordpos.store(2, SeqCst);
let result = hgetline();
assert_eq!(
result,
Some("abc".to_string()),
"c:1779 — truncate chline at hptr=3 returns 'abc'"
);
assert_eq!(hptr.load(SeqCst), 0, "c:1783 — hptr reset to 0");
assert_eq!(
chwordpos.load(SeqCst),
0,
"c:1784 — chwordpos reset to 0"
);
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, SeqCst);
chwordpos.store(saved_chwordpos, SeqCst);
}
#[test]
fn histbackword_rewinds_hptr_on_even_boundary() {
let _g = crate::test_util::global_state_lock();
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_pos = chwordpos.swap(0, SeqCst);
let saved_hptr = hptr.swap(0, 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, SeqCst);
hptr.store(999, SeqCst);
histbackword();
assert_eq!(
hptr.load(SeqCst),
7,
"c:1715 — even chwordpos must rewind hptr to chwords[pos-1]"
);
chwordpos.store(0, SeqCst);
hptr.store(123, SeqCst);
histbackword();
assert_eq!(
hptr.load(SeqCst),
123,
"c:1714 — chwordpos == 0 means no-op (hptr untouched)"
);
chwordpos.store(3, SeqCst);
hptr.store(456, SeqCst);
histbackword();
assert_eq!(
hptr.load(SeqCst),
456,
"c:1714 — odd chwordpos means mid-word, no-op"
);
chwordpos.store(saved_pos, SeqCst);
hptr.store(saved_hptr, SeqCst);
*chwords.lock().unwrap() = saved_words;
}
#[test]
fn ihwaddc_overwrites_at_hptr_not_append() {
let _g = crate::test_util::global_state_lock();
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(SeqCst);
let saved_errflag = errflag.load(SeqCst);
let saved_lexstop = lexstop.load(SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
let saved_qbang = qbang.load(SeqCst);
let saved_stophist = stophist.load(SeqCst);
let saved_hlinesz = hlinesz.load(SeqCst);
*chline.lock().unwrap() = "echo oldword extra".to_string();
hptr.store(5, SeqCst);
errflag.store(0, SeqCst);
lexstop.store(false, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(0));
qbang.store(false, SeqCst);
stophist.store(0, SeqCst);
hlinesz.store(64, 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(SeqCst),
8,
"c:368 — hptr advances over the three overwrites"
);
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, SeqCst);
errflag.store(saved_errflag, SeqCst);
lexstop.store(saved_lexstop, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
qbang.store(saved_qbang, SeqCst);
stophist.store(saved_stophist, SeqCst);
hlinesz.store(saved_hlinesz, SeqCst);
}
#[test]
fn ihwaddc_advances_hptr_on_each_push() {
let _g = crate::test_util::global_state_lock();
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(SeqCst);
let saved_errflag = errflag.load(SeqCst);
let saved_lexstop = lexstop.load(SeqCst);
let saved_inflags = crate::ported::input::inbufflags.with(|f| f.get());
let saved_qbang = qbang.load(SeqCst);
let saved_bangchar = bangchar.load(SeqCst);
let saved_stophist = stophist.load(SeqCst);
let saved_hlinesz = hlinesz.load(SeqCst);
*chline.lock().unwrap() = "AB".to_string(); hptr.store(2, SeqCst);
errflag.store(0, SeqCst);
lexstop.store(false, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(0));
qbang.store(false, SeqCst);
stophist.store(0, SeqCst);
bangchar.store(b'!' as i32, SeqCst);
hlinesz.store(64, SeqCst);
ihwaddc(b'x' as i32);
assert_eq!(
chline.lock().unwrap().as_str(),
"ABx",
"c:368 — chline grows"
);
assert_eq!(
hptr.load(SeqCst),
3,
"c:368 — hptr advances by 1"
);
ihwaddc(b'y' as i32);
assert_eq!(
hptr.load(SeqCst),
4,
"c:368 — hptr advances on each push"
);
qbang.store(true, SeqCst);
ihwaddc(b'!' as i32);
assert_eq!(
chline.lock().unwrap().as_str(),
"ABxy\\!",
"c:366 — qbang escape pushes '\\\\' before bangchar"
);
assert_eq!(
hptr.load(SeqCst),
6,
"c:366+c:368 — both pushes advance hptr"
);
errflag.store(1, SeqCst);
let hptr_before = hptr.load(SeqCst);
ihwaddc(b'z' as i32);
assert_eq!(
hptr.load(SeqCst),
hptr_before,
"c:359 — errflag short-circuits, hptr unchanged"
);
*chline.lock().unwrap() = saved_chline;
hptr.store(saved_hptr, SeqCst);
errflag.store(saved_errflag, SeqCst);
lexstop.store(saved_lexstop, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
qbang.store(saved_qbang, SeqCst);
bangchar.store(saved_bangchar, SeqCst);
stophist.store(saved_stophist, SeqCst);
hlinesz.store(saved_hlinesz, SeqCst);
}
#[test]
fn ihwend_uses_hptr_not_chline_len() {
let _g = crate::test_util::global_state_lock();
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(SeqCst);
let saved_hptr = hptr.load(SeqCst);
let saved_stop = stophist.load(SeqCst);
let saved_active = histactive.load(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, SeqCst);
hptr.store(7, SeqCst);
stophist.store(0, SeqCst);
histactive.store(0, 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(SeqCst),
2,
"c:1694 — chwordpos++ on successful close"
);
*chwords.lock().unwrap() = vec![5, 0];
chwordpos.store(1, SeqCst);
hptr.store(3, SeqCst); ihwend();
assert_eq!(
chwordpos.load(SeqCst),
0,
"c:1700 — chwordpos-- when hptr <= chwords[chwordpos-1]"
);
chwordpos.store(2, SeqCst);
hptr.store(9, SeqCst);
*chwords.lock().unwrap() = vec![0, 4, 5, 8];
ihwend();
assert_eq!(
chwordpos.load(SeqCst),
2,
"c:1691 — even chwordpos short-circuits"
);
*chline.lock().unwrap() = saved_chline;
*chwords.lock().unwrap() = saved_chwords;
chwordpos.store(saved_chwordpos, SeqCst);
hptr.store(saved_hptr, SeqCst);
stophist.store(saved_stop, SeqCst);
histactive.store(saved_active, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
}
#[test]
fn histremovedups_removes_flagged_entries_only() {
let _g = crate::test_util::global_state_lock();
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(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: 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, 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(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: 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, SeqCst);
}
#[test]
fn iaddtoline_adjusts_excs_relative_to_zlemetacs() {
let _g = crate::test_util::global_state_lock();
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(SeqCst);
let saved_zlemetacs = ZLEMETACS.load(SeqCst);
let saved_expanding = expanding.load(SeqCst);
let saved_lexstop = lexstop.load(SeqCst);
let saved_qbang = qbang.load(SeqCst);
let saved_exlast = exlast.load(SeqCst);
*chline.lock().unwrap() = String::new();
expanding.store(1, SeqCst);
lexstop.store(false, SeqCst);
qbang.store(false, SeqCst);
excs.store(10, SeqCst); ZLEMETACS.store(5, SeqCst);
crate::ported::input::inbufct.with(|c| c.set(3));
exlast.store(2, SeqCst);
iaddtoline(b'x' as i32);
assert_eq!(
excs.load(SeqCst),
12,
"c:406 — excs += 1 + inbufct - exlast (10+1+3-2=12)"
);
excs.store(6, SeqCst);
excs.store(25, SeqCst);
ZLEMETACS.store(20, SeqCst);
crate::ported::input::inbufct.with(|c| c.set(1));
exlast.store(10, SeqCst);
iaddtoline(b'y' as i32);
assert_eq!(
excs.load(SeqCst),
20,
"c:407-410 — clamp to zlemetacs(20) when post-add excs<zlemetacs"
);
excs.store(3, SeqCst);
ZLEMETACS.store(10, SeqCst);
crate::ported::input::inbufct.with(|c| c.set(1));
exlast.store(0, SeqCst);
iaddtoline(b'z' as i32);
assert_eq!(
excs.load(SeqCst),
3,
"c:405 — excs<=zlemetacs leaves excs unchanged"
);
*chline.lock().unwrap() = saved_chline;
excs.store(saved_excs, SeqCst);
ZLEMETACS.store(saved_zlemetacs, SeqCst);
expanding.store(saved_expanding, SeqCst);
lexstop.store(saved_lexstop, SeqCst);
qbang.store(saved_qbang, SeqCst);
exlast.store(saved_exlast, SeqCst);
}
#[test]
fn ihwbegin_records_hptr_not_chline_len() {
let _g = crate::test_util::global_state_lock();
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(SeqCst);
let saved_hptr = hptr.load(SeqCst);
let saved_stop = stophist.load(SeqCst);
let saved_active = histactive.load(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, SeqCst);
hptr.store(4, SeqCst);
stophist.store(0, SeqCst);
histactive.store(0, 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, SeqCst);
hptr.store(3, 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, SeqCst);
hptr.store(5, SeqCst);
stophist.store(2, SeqCst);
ihwbegin(0);
assert!(
chwords.lock().unwrap().is_empty(),
"c:1659 — stophist==2 short-circuits, no record"
);
stophist.store(0, SeqCst);
chwords.lock().unwrap().clear();
chwordpos.store(0, SeqCst);
hptr.store(5, 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, SeqCst);
hptr.store(7, 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, SeqCst);
hptr.store(saved_hptr, SeqCst);
stophist.store(saved_stop, SeqCst);
histactive.store(saved_active, SeqCst);
crate::ported::input::inbufflags.with(|f| f.set(saved_inflags));
}
#[test]
fn getargs_handles_field_indexing_and_overflow() {
let _g = crate::test_util::global_state_lock();
let he = histent {
node: 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: 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: 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 = crate::test_util::global_state_lock();
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(SeqCst);
{
let mut ring = hist_ring.lock().unwrap();
ring.clear();
ring.push(histent {
node: 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, 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: 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: hashnode {
next: None,
nam,
flags,
},
up: None,
down: None,
zle_text: None,
stim: 0,
ftim: 0,
words,
nwords,
histnum,
});
}
}
curhist.store(saved_curhist, SeqCst);
}
#[test]
fn checkcurline_flushes_to_curline_only_when_active_and_matching() {
let _g = crate::test_util::global_state_lock();
let _g = hist_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let saved_curhist = curhist.load(SeqCst);
let saved_active = histactive.load(SeqCst);
let saved_chline = chline.lock().unwrap().clone();
let saved_chwordpos = chwordpos.load(SeqCst);
let saved_chwords = chwords.lock().unwrap().clone();
let saved_curline = curline.lock().unwrap().take();
curhist.store(42, SeqCst);
histactive.store(HA_ACTIVE, SeqCst);
*chline.lock().unwrap() = "echo hello".to_string();
chwordpos.store(4, SeqCst); *chwords.lock().unwrap() = vec![0, 4, 5, 10];
*curline.lock().unwrap() = None;
let he = histent {
node: 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, SeqCst); *curline.lock().unwrap() = None;
checkcurline(&he);
assert!(
curline.lock().unwrap().is_none(),
"c:2424 — HA_ACTIVE cleared, no flush"
);
histactive.store(HA_ACTIVE, SeqCst);
let he2 = histent {
histnum: 99,
..histent {
node: 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, SeqCst);
histactive.store(saved_active, SeqCst);
*chline.lock().unwrap() = saved_chline;
chwordpos.store(saved_chwordpos, SeqCst);
*chwords.lock().unwrap() = saved_chwords;
*curline.lock().unwrap() = saved_curline;
}
#[test]
fn remlpaths_count_exceeds_components_preserves_leading_slash() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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() {
let _g = crate::test_util::global_state_lock();
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));
}
#[test]
fn casemodify_lower_lowercases_uppercase_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
casemodify("HELLO", CASMOD_LOWER),
"hello",
"c:2226-2228 — uppercase → lowercase"
);
assert_eq!(casemodify("hello", CASMOD_LOWER), "hello");
assert_eq!(casemodify("123 !? abc", CASMOD_LOWER), "123 !? abc");
assert_eq!(casemodify("ÉLITE", CASMOD_LOWER), "élite");
}
#[test]
fn casemodify_upper_uppercases_lowercase_only() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
casemodify("hello", CASMOD_UPPER),
"HELLO",
"c:2233-2236 — lowercase → uppercase"
);
assert_eq!(casemodify("HELLO", CASMOD_UPPER), "HELLO");
assert_eq!(casemodify("élite", CASMOD_UPPER), "ÉLITE");
}
#[test]
fn casemodify_caps_title_cases_at_word_boundaries() {
let _g = crate::test_util::global_state_lock();
assert_eq!(casemodify("hello world", CASMOD_CAPS), "Hello World");
assert_eq!(casemodify("foo-bar.baz", CASMOD_CAPS), "Foo-Bar.Baz");
assert_eq!(casemodify("HELLO", CASMOD_CAPS), "Hello");
}
#[test]
fn casemodify_caps_skips_combining_chars() {
let _g = crate::test_util::global_state_lock();
let input = "a\u{0301}b";
let got = casemodify(input, CASMOD_CAPS);
let expected = "A\u{0301}b";
assert_eq!(
got, expected,
"c:2241-2242 — IS_COMBINING short-circuit must not break word boundary"
);
}
#[test]
fn subst_anchor_head_recognises_pound_token() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
subst("foobar", "#foo", "baz", false),
"bazbar",
"c:2349 ASCII '#' anchor — match at head"
);
let pat = format!("{}foo", Pound);
assert_eq!(
subst("foobar", &pat, "baz", false),
"bazbar",
"c:2349 Pound token (0x84) anchor — match at head"
);
let pat = format!("{}foo", Pound);
assert_eq!(
subst("xfoo", &pat, "baz", false),
"xfoo",
"c:2349 anchor-head rejects non-prefix"
);
}
#[test]
fn subst_anchor_tail_matches_only_suffix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
subst("xxfoo", "%foo", "bar", false),
"xxbar",
"c:2354 — '%' anchors at end of string"
);
assert_eq!(
subst("foox", "%foo", "bar", false),
"foox",
"c:2354 — '%foo' must not match unless `foo` is at end"
);
}
#[test]
fn histreduceblanks_single_space_stays_one() {
assert_eq!(histreduceblanks("a b"), "a b");
}
#[test]
fn histreduceblanks_multi_space_collapses_to_one() {
assert_eq!(histreduceblanks("a b"), "a b");
}
#[test]
fn histreduceblanks_tab_collapses_with_spaces() {
assert_eq!(histreduceblanks("a \t b"), "a b");
}
#[test]
fn histreduceblanks_leading_whitespace_trimmed() {
assert_eq!(histreduceblanks(" hello"), "hello");
}
#[test]
fn histreduceblanks_trailing_whitespace_trimmed() {
assert_eq!(histreduceblanks("hello "), "hello");
}
#[test]
fn histreduceblanks_both_ends_trimmed() {
assert_eq!(histreduceblanks(" hi "), "hi");
}
#[test]
fn histreduceblanks_empty_input_returns_empty() {
assert_eq!(histreduceblanks(""), "");
}
#[test]
fn histreduceblanks_all_whitespace_becomes_empty() {
assert_eq!(histreduceblanks(" "), "");
assert_eq!(histreduceblanks("\t\t \t"), "");
}
#[test]
fn histreduceblanks_newline_preserved_not_collapsed() {
let r = histreduceblanks("a\nb");
assert_eq!(r, "a\nb", "newline must be preserved");
}
#[test]
fn histreduceblanks_multiple_newlines_preserved() {
let r = histreduceblanks("a\n\nb");
assert_eq!(r, "a\n\nb");
}
#[test]
fn histreduceblanks_space_around_newline_preserved() {
let r = histreduceblanks("a \n b");
assert_eq!(r, "a \n b");
}
#[test]
fn histreduceblanks_complex_input_normalizes() {
assert_eq!(
histreduceblanks(" a b\t\tc "),
"a b c"
);
}
#[test]
fn histsplitwords_single_word_returns_one_span() {
let words = histsplitwords("echo", false);
assert_eq!(words, vec![(0, 4)]);
}
#[test]
fn histsplitwords_empty_input_returns_no_words() {
let words = histsplitwords("", false);
assert!(words.is_empty(), "empty line has no words; got {words:?}");
}
#[test]
fn histsplitwords_only_whitespace_returns_no_words() {
let words = histsplitwords(" ", false);
assert!(words.is_empty(), "whitespace-only has no words; got {words:?}");
}
#[test]
fn histsplitwords_leading_whitespace_skipped_no_uselex() {
let words = histsplitwords(" hi", false);
assert_eq!(words, vec![(3, 5)]);
}
#[test]
fn histsplitwords_tab_separator_works_like_space() {
let words = histsplitwords("a\tb\tc", false);
assert_eq!(words.len(), 3);
for (s, e) in &words {
assert_eq!(e - s, 1, "each word is one char; got ({s},{e})");
}
}
#[test]
fn histsplitwords_spans_well_formed() {
let line = "alpha beta gamma";
for use_lex in [false, true] {
let words = histsplitwords(line, use_lex);
for (s, e) in &words {
assert!(
s < e && *e <= line.len(),
"bad span ({s},{e}) for line len {} use_lex={}",
line.len(),
use_lex
);
}
for i in 1..words.len() {
assert!(
words[i].0 >= words[i - 1].1,
"spans overlap: {:?} then {:?}",
words[i - 1],
words[i]
);
}
}
}
#[test]
fn hist_corpus_histreduceblanks_empty_is_empty() {
assert_eq!(histreduceblanks(""), "");
}
#[test]
fn hist_corpus_histreduceblanks_all_spaces_to_empty() {
assert_eq!(histreduceblanks(" "), "");
assert_eq!(histreduceblanks("\t\t\t"), "");
assert_eq!(histreduceblanks(" \t \t "), "");
}
#[test]
fn hist_corpus_histreduceblanks_single_char() {
assert_eq!(histreduceblanks("x"), "x");
}
#[test]
fn hist_corpus_histreduceblanks_mixed_space_tab() {
assert_eq!(histreduceblanks("a \t \t b"), "a b");
}
#[test]
fn hist_corpus_histreduceblanks_multibyte_passthrough() {
assert_eq!(histreduceblanks("日 本"), "日 本");
assert_eq!(histreduceblanks("日 本"), "日 本");
}
#[test]
fn hist_corpus_histreduceblanks_newlines_exact_count() {
assert_eq!(histreduceblanks("a\n\n\nb"), "a\n\n\nb");
}
#[test]
fn hist_corpus_in_word_round_trip() {
let _g = crate::test_util::global_state_lock();
let saved = hist_is_in_word();
hist_in_word(1);
assert_eq!(hist_is_in_word(), 1);
hist_in_word(0);
assert_eq!(hist_is_in_word(), 0);
hist_in_word(saved);
}
#[test]
fn hist_corpus_substfailed_does_not_panic() {
let _g = crate::test_util::global_state_lock();
let _ = substfailed();
}
}