use crate::ported::zsh_h::{features, module};
use std::sync::{Mutex, OnceLock};
use crate::DPUTS;
use crate::options::{opt_state_get, opt_state_set, optlookup};
use crate::ported::params::{setaparam, setiparam, setsparam};
use crate::ported::utils::zwarnnam;
use crate::zsh_h::isset;
pub const ZREGEX_EXTENDED: i32 = 0;
pub fn zregex_regerrwarn(prefix: &str, err_msg: &str) {
zwarnnam(prefix, err_msg); }
pub fn zcond_regex_match(a: &[&str], id: i32) -> i32 {
if a.len() < 2 {
return 0;
}
let lhstr = a[0]; let rhre = a[1]; let mut return_value: i32 = 0;
if id != ZREGEX_EXTENDED {
DPUTS!(true, "bad regex option"); return 0; }
let casematch = isset(optlookup("casematch"));
let pat_for_compile = if !casematch {
format!("(?i){}", rhre) } else {
rhre.to_string()
};
if rhre.is_empty() {
zregex_regerrwarn("-regex-match", "failed to compile regex: empty (sub)expression");
return 0;
}
let re = match regex::Regex::new(&pat_for_compile) {
Ok(r) => r,
Err(_) => {
zregex_regerrwarn("-regex-match", "failed to compile regex");
return 0; }
};
let captures = match re.captures(lhstr) {
Some(c) => c,
None => return 0, };
return_value = 1; let nsub = re.captures_len() - 1; let bashre = isset(optlookup("bashrematch"));
let ksharr = isset(optlookup("ksharrays"));
let (start, nelem) = if bashre {
(0usize, nsub + 1) } else {
(1usize, nsub) };
let mut arr: Vec<String> = Vec::with_capacity(nelem);
for n in start..=nsub {
if let Some(m) = captures.get(n) {
arr.push(m.as_str().to_string()); } else {
arr.push(String::new());
}
}
if bashre {
setsparam("BASH_REMATCH", &arr.join(":"));
return return_value;
}
let m0 = captures.get(0).expect("regex matched but no group 0");
let full = m0.as_str().to_string(); setsparam("MATCH", &full);
let so = m0.start();
let eo = m0.end();
let mbegin_chars = lhstr[..so].chars().count() as i64; let kshoff: i64 = if ksharr { 0 } else { 1 }; let mbegin = mbegin_chars + kshoff; setiparam("MBEGIN", mbegin);
let match_chars = lhstr[so..eo].chars().count() as i64;
let mend_total = mbegin_chars + match_chars;
let mend = mend_total + kshoff - 1; setiparam("MEND", mend);
if nelem > 0 {
let mut mbegin_arr: Vec<String> = Vec::with_capacity(nelem);
let mut mend_arr: Vec<String> = Vec::with_capacity(nelem);
for n in 0..nelem {
let cap_idx = start + n;
match captures.get(cap_idx) {
Some(m) => {
let beg_chars = lhstr[..m.start()].chars().count() as i64;
let len_chars = lhstr[m.start()..m.end()].chars().count() as i64;
mbegin_arr.push((beg_chars + kshoff).to_string()); mend_arr.push((beg_chars + len_chars + kshoff - 1).to_string());
}
None => {
mbegin_arr.push("-1".to_string());
mend_arr.push("-1".to_string());
}
}
}
setaparam("match", arr.clone());
setaparam("mbegin", mbegin_arr);
setaparam("mend", mend_arr);
}
return_value }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables)
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
0
}
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0
}
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["c:regex-match".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 0,
cd_list: None,
cd_size: 1,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 0,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use crate::options::{opt_state_get, opt_state_set};
use crate::regex_module::{zcond_regex_match, ZREGEX_EXTENDED};
#[test]
fn match_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["hello world", "wor.d"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn captures_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["foo=42", "([a-z]+)=([0-9]+)"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn no_match_returns_zero() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["abc", "xyz"], ZREGEX_EXTENDED);
assert_eq!(r, 0);
}
#[test]
fn invalid_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["anything", "["], ZREGEX_EXTENDED), 0);
}
#[test]
fn missing_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&[], ZREGEX_EXTENDED), 0);
assert_eq!(zcond_regex_match(&["only_lhs"], ZREGEX_EXTENDED), 0);
}
#[test]
fn casematch_off_is_case_insensitive() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", true);
let r = zcond_regex_match(&["HELLO", "hello"], ZREGEX_EXTENDED);
opt_state_set("casematch", saved);
assert_eq!(
r, 0,
"casematch=true → case-sensitive → HELLO vs hello must NOT match"
);
}
#[test]
fn casematch_unset_is_case_insensitive() {
let _g = crate::test_util::global_state_lock();
let saved = opt_state_get("casematch").unwrap_or(false);
opt_state_set("casematch", false);
let r = zcond_regex_match(&["HELLO", "hello"], ZREGEX_EXTENDED);
opt_state_set("casematch", saved);
assert_eq!(
r, 1,
"casematch=false → case-insensitive → HELLO matches hello"
);
}
#[test]
fn matching_pattern_returns_one() {
let _g = crate::test_util::global_state_lock();
let r = zcond_regex_match(&["hello world", "world"], ZREGEX_EXTENDED);
assert_eq!(r, 1);
}
#[test]
fn anchor_caret_requires_match_at_start() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["foobar", "^foo"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["barfoo", "^foo"], ZREGEX_EXTENDED), 0);
}
#[test]
fn anchor_dollar_requires_match_at_end() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["barfoo", "foo$"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["foobar", "foo$"], ZREGEX_EXTENDED), 0);
}
#[test]
fn alternation_matches_either_branch() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["xterm", "xterm|screen"], ZREGEX_EXTENDED),
1
);
assert_eq!(
zcond_regex_match(&["screen", "xterm|screen"], ZREGEX_EXTENDED),
1
);
assert_eq!(
zcond_regex_match(&["bash", "xterm|screen"], ZREGEX_EXTENDED),
0
);
}
#[test]
fn dot_matches_any_single_char() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["foo", "f.o"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["fXo", "f.o"], ZREGEX_EXTENDED), 1);
}
#[test]
fn regex_corpus_charclass_plus_quantifier_matches() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc123", "[a-z]+[0-9]+"], ZREGEX_EXTENDED),
1,
);
}
#[test]
fn regex_corpus_anchored_full_match() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "^abc$"], ZREGEX_EXTENDED),
1,
);
}
#[test]
fn regex_corpus_anchored_rejects_extra_chars() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["xabcy", "^abc$"], ZREGEX_EXTENDED),
0,
);
}
#[test]
fn regex_corpus_empty_pattern_matches_empty_input() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["", ""], ZREGEX_EXTENDED), 1);
}
#[test]
fn regex_corpus_star_quantifier() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zcond_regex_match(&["aaa", "a*"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["", "a*"], ZREGEX_EXTENDED), 1);
assert_eq!(zcond_regex_match(&["b", "a*"], ZREGEX_EXTENDED), 1,
"a* matches empty prefix of 'b'");
}
#[test]
fn regex_corpus_invalid_pattern_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "[unterminated"], ZREGEX_EXTENDED),
0,
"invalid regex returns 0 (no match + warn)",
);
}
#[test]
fn regex_corpus_wrong_id_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["abc", "abc"], 99),
0,
"unsupported id = 0",
);
}
#[test]
fn regex_corpus_one_arg_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
zcond_regex_match(&["only_one"], ZREGEX_EXTENDED),
0,
);
}
}