use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU32, AtomicUsize, Ordering};
use std::sync::Mutex;
use crate::ported::zsh_h::histent;
pub use crate::zsh_h::{CASMOD_CAPS, CASMOD_LOWER, CASMOD_NONE, CASMOD_UPPER};
use crate::zsh_h::{
isset,
BANGHIST, HFILE_FAST, HFILE_USE_OPTIONS,
HISTIGNOREALLDUPS, HISTIGNOREDUPS, HISTIGNORESPACE,
HISTNOFUNCTIONS, HISTNOSTORE, HISTREDUCEBLANKS,
INCAPPENDHISTORY, INCAPPENDHISTORYTIME, INTERACTIVE,
SHAREHISTORY, SHINSTDIN,
};
use std::io::Write;
use std::os::unix::io::AsRawFd;
use std::path::Component::*;
pub 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 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;
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);
static defev: AtomicI64 = AtomicI64::new(0);
static hist_keep_comment: 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());
static histsave_stack_size: AtomicI32 = AtomicI32::new(0);
static histsave_stack_pos: AtomicI32 = AtomicI32::new(0);
static histfile_linect: AtomicI64 = AtomicI64::new(0);
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);
fn resolve_histfile() -> Option<String> {
crate::ported::params::getsparam("HISTFILE")
}
static lockhistct: AtomicI32 = AtomicI32::new(0);
pub static lexstop: AtomicBool = AtomicBool::new(false);
pub static exit_pending: AtomicBool = AtomicBool::new(false);
static strin: AtomicI32 = AtomicI32::new(0);
pub const HIST_OLD: u32 = 1 << 0;
pub const HIST_DUP: u32 = 1 << 1;
pub const HIST_FOREIGN: u32 = 1 << 2;
pub const HIST_TMPSTORE: u32 = 1 << 3;
pub const HIST_NOWRITE: u32 = 1 << 4;
pub const HISTFLAG_DONE: i32 = 1;
pub const HISTFLAG_NOEXEC: i32 = 2;
pub const HISTFLAG_RECALL: i32 = 4;
pub const HISTFLAG_SETTY: i32 = 8;
fn ring_get(ev: i64) -> Option<histent> {
let ring = hist_ring.lock().unwrap();
for h in ring.iter() {
if h.histnum == ev {
return Some(clone_histent(h));
}
}
None
}
fn clone_histent(h: &histent) -> histent {
histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: h.node.nam.clone(),
flags: h.node.flags,
},
up: None,
down: None,
zle_text: h.zle_text.clone(),
stim: h.stim,
ftim: h.ftim,
words: h.words.clone(),
nwords: h.nwords,
histnum: h.histnum,
}
}
fn ring_position(ev: i64) -> Option<usize> {
hist_ring.lock().unwrap().iter().position(|h| h.histnum == ev)
}
fn ring_at(idx: usize) -> i64 {
hist_ring.lock().unwrap()[idx].histnum
}
fn ring_len() -> usize {
hist_ring.lock().unwrap().len()
}
fn ring_oldest() -> Option<i64> {
hist_ring.lock().unwrap().last().map(|h| h.histnum)
}
fn ring_latest() -> Option<histent> {
hist_ring.lock().unwrap().first().map(clone_histent)
}
pub fn hist_context_save(hs: &mut crate::ported::zsh_h::hist_stack, toplevel: i32) { if toplevel != 0 { *zle_chline.lock().unwrap() = Some(chline.lock().unwrap().clone()); }
hs.histactive = histactive.load(Ordering::SeqCst) as i32; hs.histdone = histdone.load(Ordering::SeqCst); hs.stophist = stophist.load(Ordering::SeqCst); hs.hline = Some(chline.lock().unwrap().clone()); hs.hptr = Some(hptr.load(Ordering::SeqCst).to_string()); hs.chwords = chwords.lock().unwrap().clone(); hs.chwordlen = chwordlen.load(Ordering::SeqCst); hs.chwordpos = chwordpos.load(Ordering::SeqCst); hs.hlinesz = hlinesz.load(Ordering::SeqCst); hs.defev = defev.load(Ordering::SeqCst); hs.hist_keep_comment = hist_keep_comment.load(Ordering::SeqCst); hs.csp = 0;
stophist.store(0, Ordering::SeqCst); chline.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst); }
pub fn hist_context_restore(hs: &crate::ported::zsh_h::hist_stack, toplevel: i32) { if toplevel != 0 { *zle_chline.lock().unwrap() = None; }
histactive.store(hs.histactive as u32, Ordering::SeqCst); histdone.store(hs.histdone, Ordering::SeqCst); stophist.store(hs.stophist, Ordering::SeqCst); *chline.lock().unwrap() = hs.hline.clone().unwrap_or_default(); hptr.store(hs.hptr.as_ref().and_then(|s| s.parse().ok()).unwrap_or(0), Ordering::SeqCst);
*chwords.lock().unwrap() = hs.chwords.clone(); chwordlen.store(hs.chwordlen, Ordering::SeqCst); chwordpos.store(hs.chwordpos, Ordering::SeqCst); hlinesz.store(hs.hlinesz, Ordering::SeqCst); defev.store(hs.defev, Ordering::SeqCst); hist_keep_comment.store(hs.hist_keep_comment, Ordering::SeqCst); }
pub fn hist_in_word(yesno: i32) {
if yesno != 0 {
histactive.fetch_or(HA_INWORD, Ordering::SeqCst);
} else {
histactive.fetch_and(!HA_INWORD, Ordering::SeqCst);
}
}
pub fn hist_is_in_word() -> i32 {
if (histactive.load(Ordering::SeqCst) & HA_INWORD) != 0 { 1 } else { 0 }
}
pub fn herrflush() { }
pub fn digitcount(s: &str) -> usize { s.chars().take_while(|c| c.is_ascii_digit()).count()
}
pub fn nohw(_c: i32) { }
pub fn nohwabort() { }
pub fn nohwe() { }
pub fn ihwbegin(offset: i32) { let stop = stophist.load(Ordering::SeqCst);
let active = histactive.load(Ordering::SeqCst);
if stop == 2 || (active & HA_INWORD) != 0 {
return;
}
let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 != 0 {
chwordpos.fetch_sub(1, Ordering::SeqCst);
}
let start = (chline.lock().unwrap().len() as i32 + offset).max(0) as i16;
let mut words = chwords.lock().unwrap();
let idx = chwordpos.load(Ordering::SeqCst) as usize;
if words.len() <= idx {
words.resize(idx + 1, 0);
}
words[idx] = start;
chwordpos.fetch_add(1, Ordering::SeqCst);
}
pub fn ihwabort() { let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 != 0 {
chwordpos.fetch_sub(1, Ordering::SeqCst);
}
hist_keep_comment.store(1, Ordering::SeqCst);
}
pub fn ihwend() { let stop = stophist.load(Ordering::SeqCst);
let active = histactive.load(Ordering::SeqCst);
if stop == 2 || (active & HA_INWORD) != 0 {
return;
}
let pos = chwordpos.load(Ordering::SeqCst);
if pos % 2 == 0 {
return;
}
let cur = chline.lock().unwrap().len() as i16;
let mut words = chwords.lock().unwrap();
let start_idx = (pos - 1) as usize;
if cur > words[start_idx] {
let end_idx = pos as usize;
if words.len() <= end_idx {
words.resize(end_idx + 1, 0);
}
words[end_idx] = cur;
chwordpos.fetch_add(1, Ordering::SeqCst);
} else {
chwordpos.fetch_sub(1, Ordering::SeqCst);
}
}
pub fn ihwaddc(c: i32) { use crate::ported::zsh_h::{INP_ALIAS, INP_HIST};
if crate::ported::utils::errflag.load(Ordering::SeqCst) != 0 || lexstop.load(Ordering::SeqCst) {
return;
}
let inbufflags = crate::ported::input::inbufflags.with(|f| f.get());
if (inbufflags & (INP_ALIAS | INP_HIST)) == INP_ALIAS {
return;
}
let chline_empty = chline.lock().unwrap().is_empty();
if chline_empty {
return;
}
let bc = bangchar.load(Ordering::SeqCst);
if c == bc && stophist.load(Ordering::SeqCst) < 2 && qbang.load(Ordering::SeqCst) {
chline.lock().unwrap().push('\\'); }
chline.lock().unwrap().push(c as u8 as char);
let cur_len = chline.lock().unwrap().len() as i32;
let sz = hlinesz.load(Ordering::SeqCst);
if cur_len >= sz {
let new_sz = sz + 64;
hlinesz.store(new_sz, Ordering::SeqCst);
}
}
pub fn iaddtoline(c: i32) { chline.lock().unwrap().push(c as u8 as char);
}
pub fn safeinungetc(_c: i32) {
}
pub fn quietgethist(ev: i64) -> Option<histent> { ring_get(ev)
}
pub fn gethist(ev: i64) -> Option<histent> { let ret = quietgethist(ev);
if ret.is_none() {
herrflush();
crate::ported::utils::zerr(&format!("no such event: {}", ev));
}
ret
}
pub fn up_histent(current: i64) -> Option<i64> { let pos = ring_position(current)?;
if pos + 1 >= ring_len() {
None
} else {
Some(ring_at(pos + 1))
}
}
pub fn down_histent(current: i64) -> Option<i64> { let pos = ring_position(current)?;
if pos == 0 {
None
} else {
Some(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 prepnexthistent() -> i64 { let cap = histsiz.load(Ordering::SeqCst);
if histlinect.load(Ordering::SeqCst) >= cap {
if let Some(oldest) = ring_oldest() {
let mut ring = hist_ring.lock().unwrap();
ring.retain(|h| h.histnum != oldest);
histlinect.fetch_sub(1, Ordering::SeqCst);
}
}
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
n
}
pub fn putoldhistentryontop(_keep_going: i32) -> i32 {
let mut ring = hist_ring.lock().unwrap();
if let Some(oldest) = ring.last().map(|h| h.histnum) {
let pos = ring.iter().position(|h| h.histnum == oldest).unwrap();
let entry = ring.remove(pos);
ring.insert(0, entry);
return 1;
}
0
}
pub fn resizehistents() {
let cap = histsiz.load(Ordering::SeqCst);
while histlinect.load(Ordering::SeqCst) > cap {
if let Some(oldest) = ring_oldest() {
let mut ring = hist_ring.lock().unwrap();
ring.retain(|h| h.histnum != oldest);
histlinect.fetch_sub(1, Ordering::SeqCst);
} else {
break;
}
}
}
pub fn linkcurline() { let new_hist = curhist.fetch_add(1, Ordering::SeqCst) + 1; let mut cur = curline.lock().unwrap();
*cur = Some(make_histent(new_hist, String::new())); }
pub fn unlinkcurline() { *curline.lock().unwrap() = None; curhist.fetch_sub(1, Ordering::SeqCst); }
pub fn 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;
}
}
}
Some(cur)
}
pub fn hcomsearch(prefix: &str) -> Option<i64> {
let mut cur = curhist.load(Ordering::SeqCst);
while let Some(prev) = up_histent(cur) {
cur = prev;
if let Some(entry) = ring_get(cur) {
if (entry.node.flags as u32 & HIST_FOREIGN) != 0 {
continue;
}
if entry.node.nam.starts_with(prefix) {
return Some(cur);
}
}
}
None
}
pub fn hconsearch(needle: &str, start: Option<i64>) -> Option<i64> {
let mut cur = start.unwrap_or_else(|| curhist.load(Ordering::SeqCst));
while let Some(prev) = up_histent(cur) {
cur = prev;
if let Some(entry) = ring_get(cur) {
if (entry.node.flags as u32 & HIST_FOREIGN) != 0 {
continue;
}
if entry.node.nam.contains(needle) {
return Some(cur);
}
}
}
None
}
pub fn checkcurline(line: &str) -> i32 {
if let Some(latest) = ring_latest() {
if latest.node.nam == line { 1 } else { 0 }
} else {
0
}
}
pub fn hbegin(dohist: i32) {
crate::ported::utils::errflag.fetch_and( !crate::ported::utils::ERRFLAG_ERROR,
Ordering::Relaxed,
);
histdone.store(0, Ordering::SeqCst); let interact = isset(INTERACTIVE);
let shinstdin = isset(SHINSTDIN);
if dohist == 0 { stophist.store(2, Ordering::SeqCst); } else if dohist != 2 { stophist.store(if !interact || !shinstdin { 2 } else { 0 }, Ordering::SeqCst);
} else { stophist.store(0, Ordering::SeqCst); }
if stophist.load(Ordering::SeqCst) == 2 { chline.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); hlinesz.store(0, Ordering::SeqCst); chwords.lock().unwrap().clear(); chwordlen.store(0, Ordering::SeqCst); } else { let mut buf = chline.lock().unwrap(); buf.clear();
buf.reserve(64);
hlinesz.store(64, Ordering::SeqCst); drop(buf);
let mut w = chwords.lock().unwrap(); w.clear();
w.reserve(64);
chwordlen.store(64, Ordering::SeqCst);
drop(w);
if !isset(BANGHIST) { stophist.store(4, Ordering::SeqCst); }
}
chwordpos.store(0, Ordering::SeqCst);
{ let mut ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first_mut() {
if top.ftim == 0 && strin.load(Ordering::SeqCst) == 0 {
top.ftim = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0); }
}
}
if (dohist == 2 || (interact && shinstdin)) && strin.load(Ordering::SeqCst) == 0
{
histactive.store(HA_ACTIVE, Ordering::SeqCst); linkcurline(); defev.store(addhistnum(curhist.load(Ordering::SeqCst), -1, HIST_FOREIGN as i32),
Ordering::SeqCst);
} else {
histactive.store(HA_ACTIVE | HA_NOINC, Ordering::SeqCst); }
if isset(INCAPPENDHISTORYTIME) && !isset(SHAREHISTORY)
&& !isset(INCAPPENDHISTORY)
&& (histactive.load(Ordering::SeqCst) & HA_NOINC) == 0
&& strin.load(Ordering::SeqCst) == 0
&& histsave_stack_pos.load(Ordering::SeqCst) == 0
{
let hf = resolve_histfile(); savehistfile(hf.as_deref(), 0 | HFILE_USE_OPTIONS as i32
| HFILE_FAST as i32);
}
}
fn should_ignore_line(prog: Option<&[u8]>) -> i32 { let line = chline.lock().unwrap().clone();
if isset(HISTIGNORESPACE) { if line.starts_with(' ') { return 1; }
}
if prog.is_none() { return 0; }
if isset(HISTNOFUNCTIONS) { return 0;
}
if isset(HISTNOSTORE) { let mut b: &str = &line;
let mut saw_builtin = false;
if let Some(rest) = b.strip_prefix("builtin ") { b = rest;
saw_builtin = true;
}
if (b == "history" || b.starts_with("history ")) && (saw_builtin )
{
return 1; }
if (b == "r" || b.starts_with("r ")) && (saw_builtin )
{
return 1;
}
if let Some(rest) = b.strip_prefix("fc -") { if (saw_builtin )
&& rest.chars().take_while(|c| c.is_ascii_alphabetic())
.any(|c| c == 'l')
{
return 1; }
}
}
0 }
pub fn hend(prog: Option<&[u8]>) -> i32 { let stack_pos = histsave_stack_pos.load(Ordering::SeqCst); let mut save: i32 = 1; let mut hookret: i32 = 0;
crate::ported::signals::queue_signals(); if (histdone.load(Ordering::SeqCst) & HISTFLAG_SETTY) != 0 { }
let active = histactive.load(Ordering::SeqCst);
if (active & HA_NOINC) == 0 { unlinkcurline(); }
if (active & HA_NOINC) != 0 { chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst); crate::ported::signals::unqueue_signals(); return 1; }
let cur_ignore_all = if isset(HISTIGNOREALLDUPS) { 1 } else { 0 }; let prev_ignore_all = hist_ignore_all_dups.load(Ordering::SeqCst);
if prev_ignore_all != cur_ignore_all && {
hist_ignore_all_dups.store(cur_ignore_all, Ordering::SeqCst); cur_ignore_all != 0
}
{
histremovedups(); }
let chline_text = chline.lock().unwrap().clone();
if !chline_text.is_empty() { let save_errflag = crate::ported::utils::errflag .load(Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed); let args = vec!["zshaddhistory".to_string(), chline_text.clone()]; hookret = crate::ported::utils::callhookfunc( "zshaddhistory", Some(&args), true);
let new_errflag = (crate::ported::utils::errflag .load(Ordering::Relaxed)
& !crate::ported::utils::ERRFLAG_ERROR) | save_errflag;
crate::ported::utils::errflag.store(new_errflag, Ordering::Relaxed);
}
let hf = resolve_histfile(); if isset(SHAREHISTORY) && lockhistfile(hf.as_deref(), 0) == 0
{
readhistfile(hf.as_deref(), 0, HFILE_USE_OPTIONS as i32 | HFILE_FAST as i32);
}
let flag = histdone.load(Ordering::SeqCst); histdone.store(0, Ordering::SeqCst); let hptr_pos = hptr.load(Ordering::SeqCst);
let mut text = chline_text;
if hptr_pos < 1 { save = 0; } else {
if text.ends_with('\n') { if text.len() > 1 { text.pop(); if hptr.load(Ordering::SeqCst) > 0 {
hptr.fetch_sub(1, Ordering::SeqCst);
}
} else {
save = 0; }
}
if chwordpos.load(Ordering::SeqCst) <= 2 && hist_keep_comment.load(Ordering::SeqCst) == 0
{
save = 0; } else if should_ignore_line(prog) != 0 { save = -1; } else if hookret == 2 { save = -2; } else if hookret != 0 { save = -1; }
}
if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) != 0 { let ptr = text.clone(); if (flag & (HISTFLAG_DONE | HISTFLAG_RECALL)) == HISTFLAG_DONE { print!("{}\n", ptr);
let _ = std::io::stdout().flush();
}
if (flag & HISTFLAG_RECALL) != 0 { save = 0; }
}
if save != 0 || text.starts_with(' ') { let mut ring = hist_ring.lock().unwrap();
let mut idx: usize = 0;
while idx < ring.len()
&& (ring[idx].node.flags as u32 & HIST_FOREIGN) != 0
{
idx += 1;
}
if idx < ring.len()
&& (ring[idx].node.flags as u32 & HIST_TMPSTORE) != 0
{
if idx == 0 { curhist.fetch_sub(1, Ordering::SeqCst); }
ring.remove(idx); histlinect.fetch_sub(1, Ordering::SeqCst);
}
}
if save != 0 { if chwordpos.load(Ordering::SeqCst) % 2 != 0 {
ihwend();
}
let cwp = chwordpos.load(Ordering::SeqCst);
if cwp > 1 {
let words = chwords.lock().unwrap();
let last = words.get((cwp - 2) as usize).copied().unwrap_or(0);
if (last as usize) >= text.len() { drop(words);
chwordpos.fetch_sub(2, Ordering::SeqCst);
} else {
drop(words);
}
if isset(HISTREDUCEBLANKS) { text = histreduceblanks(&text); }
}
let newflags: u32 = if save == -1 { HIST_TMPSTORE } else if save == -2 { HIST_NOWRITE }
else { 0 };
let mut he_idx: Option<usize> = None;
let mut overwrite_old: u32 = 0;
if (isset(HISTIGNOREDUPS) || isset(HISTIGNOREALLDUPS)) && save > 0
{
let ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first() {
if top.node.nam == text { overwrite_old = top.node.flags as u32 & HIST_OLD; he_idx = Some(0);
}
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let cwp = chwordpos.load(Ordering::SeqCst);
let chwords_snapshot: Vec<i16> = chwords.lock().unwrap().clone();
let nwords = (cwp / 2) as i32;
if let Some(0) = he_idx { let mut ring = hist_ring.lock().unwrap();
if let Some(top) = ring.first_mut() {
top.node.nam = text.clone(); top.stim = now; top.ftim = 0; top.node.flags = (newflags | overwrite_old) as i32; top.nwords = nwords; top.words = if cwp > 0 {
chwords_snapshot[..cwp as usize].to_vec() } else {
Vec::new()
};
}
} else {
let n = prepnexthistent(); let mut he = make_histent(n, text.clone());
he.stim = now;
he.ftim = 0;
he.node.flags = newflags as i32;
he.nwords = nwords;
if cwp > 0 {
he.words = chwords_snapshot[..cwp as usize].to_vec();
}
let mut ring = hist_ring.lock().unwrap();
ring.insert(0, he);
histlinect.fetch_add(1, Ordering::SeqCst);
if (newflags & HIST_TMPSTORE) == 0 { crate::ported::hashtable::addhistnode(&text, n as i32);
}
}
}
chline.lock().unwrap().clear(); chwords.lock().unwrap().clear(); hptr.store(0, Ordering::SeqCst); histactive.store(0, Ordering::SeqCst);
let share = isset(SHAREHISTORY);
let do_inc = if share {
histfileIsLocked() != 0 } else {
isset(INCAPPENDHISTORY) || (isset(INCAPPENDHISTORYTIME) && histsave_stack_pos.load(Ordering::SeqCst) != 0) };
if do_inc {
savehistfile(hf.as_deref(), 0 | HFILE_USE_OPTIONS as i32
| HFILE_FAST as i32);
}
unlockhistfile(hf.as_deref().unwrap_or(""));
while histsave_stack_pos.load(Ordering::SeqCst) > stack_pos { pophiststack(); }
hist_keep_comment.store(0, Ordering::SeqCst); crate::ported::signals::unqueue_signals(); if (flag & HISTFLAG_NOEXEC) != 0
|| crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
0 } else {
1
}
}
pub fn strinbeg(dohist: i32) {
strin.fetch_add(1, Ordering::SeqCst);
hbegin(dohist);
}
pub fn strinend() {
hend(None);
strin.fetch_sub(1, Ordering::SeqCst);
}
pub fn inithist() { histsiz.store(1000, Ordering::SeqCst);
savehistsiz.store(1000, Ordering::SeqCst);
curhist.store(0, Ordering::SeqCst);
histlinect.store(0, Ordering::SeqCst);
}
pub fn lockhistfile(fn_path: Option<&str>, keep_trying: i32) -> i32 { let path: String = match fn_path { Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return 1, },
};
if lockhistct.fetch_add(1, Ordering::SeqCst) > 0 {
return 0;
}
let max_tries = if keep_trying != 0 { 30 } else { 1 };
for attempt in 0..max_tries {
if flockhistfile(&path) != 0 {
return 0;
}
if attempt + 1 < max_tries {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
lockhistct.fetch_sub(1, Ordering::SeqCst);
if keep_trying != 0 { 2 } else { 1 }
}
pub fn unlockhistfile(path: &str) {
let prev = lockhistct.fetch_sub(1, Ordering::SeqCst);
if prev <= 0 {
lockhistct.store(0, Ordering::SeqCst);
return;
}
if prev == 1 {
let lockpath = format!("{}.lock", path);
let _ = std::fs::remove_file(&lockpath);
}
}
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 }
}
#[allow(non_snake_case)]
pub fn histfileIsLocked() -> i32 {
if lockhistct.load(Ordering::SeqCst) > 0 { 1 } else { 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 readhistfile(fn_path: Option<&str>, _err: i32, _readflags: i32) { let path: String = match fn_path {
Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return,
},
};
let contents = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return,
};
if contents.is_empty() { return; }
let _ = lockhistfile(Some(&path), 1);
let mut current: Option<(i64, i64, String)> = None;
for raw_line in contents.lines() {
if let Some((stim, ftim, ref mut text)) = current {
if text.ends_with('\\') {
text.pop();
text.push('\n');
text.push_str(raw_line);
current = Some((stim, ftim, text.clone()));
continue;
}
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
let mut entry = make_histent(n, text.clone());
entry.stim = stim;
entry.ftim = ftim;
entry.node.flags |= HIST_OLD as i32;
hist_ring.lock().unwrap().insert(0, entry);
histlinect.fetch_add(1, Ordering::SeqCst);
current = None;
}
if let Some(rest) = raw_line.strip_prefix(": ") {
if let Some((meta, text)) = rest.split_once(';') {
if let Some((stim_s, dur_s)) = meta.split_once(':') {
let stim: i64 = stim_s.parse().unwrap_or(0);
let dur: i64 = dur_s.parse().unwrap_or(0);
let ftim = stim + dur;
current = Some((stim, ftim, text.to_string()));
continue;
}
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
current = Some((now, now, raw_line.to_string()));
}
if let Some((stim, ftim, text)) = current {
let n = curhist.fetch_add(1, Ordering::SeqCst) + 1;
let mut entry = make_histent(n, text);
entry.stim = stim;
entry.ftim = ftim;
entry.node.flags |= HIST_OLD as i32;
hist_ring.lock().unwrap().insert(0, entry);
histlinect.fetch_add(1, Ordering::SeqCst);
}
unlockhistfile(&path);
resizehistents();
}
pub fn savehistfile(fn_path: Option<&str>, _writeflags: i32) { let path: String = match fn_path {
Some(p) => p.to_string(),
None => match resolve_histfile() {
Some(p) => p,
None => return,
},
};
let _ = lockhistfile(Some(&path), 1);
if let Ok(mut file) = std::fs::OpenOptions::new()
.write(true).create(true).truncate(true).open(&path)
{
let cap = savehistsiz.load(Ordering::SeqCst).max(0) as usize;
let ring = hist_ring.lock().unwrap();
let mut count = 0;
for entry in ring.iter().rev() {
if cap > 0 && count >= cap { break; }
let dur = entry.ftim.saturating_sub(entry.stim);
let _ = writeln!(file, ": {}:{};{}", entry.stim, dur, entry.node.nam);
count += 1;
}
}
unlockhistfile(&path);
}
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()))
}
fn make_histent(num: i64, text: String) -> histent {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
histent {
node: crate::ported::zsh_h::hashnode {
next: None,
nam: text,
flags: 0,
},
up: None,
down: None,
zle_text: None,
stim: now,
ftim: now,
words: Vec::new(),
nwords: 0,
histnum: num,
}
}
pub fn pushhiststack(hf: Option<&str>, hs: i64, shs: i64, level: i32) { let snap = histsave { lasthist: histfile_stats {
text: None, stim: 0, mtim: 0, fpos: 0, fsiz: 0,
interrupted: 0, next_write_ev: 0,
},
histfile: hf.map(|s| s.to_string()), hist_ring: std::mem::take(&mut *hist_ring.lock().unwrap()), curhist: curhist.load(Ordering::SeqCst), histlinect: histlinect.load(Ordering::SeqCst), histsiz: histsiz.load(Ordering::SeqCst), savehistsiz: savehistsiz.load(Ordering::SeqCst), locallevel: level, };
histsave_stack.lock().unwrap().push(snap); histsave_stack_size.fetch_add(1, Ordering::SeqCst);
histsave_stack_pos.fetch_add(1, Ordering::SeqCst);
histsiz.store(hs, Ordering::SeqCst); savehistsiz.store(shs, Ordering::SeqCst); curhist.store(0, Ordering::SeqCst); histlinect.store(0, Ordering::SeqCst);
let _ = hf;
}
pub fn pophiststack() { if let Some(snap) = histsave_stack.lock().unwrap().pop() {
*hist_ring.lock().unwrap() = snap.hist_ring;
curhist.store(snap.curhist, Ordering::SeqCst);
histlinect.store(snap.histlinect, Ordering::SeqCst);
histsiz.store(snap.histsiz, Ordering::SeqCst);
let _ = snap.histfile; savehistsiz.store(snap.savehistsiz, Ordering::SeqCst);
histsave_stack_size.fetch_sub(1, Ordering::SeqCst);
histsave_stack_pos.fetch_sub(1, Ordering::SeqCst);
}
}
pub fn saveandpophiststack(writeflags: i32) {
savehistfile(None, writeflags);
pophiststack();
}
pub fn chrealpath(path: &str) -> Option<String> {
std::fs::canonicalize(path).ok().map(|p| p.to_string_lossy().into_owned())
}
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 bufferwords(line: &str, cursor_pos: usize) -> (Vec<String>, usize) {
let words: Vec<String> = line.split_whitespace().map(String::from).collect();
let mut pos = 0;
let mut word_idx = 0;
for (i, word) in line.split_whitespace().enumerate() {
if let Some(start) = line[pos..].find(word) {
let wstart = pos + start;
let wend = wstart + word.len();
if cursor_pos >= wstart && cursor_pos <= wend {
word_idx = i;
break;
}
pos = wend;
}
}
(words, word_idx)
}
pub fn histsplitwords(line: &str) -> Vec<(usize, usize)> {
let mut words = Vec::new();
let mut in_word = false;
let mut word_start = 0;
let mut in_quote = false;
let mut quote_char = '\0';
for (i, c) in line.char_indices() {
if in_quote {
if c == quote_char { in_quote = false; }
continue;
}
if c == '\'' || c == '"' {
in_quote = true;
quote_char = c;
if !in_word { word_start = i; in_word = true; }
continue;
}
if c.is_ascii_whitespace() {
if in_word { words.push((word_start, i)); in_word = false; }
} else if !in_word {
word_start = i; in_word = true;
}
}
if in_word { words.push((word_start, line.len())); }
words
}
pub fn subst(s: &str, in_pattern: &str, out_pattern: &str, global: bool) -> String {
if in_pattern.is_empty() { return s.to_string(); }
let mut anchor_start = false;
let mut anchor_end = false;
let mut pat = in_pattern;
if let Some(rest) = pat.strip_prefix('#') { anchor_start = true; pat = rest; }
if let Some(rest) = pat.strip_prefix('%') { anchor_end = true; pat = rest; }
if pat.is_empty() { return s.to_string(); }
let out_expanded = convamps(out_pattern, pat);
if anchor_start && anchor_end {
if s == pat { return out_expanded; }
return s.to_string();
}
if anchor_start {
if let Some(rest) = s.strip_prefix(pat) {
return format!("{}{}", out_expanded, rest);
}
return s.to_string();
}
if anchor_end {
if s.ends_with(pat) {
let prefix_len = s.len() - pat.len();
return format!("{}{}", &s[..prefix_len], out_expanded);
}
return s.to_string();
}
if global { s.replace(pat, &out_expanded) } else { s.replacen(pat, &out_expanded, 1) }
}
fn convamps(out: &str, in_pattern: &str) -> String {
let mut result = String::with_capacity(out.len());
let mut chars = out.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next) = chars.peek() { result.push(next); chars.next(); }
} else if c == '&' {
result.push_str(in_pattern);
} else {
result.push(c);
}
}
result
}
pub fn casemodify(s: &str, how: i32) -> String { let mut result = String::with_capacity(s.len());
let mut nextupper = true;
for c in s.chars() {
let modified = match how {
x if x == CASMOD_LOWER => c.to_lowercase().collect::<String>(),
x if x == CASMOD_UPPER => c.to_uppercase().collect::<String>(),
x if x == CASMOD_CAPS => {
if !c.is_alphanumeric() {
nextupper = true;
c.to_string()
} else if nextupper {
nextupper = false;
c.to_uppercase().collect::<String>()
} else {
c.to_lowercase().collect::<String>()
}
}
_ => c.to_string(),
};
let _ = CASMOD_NONE; result.push_str(&modified);
}
result
}
pub fn 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 remlpaths(s: &str, count: i32) -> String { let s = s.trim_end_matches('/');
if s.is_empty() { return String::new(); }
let parts: Vec<&str> = s.split('/').filter(|p| !p.is_empty()).collect();
let n = if count == 0 { 1 } else { count as usize };
let take_n = n.min(parts.len());
if take_n == 0 { return String::new(); }
parts.iter().rev().take(take_n).rev().copied().collect::<Vec<&str>>().join("/")
}
pub fn remtext(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('.') {
if dot_pos > 0 {
return format!("{}/{}", &s[..slash_pos], &after_slash[..dot_pos]);
}
}
return s.to_string();
}
if let Some(dot_pos) = s.rfind('.') {
if dot_pos > 0 { return s[..dot_pos].to_string(); }
}
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 quotebreak(s: &str) -> String { let mut result = String::with_capacity(s.len() + 10);
result.push('\'');
for c in s.chars() {
if c == '\'' { result.push_str("'\\''"); }
else if c.is_whitespace() {
result.push('\''); result.push(c); result.push('\'');
} else {
result.push(c);
}
}
result.push('\'');
result
}
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 (i, &c) in bytes.iter().enumerate() {
if c == '\'' {
inquotes = !inquotes;
out.push('\''); out.push('\\'); out.push('\''); out.push('\'');
} else if c.is_whitespace() && !inquotes && prev != '\\' {
out.push('\''); out.push(c); out.push('\'');
} else {
out.push(c);
}
prev = if i < bytes.len() { c } else { prev };
}
out.push('\'');
out
}
pub fn getargspec(argc: usize, c: char, marg: Option<usize>, evset: bool) -> Option<usize> {
match c {
'0' => Some(0),
'1'..='9' => Some(c.to_digit(10).unwrap() as usize),
'^' => Some(1),
'$' => Some(argc),
'%' => { if evset { return None; } marg }
_ => None,
}
}
pub fn histreduceblanks(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut prev_space = false;
for c in text.chars() {
if c.is_whitespace() {
if !prev_space { result.push(' '); prev_space = true; }
} else {
result.push(c); prev_space = false;
}
}
result.trim().to_string()
}
pub fn hgetline(entry: &histent) -> String {
entry.node.nam.clone()
}
pub fn addhistnum(hl: i64, mut n: i32, xflags: i32) -> i64 { let dir: i32 = if n < 0 { -1 } else if n > 0 { 1 } else { 0 }; let he = gethistent(hl, dir); let he = match he {
None => return 0, Some(h) => h,
};
if he != hl { n -= dir; }
let final_he = if n != 0 { movehistent(he, n, xflags as u32) } else {
Some(he)
};
match final_he { None => {
if dir < 0 { firsthist() - 1
} else {
curhist.load(Ordering::SeqCst) + 1
}
}
Some(h) => h, }
}
pub fn firsthist() -> i64 {
let ring = hist_ring.lock().unwrap();
ring.last().map(|h| h.histnum).unwrap_or(1)
}
pub fn hwrep(entry: &histent, replacement: &str, word_idx: usize) -> String {
let words: Vec<&str> = entry.node.nam.split_whitespace().collect();
if word_idx >= words.len() { return entry.node.nam.clone(); }
let mut new_words: Vec<String> = words.iter().map(|s| s.to_string()).collect();
new_words[word_idx] = replacement.to_string();
new_words.join(" ")
}
pub fn histremovedups() {
let mut ring = hist_ring.lock().unwrap();
let mut seen = std::collections::HashSet::new();
ring.retain(|h| seen.insert(h.node.nam.clone()));
let new_ct = ring.len() as i64;
drop(ring);
histlinect.store(new_ct, Ordering::SeqCst);
}
pub fn getargc(entry: &histent) -> usize {
entry.nwords as usize
}
pub fn substfailed() {
crate::ported::utils::zerr("substitution failed");
}
pub fn hwget() -> Option<(i32, String)> {
let pos = chwordpos.load(Ordering::SeqCst);
if pos == 0 || pos % 2 != 0 { return None; }
let words = chwords.lock().unwrap();
let start_idx = (pos - 2) as usize;
let end_idx = (pos - 1) as usize;
if end_idx >= words.len() { return None; }
let start = words[start_idx];
let end = words[end_idx];
let line = chline.lock().unwrap();
let s = (start.max(0)) as usize;
let e = (end.max(0) as usize).min(line.len());
if s > e || s >= line.len() { return None; }
Some((start as i32, line[s..e].to_string()))
}
pub fn histbackword(line: &str, pos: usize) -> usize {
if pos == 0 { return 0; }
let bytes = line.as_bytes();
let mut p = pos.min(bytes.len());
while p > 0 && bytes[p - 1].is_ascii_whitespace() { p -= 1; }
while p > 0 && !bytes[p - 1].is_ascii_whitespace() { p -= 1; }
p
}
pub fn hdynread(_stop: i32) -> Option<String> {
None
}
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 getargs(entry: &histent, arg1: usize, arg2: usize) -> Option<String> {
let nwords = entry.words.len() / 2;
if nwords == 0 || arg2 < arg1 || arg1 >= nwords || arg2 >= nwords {
herrflush();
crate::ported::utils::zerr("no such word in event");
return None;
}
if arg1 == 0 && arg2 == nwords - 1 {
return Some(entry.node.nam.clone());
}
let pos1 = entry.words.get(arg1 * 2).copied().unwrap_or(0) as usize;
let pos2 = entry.words.get(arg2 * 2 + 1).copied().unwrap_or(0) as usize;
if pos2 > entry.node.nam.len() || pos1 > pos2 {
herrflush();
crate::ported::utils::zerr("history event too long, can't index requested words");
return None;
}
Some(entry.node.nam[pos1..pos2].to_string())
}
pub fn apply_history_modifiers(val: &str, modifiers: &str) -> String {
let mut result = val.to_string();
let mut chars = modifiers.chars().peekable();
while let Some(c) = chars.next() {
match c {
':' => continue,
'A' => {
if let Ok(abs) = std::fs::canonicalize(&result) {
result = abs.to_string_lossy().to_string();
} else {
let joined = if result.starts_with('/') {
std::path::PathBuf::from(&result)
} else if let Ok(cwd) = std::env::current_dir() {
cwd.join(&result)
} else {
std::path::PathBuf::from(&result)
};
let mut parts: Vec<String> = Vec::new();
for comp in joined.components() {
match comp {
CurDir => {}
ParentDir => {
parts.pop();
}
Normal(s) => parts.push(s.to_string_lossy().to_string()),
RootDir => parts.insert(0, String::new()),
Prefix(p) => {
parts.insert(0, p.as_os_str().to_string_lossy().to_string())
}
}
}
result = parts.join("/");
if result.is_empty() {
result = "/".to_string();
}
}
}
'a' => {
if !result.starts_with('/') {
if let Ok(cwd) = std::env::current_dir() {
result = cwd.join(&result).to_string_lossy().to_string();
}
}
}
'h' => {
let trimmed = result.trim_end_matches('/');
if trimmed.is_empty() {
result = "/".to_string();
} else if let Some(pos) = trimmed.rfind('/') {
if pos == 0 {
result = "/".to_string();
} else {
result = trimmed[..pos].to_string();
}
} else {
result = ".".to_string();
}
}
't' => {
let trimmed = result.trim_end_matches('/');
if let Some(pos) = trimmed.rfind('/') {
result = trimmed[pos + 1..].to_string();
} else {
result = trimmed.to_string();
}
}
'r' => {
if let Some(dot_pos) = result.rfind('.') {
let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
if dot_pos > slash_pos {
result = result[..dot_pos].to_string();
}
}
}
'e' => {
if let Some(dot_pos) = result.rfind('.') {
let slash_pos = result.rfind('/').map(|p| p + 1).unwrap_or(0);
if dot_pos > slash_pos {
result = result[dot_pos + 1..].to_string();
} else {
result = String::new();
}
} else {
result = String::new();
}
}
'l' => {
result = casemodify(&result, CASMOD_LOWER);
}
'u' => {
result = casemodify(&result, CASMOD_UPPER);
}
'C' => {
result = casemodify(&result, CASMOD_CAPS);
}
'q' => {
let mut out = String::with_capacity(result.len() + 8);
for ch in result.chars() {
if " \t\n'\"\\$`;|&<>()[]{}*?#~!".contains(ch) {
out.push('\\');
}
out.push(ch);
}
result = out;
}
'x' => {
result = crate::hist::quotebreak(&result);
}
'Q' => {
let bytes: Vec<char> = result.chars().collect();
let mut out = String::with_capacity(result.len());
let mut j = 0;
let mut in_dq = false;
let mut in_sq = false;
while j < bytes.len() {
let c = bytes[j];
if in_sq {
if c == '\'' {
in_sq = false;
} else {
out.push(c);
}
j += 1;
continue;
}
if in_dq {
if c == '"' {
in_dq = false;
} else if c == '\\' && j + 1 < bytes.len() {
j += 1;
out.push(bytes[j]);
} else {
out.push(c);
}
j += 1;
continue;
}
match c {
'\'' => in_sq = true,
'"' => in_dq = true,
'\\' if j + 1 < bytes.len() => {
j += 1;
out.push(bytes[j]);
}
_ => out.push(c),
}
j += 1;
}
result = out;
}
'P' => {
if let Ok(real) = std::fs::canonicalize(&result) {
result = real.to_string_lossy().to_string();
}
}
'g' | 's' | '&' => {
let (global, do_parse) = match c {
's' => (false, true),
'&' => (false, false),
_ => { match chars.next() {
Some('s') => (true, true),
Some('&') => (true, false),
_ => break,
}
}
};
let (pat, rep) = if do_parse {
let delim = chars.next().unwrap_or('/');
let mut old = String::new();
while let Some(&ch) = chars.peek() {
if ch == delim { chars.next(); break; }
chars.next();
if ch == '\\' {
if let Some(&n) = chars.peek() {
if n == delim { chars.next(); old.push(delim); continue; }
}
}
old.push(ch);
}
let mut new = String::new();
while let Some(&ch) = chars.peek() {
if ch == delim { chars.next(); break; }
chars.next();
if ch == '\\' {
if let Some(&n) = chars.peek() {
if n == delim { chars.next(); new.push(delim); continue; }
}
}
new.push(ch);
}
if !old.is_empty() {
LAST_SUBST_OLD.with(|c| *c.borrow_mut() = old.clone());
LAST_SUBST_NEW.with(|c| *c.borrow_mut() = new.clone());
}
if old.is_empty() {
let lo = LAST_SUBST_OLD.with(|c| c.borrow().clone());
let ln = LAST_SUBST_NEW.with(|c| c.borrow().clone());
(lo, ln)
} else {
(old, new)
}
} else {
(
LAST_SUBST_OLD.with(|c| c.borrow().clone()),
LAST_SUBST_NEW.with(|c| c.borrow().clone()),
)
};
if !pat.is_empty() {
result = if global {
result.replace(&pat, &rep)
} else {
result.replacen(&pat, &rep, 1)
};
}
}
'U' | 'L' | 'V' | 'X' => {
crate::ported::utils::zerr(&format!("unrecognized modifier `{}'", c));
result = String::new();
break;
}
_ => break,
}
}
result
}
thread_local! {
static LAST_SUBST_OLD: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
static LAST_SUBST_NEW: std::cell::RefCell<String> =
const { std::cell::RefCell::new(String::new()) };
}
#[cfg(test)]
mod subst_modifier_tests {
use super::*;
#[test]
fn s_replaces_first_occurrence() {
assert_eq!(apply_history_modifiers("foo bar foo", ":s/foo/baz/"),
"baz bar foo");
}
#[test]
fn gs_replaces_all_occurrences() {
assert_eq!(apply_history_modifiers("foo bar foo", ":gs/foo/baz/"),
"baz bar baz");
}
#[test]
fn ampersand_repeats_last_subst() {
let first = apply_history_modifiers("xxx", ":s/x/y/");
let second = apply_history_modifiers("xxxx", ":&");
assert_eq!(first, "yxx");
assert_eq!(second, "yxxx");
}
#[test]
fn g_ampersand_repeats_last_subst_globally() {
let _ = apply_history_modifiers("init", ":s/i/X/");
assert_eq!(apply_history_modifiers("aiibii", ":g&"), "aXXbXX");
}
#[test]
fn s_alternate_delimiter() {
assert_eq!(apply_history_modifiers("a-b-c", ":s|-|+|"), "a+b-c");
}
#[test]
fn s_escaped_delimiter_in_pattern() {
assert_eq!(apply_history_modifiers("a/b", r":s/\//#/"), "a#b");
}
}