use std::path::Path;
pub use crate::ported::zsh_h::{PM_ARRAY, PM_HASHED, PM_INTEGER, PM_READONLY, PM_UNIQUE};
pub fn declare_locals(names: &[&str], kind: u32) {
use crate::ported::params::{createparam, locallevel, paramtab};
use crate::ported::zsh_h::{PM_HIDE, PM_LOCAL, PM_SPECIAL};
use std::sync::atomic::Ordering;
let cur = locallevel.load(Ordering::Relaxed); if cur == 0 {
return;
}
for name in names {
let (needs_shadow, newspecial) = paramtab()
.read()
.ok()
.and_then(|t| {
t.get(*name).map(|pm| {
let special = (pm.node.flags as u32 & PM_SPECIAL) != 0
&& (kind & PM_HIDE) == 0
&& (pm.node.flags as u32 & PM_HIDE) == 0;
(pm.level < cur, special)
})
})
.unwrap_or((true, false));
if !needs_shadow {
continue;
}
let _ = createparam(name, (kind | PM_LOCAL) as i32);
if let Ok(mut tab) = paramtab().write() {
if let Some(pm) = tab.get_mut(*name) {
pm.level = cur;
pm.node.flags |= (kind & PM_UNIQUE) as i32;
if newspecial {
pm.node.flags |= PM_SPECIAL as i32;
}
}
}
}
}
pub struct LocalScope {
saved: Vec<(String, Option<Box<crate::ported::zsh_h::param>>)>,
}
impl LocalScope {
pub fn declare(names: &[&str], kind: u32) -> Self {
let mut scope = LocalScope { saved: Vec::new() };
scope.also(names, kind);
scope
}
pub fn also(&mut self, names: &[&str], kind: u32) {
if let Ok(tab) = crate::ported::params::paramtab().read() {
for name in names {
self.saved
.push(((*name).to_string(), tab.get(*name).cloned()));
}
}
declare_locals(names, kind);
}
pub fn also_keeping_value(&mut self, names: &[&str]) {
if let Ok(tab) = crate::ported::params::paramtab().read() {
for name in names {
self.saved
.push(((*name).to_string(), tab.get(*name).cloned()));
}
}
declare_locals_keeping_value(names);
}
}
impl Drop for LocalScope {
fn drop(&mut self) {
if let Ok(mut tab) = crate::ported::params::paramtab().write() {
for (name, prev) in self.saved.iter().rev() {
match prev {
Some(pm) => {
tab.insert(name.clone(), pm.clone());
}
None => {
tab.remove(name);
}
}
}
}
}
}
pub fn mark_readonly(names: &[&str]) {
use crate::ported::params::{locallevel, paramtab};
use crate::ported::zsh_h::PM_READONLY;
use std::sync::atomic::Ordering;
if locallevel.load(Ordering::Relaxed) == 0 {
return;
}
if let Ok(mut tab) = paramtab().write() {
for name in names {
if let Some(pm) = tab.get_mut(*name) {
pm.node.flags |= PM_READONLY as i32;
}
}
}
}
fn empty_ops() -> crate::ported::zsh_h::options {
crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
pub fn zstyle_t(ctx: &str, style: &str) -> i32 {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-t".to_string(), ctx.to_string(), style.to_string()],
&empty_ops(),
0,
)
}
#[allow(non_snake_case)]
pub fn zstyle_T(ctx: &str, style: &str) -> i32 {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-T".to_string(), ctx.to_string(), style.to_string()],
&empty_ops(),
0,
)
}
pub fn set_bridge_argv(name: &str, args: &[String]) {
declare_locals(&[name], PM_ARRAY);
let _ = crate::ported::params::setaparam(name, args.to_vec());
}
pub fn declare_locals_keeping_value(names: &[&str]) {
for name in names {
let inherited = crate::ported::params::getsparam(name);
declare_locals(&[name], 0);
if let Some(v) = inherited {
let _ = crate::ported::params::setsparam(name, &v);
}
}
}
pub fn compinit_scan_dirs(env_fpath: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
match crate::ported::params::getaparam("fpath") {
Some(live) if !live.is_empty() => live.iter().map(std::path::PathBuf::from).collect(),
_ => env_fpath.to_vec(),
}
}
pub fn is_executable(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = path.metadata() {
let mode = meta.permissions().mode();
return mode & 0o111 != 0;
}
}
#[cfg(not(unix))]
{
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
return matches!(ext.as_str(), "exe" | "bat" | "cmd" | "com");
}
}
false
}
pub fn glob_matches(pattern: &str, text: &str) -> bool {
if let Some(rest) = pattern.strip_prefix('(') {
if let Some(close) = find_top_close_paren(rest) {
let group = &rest[..close];
let after = &rest[close + 1..];
return group.split('|').any(|alt| {
let combined = format!("{}{}", alt, after);
glob_matches(&combined, text)
});
}
}
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
glob_helper(&pat, &txt)
}
fn find_top_close_paren(s: &str) -> Option<usize> {
let mut depth: i32 = 1;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn glob_helper(pat: &[char], txt: &[char]) -> bool {
if pat.is_empty() {
return txt.is_empty();
}
if pat[0] == '(' {
let rest: String = pat[1..].iter().collect();
let txt_str: String = txt.iter().collect();
if let Some(close) = find_top_close_paren(&rest) {
let group = &rest[..close];
let after = &rest[close + 1..];
return group.split('|').any(|alt| {
let combined = format!("{}{}", alt, after);
glob_matches(&combined, &txt_str)
});
}
}
match pat[0] {
'*' => {
for i in 0..=txt.len() {
if glob_helper(&pat[1..], &txt[i..]) {
return true;
}
}
false
}
'?' => !txt.is_empty() && glob_helper(&pat[1..], &txt[1..]),
c => !txt.is_empty() && txt[0] == c && glob_helper(&pat[1..], &txt[1..]),
}
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
glob_matches(pattern, text)
}
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
let mut dp = vec![vec![0; n + 1]; m + 1];
#[allow(clippy::needless_range_loop)]
for i in 0..=m {
dp[i][0] = i;
}
#[allow(clippy::needless_range_loop)]
for j in 0..=n {
dp[0][j] = j;
}
for i in 1..=m {
for j in 1..=n {
let cost = if a_chars[i - 1] == b_chars[j - 1] {
0
} else {
1
};
dp[i][j] = (dp[i - 1][j] + 1)
.min(dp[i][j - 1] + 1)
.min(dp[i - 1][j - 1] + cost);
}
}
dp[m][n]
}
pub fn is_ignored(s: &str, patterns: &[String]) -> bool {
for pattern in patterns {
if glob_match(pattern, s) {
return true;
}
}
false
}
pub fn get_ignored_patterns(context: &str) -> Vec<String> {
crate::ported::modules::zutil::lookupstyle(context, "ignored-patterns")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bridge_argv_is_declared_local_not_created_global() {
let _g = crate::test_util::global_state_lock();
crate::ported::utils::inc_locallevel();
let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
set_bridge_argv("__compsys_argv", &["-J".to_string(), "grp".to_string()]);
let ty = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("__compsys_argv")
.map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
})
.unwrap_or_default();
assert_eq!(
ty, "array-local",
"bridge argv must be `local -a`; `array` means every call \
re-creates a global and WARN_CREATE_GLOBAL prints a line"
);
assert_eq!(
crate::ported::params::getaparam("__compsys_argv").unwrap_or_default(),
vec!["-J".to_string(), "grp".to_string()],
"declaring it local must not cost the value zparseopts reads"
);
}));
crate::ported::params::endparamscope();
let _ = crate::ported::params::unsetparam("__compsys_argv");
if let Err(p) = out {
std::panic::resume_unwind(p);
}
}
#[test]
fn declare_locals_carries_the_shell_kind_and_unwinds() {
let _g = crate::test_util::global_state_lock();
crate::ported::utils::inc_locallevel();
let cases: [(&str, u32, &str); 4] = [
("zzlk_scalar", 0, "scalar"),
("zzlk_array", PM_ARRAY, "array"),
("zzlk_assoc", PM_HASHED, "association"),
("zzlk_uniq", PM_ARRAY | PM_UNIQUE, "unique"),
];
let inner = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
for (name, kind, want) in cases {
declare_locals(&[name], kind);
let ty = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get(name)
.map(|pm| crate::ported::modules::parameter::paramtypestr(pm))
})
.unwrap_or_default();
assert!(
ty.contains(want) && ty.contains("local"),
"declare_locals({name}, {kind:#x}) produced `{ty}`, \
expected it to contain `{want}` and `local`"
);
}
}));
crate::ported::params::endparamscope();
let survivors: Vec<&str> = cases
.iter()
.map(|(n, _, _)| *n)
.filter(|n| {
crate::ported::params::paramtab()
.read()
.map(|t| t.get(*n).is_some())
.unwrap_or(false)
})
.collect();
for (n, _, _) in cases {
let _ = crate::ported::params::unsetparam(n);
}
if let Err(p) = inner {
std::panic::resume_unwind(p);
}
assert!(
survivors.is_empty(),
"{survivors:?} outlived endparamscope, i.e. pm->level was never \
stamped and every port that declares a name this way still leaks it"
);
}
#[test]
fn compinit_scans_the_live_fpath_array_not_the_startup_env() {
use std::path::PathBuf;
let _g = crate::test_util::global_state_lock();
let env_seeded = vec![PathBuf::from("/from/FPATH/env")];
crate::ported::params::setaparam(
"fpath",
vec!["/live/one".to_string(), "/live/two".to_string()],
);
assert_eq!(
compinit_scan_dirs(&env_seeded),
vec![PathBuf::from("/live/one"), PathBuf::from("/live/two")],
"sh:523 scans $fpath"
);
crate::ported::params::setaparam("fpath", Vec::new());
assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
crate::ported::params::unsetparam("fpath");
assert_eq!(compinit_scan_dirs(&env_seeded), env_seeded);
}
#[test]
fn mark_readonly_is_scoped_to_a_function() {
use crate::ported::modules::parameter::paramtypestr;
let _g = crate::test_util::global_state_lock();
crate::ported::params::locallevel.store(0, std::sync::atomic::Ordering::Relaxed);
let type_of = |n: &str| {
crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(n).map(|pm| paramtypestr(pm)))
.unwrap_or_default()
};
crate::ported::params::setaparam("_ro_probe", vec!["a".to_string()]);
mark_readonly(&["_ro_probe"]);
assert_eq!(type_of("_ro_probe"), "array", "no scope — no readonly bit");
crate::ported::utils::inc_locallevel();
declare_locals(&["_ro_probe"], PM_ARRAY);
crate::ported::params::setaparam("_ro_probe", vec!["b".to_string()]);
mark_readonly(&["_ro_probe"]);
assert_eq!(type_of("_ro_probe"), "array-local-readonly");
crate::ported::params::endparamscope();
assert_eq!(
type_of("_ro_probe"),
"array",
"endparamscope must unwind the readonly shadow"
);
crate::ported::params::unsetparam("_ro_probe");
}
#[test]
fn test_glob_match_simple() {
assert!(glob_match("*.txt", "file.txt"));
assert!(glob_match("*.txt", ".txt"));
assert!(!glob_match("*.txt", "file.rs"));
}
#[test]
fn test_glob_match_question() {
assert!(glob_match("file?.txt", "file1.txt"));
assert!(glob_match("file?.txt", "fileX.txt"));
assert!(!glob_match("file?.txt", "file.txt"));
assert!(!glob_match("file?.txt", "file12.txt"));
}
#[test]
fn test_glob_match_star_middle() {
assert!(glob_match("foo*bar", "foobar"));
assert!(glob_match("foo*bar", "foo123bar"));
assert!(glob_match("foo*bar", "fooXYZbar"));
assert!(!glob_match("foo*bar", "foobaz"));
}
#[test]
fn test_glob_match_multiple_stars() {
assert!(glob_match("*foo*", "foo"));
assert!(glob_match("*foo*", "afoo"));
assert!(glob_match("*foo*", "foob"));
assert!(glob_match("*foo*", "afoob"));
assert!(!glob_match("*foo*", "bar"));
}
#[test]
fn test_glob_match_exact() {
assert!(glob_match("exact", "exact"));
assert!(!glob_match("exact", "exacty"));
assert!(!glob_match("exact", "xact"));
}
}
pub fn call_compfn(name: &str, args: &[String], fallback: impl FnOnce() -> i32) -> i32 {
crate::ported::exec::dispatch_function_call(name, args).unwrap_or_else(fallback)
}
pub struct FnScope {
saved: Option<String>,
saved_lineno: u64,
}
impl FnScope {
pub fn enter(name: &str) -> Self {
let saved = crate::ported::utils::scriptname_get();
crate::ported::utils::set_scriptname(Some(name.to_string()));
let saved_lineno = crate::ported::lex::lineno();
crate::ported::lex::set_lineno(0);
FnScope {
saved,
saved_lineno,
}
}
}
impl Drop for FnScope {
fn drop(&mut self) {
crate::ported::utils::set_scriptname(self.saved.take());
crate::ported::lex::set_lineno(self.saved_lineno);
}
}
pub fn set_sh_lineno(line: u64) {
crate::ported::lex::set_lineno(line);
}
pub fn eval_comp(comp: &str, line: u64) -> i32 {
set_sh_lineno(line);
let oscriptname = crate::ported::utils::scriptname_get(); let fstack = crate::ported::exec::EvalFuncstackFrame::push(); if fstack.pushed() {
crate::ported::utils::set_scriptname(Some("(eval)".to_string()));
}
let mut lastval = crate::ported::exec::execute_script(comp).unwrap_or(1);
{
let ef = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
if ef != 0 && lastval == 0 {
lastval = ef;
}
}
drop(fstack); crate::ported::utils::errflag.fetch_and(
!crate::ported::zsh_h::ERRFLAG_ERROR,
std::sync::atomic::Ordering::Relaxed,
);
crate::ported::utils::set_scriptname(oscriptname); lastval }
pub fn dispatch_action_command(cmd: &str, argv: &[String], line: u64) -> i32 {
set_sh_lineno(line);
if cmd == "compadd" {
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
return crate::ported::zle::complete::bin_compadd("compadd", argv, &ops, 0);
}
if let Some(rc) = crate::ported::exec::dispatch_function_call(cmd, argv) {
return rc;
}
if crate::ported::builtin::createbuiltintable().contains_key(cmd)
|| crate::ported::exec::findcmd(cmd, 0, 0).is_some()
{
return 1;
}
let _subsh = crate::ported::exec::SubshStateGuard::enter(); crate::ported::utils::zwarn(&format!("command not found: {}", cmd)); 127 }
pub fn capture_builtin_stdout(discard_stderr: bool, run: impl FnOnce()) -> String {
let (fd, path) = match crate::ported::utils::gettempfile(None) {
Some(t) => t,
None => return String::new(),
};
let _ = std::io::Write::flush(&mut std::io::stdout());
let saved_out = unsafe { libc::dup(1) };
let saved_err = if discard_stderr {
unsafe { libc::dup(2) }
} else {
-1
};
let devnull = if discard_stderr {
unsafe { libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY) }
} else {
-1
};
let redirected = saved_out >= 0 && unsafe { libc::dup2(fd, 1) } >= 0;
if redirected {
let err_redirected =
devnull >= 0 && saved_err >= 0 && unsafe { libc::dup2(devnull, 2) } >= 0;
run();
let _ = std::io::Write::flush(&mut std::io::stdout());
unsafe {
libc::dup2(saved_out, 1);
}
if err_redirected {
unsafe {
libc::dup2(saved_err, 2);
}
}
}
for f in [saved_out, saved_err, devnull, fd] {
if f >= 0 {
unsafe {
libc::close(f);
}
}
}
let text = if redirected {
std::fs::read_to_string(&path).unwrap_or_default()
} else {
String::new()
};
let _ = std::fs::remove_file(&path);
text
}
#[cfg(test)]
mod capture_builtin_stdout_tests {
use super::*;
fn write_fd(fd: i32, s: &str) {
unsafe {
libc::write(fd, s.as_ptr() as *const libc::c_void, s.len());
}
}
#[test]
fn captures_stdout_and_restores_fd1() {
let _g = crate::test_util::global_state_lock();
let text = capture_builtin_stdout(false, || {
write_fd(1, "one\ntwo\n");
});
assert_eq!(text, "one\ntwo\n");
assert!(
unsafe { libc::fcntl(1, libc::F_GETFD) } >= 0,
"fd 1 was not restored"
);
}
#[test]
fn discard_stderr_leaves_fd2_usable() {
let _g = crate::test_util::global_state_lock();
let text = capture_builtin_stdout(true, || {
write_fd(1, "out\n");
write_fd(2, "this must not be captured\n");
});
assert_eq!(text, "out\n");
assert!(
unsafe { libc::fcntl(2, libc::F_GETFD) } >= 0,
"fd 2 was not restored"
);
}
}
#[cfg(test)]
mod lineno_scope_tests {
use super::*;
use crate::ported::lex::{lineno, set_lineno};
#[test]
fn fn_scope_zeroes_lineno_for_the_port_body() {
let _g = crate::test_util::global_state_lock();
set_lineno(218); {
let _s = FnScope::enter("_describe");
assert_eq!(lineno(), 0, "port body must start with no known line");
}
}
#[test]
fn fn_scope_restores_the_callers_lineno_on_exit() {
let _g = crate::test_util::global_state_lock();
set_lineno(218);
{
let _s = FnScope::enter("_describe");
set_sh_lineno(129);
assert_eq!(lineno(), 129);
}
assert_eq!(lineno(), 218, "caller's line must survive the port call");
}
#[test]
fn nested_fn_scopes_unwind_to_the_right_line() {
let _g = crate::test_util::global_state_lock();
set_lineno(0);
let outer = FnScope::enter("_describe");
set_sh_lineno(122);
{
let _inner = FnScope::enter("_tags");
assert_eq!(lineno(), 0);
set_sh_lineno(36); assert_eq!(lineno(), 36);
}
assert_eq!(lineno(), 122, "_describe's line must survive _tags");
drop(outer);
assert_eq!(lineno(), 0);
}
}
#[cfg(test)]
mod diagnostic_framing_tests {
use std::sync::atomic::Ordering;
#[test]
fn the_not_found_diagnostic_does_not_trash_the_line_editor() {
use crate::ported::builtins::sched::zleactive;
use crate::ported::init::zle_load_state;
use crate::ported::zle::zle_refresh::{RESETNEEDED, TRASHEDZLE};
let _g = crate::test_util::global_state_lock();
let mut master: libc::c_int = 0;
let mut slave: libc::c_int = 0;
let rc = unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut::<libc::termios>(),
std::ptr::null_mut::<libc::winsize>(),
)
};
assert_eq!(rc, 0, "openpty failed; the probe needs a terminal on fd 2");
let saved_stderr = unsafe { libc::dup(2) };
assert!(saved_stderr >= 0, "dup(2) failed");
assert!(
unsafe { libc::dup2(slave, 2) } >= 0,
"dup2 onto fd 2 failed"
);
assert_eq!(
unsafe { libc::isatty(2) },
1,
"fd 2 must be a terminal or Src/utils.c:144 skips the hook"
);
let saved = (
zleactive.load(Ordering::Relaxed),
zle_load_state.load(Ordering::SeqCst),
TRASHEDZLE.load(Ordering::Relaxed),
RESETNEEDED.load(Ordering::Relaxed),
);
zleactive.store(1, Ordering::Relaxed);
zle_load_state.store(1, Ordering::SeqCst);
TRASHEDZLE.store(0, Ordering::Relaxed);
RESETNEEDED.store(0, Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed);
let status = super::dispatch_action_command("nosuchcmd_zz_framing_probe", &[], 63);
let trashed = TRASHEDZLE.load(Ordering::Relaxed);
let reset = RESETNEEDED.load(Ordering::Relaxed);
let still_active = zleactive.load(Ordering::Relaxed);
zleactive.store(saved.0, Ordering::Relaxed);
zle_load_state.store(saved.1, Ordering::SeqCst);
TRASHEDZLE.store(saved.2, Ordering::Relaxed);
RESETNEEDED.store(saved.3, Ordering::Relaxed);
crate::ported::utils::errflag.store(0, Ordering::Relaxed);
unsafe {
libc::dup2(saved_stderr, 2);
libc::close(saved_stderr);
libc::close(slave);
libc::close(master);
}
assert_eq!(status, 127, "c:908 — a name that resolves nowhere is 127");
assert_eq!(
trashed, 0,
"trashzle ran: the diagnostic moved the cursor off the command \
line's row (Src/Zle/zle_main.c:2071 is false in C's forked child)"
);
assert_eq!(
reset, 0,
"resetneeded was raised: the next zrefresh repaints the prompt \
BELOW the diagnostic, which zsh never does here"
);
assert_eq!(
still_active, 1,
"the entersubsh stand-in must restore zleactive when it drops"
);
}
}
#[cfg(test)]
mod zstyle_bool_tests {
use super::{empty_ops, zstyle_T, zstyle_t};
const CTX: &str = ":completion:zstyle-bool-probe:zstyle-bool-probe:";
fn set_style(value: &str) {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&[CTX.to_string(), "boolprobe".to_string(), value.to_string()],
&empty_ops(),
0,
);
}
fn del_style() {
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-d".to_string(), CTX.to_string(), "boolprobe".to_string()],
&empty_ops(),
0,
);
}
#[test]
fn value_decides_the_exit_not_mere_definition() {
let _g = crate::test_util::global_state_lock();
del_style();
assert_eq!(zstyle_t(CTX, "boolprobe"), 2, "-t, no pattern matched → 2");
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T, no pattern matched → 0");
for v in ["true", "yes", "on", "1"] {
del_style();
set_style(v);
assert_eq!(zstyle_t(CTX, "boolprobe"), 0, "-t on `{v}` must be 0");
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on `{v}` must be 0");
}
for v in ["maybe", "2", "yes-ish", "-1"] {
del_style();
set_style(v);
assert_eq!(
zstyle_t(CTX, "boolprobe"),
1,
"-t on `{v}` must be 1 — only true/yes/on/1 are true"
);
assert_eq!(
zstyle_T(CTX, "boolprobe"),
1,
"-T on `{v}` must be 1 — a set-but-not-true value is FALSE"
);
}
del_style();
crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&[CTX.to_string(), "boolprobe".to_string()],
&empty_ops(),
0,
);
assert_eq!(zstyle_T(CTX, "boolprobe"), 0, "-T on a valueless style → 0");
for v in ["false", "no", "off", "0"] {
del_style();
set_style(v);
assert_eq!(
zstyle_t(CTX, "boolprobe"),
1,
"-t on `{v}` must be 1, not 0 — the style is OFF"
);
assert_eq!(
zstyle_T(CTX, "boolprobe"),
1,
"-T on `{v}` must be 1, not 0 — the style is OFF"
);
assert_eq!(
crate::ported::modules::zutil::testforstyle(CTX, "boolprobe"),
0,
"`testforstyle` reports `{v}` as TRUE — it is zstyle -q's \
primitive (zutil.c:465/749-756) and never reads the value"
);
}
del_style();
}
}