Skip to main content

zsh/
utils.rs

1//! Utility functions for zshrs
2//!
3//! Port from zsh/Src/utils.c
4//!
5//! Provides miscellaneous utilities: error handling, file operations,
6//! string utilities, and character classification.
7
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10
11/// Script name for error messages
12pub static mut SCRIPT_NAME: Option<String> = None;
13/// Script filename
14pub static mut SCRIPT_FILENAME: Option<String> = None;
15
16/// Print an error message
17pub fn zerr(msg: &str) {
18    eprintln!("zsh: {}", msg);
19}
20
21/// Print an error message with command name
22pub fn zerrnam(cmd: &str, msg: &str) {
23    eprintln!("{}: {}", cmd, msg);
24}
25
26/// Print a warning message
27pub fn zwarn(msg: &str) {
28    eprintln!("zsh: warning: {}", msg);
29}
30
31/// Print a warning with command name  
32pub fn zwarnnam(cmd: &str, msg: &str) {
33    eprintln!("{}: warning: {}", cmd, msg);
34}
35
36/// Print formatted error with optional errno
37pub fn zerrmsg(msg: &str, errno: Option<i32>) {
38    if let Some(e) = errno {
39        let errmsg = std::io::Error::from_raw_os_error(e);
40        eprintln!("zsh: {}: {}", msg, errmsg);
41    } else {
42        eprintln!("zsh: {}", msg);
43    }
44}
45
46/// Check if a path is a directory
47pub fn is_directory(path: &str) -> bool {
48    Path::new(path).is_dir()
49}
50
51/// Check if a file exists and is executable
52pub fn is_executable(path: &str) -> bool {
53    #[cfg(unix)]
54    {
55        use std::os::unix::fs::PermissionsExt;
56        if let Ok(meta) = std::fs::metadata(path) {
57            let mode = meta.permissions().mode();
58            return meta.is_file() && (mode & 0o111 != 0);
59        }
60        false
61    }
62    #[cfg(not(unix))]
63    {
64        Path::new(path).is_file()
65    }
66}
67
68/// Find an executable in PATH
69pub fn find_in_path(name: &str) -> Option<PathBuf> {
70    if name.contains('/') {
71        let path = PathBuf::from(name);
72        if is_executable(name) {
73            return Some(path);
74        }
75        return None;
76    }
77
78    if let Ok(path_var) = std::env::var("PATH") {
79        for dir in path_var.split(':') {
80            let full_path = PathBuf::from(dir).join(name);
81            if let Some(path_str) = full_path.to_str() {
82                if is_executable(path_str) {
83                    return Some(full_path);
84                }
85            }
86        }
87    }
88    None
89}
90
91/// Expand tilde in a path
92pub fn expand_tilde(path: &str) -> String {
93    if !path.starts_with('~') {
94        return path.to_string();
95    }
96
97    let (user, rest) = if let Some(pos) = path[1..].find('/') {
98        (&path[1..pos + 1], &path[pos + 1..])
99    } else {
100        (&path[1..], "")
101    };
102
103    if user.is_empty() {
104        if let Ok(home) = std::env::var("HOME") {
105            return format!("{}{}", home, rest);
106        }
107    } else {
108        #[cfg(unix)]
109        {
110            if let Some(dir) = get_user_home(user) {
111                return format!("{}{}", dir, rest);
112            }
113        }
114    }
115
116    path.to_string()
117}
118
119#[cfg(unix)]
120fn get_user_home(user: &str) -> Option<String> {
121    use std::ffi::CString;
122    unsafe {
123        let c_user = CString::new(user).ok()?;
124        let pw = libc::getpwnam(c_user.as_ptr());
125        if pw.is_null() {
126            return None;
127        }
128        let dir = std::ffi::CStr::from_ptr((*pw).pw_dir);
129        dir.to_str().ok().map(|s| s.to_string())
130    }
131}
132
133/// Nicely format a string for display (escape unprintable chars)
134pub fn nicechar(c: char) -> String {
135    if c.is_ascii_control() {
136        match c {
137            '\n' => "\\n".to_string(),
138            '\t' => "\\t".to_string(),
139            '\r' => "\\r".to_string(),
140            '\x1b' => "\\e".to_string(),
141            _ => format!("^{}", ((c as u8) + 64) as char),
142        }
143    } else if c == '\x7f' {
144        "^?".to_string()
145    } else {
146        c.to_string()
147    }
148}
149
150/// Nicely format a string
151pub fn nicezputs(s: &str) -> String {
152    s.chars().map(nicechar).collect()
153}
154
155/// Check if character is a word character
156pub fn is_word_char(c: char, wordchars: &str) -> bool {
157    c.is_alphanumeric() || wordchars.contains(c)
158}
159
160/// Check if character is an IFS character
161pub fn is_ifs_char(c: char, ifs: &str) -> bool {
162    ifs.contains(c)
163}
164
165/// Convert character to lowercase
166pub fn tulower(c: char) -> char {
167    c.to_lowercase().next().unwrap_or(c)
168}
169
170/// Convert character to uppercase
171pub fn tuupper(c: char) -> char {
172    c.to_uppercase().next().unwrap_or(c)
173}
174
175/// Check if string is a valid identifier
176pub fn is_identifier(s: &str) -> bool {
177    let mut chars = s.chars();
178    match chars.next() {
179        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
180        _ => return false,
181    }
182    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
183}
184
185/// Check if string looks like a number
186pub fn is_number(s: &str) -> bool {
187    let s = s.trim();
188    if s.is_empty() {
189        return false;
190    }
191    let s = s
192        .strip_prefix('-')
193        .or_else(|| s.strip_prefix('+'))
194        .unwrap_or(s);
195    if s.is_empty() {
196        return false;
197    }
198    s.chars().all(|c| c.is_ascii_digit())
199}
200
201/// Check if string is a valid floating point number
202pub fn is_float(s: &str) -> bool {
203    s.parse::<f64>().is_ok()
204}
205
206/// Get monotonic time in nanoseconds
207pub fn monotonic_time_ns() -> u64 {
208    use std::time::Instant;
209    static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
210    let start = START.get_or_init(Instant::now);
211    start.elapsed().as_nanos() as u64
212}
213
214/// Sleep for a given number of seconds (fractional)
215pub fn zsleep(seconds: f64) {
216    let duration = std::time::Duration::from_secs_f64(seconds);
217    std::thread::sleep(duration);
218}
219
220/// Write a string to a file descriptor
221pub fn write_to_fd(fd: i32, data: &str) -> io::Result<()> {
222    #[cfg(unix)]
223    {
224        use std::os::unix::io::FromRawFd;
225        let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
226        write!(file, "{}", data)?;
227        std::mem::forget(file); // Don't close the fd
228        Ok(())
229    }
230    #[cfg(not(unix))]
231    {
232        let _ = (fd, data);
233        Err(io::Error::new(io::ErrorKind::Unsupported, "Not supported"))
234    }
235}
236
237/// Move a file descriptor to a high number (>10)
238pub fn move_fd(fd: i32) -> i32 {
239    #[cfg(unix)]
240    {
241        if fd < 10 {
242            unsafe {
243                let newfd = libc::fcntl(fd, libc::F_DUPFD, 10);
244                if newfd >= 0 {
245                    libc::close(fd);
246                    return newfd;
247                }
248            }
249        }
250        fd
251    }
252    #[cfg(not(unix))]
253    {
254        fd
255    }
256}
257
258/// Close a file descriptor
259pub fn zclose(fd: i32) {
260    #[cfg(unix)]
261    unsafe {
262        libc::close(fd);
263    }
264}
265
266/// Check if a file descriptor is a tty
267pub fn is_tty(fd: i32) -> bool {
268    #[cfg(unix)]
269    unsafe {
270        libc::isatty(fd) != 0
271    }
272    #[cfg(not(unix))]
273    {
274        let _ = fd;
275        false
276    }
277}
278
279/// Get terminal width
280pub fn get_term_width() -> usize {
281    #[cfg(unix)]
282    {
283        unsafe {
284            let mut ws: libc::winsize = std::mem::zeroed();
285            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
286                return ws.ws_col as usize;
287            }
288        }
289    }
290    std::env::var("COLUMNS")
291        .ok()
292        .and_then(|s| s.parse().ok())
293        .unwrap_or(80)
294}
295
296/// Get terminal height
297pub fn get_term_height() -> usize {
298    #[cfg(unix)]
299    {
300        unsafe {
301            let mut ws: libc::winsize = std::mem::zeroed();
302            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_row > 0 {
303                return ws.ws_row as usize;
304            }
305        }
306    }
307    std::env::var("LINES")
308        .ok()
309        .and_then(|s| s.parse().ok())
310        .unwrap_or(24)
311}
312
313/// Quote type constants for quotestring()
314/// Port from zsh.h QT_* enum
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum QuoteType {
317    None = 0,
318    Backslash = 1,
319    Single = 2,
320    Double = 3,
321    Dollars = 4,
322    Backtick = 5,
323    SingleOptional = 6,
324    BackslashPattern = 7,
325    BackslashShownull = 8,
326}
327
328impl QuoteType {
329    /// Convert q flag count to QuoteType
330    /// (q)=Backslash, (qq)=Single, (qqq)=Double, (qqqq)=Dollars
331    pub fn from_q_count(count: u32) -> Self {
332        match count {
333            0 => QuoteType::None,
334            1 => QuoteType::Backslash,
335            2 => QuoteType::Single,
336            3 => QuoteType::Double,
337            _ => QuoteType::Dollars,
338        }
339    }
340}
341
342/// Check if character is special for shell
343/// Port from ispecial() macro in zsh.h
344fn is_special(c: char) -> bool {
345    matches!(
346        c,
347        '|' | '&'
348            | ';'
349            | '<'
350            | '>'
351            | '('
352            | ')'
353            | '$'
354            | '`'
355            | '"'
356            | '\''
357            | '\\'
358            | ' '
359            | '\t'
360            | '\n'
361            | '='
362            | '['
363            | ']'
364            | '*'
365            | '?'
366            | '#'
367            | '~'
368            | '{'
369            | '}'
370            | '!'
371            | '^'
372    )
373}
374
375/// Check if character is a pattern character
376/// Port from ipattern() macro in zsh.h
377fn is_pattern(c: char) -> bool {
378    matches!(
379        c,
380        '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
381    )
382}
383
384/// Quote a string according to the specified type
385/// Port from zsh/Src/utils.c quotestring() (lines 6141-6452)
386pub fn quotestring(s: &str, quote_type: QuoteType) -> String {
387    if s.is_empty() {
388        return match quote_type {
389            QuoteType::None => String::new(),
390            QuoteType::BackslashShownull | QuoteType::Backslash => "''".to_string(),
391            QuoteType::Single | QuoteType::SingleOptional => "''".to_string(),
392            QuoteType::Double => "\"\"".to_string(),
393            QuoteType::Dollars => "$''".to_string(),
394            QuoteType::BackslashPattern => String::new(),
395            QuoteType::Backtick => String::new(),
396        };
397    }
398
399    match quote_type {
400        QuoteType::None => s.to_string(),
401
402        QuoteType::BackslashPattern => {
403            // Only quote pattern characters (lines 6242-6247)
404            let mut result = String::with_capacity(s.len() * 2);
405            for c in s.chars() {
406                if is_pattern(c) {
407                    result.push('\\');
408                }
409                result.push(c);
410            }
411            result
412        }
413
414        QuoteType::Backslash | QuoteType::BackslashShownull => {
415            // Backslash quoting (lines 6260-6416)
416            let mut result = String::with_capacity(s.len() * 2);
417            for c in s.chars() {
418                if is_special(c) {
419                    result.push('\\');
420                }
421                result.push(c);
422            }
423            result
424        }
425
426        QuoteType::Single => {
427            // Single quote: 'string' (lines 6359-6382)
428            let mut result = String::with_capacity(s.len() + 4);
429            result.push('\'');
430            for c in s.chars() {
431                if c == '\'' {
432                    // End quote, add escaped quote, start new quote
433                    result.push_str("'\\''");
434                } else if c == '\n' {
435                    // Newlines need $'...' quoting
436                    result.push_str("'$'\\n''");
437                } else {
438                    result.push(c);
439                }
440            }
441            result.push('\'');
442            result
443        }
444
445        QuoteType::SingleOptional => {
446            // Only add quotes where necessary (lines 6314-6363)
447            let needs_quoting = s.chars().any(|c| is_special(c));
448            if !needs_quoting {
449                return s.to_string();
450            }
451
452            let mut result = String::with_capacity(s.len() + 4);
453            let mut in_quotes = false;
454
455            for c in s.chars() {
456                if c == '\'' {
457                    if in_quotes {
458                        result.push('\'');
459                        in_quotes = false;
460                    }
461                    result.push_str("\\'");
462                } else if is_special(c) {
463                    if !in_quotes {
464                        result.push('\'');
465                        in_quotes = true;
466                    }
467                    result.push(c);
468                } else {
469                    if in_quotes {
470                        result.push('\'');
471                        in_quotes = false;
472                    }
473                    result.push(c);
474                }
475            }
476            if in_quotes {
477                result.push('\'');
478            }
479            result
480        }
481
482        QuoteType::Double => {
483            // Double quote: "string" (lines 6272-6280, 6311-6312)
484            let mut result = String::with_capacity(s.len() + 4);
485            result.push('"');
486            for c in s.chars() {
487                if matches!(c, '$' | '`' | '"' | '\\') {
488                    result.push('\\');
489                }
490                result.push(c);
491            }
492            result.push('"');
493            result
494        }
495
496        QuoteType::Dollars => {
497            // $'...' quoting with escape sequences (lines 6203-6241)
498            let mut result = String::with_capacity(s.len() + 4);
499            result.push_str("$'");
500            for c in s.chars() {
501                match c {
502                    '\\' | '\'' => {
503                        result.push('\\');
504                        result.push(c);
505                    }
506                    '\n' => result.push_str("\\n"),
507                    '\r' => result.push_str("\\r"),
508                    '\t' => result.push_str("\\t"),
509                    '\x1b' => result.push_str("\\e"),
510                    '\x07' => result.push_str("\\a"),
511                    '\x08' => result.push_str("\\b"),
512                    '\x0c' => result.push_str("\\f"),
513                    '\x0b' => result.push_str("\\v"),
514                    c if c.is_ascii_control() => {
515                        // Octal escape for control characters
516                        result.push_str(&format!("\\{:03o}", c as u8));
517                    }
518                    c => result.push(c),
519                }
520            }
521            result.push('\'');
522            result
523        }
524
525        QuoteType::Backtick => {
526            // Backtick quoting (minimal - just escape backticks)
527            s.replace('`', "\\`")
528        }
529    }
530}
531
532/// Quote a string for safe shell use (convenience wrapper)
533pub fn quote_string(s: &str) -> String {
534    if s.is_empty() {
535        return "''".to_string();
536    }
537
538    let needs_quotes = s.chars().any(is_special);
539
540    if !needs_quotes {
541        s.to_string()
542    } else {
543        quotestring(s, QuoteType::Single)
544    }
545}
546
547/// Split a string respecting quotes
548pub fn split_quoted(s: &str) -> Vec<String> {
549    let mut result = Vec::new();
550    let mut current = String::new();
551    let mut in_single_quote = false;
552    let mut in_double_quote = false;
553    let mut escape_next = false;
554
555    for c in s.chars() {
556        if escape_next {
557            current.push(c);
558            escape_next = false;
559            continue;
560        }
561
562        match c {
563            '\\' if !in_single_quote => escape_next = true,
564            '\'' if !in_double_quote => in_single_quote = !in_single_quote,
565            '"' if !in_single_quote => in_double_quote = !in_double_quote,
566            ' ' | '\t' if !in_single_quote && !in_double_quote => {
567                if !current.is_empty() {
568                    result.push(std::mem::take(&mut current));
569                }
570            }
571            _ => current.push(c),
572        }
573    }
574
575    if !current.is_empty() {
576        result.push(current);
577    }
578
579    result
580}
581
582/// Split string by separator - port from zsh/Src/utils.c sepsplit() lines 3961-3992
583///
584/// If sep is None, performs IFS-style word splitting (spacesplit).
585/// Otherwise splits on the given separator string.
586/// allownull: if true, allows empty strings in result
587pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
588    // Handle Nularg at start (zsh internal marker) - line 3968
589    let s = if s.starts_with('\x00') && s.len() > 1 {
590        &s[1..]
591    } else {
592        s
593    };
594
595    match sep {
596        None => spacesplit(s, allownull),
597        Some(sep) if sep.is_empty() => {
598            // Empty separator: split into characters
599            if allownull {
600                s.chars().map(|c| c.to_string()).collect()
601            } else {
602                s.chars()
603                    .map(|c| c.to_string())
604                    .filter(|c| !c.is_empty())
605                    .collect()
606            }
607        }
608        Some(sep) => {
609            let parts: Vec<String> = s.split(sep).map(|p| p.to_string()).collect();
610            if allownull {
611                parts
612            } else {
613                parts.into_iter().filter(|p| !p.is_empty()).collect()
614            }
615        }
616    }
617}
618
619/// IFS-style word splitting - port from zsh/Src/utils.c spacesplit()
620///
621/// Splits on whitespace (space, tab, newline), treating consecutive
622/// whitespace as a single separator.
623pub fn spacesplit(s: &str, allownull: bool) -> Vec<String> {
624    if allownull {
625        s.split(|c: char| c == ' ' || c == '\t' || c == '\n')
626            .map(|p| p.to_string())
627            .collect()
628    } else {
629        s.split_whitespace().map(|p| p.to_string()).collect()
630    }
631}
632
633/// Join array with separator - port from zsh/Src/utils.c sepjoin() lines 3926-3958
634///
635/// If sep is None, uses first char of IFS (defaults to space).
636pub fn sepjoin(arr: &[String], sep: Option<&str>) -> String {
637    if arr.is_empty() {
638        return String::new();
639    }
640    let sep = sep.unwrap_or(" ");
641    arr.join(sep)
642}
643
644/// Parse a string to a signed integer with base detection
645/// Port from zsh/Src/utils.c zstrtol() lines 2384-2516
646pub fn zstrtol(s: &str) -> Option<i64> {
647    let s = s.trim();
648    if s.is_empty() {
649        return None;
650    }
651
652    let (neg, rest) = if s.starts_with('-') {
653        (true, &s[1..])
654    } else if s.starts_with('+') {
655        (false, &s[1..])
656    } else {
657        (false, s)
658    };
659
660    let (base, rest) = if rest.starts_with("0x") || rest.starts_with("0X") {
661        (16, &rest[2..])
662    } else if rest.starts_with("0b") || rest.starts_with("0B") {
663        (2, &rest[2..])
664    } else if rest.starts_with('0') && rest.len() > 1 {
665        (8, &rest[1..])
666    } else {
667        (10, rest)
668    };
669
670    let rest = rest.replace('_', "");
671    let val = u64::from_str_radix(&rest, base).ok()?;
672    let result = val as i64;
673    Some(if neg { -result } else { result })
674}
675
676/// Parse unsigned integer with underscore support
677/// Port from zsh/Src/utils.c zstrtoul_underscore() lines 2528-2575
678pub fn zstrtoul_underscore(s: &str) -> Option<u64> {
679    let s = s.trim();
680    let s = s.strip_prefix('+').unwrap_or(s);
681
682    let (base, rest) = if s.starts_with("0x") || s.starts_with("0X") {
683        (16, &s[2..])
684    } else if s.starts_with("0b") || s.starts_with("0B") {
685        (2, &s[2..])
686    } else if s.starts_with('0') && s.len() > 1 {
687        (8, &s[1..])
688    } else {
689        (10, s)
690    };
691
692    let rest = rest.replace('_', "");
693    u64::from_str_radix(&rest, base).ok()
694}
695
696/// Convert integer to string with specified base
697/// Port from zsh/Src/utils.c convbase()
698pub fn convbase(val: i64, base: u32) -> String {
699    match base {
700        2 => format!("0b{:b}", val),
701        8 => format!("0{:o}", val),
702        16 => format!("0x{:x}", val),
703        _ => val.to_string(),
704    }
705}
706
707/// Set blocking/nonblocking on a file descriptor
708/// Port from zsh/Src/utils.c setblock_fd() lines 2578-2618
709pub fn setblock_fd(fd: i32, blocking: bool) -> bool {
710    #[cfg(unix)]
711    {
712        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
713        if flags < 0 {
714            return false;
715        }
716        let new_flags = if blocking {
717            flags & !libc::O_NONBLOCK
718        } else {
719            flags | libc::O_NONBLOCK
720        };
721        if new_flags != flags {
722            unsafe { libc::fcntl(fd, libc::F_SETFL, new_flags) >= 0 }
723        } else {
724            true
725        }
726    }
727    #[cfg(not(unix))]
728    {
729        let _ = (fd, blocking);
730        false
731    }
732}
733
734/// Read poll - check for pending input
735/// Port from zsh/Src/utils.c read_poll() lines 2643-2730
736pub fn read_poll(fd: i32, timeout_us: i64) -> bool {
737    #[cfg(unix)]
738    {
739        use std::os::unix::io::RawFd;
740        let mut fds = [libc::pollfd {
741            fd: fd as RawFd,
742            events: libc::POLLIN,
743            revents: 0,
744        }];
745        let timeout_ms = (timeout_us / 1000) as i32;
746        let result = unsafe { libc::poll(fds.as_mut_ptr(), 1, timeout_ms) };
747        result > 0 && (fds[0].revents & libc::POLLIN) != 0
748    }
749    #[cfg(not(unix))]
750    {
751        let _ = (fd, timeout_us);
752        false
753    }
754}
755
756/// Check glob qualifier syntax
757/// Port from zsh/Src/utils.c checkglobqual()
758pub fn checkglobqual(s: &str) -> bool {
759    if !s.ends_with(')') {
760        return false;
761    }
762    let mut depth = 0;
763    let mut in_bracket = false;
764    for c in s.chars() {
765        match c {
766            '[' if !in_bracket => in_bracket = true,
767            ']' if in_bracket => in_bracket = false,
768            '(' if !in_bracket => depth += 1,
769            ')' if !in_bracket => {
770                if depth > 0 {
771                    depth -= 1;
772                } else {
773                    return false;
774                }
775            }
776            _ => {}
777        }
778    }
779    depth == 0
780}
781
782/// Compute edit distance between two strings (for spelling correction)
783/// Port from zsh/Src/utils.c spdist() lines 4675-4759
784pub fn spdist(s: &str, t: &str, max_dist: usize) -> usize {
785    let s_chars: Vec<char> = s.chars().collect();
786    let t_chars: Vec<char> = t.chars().collect();
787    let m = s_chars.len();
788    let n = t_chars.len();
789
790    if m.abs_diff(n) > max_dist {
791        return max_dist + 1;
792    }
793
794    let mut prev: Vec<usize> = (0..=n).collect();
795    let mut curr = vec![0; n + 1];
796
797    for i in 1..=m {
798        curr[0] = i;
799        for j in 1..=n {
800            let cost = if s_chars[i - 1] == t_chars[j - 1] {
801                0
802            } else {
803                1
804            };
805            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
806        }
807        std::mem::swap(&mut prev, &mut curr);
808    }
809
810    prev[n]
811}
812
813/// Get temporary file/directory name
814/// Port from zsh/Src/utils.c gettempname()
815pub fn gettempname(prefix: Option<&str>, dir: bool) -> Option<String> {
816    let prefix = prefix.unwrap_or("zsh");
817    let tmp_dir = std::env::var("TMPDIR")
818        .or_else(|_| std::env::var("TMP"))
819        .or_else(|_| std::env::var("TEMP"))
820        .unwrap_or_else(|_| "/tmp".to_string());
821
822    let pid = std::process::id();
823    let timestamp = std::time::SystemTime::now()
824        .duration_since(std::time::UNIX_EPOCH)
825        .map(|d| d.as_nanos())
826        .unwrap_or(0);
827
828    let name = format!("{}/{}{}_{}", tmp_dir, prefix, pid, timestamp);
829
830    if dir {
831        std::fs::create_dir_all(&name).ok()?;
832    }
833    Some(name)
834}
835
836/// Check if metafied - port from zsh/Src/utils.c has_token()
837pub fn has_token(s: &str) -> bool {
838    s.bytes().any(|b| b == 0x83) // Meta character
839}
840
841/// Array length - port from arrlen()
842pub fn arrlen<T>(arr: &[T]) -> usize {
843    arr.len()
844}
845
846/// Duplicate string prefix
847pub fn dupstrpfx(s: &str, len: usize) -> String {
848    s.chars().take(len).collect()
849}
850
851const META_CHAR: char = '\u{83}';
852
853/// Unmetafy string (from utils.c unmeta lines 4930-5051)
854pub fn unmeta(s: &str) -> String {
855    let mut result = String::with_capacity(s.len());
856    let chars: Vec<char> = s.chars().collect();
857    let mut i = 0;
858    while i < chars.len() {
859        if chars[i] == META_CHAR && i + 1 < chars.len() {
860            let c = (chars[i + 1] as u8) ^ 32;
861            result.push(c as char);
862            i += 2;
863        } else {
864            result.push(chars[i]);
865            i += 1;
866        }
867    }
868    result
869}
870
871/// Metafy string (from utils.c metafy)
872pub fn metafy(s: &str) -> String {
873    let mut result = String::with_capacity(s.len() * 2);
874    for c in s.chars() {
875        let b = c as u32;
876        if b < 32 || (b >= 0x83 && b <= 0x9b) {
877            result.push(META_CHAR);
878            result.push(char::from_u32((c as u8 ^ 32) as u32).unwrap_or(c));
879        } else {
880            result.push(c);
881        }
882    }
883    result
884}
885
886/// Unmetafied string length (from utils.c ztrlen lines 5135-5152)
887pub fn ztrlen(s: &str) -> usize {
888    let mut len = 0;
889    let chars: Vec<char> = s.chars().collect();
890    let mut i = 0;
891    while i < chars.len() {
892        len += 1;
893        if chars[i] == META_CHAR && i + 1 < chars.len() {
894            i += 2;
895        } else {
896            i += 1;
897        }
898    }
899    len
900}
901
902/// Compare strings with meta handling (from utils.c ztrcmp lines 5106-5130)
903pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
904    unmeta(s1).cmp(&unmeta(s2))
905}
906
907/// String pointer subtraction with meta handling (from utils.c ztrsub)
908pub fn ztrsub(t: &str, s: &str) -> usize {
909    ztrlen(&t[..t.len().saturating_sub(s.len())])
910}
911
912/// Get home directory for user by name (from utils.c getpwnam handling)
913pub fn get_user_home_by_name(username: &str) -> Option<String> {
914    #[cfg(unix)]
915    {
916        use std::ffi::CString;
917        let c_user = CString::new(username).ok()?;
918        let pwd = unsafe { libc::getpwnam(c_user.as_ptr()) };
919        if pwd.is_null() {
920            return None;
921        }
922        let home = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_dir) };
923        home.to_str().ok().map(|s| s.to_string())
924    }
925    #[cfg(not(unix))]
926    {
927        let _ = username;
928        None
929    }
930}
931
932/// Get username from UID (from utils.c getpwuid handling)
933pub fn get_username(uid: u32) -> Option<String> {
934    #[cfg(unix)]
935    {
936        let pwd = unsafe { libc::getpwuid(uid) };
937        if pwd.is_null() {
938            return None;
939        }
940        let name = unsafe { std::ffi::CStr::from_ptr((*pwd).pw_name) };
941        name.to_str().ok().map(|s| s.to_string())
942    }
943    #[cfg(not(unix))]
944    {
945        let _ = uid;
946        None
947    }
948}
949
950/// Get group name from GID (from utils.c getgrgid handling)
951pub fn get_groupname(gid: u32) -> Option<String> {
952    #[cfg(unix)]
953    {
954        let grp = unsafe { libc::getgrgid(gid) };
955        if grp.is_null() {
956            return None;
957        }
958        let name = unsafe { std::ffi::CStr::from_ptr((*grp).gr_name) };
959        name.to_str().ok().map(|s| s.to_string())
960    }
961    #[cfg(not(unix))]
962    {
963        let _ = gid;
964        None
965    }
966}
967
968/// Compare strings case-insensitively (from utils.c zstricmp)
969pub fn zstricmp(s1: &str, s2: &str) -> std::cmp::Ordering {
970    s1.to_lowercase().cmp(&s2.to_lowercase())
971}
972
973/// Find needle in haystack (from utils.c zstrstr)
974pub fn zstrstr(haystack: &str, needle: &str) -> Option<usize> {
975    haystack.find(needle)
976}
977
978/// String duplicate (from utils.c ztrdup)
979pub fn ztrdup(s: &str) -> String {
980    s.to_string()
981}
982
983/// Duplicate n characters (from utils.c ztrncpy)
984pub fn ztrncpy(s: &str, n: usize) -> String {
985    s.chars().take(n).collect()
986}
987
988/// String concat (from utils.c dyncat)
989pub fn dyncat(s1: &str, s2: &str) -> String {
990    format!("{}{}", s1, s2)
991}
992
993/// Triple concat (from utils.c tricat)
994pub fn tricat(s1: &str, s2: &str, s3: &str) -> String {
995    format!("{}{}{}", s1, s2, s3)
996}
997
998/// Buffer concat (from utils.c bicat)
999pub fn bicat(s1: &str, s2: &str) -> String {
1000    format!("{}{}", s1, s2)
1001}
1002
1003/// Numeric string comparison (from utils.c nstrncmp)
1004pub fn nstrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
1005    let n1: i64 = s1.parse().unwrap_or(0);
1006    let n2: i64 = s2.parse().unwrap_or(0);
1007    n1.cmp(&n2)
1008}
1009
1010/// Inverted numeric comparison (from utils.c invnstrncmp)
1011pub fn invnstrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
1012    nstrcmp(s2, s1)
1013}
1014
1015/// Check if string ends with suffix (from utils.c)
1016pub fn str_ends_with(s: &str, suffix: &str) -> bool {
1017    s.ends_with(suffix)
1018}
1019
1020/// Check if string starts with prefix
1021pub fn str_starts_with(s: &str, prefix: &str) -> bool {
1022    s.starts_with(prefix)
1023}
1024
1025/// Get basename of path (from utils.c)
1026pub fn zbasename(path: &str) -> &str {
1027    std::path::Path::new(path)
1028        .file_name()
1029        .and_then(|n| n.to_str())
1030        .unwrap_or(path)
1031}
1032
1033/// Get dirname of path (from utils.c)
1034pub fn zdirname(path: &str) -> &str {
1035    std::path::Path::new(path)
1036        .parent()
1037        .and_then(|p| p.to_str())
1038        .unwrap_or(".")
1039}
1040
1041/// Check if character is a simple word character (from utils.c)
1042pub fn is_word_char_simple(c: char) -> bool {
1043    c.is_alphanumeric() || c == '_'
1044}
1045
1046/// Get next word boundary (from utils.c)
1047pub fn next_word_boundary(s: &str, pos: usize) -> usize {
1048    let chars: Vec<char> = s.chars().collect();
1049    let mut i = pos;
1050
1051    while i < chars.len() && is_word_char_simple(chars[i]) {
1052        i += 1;
1053    }
1054    while i < chars.len() && !is_word_char_simple(chars[i]) {
1055        i += 1;
1056    }
1057    i
1058}
1059
1060/// Get previous word boundary (from utils.c)
1061pub fn prev_word_boundary(s: &str, pos: usize) -> usize {
1062    let chars: Vec<char> = s.chars().collect();
1063    let mut i = pos.min(chars.len());
1064
1065    while i > 0 && !is_word_char_simple(chars[i - 1]) {
1066        i -= 1;
1067    }
1068    while i > 0 && is_word_char_simple(chars[i - 1]) {
1069        i -= 1;
1070    }
1071    i
1072}
1073
1074/// Path normalization (from utils.c xsymlink handling)
1075pub fn normalize_path(path: &str) -> String {
1076    let mut components: Vec<&str> = Vec::new();
1077    let absolute = path.starts_with('/');
1078
1079    for part in path.split('/') {
1080        match part {
1081            "" | "." => continue,
1082            ".." => {
1083                if !components.is_empty() && components.last() != Some(&"..") {
1084                    components.pop();
1085                } else if !absolute {
1086                    components.push("..");
1087                }
1088            }
1089            _ => components.push(part),
1090        }
1091    }
1092
1093    let result = components.join("/");
1094    if absolute {
1095        format!("/{}", result)
1096    } else if result.is_empty() {
1097        ".".to_string()
1098    } else {
1099        result
1100    }
1101}
1102
1103/// Check access with effective UID (from utils.c eaccess)
1104pub fn eaccess(path: &str, mode: i32) -> bool {
1105    #[cfg(unix)]
1106    {
1107        use std::ffi::CString;
1108        let c_path = match CString::new(path) {
1109            Ok(p) => p,
1110            Err(_) => return false,
1111        };
1112        unsafe { libc::access(c_path.as_ptr(), mode) == 0 }
1113    }
1114    #[cfg(not(unix))]
1115    {
1116        let _ = (path, mode);
1117        false
1118    }
1119}
1120
1121/// Word count for strings
1122pub fn wordcount(s: &str) -> usize {
1123    s.split_whitespace().count()
1124}
1125
1126/// Character count for strings
1127pub fn charcount(s: &str) -> usize {
1128    s.chars().count()
1129}
1130
1131/// Line count for strings
1132pub fn linecount(s: &str) -> usize {
1133    s.lines().count()
1134}
1135
1136/// Join array with delimiter (from utils.c zjoin)
1137pub fn zjoin(arr: &[String], delim: char) -> String {
1138    arr.join(&delim.to_string())
1139}
1140
1141/// Split colon-separated list (from utils.c colonsplit)
1142pub fn colonsplit(s: &str, uniq: bool) -> Vec<String> {
1143    let mut result = Vec::new();
1144    for item in s.split(':') {
1145        if !item.is_empty() {
1146            if uniq && result.contains(&item.to_string()) {
1147                continue;
1148            }
1149            result.push(item.to_string());
1150        }
1151    }
1152    result
1153}
1154
1155/// Skip whitespace separators (from utils.c skipwsep)
1156pub fn skipwsep(s: &str) -> &str {
1157    s.trim_start()
1158}
1159
1160/// Check if character is a whitespace separator
1161pub fn iwsep(c: char) -> bool {
1162    c == ' ' || c == '\t'
1163}
1164
1165/// Check if character needs metafication
1166pub fn imeta(c: char) -> bool {
1167    (c as u32) < 32 || c == '\x7f' || c == '\u{83}'
1168}
1169
1170/// Get nice representation of control character
1171pub fn nicechar_ctrl(c: char) -> String {
1172    let c_byte = c as u8;
1173    if c_byte < 32 {
1174        format!("^{}", (c_byte + 64) as char)
1175    } else if c_byte == 127 {
1176        "^?".to_string()
1177    } else {
1178        c.to_string()
1179    }
1180}
1181
1182/// Format time struct (from utils.c ztrftime)
1183pub fn ztrftime(fmt: &str, time: std::time::SystemTime) -> String {
1184    use std::time::UNIX_EPOCH;
1185
1186    let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
1187    let secs = duration.as_secs() as i64;
1188
1189    #[cfg(unix)]
1190    unsafe {
1191        let tm = libc::localtime(&secs);
1192        if tm.is_null() {
1193            return String::new();
1194        }
1195
1196        let mut buf = vec![0u8; 256];
1197        let c_fmt = std::ffi::CString::new(fmt).unwrap_or_default();
1198        let len = libc::strftime(
1199            buf.as_mut_ptr() as *mut libc::c_char,
1200            buf.len(),
1201            c_fmt.as_ptr(),
1202            tm,
1203        );
1204
1205        if len > 0 {
1206            buf.truncate(len);
1207            String::from_utf8_lossy(&buf).to_string()
1208        } else {
1209            String::new()
1210        }
1211    }
1212
1213    #[cfg(not(unix))]
1214    {
1215        let _ = (fmt, secs);
1216        String::new()
1217    }
1218}
1219
1220/// Get current time formatted
1221pub fn current_time_fmt(fmt: &str) -> String {
1222    ztrftime(fmt, std::time::SystemTime::now())
1223}
1224
1225/// Print-safe string representation
1226pub fn printsafe(s: &str) -> String {
1227    let mut result = String::with_capacity(s.len());
1228    for c in s.chars() {
1229        if c.is_control() {
1230            if c == '\n' {
1231                result.push_str("\\n");
1232            } else if c == '\t' {
1233                result.push_str("\\t");
1234            } else if c == '\r' {
1235                result.push_str("\\r");
1236            } else {
1237                result.push_str(&format!("\\x{:02x}", c as u32));
1238            }
1239        } else {
1240            result.push(c);
1241        }
1242    }
1243    result
1244}
1245
1246/// Escape string for shell
1247pub fn shescape(s: &str) -> String {
1248    if s.chars()
1249        .all(|c| c.is_alphanumeric() || c == '_' || c == '/' || c == '.' || c == '-')
1250    {
1251        return s.to_string();
1252    }
1253
1254    let mut result = String::with_capacity(s.len() + 2);
1255    result.push('\'');
1256    for c in s.chars() {
1257        if c == '\'' {
1258            result.push_str("'\\''");
1259        } else {
1260            result.push(c);
1261        }
1262    }
1263    result.push('\'');
1264    result
1265}
1266
1267/// Unescape string
1268pub fn unescape(s: &str) -> String {
1269    let mut result = String::with_capacity(s.len());
1270    let mut chars = s.chars().peekable();
1271
1272    while let Some(c) = chars.next() {
1273        if c == '\\' {
1274            match chars.next() {
1275                Some('n') => result.push('\n'),
1276                Some('t') => result.push('\t'),
1277                Some('r') => result.push('\r'),
1278                Some('\\') => result.push('\\'),
1279                Some('\'') => result.push('\''),
1280                Some('"') => result.push('"'),
1281                Some('0') => result.push('\0'),
1282                Some('a') => result.push('\x07'),
1283                Some('b') => result.push('\x08'),
1284                Some('e') => result.push('\x1b'),
1285                Some('f') => result.push('\x0c'),
1286                Some('v') => result.push('\x0b'),
1287                Some('x') => {
1288                    let mut hex = String::new();
1289                    for _ in 0..2 {
1290                        if let Some(&c) = chars.peek() {
1291                            if c.is_ascii_hexdigit() {
1292                                hex.push(chars.next().unwrap());
1293                            } else {
1294                                break;
1295                            }
1296                        }
1297                    }
1298                    if let Ok(val) = u8::from_str_radix(&hex, 16) {
1299                        result.push(val as char);
1300                    }
1301                }
1302                Some(c) => result.push(c),
1303                None => result.push('\\'),
1304            }
1305        } else {
1306            result.push(c);
1307        }
1308    }
1309    result
1310}
1311
1312/// Check if string contains only printable characters
1313pub fn isprintable(s: &str) -> bool {
1314    s.chars().all(|c| !c.is_control() || c == '\n' || c == '\t')
1315}
1316
1317/// Get terminal width (fallback to 80)
1318pub fn term_columns() -> usize {
1319    #[cfg(unix)]
1320    {
1321        use std::mem::MaybeUninit;
1322        unsafe {
1323            let mut ws: MaybeUninit<libc::winsize> = MaybeUninit::uninit();
1324            if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ws.as_mut_ptr()) == 0 {
1325                let ws = ws.assume_init();
1326                if ws.ws_col > 0 {
1327                    return ws.ws_col as usize;
1328                }
1329            }
1330        }
1331    }
1332    std::env::var("COLUMNS")
1333        .ok()
1334        .and_then(|s| s.parse().ok())
1335        .unwrap_or(80)
1336}
1337
1338/// Get terminal lines (fallback to 24)
1339pub fn term_lines() -> usize {
1340    #[cfg(unix)]
1341    {
1342        use std::mem::MaybeUninit;
1343        unsafe {
1344            let mut ws: MaybeUninit<libc::winsize> = MaybeUninit::uninit();
1345            if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, ws.as_mut_ptr()) == 0 {
1346                let ws = ws.assume_init();
1347                if ws.ws_row > 0 {
1348                    return ws.ws_row as usize;
1349                }
1350            }
1351        }
1352    }
1353    std::env::var("LINES")
1354        .ok()
1355        .and_then(|s| s.parse().ok())
1356        .unwrap_or(24)
1357}
1358
1359/// Sleep for milliseconds
1360pub fn zsleep_ms(ms: u64) {
1361    std::thread::sleep(std::time::Duration::from_millis(ms));
1362}
1363
1364/// Get hostname
1365pub fn gethostname() -> String {
1366    #[cfg(unix)]
1367    {
1368        let mut buf = vec![0u8; 256];
1369        unsafe {
1370            if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) == 0 {
1371                let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
1372                return String::from_utf8_lossy(&buf[..len]).to_string();
1373            }
1374        }
1375    }
1376    std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
1377}
1378
1379/// Get current working directory
1380pub fn zgetcwd() -> Option<String> {
1381    std::env::current_dir()
1382        .ok()
1383        .map(|p| p.to_string_lossy().to_string())
1384}
1385
1386/// Set current working directory
1387pub fn zchdir(path: &str) -> bool {
1388    std::env::set_current_dir(path).is_ok()
1389}
1390
1391/// Check if path is absolute
1392pub fn isabspath(path: &str) -> bool {
1393    path.starts_with('/')
1394}
1395
1396/// Make path absolute
1397pub fn makeabspath(path: &str) -> String {
1398    if isabspath(path) {
1399        return path.to_string();
1400    }
1401    if let Some(cwd) = zgetcwd() {
1402        format!("{}/{}", cwd, path)
1403    } else {
1404        path.to_string()
1405    }
1406}
1407
1408/// Get real (canonical) path
1409pub fn realpath(path: &str) -> Option<String> {
1410    std::fs::canonicalize(path)
1411        .ok()
1412        .map(|p| p.to_string_lossy().to_string())
1413}
1414
1415/// Check if file exists
1416pub fn file_exists(path: &str) -> bool {
1417    std::path::Path::new(path).exists()
1418}
1419
1420/// Check if path is a file
1421pub fn is_file(path: &str) -> bool {
1422    std::path::Path::new(path).is_file()
1423}
1424
1425/// Check if path is a directory
1426pub fn is_dir(path: &str) -> bool {
1427    std::path::Path::new(path).is_dir()
1428}
1429
1430/// Check if path is a symlink
1431pub fn is_link(path: &str) -> bool {
1432    std::fs::symlink_metadata(path)
1433        .map(|m| m.file_type().is_symlink())
1434        .unwrap_or(false)
1435}
1436
1437/// Get file size
1438pub fn file_size(path: &str) -> Option<u64> {
1439    std::fs::metadata(path).ok().map(|m| m.len())
1440}
1441
1442/// Get file modification time as seconds since epoch
1443pub fn file_mtime(path: &str) -> Option<i64> {
1444    std::fs::metadata(path)
1445        .ok()
1446        .and_then(|m| m.modified().ok())
1447        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
1448        .map(|d| d.as_secs() as i64)
1449}
1450
1451/// Read file contents to string
1452pub fn read_file(path: &str) -> Option<String> {
1453    std::fs::read_to_string(path).ok()
1454}
1455
1456/// Read file lines
1457pub fn read_lines(path: &str) -> Option<Vec<String>> {
1458    std::fs::read_to_string(path)
1459        .ok()
1460        .map(|s| s.lines().map(|l| l.to_string()).collect())
1461}
1462
1463/// Write string to file
1464pub fn write_file(path: &str, contents: &str) -> bool {
1465    std::fs::write(path, contents).is_ok()
1466}
1467
1468/// Append to file
1469pub fn append_file(path: &str, contents: &str) -> bool {
1470    use std::io::Write;
1471    std::fs::OpenOptions::new()
1472        .append(true)
1473        .create(true)
1474        .open(path)
1475        .and_then(|mut f| f.write_all(contents.as_bytes()))
1476        .is_ok()
1477}
1478
1479/// List directory contents
1480pub fn list_dir(path: &str) -> Option<Vec<String>> {
1481    std::fs::read_dir(path).ok().map(|entries| {
1482        entries
1483            .filter_map(|e| e.ok())
1484            .map(|e| e.file_name().to_string_lossy().to_string())
1485            .collect()
1486    })
1487}
1488
1489/// Create directory
1490pub fn mkdir(path: &str) -> bool {
1491    std::fs::create_dir(path).is_ok()
1492}
1493
1494/// Create directory recursively
1495pub fn mkdir_p(path: &str) -> bool {
1496    std::fs::create_dir_all(path).is_ok()
1497}
1498
1499/// Remove file
1500pub fn rm_file(path: &str) -> bool {
1501    std::fs::remove_file(path).is_ok()
1502}
1503
1504/// Remove directory
1505pub fn rm_dir(path: &str) -> bool {
1506    std::fs::remove_dir(path).is_ok()
1507}
1508
1509/// Remove directory recursively
1510pub fn rm_dir_all(path: &str) -> bool {
1511    std::fs::remove_dir_all(path).is_ok()
1512}
1513
1514/// Copy file
1515pub fn copy_file(src: &str, dst: &str) -> bool {
1516    std::fs::copy(src, dst).is_ok()
1517}
1518
1519/// Rename/move file
1520pub fn rename_file(src: &str, dst: &str) -> bool {
1521    std::fs::rename(src, dst).is_ok()
1522}
1523
1524/// Create symlink
1525pub fn symlink(src: &str, dst: &str) -> bool {
1526    #[cfg(unix)]
1527    {
1528        std::os::unix::fs::symlink(src, dst).is_ok()
1529    }
1530    #[cfg(not(unix))]
1531    {
1532        let _ = (src, dst);
1533        false
1534    }
1535}
1536
1537/// Read symlink target
1538pub fn readlink(path: &str) -> Option<String> {
1539    std::fs::read_link(path)
1540        .ok()
1541        .map(|p| p.to_string_lossy().to_string())
1542}
1543
1544/// Get environment variable
1545pub fn getenv(name: &str) -> Option<String> {
1546    std::env::var(name).ok()
1547}
1548
1549/// Set environment variable
1550pub fn setenv(name: &str, value: &str) {
1551    std::env::set_var(name, value);
1552}
1553
1554/// Unset environment variable
1555pub fn unsetenv(name: &str) {
1556    std::env::remove_var(name);
1557}
1558
1559/// Get all environment variables
1560pub fn environ() -> Vec<(String, String)> {
1561    std::env::vars().collect()
1562}
1563
1564/// Get current user ID
1565pub fn getuid() -> u32 {
1566    #[cfg(unix)]
1567    unsafe {
1568        libc::getuid()
1569    }
1570    #[cfg(not(unix))]
1571    0
1572}
1573
1574/// Get effective user ID
1575pub fn geteuid() -> u32 {
1576    #[cfg(unix)]
1577    unsafe {
1578        libc::geteuid()
1579    }
1580    #[cfg(not(unix))]
1581    0
1582}
1583
1584/// Get current group ID
1585pub fn getgid() -> u32 {
1586    #[cfg(unix)]
1587    unsafe {
1588        libc::getgid()
1589    }
1590    #[cfg(not(unix))]
1591    0
1592}
1593
1594/// Get effective group ID
1595pub fn getegid() -> u32 {
1596    #[cfg(unix)]
1597    unsafe {
1598        libc::getegid()
1599    }
1600    #[cfg(not(unix))]
1601    0
1602}
1603
1604/// Get process ID
1605pub fn getpid() -> i32 {
1606    std::process::id() as i32
1607}
1608
1609/// Get parent process ID
1610pub fn getppid() -> i32 {
1611    #[cfg(unix)]
1612    unsafe {
1613        libc::getppid()
1614    }
1615    #[cfg(not(unix))]
1616    0
1617}
1618
1619/// Check if running as root
1620pub fn is_root() -> bool {
1621    geteuid() == 0
1622}
1623
1624/// Get umask
1625pub fn getumask() -> u32 {
1626    #[cfg(unix)]
1627    unsafe {
1628        let mask = libc::umask(0);
1629        libc::umask(mask);
1630        mask as u32
1631    }
1632    #[cfg(not(unix))]
1633    0o022
1634}
1635
1636/// Set umask
1637pub fn setumask(mask: u32) -> u32 {
1638    #[cfg(unix)]
1639    unsafe {
1640        libc::umask(mask as libc::mode_t) as u32
1641    }
1642    #[cfg(not(unix))]
1643    {
1644        let _ = mask;
1645        0
1646    }
1647}
1648
1649/// Get current time as seconds since epoch
1650pub fn time_now() -> i64 {
1651    std::time::SystemTime::now()
1652        .duration_since(std::time::UNIX_EPOCH)
1653        .map(|d| d.as_secs() as i64)
1654        .unwrap_or(0)
1655}
1656
1657/// Get current time with nanoseconds
1658pub fn time_now_ns() -> (i64, i64) {
1659    let dur = std::time::SystemTime::now()
1660        .duration_since(std::time::UNIX_EPOCH)
1661        .unwrap_or_default();
1662    (dur.as_secs() as i64, dur.subsec_nanos() as i64)
1663}
1664
1665/// Format seconds as HH:MM:SS
1666pub fn format_time(secs: i64) -> String {
1667    let hours = secs / 3600;
1668    let mins = (secs % 3600) / 60;
1669    let secs = secs % 60;
1670    if hours > 0 {
1671        format!("{}:{:02}:{:02}", hours, mins, secs)
1672    } else {
1673        format!("{}:{:02}", mins, secs)
1674    }
1675}
1676
1677/// Parse HH:MM:SS to seconds
1678pub fn parse_time(s: &str) -> Option<i64> {
1679    let parts: Vec<&str> = s.split(':').collect();
1680    match parts.len() {
1681        1 => parts[0].parse().ok(),
1682        2 => {
1683            let mins: i64 = parts[0].parse().ok()?;
1684            let secs: i64 = parts[1].parse().ok()?;
1685            Some(mins * 60 + secs)
1686        }
1687        3 => {
1688            let hours: i64 = parts[0].parse().ok()?;
1689            let mins: i64 = parts[1].parse().ok()?;
1690            let secs: i64 = parts[2].parse().ok()?;
1691            Some(hours * 3600 + mins * 60 + secs)
1692        }
1693        _ => None,
1694    }
1695}
1696
1697/// Generate random integer
1698pub fn random_int() -> u32 {
1699    use std::collections::hash_map::RandomState;
1700    use std::hash::{BuildHasher, Hasher};
1701    RandomState::new().build_hasher().finish() as u32
1702}
1703
1704/// Generate random integer in range [0, max)
1705pub fn random_range(max: u32) -> u32 {
1706    if max == 0 {
1707        0
1708    } else {
1709        random_int() % max
1710    }
1711}
1712
1713/// Hash a string (simple djb2)
1714pub fn hash_string(s: &str) -> u64 {
1715    let mut hash: u64 = 5381;
1716    for c in s.bytes() {
1717        hash = hash.wrapping_mul(33).wrapping_add(c as u64);
1718    }
1719    hash
1720}
1721
1722// ---------------------------------------------------------------------------
1723// Missing utility functions ported from utils.c
1724// ---------------------------------------------------------------------------
1725
1726/// Split path into components (from utils.c slashsplit)
1727pub fn slashsplit(s: &str) -> Vec<String> {
1728    s.split('/')
1729        .filter(|s| !s.is_empty())
1730        .map(String::from)
1731        .collect()
1732}
1733
1734/// Split on '=' returning (name, value) (from utils.c equalsplit)
1735pub fn equalsplit(s: &str) -> Option<(String, String)> {
1736    let eq = s.find('=')?;
1737    Some((s[..eq].to_string(), s[eq + 1..].to_string()))
1738}
1739
1740/// Make single-element array (from utils.c mkarray)
1741pub fn mkarray(s: Option<&str>) -> Vec<String> {
1742    match s {
1743        Some(val) => vec![val.to_string()],
1744        None => Vec::new(),
1745    }
1746}
1747
1748/// Free array (no-op in Rust, provided for API compat)
1749pub fn freearray(_arr: Vec<String>) {
1750    // Rust Drop handles this
1751}
1752
1753/// Check if s is a prefix of t (from utils.c strpfx)
1754pub fn strpfx(s: &str, t: &str) -> bool {
1755    t.starts_with(s)
1756}
1757
1758/// Check if s is a suffix of t (from utils.c strsfx)
1759pub fn strsfx(s: &str, t: &str) -> bool {
1760    t.ends_with(s)
1761}
1762
1763/// Ring the terminal bell (from utils.c zbeep)
1764pub fn zbeep() {
1765    eprint!("\x07");
1766}
1767
1768/// Convert file mode to octal string (from utils.c mode_to_octal)
1769pub fn mode_to_octal(mode: u32) -> String {
1770    format!("{:04o}", mode & 0o7777)
1771}
1772
1773/// Go up n directories (from utils.c upchdir)
1774pub fn upchdir(n: usize) -> io::Result<()> {
1775    let mut path = String::new();
1776    for i in 0..n {
1777        if i > 0 {
1778            path.push('/');
1779        }
1780        path.push_str("..");
1781    }
1782    std::env::set_current_dir(&path)?;
1783    Ok(())
1784}
1785
1786/// Change directory with safeguards (from utils.c lchdir)
1787pub fn lchdir(path: &str) -> io::Result<()> {
1788    let resolved = if path.starts_with('/') {
1789        PathBuf::from(path)
1790    } else {
1791        let cwd = std::env::current_dir()?;
1792        cwd.join(path)
1793    };
1794    std::env::set_current_dir(&resolved)?;
1795    Ok(())
1796}
1797
1798/// Adjust terminal window size (from utils.c adjustwinsize)
1799pub fn adjustwinsize() -> (usize, usize) {
1800    let cols = get_term_width();
1801    let rows = get_term_height();
1802    (cols, rows)
1803}
1804
1805/// Spelling correction distance (from utils.c spdist, already exists but adding spckword)
1806/// Check if word is close enough to correct (from utils.c spckword)
1807pub fn spckword(word: &str, candidates: &[&str], threshold: usize) -> Option<String> {
1808    let mut best = None;
1809    let mut best_dist = threshold + 1;
1810    for &candidate in candidates {
1811        let dist = spdist(word, candidate, threshold);
1812        if dist < best_dist {
1813            best_dist = dist;
1814            best = Some(candidate.to_string());
1815        }
1816    }
1817    best
1818}
1819
1820/// Simple interactive query (from utils.c getquery)
1821pub fn getquery(prompt: &str, valid_chars: &str) -> Option<char> {
1822    eprint!("{}", prompt);
1823    let _ = io::stderr().flush();
1824
1825    let mut buf = [0u8; 1];
1826    #[cfg(unix)]
1827    {
1828        use std::io::Read;
1829        if std::io::stdin().read_exact(&mut buf).is_ok() {
1830            let c = buf[0] as char;
1831            if valid_chars.is_empty() || valid_chars.contains(c) {
1832                return Some(c);
1833            }
1834        }
1835    }
1836    None
1837}
1838
1839/// Read a single character (from utils.c read1char)
1840pub fn read1char() -> Option<char> {
1841    #[cfg(unix)]
1842    {
1843        use std::io::Read;
1844        let mut buf = [0u8; 1];
1845        if std::io::stdin().read_exact(&mut buf).is_ok() {
1846            return Some(buf[0] as char);
1847        }
1848    }
1849    None
1850}
1851
1852/// Check before removing directory tree (from utils.c checkrmall)
1853pub fn checkrmall(path: &str) -> bool {
1854    if let Some(c) = getquery(
1855        &format!("zsh: sure you want to delete all of {}? [yn] ", path),
1856        "yn",
1857    ) {
1858        c == 'y' || c == 'Y'
1859    } else {
1860        false
1861    }
1862}
1863
1864/// Resolve symlinks in path (from utils.c xsymlinks/xsymlink)
1865pub fn xsymlink(path: &str) -> String {
1866    match std::fs::canonicalize(path) {
1867        Ok(p) => p.to_string_lossy().to_string(),
1868        Err(_) => path.to_string(),
1869    }
1870}
1871
1872/// Check if running with elevated privileges (from utils.c privasserted)
1873pub fn privasserted() -> bool {
1874    #[cfg(unix)]
1875    {
1876        unsafe { libc::getuid() != libc::geteuid() || libc::getgid() != libc::getegid() }
1877    }
1878    #[cfg(not(unix))]
1879    {
1880        false
1881    }
1882}
1883
1884/// Get the current working directory (port of findpwd/set_pwd_env)
1885pub fn findpwd() -> String {
1886    std::env::current_dir()
1887        .map(|p| p.to_string_lossy().to_string())
1888        .unwrap_or_else(|_| ".".to_string())
1889}
1890
1891/// Check if path is the current directory (from utils.c ispwd)
1892pub fn ispwd(path: &str) -> bool {
1893    if let Ok(cwd) = std::env::current_dir() {
1894        cwd.to_string_lossy() == path
1895    } else {
1896        false
1897    }
1898}
1899
1900/// Print directory name with ~ substitution (from utils.c fprintdir)
1901pub fn fprintdir(path: &str, home: &str) -> String {
1902    if !home.is_empty() && path.starts_with(home) {
1903        let rest = &path[home.len()..];
1904        if rest.is_empty() || rest.starts_with('/') {
1905            return format!("~{}", rest);
1906        }
1907    }
1908    path.to_string()
1909}
1910
1911/// Duplicate array (from utils.c arrdup)
1912pub fn arrdup(arr: &[String]) -> Vec<String> {
1913    arr.to_vec()
1914}
1915
1916/// Duplicate array with max elements (from utils.c arrdup_max)
1917pub fn arrdup_max(arr: &[String], max: usize) -> Vec<String> {
1918    arr.iter().take(max).cloned().collect()
1919}
1920
1921/// Read/write loop wrappers (from utils.c read_loop/write_loop)
1922pub fn read_loop(fd: i32, buf: &mut [u8]) -> io::Result<usize> {
1923    #[cfg(unix)]
1924    {
1925        let mut total = 0;
1926        while total < buf.len() {
1927            let n = unsafe {
1928                libc::read(
1929                    fd,
1930                    buf[total..].as_mut_ptr() as *mut libc::c_void,
1931                    buf.len() - total,
1932                )
1933            };
1934            if n <= 0 {
1935                if n < 0 {
1936                    let e = io::Error::last_os_error();
1937                    if e.kind() == io::ErrorKind::Interrupted {
1938                        continue;
1939                    }
1940                    return Err(e);
1941                }
1942                break;
1943            }
1944            total += n as usize;
1945        }
1946        Ok(total)
1947    }
1948    #[cfg(not(unix))]
1949    {
1950        let _ = (fd, buf);
1951        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
1952    }
1953}
1954
1955pub fn write_loop(fd: i32, buf: &[u8]) -> io::Result<usize> {
1956    #[cfg(unix)]
1957    {
1958        let mut total = 0;
1959        while total < buf.len() {
1960            let n = unsafe {
1961                libc::write(
1962                    fd,
1963                    buf[total..].as_ptr() as *const libc::c_void,
1964                    buf.len() - total,
1965                )
1966            };
1967            if n <= 0 {
1968                if n < 0 {
1969                    let e = io::Error::last_os_error();
1970                    if e.kind() == io::ErrorKind::Interrupted {
1971                        continue;
1972                    }
1973                    return Err(e);
1974                }
1975                break;
1976            }
1977            total += n as usize;
1978        }
1979        Ok(total)
1980    }
1981    #[cfg(not(unix))]
1982    {
1983        let _ = (fd, buf);
1984        Err(io::Error::new(io::ErrorKind::Unsupported, "not unix"))
1985    }
1986}
1987
1988/// Redup: duplicate fd x to y (from utils.c redup)
1989pub fn redup(x: i32, y: i32) {
1990    #[cfg(unix)]
1991    {
1992        if x != y {
1993            unsafe {
1994                libc::dup2(x, y);
1995                libc::close(x);
1996            }
1997        }
1998    }
1999    #[cfg(not(unix))]
2000    {
2001        let _ = (x, y);
2002    }
2003}
2004
2005/// Check if a character type at end of string (from utils.c itype_end)
2006/// Returns the position after the identifier characters
2007pub fn itype_end(s: &str, allow_digits_start: bool) -> usize {
2008    let mut chars = s.chars().peekable();
2009    let mut pos = 0;
2010
2011    if let Some(&first) = chars.peek() {
2012        if !allow_digits_start && first.is_ascii_digit() {
2013            return 0;
2014        }
2015        if !first.is_alphanumeric() && first != '_' && first != '.' {
2016            return 0;
2017        }
2018    }
2019
2020    for c in s.chars() {
2021        if c.is_alphanumeric() || c == '_' || c == '.' {
2022            pos += c.len_utf8();
2023        } else {
2024            break;
2025        }
2026    }
2027    pos
2028}
2029
2030/// Initialize character type table (from utils.c inittyptab)
2031/// In Rust we use Unicode-aware char methods, so this is mostly a no-op
2032pub fn inittyptab() {
2033    // Rust handles character classification natively
2034}
2035
2036/// Skip whitespace separators from IFS (port helper, with custom IFS)
2037pub fn skipwsep_ifs<'a>(s: &'a str, ifs: &str) -> &'a str {
2038    let bytes = s.as_bytes();
2039    let mut i = 0;
2040    while i < bytes.len() {
2041        let c = bytes[i] as char;
2042        if !ifs.contains(c) || !c.is_ascii_whitespace() {
2043            break;
2044        }
2045        i += 1;
2046    }
2047    &s[i..]
2048}
2049
2050/// Find a separator in string (from utils.c findsep)
2051pub fn findsep(s: &str, sep: Option<&str>) -> Option<usize> {
2052    match sep {
2053        Some(sep) if sep.len() == 1 => s.find(sep.chars().next().unwrap()),
2054        Some(sep) => s.find(sep),
2055        None => {
2056            // Default: split on whitespace
2057            s.find(|c: char| c.is_ascii_whitespace())
2058        }
2059    }
2060}
2061
2062/// Count words in string (from utils.c wordcount - extended version)
2063pub fn wordcount_sep(s: &str, sep: Option<&str>) -> usize {
2064    match sep {
2065        Some(sep) => s.split(sep).filter(|w| !w.is_empty()).count(),
2066        None => s.split_whitespace().count(),
2067    }
2068}
2069
2070/// Find word at position (from utils.c findword)
2071pub fn findword<'a>(s: &'a str, sep: Option<&'a str>) -> Option<(&'a str, &'a str)> {
2072    let s = match sep {
2073        Some(_) => s,
2074        None => s.trim_start(),
2075    };
2076    if s.is_empty() {
2077        return None;
2078    }
2079    match sep {
2080        Some(sep) => {
2081            if let Some(pos) = s.find(sep) {
2082                Some((&s[..pos], &s[pos + sep.len()..]))
2083            } else {
2084                Some((s, ""))
2085            }
2086        }
2087        None => {
2088            let end = s.find(|c: char| c.is_ascii_whitespace()).unwrap_or(s.len());
2089            Some((&s[..end], &s[end..]))
2090        }
2091    }
2092}
2093
2094/// Parse getkeystring escape sequences (from utils.c getkeystring)
2095/// Handles \n \t \r \e \a \b \f \v \\ \' \" \xNN \uNNNN \UNNNNNNNN \0NNN
2096pub fn getkeystring(s: &str) -> (String, usize) {
2097    let mut result = String::new();
2098    let mut chars = s.chars().peekable();
2099    let mut consumed = 0;
2100
2101    while let Some(c) = chars.next() {
2102        consumed += c.len_utf8();
2103        if c != '\\' {
2104            result.push(c);
2105            continue;
2106        }
2107        match chars.next() {
2108            Some('n') => {
2109                result.push('\n');
2110                consumed += 1;
2111            }
2112            Some('t') => {
2113                result.push('\t');
2114                consumed += 1;
2115            }
2116            Some('r') => {
2117                result.push('\r');
2118                consumed += 1;
2119            }
2120            Some('e') | Some('E') => {
2121                result.push('\x1b');
2122                consumed += 1;
2123            }
2124            Some('a') => {
2125                result.push('\x07');
2126                consumed += 1;
2127            }
2128            Some('b') => {
2129                result.push('\x08');
2130                consumed += 1;
2131            }
2132            Some('f') => {
2133                result.push('\x0c');
2134                consumed += 1;
2135            }
2136            Some('v') => {
2137                result.push('\x0b');
2138                consumed += 1;
2139            }
2140            Some('\\') => {
2141                result.push('\\');
2142                consumed += 1;
2143            }
2144            Some('\'') => {
2145                result.push('\'');
2146                consumed += 1;
2147            }
2148            Some('"') => {
2149                result.push('"');
2150                consumed += 1;
2151            }
2152            Some('x') => {
2153                consumed += 1;
2154                let mut hex = String::new();
2155                for _ in 0..2 {
2156                    if let Some(&c) = chars.peek() {
2157                        if c.is_ascii_hexdigit() {
2158                            hex.push(chars.next().unwrap());
2159                            consumed += 1;
2160                        } else {
2161                            break;
2162                        }
2163                    }
2164                }
2165                if let Ok(val) = u8::from_str_radix(&hex, 16) {
2166                    result.push(val as char);
2167                }
2168            }
2169            Some('u') => {
2170                consumed += 1;
2171                let mut hex = String::new();
2172                for _ in 0..4 {
2173                    if let Some(&c) = chars.peek() {
2174                        if c.is_ascii_hexdigit() {
2175                            hex.push(chars.next().unwrap());
2176                            consumed += 1;
2177                        } else {
2178                            break;
2179                        }
2180                    }
2181                }
2182                if let Ok(val) = u32::from_str_radix(&hex, 16) {
2183                    if let Some(c) = char::from_u32(val) {
2184                        result.push(c);
2185                    }
2186                }
2187            }
2188            Some('U') => {
2189                consumed += 1;
2190                let mut hex = String::new();
2191                for _ in 0..8 {
2192                    if let Some(&c) = chars.peek() {
2193                        if c.is_ascii_hexdigit() {
2194                            hex.push(chars.next().unwrap());
2195                            consumed += 1;
2196                        } else {
2197                            break;
2198                        }
2199                    }
2200                }
2201                if let Ok(val) = u32::from_str_radix(&hex, 16) {
2202                    if let Some(c) = char::from_u32(val) {
2203                        result.push(c);
2204                    }
2205                }
2206            }
2207            Some(c @ '0'..='7') => {
2208                consumed += 1;
2209                let mut oct = String::new();
2210                oct.push(c);
2211                for _ in 0..2 {
2212                    if let Some(&c) = chars.peek() {
2213                        if c >= '0' && c <= '7' {
2214                            oct.push(chars.next().unwrap());
2215                            consumed += 1;
2216                        } else {
2217                            break;
2218                        }
2219                    }
2220                }
2221                if let Ok(val) = u8::from_str_radix(&oct, 8) {
2222                    result.push(val as char);
2223                }
2224            }
2225            Some('c') => {
2226                consumed += 1;
2227                // \cX = control character
2228                if let Some(c) = chars.next() {
2229                    consumed += 1;
2230                    result.push((c as u8 & 0x1f) as char);
2231                }
2232            }
2233            Some(c) => {
2234                consumed += 1;
2235                result.push('\\');
2236                result.push(c);
2237            }
2238            None => {
2239                result.push('\\');
2240            }
2241        }
2242    }
2243    (result, consumed)
2244}
2245
2246/// Convert UCS-4 to UTF-8 (from utils.c ucs4toutf8)
2247pub fn ucs4toutf8(codepoint: u32) -> Option<String> {
2248    char::from_u32(codepoint).map(|c| c.to_string())
2249}
2250
2251/// Duplicate a string with quoting for display (from utils.c quotedzputs)
2252pub fn quotedzputs(s: &str) -> String {
2253    let mut result = String::with_capacity(s.len());
2254    for c in s.chars() {
2255        if c == '\'' {
2256            result.push_str("'\\''");
2257        } else if is_special(c) {
2258            result.push('\\');
2259            result.push(c);
2260        } else if c.is_ascii_control() {
2261            result.push_str(&format!("$'\\x{:02x}'", c as u8));
2262        } else {
2263            result.push(c);
2264        }
2265    }
2266    result
2267}
2268
2269/// Nice format for string display (from utils.c mb_niceformat)
2270pub fn niceformat(s: &str) -> String {
2271    let mut result = String::new();
2272    for c in s.chars() {
2273        if c.is_ascii_control() {
2274            result.push_str(&nicechar(c));
2275        } else {
2276            result.push(c);
2277        }
2278    }
2279    result
2280}
2281
2282/// Check if nice formatting is needed (from utils.c is_mb_niceformat)
2283pub fn is_niceformat(s: &str) -> bool {
2284    s.chars().any(|c| c.is_ascii_control())
2285}
2286
2287/// Check for special characters that need quoting (from utils.c hasspecial)
2288pub fn hasspecial(s: &str) -> bool {
2289    s.chars().any(|c| is_special(c))
2290}
2291
2292/// Print/format time in HH:MM:SS (from utils.c printhhmmss)
2293pub fn printhhmmss(secs: f64) -> String {
2294    let total_secs = secs as u64;
2295    let hours = total_secs / 3600;
2296    let mins = (total_secs % 3600) / 60;
2297    let s = total_secs % 60;
2298    let frac = secs - total_secs as f64;
2299    if hours > 0 {
2300        format!(
2301            "{}:{:02}:{:02}.{:03}",
2302            hours,
2303            mins,
2304            s,
2305            (frac * 1000.0) as u64
2306        )
2307    } else {
2308        format!("{}:{:02}.{:03}", mins, s, (frac * 1000.0) as u64)
2309    }
2310}
2311
2312/// Get or set the file creation mask (wrapper over umask)
2313pub fn getumask_value() -> u32 {
2314    #[cfg(unix)]
2315    {
2316        let mask = unsafe { libc::umask(0o022) };
2317        unsafe { libc::umask(mask) };
2318        mask as u32
2319    }
2320    #[cfg(not(unix))]
2321    {
2322        0o022
2323    }
2324}
2325
2326/// Attach to the controlling tty's process group (from utils.c attachtty)
2327#[cfg(unix)]
2328pub fn attachtty(pgrp: i32) {
2329    unsafe {
2330        libc::tcsetpgrp(0, pgrp);
2331    }
2332}
2333
2334/// Get the terminal's process group (from utils.c gettygrp)
2335#[cfg(unix)]
2336pub fn gettygrp() -> i32 {
2337    unsafe { libc::tcgetpgrp(0) }
2338}
2339
2340/// Check if directory is readable with entries (from utils.c)
2341pub fn zreaddir(path: &str) -> Vec<String> {
2342    match std::fs::read_dir(path) {
2343        Ok(entries) => entries
2344            .filter_map(|e| e.ok())
2345            .filter_map(|e| e.file_name().into_string().ok())
2346            .filter(|s| s != "." && s != "..")
2347            .collect(),
2348        Err(_) => Vec::new(),
2349    }
2350}
2351
2352/// Initialize terminal (from utils.c zsetupterm)
2353pub fn zsetupterm() -> bool {
2354    // Rust doesn't need explicit terminal setup like C terminfo
2355    // Return true if terminal seems usable
2356    is_tty(1)
2357}
2358
2359/// Delete terminal setup (from utils.c zdeleteterm)
2360pub fn zdeleteterm() {
2361    // No-op in Rust
2362}
2363
2364/// Put raw character to terminal (from utils.c putraw)
2365pub fn putraw(c: char) {
2366    print!("{}", c);
2367}
2368
2369/// Put character to shell output (from utils.c putshout)
2370pub fn putshout(c: char) {
2371    print!("{}", c);
2372}
2373
2374/// Nice char with quoting selection (from utils.c nicechar_sel)
2375pub fn nicechar_sel(c: char, quotable: bool) -> String {
2376    if quotable && is_special(c) {
2377        format!("\\{}", c)
2378    } else {
2379        nicechar(c)
2380    }
2381}
2382
2383/// Initialize multibyte state (from utils.c mb_charinit) - no-op in Rust
2384pub fn mb_charinit() {
2385    // Rust handles UTF-8 natively
2386}
2387
2388/// Wide char nice format (from utils.c wcs_nicechar_sel)
2389pub fn wcs_nicechar_sel(c: char, quotable: bool) -> String {
2390    nicechar_sel(c, quotable)
2391}
2392
2393/// Wide char nice format (from utils.c wcs_nicechar)
2394pub fn wcs_nicechar(c: char) -> String {
2395    nicechar(c)
2396}
2397
2398/// Check if wide char needs nice formatting (from utils.c is_wcs_nicechar)
2399pub fn is_wcs_nicechar(c: char) -> bool {
2400    c.is_ascii_control()
2401}
2402
2403/// Get wide character width (from utils.c zwcwidth)
2404pub fn zwcwidth(c: char) -> usize {
2405    unicode_width::UnicodeWidthChar::width(c).unwrap_or(1)
2406}
2407
2408/// Find program in PATH (from utils.c pathprog)
2409pub fn pathprog(prog: &str) -> Option<PathBuf> {
2410    if prog.contains('/') {
2411        let p = PathBuf::from(prog);
2412        return if p.exists() { Some(p) } else { None };
2413    }
2414    find_in_path(prog)
2415}
2416
2417/// Print symlink target if it is one (from utils.c print_if_link)
2418pub fn print_if_link(path: &str) -> Option<String> {
2419    match std::fs::read_link(path) {
2420        Ok(target) => Some(format!("{} -> {}", path, target.display())),
2421        Err(_) => None,
2422    }
2423}
2424
2425/// Substitute named directory in path (from utils.c substnamedir)
2426pub fn substnamedir(
2427    path: &str,
2428    home: &str,
2429    named_dirs: &std::collections::HashMap<String, String>,
2430) -> String {
2431    // Try home first
2432    if !home.is_empty() && path.starts_with(home) {
2433        let rest = &path[home.len()..];
2434        if rest.is_empty() || rest.starts_with('/') {
2435            return format!("~{}", rest);
2436        }
2437    }
2438    // Try named dirs
2439    let mut best_name = "";
2440    let mut best_len = 0;
2441    for (name, dir) in named_dirs {
2442        if path.starts_with(dir.as_str()) && dir.len() > best_len {
2443            let rest = &path[dir.len()..];
2444            if rest.is_empty() || rest.starts_with('/') {
2445                best_name = name;
2446                best_len = dir.len();
2447            }
2448        }
2449    }
2450    if best_len > 0 {
2451        format!("~{}{}", best_name, &path[best_len..])
2452    } else {
2453        path.to_string()
2454    }
2455}
2456
2457/// Scan for named directory matches (from utils.c finddir_scan)
2458pub fn finddir_scan(
2459    path: &str,
2460    named_dirs: &std::collections::HashMap<String, String>,
2461) -> Option<(String, String)> {
2462    let mut best = None;
2463    let mut best_len = 0;
2464    for (name, dir) in named_dirs {
2465        if path.starts_with(dir.as_str()) && dir.len() > best_len {
2466            let rest = &path[dir.len()..];
2467            if rest.is_empty() || rest.starts_with('/') {
2468                best = Some((name.clone(), rest.to_string()));
2469                best_len = dir.len();
2470            }
2471        }
2472    }
2473    best
2474}
2475
2476/// Find named directory for path (from utils.c finddir)
2477pub fn finddir(
2478    path: &str,
2479    home: &str,
2480    named_dirs: &std::collections::HashMap<String, String>,
2481) -> Option<String> {
2482    if !home.is_empty() && path.starts_with(home) {
2483        let rest = &path[home.len()..];
2484        if rest.is_empty() || rest.starts_with('/') {
2485            return Some(format!("~{}", rest));
2486        }
2487    }
2488    finddir_scan(path, named_dirs).map(|(name, rest)| format!("~{}{}", name, rest))
2489}
2490
2491/// Add user directory (from utils.c adduserdir)
2492pub fn adduserdir(
2493    named_dirs: &mut std::collections::HashMap<String, String>,
2494    name: &str,
2495    dir: &str,
2496) {
2497    named_dirs.insert(name.to_string(), dir.to_string());
2498}
2499
2500/// Get named directory (from utils.c getnameddir)
2501pub fn getnameddir(
2502    name: &str,
2503    named_dirs: &std::collections::HashMap<String, String>,
2504) -> Option<String> {
2505    named_dirs.get(name).cloned()
2506}
2507
2508/// Compare directory paths (from utils.c dircmp)
2509pub fn dircmp(s: &str, t: &str) -> bool {
2510    let s = s.trim_end_matches('/');
2511    let t = t.trim_end_matches('/');
2512    s == t
2513}
2514
2515/// Pre-prompt function list (from utils.c addprepromptfn/delprepromptfn)
2516pub type PrepromptFn = Box<dyn Fn()>;
2517
2518/// Hook function manager (from utils.c callhookfunc)
2519pub struct HookManager {
2520    hooks: std::collections::HashMap<String, Vec<String>>,
2521}
2522
2523impl Default for HookManager {
2524    fn default() -> Self {
2525        Self::new()
2526    }
2527}
2528
2529impl HookManager {
2530    pub fn new() -> Self {
2531        HookManager {
2532            hooks: std::collections::HashMap::new(),
2533        }
2534    }
2535
2536    pub fn add(&mut self, name: &str, func: &str) {
2537        self.hooks
2538            .entry(name.to_string())
2539            .or_default()
2540            .push(func.to_string());
2541    }
2542
2543    pub fn remove(&mut self, name: &str, func: &str) {
2544        if let Some(list) = self.hooks.get_mut(name) {
2545            list.retain(|f| f != func);
2546        }
2547    }
2548
2549    pub fn get(&self, name: &str) -> Option<&Vec<String>> {
2550        self.hooks.get(name)
2551    }
2552
2553    pub fn has(&self, name: &str) -> bool {
2554        self.hooks.get(name).map(|v| !v.is_empty()).unwrap_or(false)
2555    }
2556}
2557
2558/// Timed function entry (from utils.c addtimedfn/deltimedfn)
2559pub struct TimedFn {
2560    pub func: String,
2561    pub when: i64,
2562}
2563
2564/// Pre-prompt processing (from utils.c preprompt)
2565pub fn preprompt_actions() {
2566    // In Rust, this is handled by the exec loop:
2567    // - Check mail
2568    // - Run precmd hooks
2569    // - Update terminal title
2570    // - Check for background job notifications
2571}
2572
2573/// Check mail paths (from utils.c checkmailpath)
2574pub fn checkmailpath(paths: &[String]) -> Vec<String> {
2575    let mut messages = Vec::new();
2576    for path in paths {
2577        // PATH?message format
2578        let (file, msg) = if let Some(pos) = path.find('?') {
2579            (&path[..pos], Some(&path[pos + 1..]))
2580        } else {
2581            (path.as_str(), None)
2582        };
2583
2584        if let Ok(meta) = std::fs::metadata(file) {
2585            if let Ok(modified) = meta.modified() {
2586                if let Ok(elapsed) = modified.elapsed() {
2587                    if elapsed.as_secs() < 60 {
2588                        let default_msg = format!("You have new mail in {}", file);
2589                        messages.push(msg.unwrap_or(&default_msg).to_string());
2590                    }
2591                }
2592            }
2593        }
2594    }
2595    messages
2596}
2597
2598/// Print prompt4 (PS4 for trace output) (from utils.c printprompt4)
2599pub fn printprompt4(ps4: &str) -> String {
2600    // PS4 expansion - typically "+ " or "+%N:%i> "
2601    // Simple expansion for now
2602    ps4.replace("%N", "").replace("%i", "").replace("%_", "")
2603}
2604
2605/// Get terminal info (from utils.c gettyinfo/fdgettyinfo)
2606#[cfg(unix)]
2607pub fn gettyinfo(fd: i32) -> Option<libc::termios> {
2608    let mut termios: libc::termios = unsafe { std::mem::zeroed() };
2609    if unsafe { libc::tcgetattr(fd, &mut termios) } == 0 {
2610        Some(termios)
2611    } else {
2612        None
2613    }
2614}
2615
2616/// Set terminal info (from utils.c settyinfo/fdsettyinfo)
2617#[cfg(unix)]
2618pub fn settyinfo(fd: i32, ti: &libc::termios) -> bool {
2619    unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, ti) == 0 }
2620}
2621
2622/// Adjust terminal lines (from utils.c adjustlines)
2623pub fn adjustlines() -> usize {
2624    get_term_height()
2625}
2626
2627/// Adjust terminal columns (from utils.c adjustcolumns)
2628pub fn adjustcolumns() -> usize {
2629    get_term_width()
2630}
2631
2632/// Check fd table for valid file descriptors (from utils.c check_fd_table)
2633pub fn check_fd_table(fd: i32) -> bool {
2634    #[cfg(unix)]
2635    {
2636        unsafe { libc::fcntl(fd, libc::F_GETFD) != -1 }
2637    }
2638    #[cfg(not(unix))]
2639    {
2640        let _ = fd;
2641        false
2642    }
2643}
2644
2645/// Move file descriptor to a high number (from utils.c movefd)
2646pub fn movefd(fd: i32) -> i32 {
2647    #[cfg(unix)]
2648    {
2649        if fd < 10 {
2650            let new_fd = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) };
2651            if new_fd >= 0 {
2652                unsafe { libc::close(fd) };
2653                // Set close-on-exec
2654                unsafe { libc::fcntl(new_fd, libc::F_SETFD, libc::FD_CLOEXEC) };
2655                return new_fd;
2656            }
2657        }
2658        fd
2659    }
2660    #[cfg(not(unix))]
2661    {
2662        fd
2663    }
2664}
2665
2666/// Add module file descriptor (from utils.c addmodulefd)
2667pub fn addmodulefd(fd: i32) {
2668    #[cfg(unix)]
2669    {
2670        // Set close-on-exec
2671        unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
2672    }
2673    #[cfg(not(unix))]
2674    {
2675        let _ = fd;
2676    }
2677}
2678
2679/// Add lock file descriptor (from utils.c addlockfd)
2680pub fn addlockfd(fd: i32, cloexec: bool) {
2681    #[cfg(unix)]
2682    {
2683        if cloexec {
2684            unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
2685        }
2686    }
2687    #[cfg(not(unix))]
2688    {
2689        let _ = (fd, cloexec);
2690    }
2691}
2692
2693/// Close lock file descriptor (from utils.c zcloselockfd)
2694pub fn zcloselockfd(fd: i32) {
2695    zclose(fd);
2696}
2697
2698/// Parse integer with underscore separators (from utils.c zstrtol_underscore)
2699pub fn zstrtol_underscore(s: &str, base: u32) -> Option<i64> {
2700    let cleaned: String = s.chars().filter(|&c| c != '_').collect();
2701    if base == 0 || base == 10 {
2702        cleaned.parse().ok()
2703    } else {
2704        i64::from_str_radix(&cleaned, base).ok()
2705    }
2706}
2707
2708/// Compute time difference in microseconds (from utils.c timespec_diff_us)
2709pub fn timespec_diff_us(t1: &std::time::Instant, t2: &std::time::Instant) -> i64 {
2710    if *t2 > *t1 {
2711        t2.duration_since(*t1).as_micros() as i64
2712    } else {
2713        -(t1.duration_since(*t2).as_micros() as i64)
2714    }
2715}
2716
2717/// Get monotonic time (from utils.c zmonotime)
2718pub fn zmonotime() -> i64 {
2719    std::time::Instant::now().elapsed().as_secs() as i64
2720}
2721
2722/// Sleep random amount up to max microseconds (from utils.c zsleep_random)
2723pub fn zsleep_random(max_us: u64) {
2724    let us = (std::process::id() as u64 * 1103515245 + 12345) % max_us;
2725    std::thread::sleep(std::time::Duration::from_micros(us));
2726}
2727
2728/// Suppress query (from utils.c noquery)
2729pub fn noquery(_purge: bool) -> bool {
2730    false
2731}
2732
2733/// Scan for spelling correction (from utils.c spscan)
2734pub fn spscan(name: &str, candidates: &[String], threshold: usize) -> Option<String> {
2735    let mut best = None;
2736    let mut best_dist = threshold + 1;
2737    for candidate in candidates {
2738        let dist = spdist(name, candidate, threshold);
2739        if dist < best_dist {
2740            best_dist = dist;
2741            best = Some(candidate.clone());
2742        }
2743    }
2744    best
2745}
2746
2747/// Get shell function by name (from utils.c getshfunc)
2748pub fn getshfunc(
2749    name: &str,
2750    functions: &std::collections::HashMap<String, String>,
2751) -> Option<String> {
2752    functions.get(name).cloned()
2753}
2754
2755/// Make comma character special (from utils.c makecommaspecial)
2756pub fn makecommaspecial(_yes: bool) {
2757    // Character type table manipulation - handled differently in Rust
2758}
2759
2760/// Duplicate array with zsh allocation (from utils.c zarrdup)
2761pub fn zarrdup(arr: &[String]) -> Vec<String> {
2762    arr.to_vec()
2763}
2764
2765/// Spelling correction: find closest match (from utils.c spname)
2766pub fn spname(name: &str, dir: &str) -> Option<String> {
2767    let entries = match std::fs::read_dir(dir) {
2768        Ok(e) => e,
2769        Err(_) => return None,
2770    };
2771
2772    let mut best = None;
2773    let mut best_dist = 4; // threshold
2774
2775    for entry in entries.flatten() {
2776        if let Some(entry_name) = entry.file_name().to_str() {
2777            let dist = spdist(name, entry_name, best_dist);
2778            if dist < best_dist {
2779                best_dist = dist;
2780                best = Some(entry_name.to_string());
2781            }
2782        }
2783    }
2784    best
2785}
2786
2787/// Spelling correction with full path (from utils.c mindist)
2788pub fn mindist(dir: &str, name: &str) -> Option<(String, usize)> {
2789    let entries = match std::fs::read_dir(dir) {
2790        Ok(e) => e,
2791        Err(_) => return None,
2792    };
2793
2794    let mut best = None;
2795    let mut best_dist = 4;
2796
2797    for entry in entries.flatten() {
2798        if let Some(entry_name) = entry.file_name().to_str() {
2799            let dist = spdist(name, entry_name, best_dist);
2800            if dist < best_dist {
2801                best_dist = dist;
2802                best = Some(entry_name.to_string());
2803            }
2804        }
2805    }
2806    best.map(|name| (name, best_dist))
2807}
2808
2809/// Unmetafy string (from utils.c unmetafy) - zsh meta encoding to plain
2810pub fn unmetafy(s: &str) -> String {
2811    let bytes = s.as_bytes();
2812    let mut result = Vec::with_capacity(bytes.len());
2813    let mut i = 0;
2814    while i < bytes.len() {
2815        if bytes[i] == 0x83 && i + 1 < bytes.len() {
2816            // Meta character
2817            result.push(bytes[i + 1] ^ 32);
2818            i += 2;
2819        } else {
2820            result.push(bytes[i]);
2821            i += 1;
2822        }
2823    }
2824    String::from_utf8_lossy(&result).to_string()
2825}
2826
2827/// Count meta characters in string (from utils.c metalen)
2828pub fn metalen(s: &str, len: usize) -> usize {
2829    let bytes = s.as_bytes();
2830    let mut count = 0;
2831    let mut i = 0;
2832    while i < len.min(bytes.len()) {
2833        if bytes[i] == 0x83 {
2834            i += 2;
2835        } else {
2836            i += 1;
2837        }
2838        count += 1;
2839    }
2840    count
2841}
2842
2843/// Dup string nicely (from utils.c nicedup)
2844pub fn nicedup(s: &str) -> String {
2845    niceformat(s)
2846}
2847
2848/// Count nice string length (from utils.c niceztrlen)
2849pub fn niceztrlen(s: &str) -> usize {
2850    niceformat(s).len()
2851}
2852
2853/// Duplicate and double-quote a string (from utils.c dquotedztrdup)
2854pub fn dquotedztrdup(s: &str) -> String {
2855    let mut result = String::with_capacity(s.len() + 4);
2856    for c in s.chars() {
2857        if matches!(c, '$' | '`' | '"' | '\\') {
2858            result.push('\\');
2859        }
2860        result.push(c);
2861    }
2862    result
2863}
2864
2865/// Restore saved directory (from utils.c restoredir)
2866pub fn restoredir(saved: &str) -> bool {
2867    std::env::set_current_dir(saved).is_ok()
2868}
2869
2870/// Convert float for output (from utils.c convfloat)
2871pub fn convfloat(dval: f64, digits: i32, flags: u32) -> String {
2872    crate::params::format_float(dval, digits, flags)
2873}
2874
2875/// Convert float with underscores (from utils.c convfloat_underscore)
2876pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
2877    crate::params::convfloat_underscore(dval, underscore)
2878}
2879
2880/// Convert UCS-4 to multibyte (from utils.c ucs4tomb)
2881pub fn ucs4tomb(wval: u32) -> Option<String> {
2882    char::from_u32(wval).map(|c| c.to_string())
2883}
2884
2885#[cfg(test)]
2886mod tests {
2887    use super::*;
2888
2889    #[test]
2890    fn test_sepsplit() {
2891        assert_eq!(sepsplit("a:b:c", Some(":"), false), vec!["a", "b", "c"]);
2892        assert_eq!(sepsplit("a::b", Some(":"), false), vec!["a", "b"]);
2893        assert_eq!(sepsplit("a::b", Some(":"), true), vec!["a", "", "b"]);
2894    }
2895
2896    #[test]
2897    fn test_spacesplit() {
2898        assert_eq!(spacesplit("a b c", false), vec!["a", "b", "c"]);
2899        assert_eq!(spacesplit("a  b", false), vec!["a", "b"]);
2900    }
2901
2902    #[test]
2903    fn test_sepjoin() {
2904        assert_eq!(
2905            sepjoin(&["a".into(), "b".into(), "c".into()], Some(":")),
2906            "a:b:c"
2907        );
2908        assert_eq!(sepjoin(&["a".into(), "b".into()], None), "a b");
2909    }
2910
2911    #[test]
2912    fn test_is_identifier() {
2913        assert!(is_identifier("foo"));
2914        assert!(is_identifier("_bar"));
2915        assert!(is_identifier("baz123"));
2916        assert!(!is_identifier("123abc"));
2917        assert!(!is_identifier("foo-bar"));
2918    }
2919
2920    #[test]
2921    fn test_is_number() {
2922        assert!(is_number("123"));
2923        assert!(is_number("-456"));
2924        assert!(is_number("+789"));
2925        assert!(!is_number("12.34"));
2926        assert!(!is_number("abc"));
2927    }
2928
2929    #[test]
2930    fn test_nicechar() {
2931        assert_eq!(nicechar('\n'), "\\n");
2932        assert_eq!(nicechar('\t'), "\\t");
2933        assert_eq!(nicechar('a'), "a");
2934    }
2935
2936    #[test]
2937    fn test_quote_string() {
2938        assert_eq!(quote_string("simple"), "simple");
2939        assert_eq!(quote_string("has space"), "'has space'");
2940        assert_eq!(quote_string("it's"), "'it'\\''s'");
2941    }
2942
2943    #[test]
2944    fn test_quotestring_backslash() {
2945        assert_eq!(quotestring("hello", QuoteType::Backslash), "hello");
2946        assert_eq!(
2947            quotestring("has space", QuoteType::Backslash),
2948            "has\\ space"
2949        );
2950        assert_eq!(quotestring("$var", QuoteType::Backslash), "\\$var");
2951    }
2952
2953    #[test]
2954    fn test_quotestring_single() {
2955        assert_eq!(quotestring("hello", QuoteType::Single), "'hello'");
2956        assert_eq!(quotestring("it's", QuoteType::Single), "'it'\\''s'");
2957    }
2958
2959    #[test]
2960    fn test_quotestring_double() {
2961        assert_eq!(quotestring("hello", QuoteType::Double), "\"hello\"");
2962        assert_eq!(
2963            quotestring("say \"hi\"", QuoteType::Double),
2964            "\"say \\\"hi\\\"\""
2965        );
2966    }
2967
2968    #[test]
2969    fn test_quotestring_dollars() {
2970        assert_eq!(quotestring("hello", QuoteType::Dollars), "$'hello'");
2971        assert_eq!(
2972            quotestring("line\nbreak", QuoteType::Dollars),
2973            "$'line\\nbreak'"
2974        );
2975        assert_eq!(
2976            quotestring("tab\there", QuoteType::Dollars),
2977            "$'tab\\there'"
2978        );
2979    }
2980
2981    #[test]
2982    fn test_quotestring_pattern() {
2983        assert_eq!(quotestring("*.txt", QuoteType::BackslashPattern), "\\*.txt");
2984        assert_eq!(
2985            quotestring("file[1]", QuoteType::BackslashPattern),
2986            "file\\[1\\]"
2987        );
2988    }
2989
2990    #[test]
2991    fn test_quotetype_from_q_count() {
2992        assert_eq!(QuoteType::from_q_count(1), QuoteType::Backslash);
2993        assert_eq!(QuoteType::from_q_count(2), QuoteType::Single);
2994        assert_eq!(QuoteType::from_q_count(3), QuoteType::Double);
2995        assert_eq!(QuoteType::from_q_count(4), QuoteType::Dollars);
2996    }
2997
2998    #[test]
2999    fn test_split_quoted() {
3000        let result = split_quoted("foo bar baz");
3001        assert_eq!(result, vec!["foo", "bar", "baz"]);
3002
3003        let result = split_quoted("'hello world' test");
3004        assert_eq!(result, vec!["hello world", "test"]);
3005
3006        let result = split_quoted("\"double quoted\" value");
3007        assert_eq!(result, vec!["double quoted", "value"]);
3008    }
3009
3010    #[test]
3011    fn test_expand_tilde() {
3012        // Just test that it doesn't crash - actual expansion depends on env
3013        let result = expand_tilde("~/test");
3014        assert!(!result.starts_with('~') || result == "~/test");
3015    }
3016
3017    #[test]
3018    fn test_tulower_tuupper() {
3019        assert_eq!(tulower('A'), 'a');
3020        assert_eq!(tuupper('a'), 'A');
3021        assert_eq!(tulower('1'), '1');
3022    }
3023}
3024
3025// ---------------------------------------------------------------------------
3026// Remaining 33 missing utils.c functions
3027// ---------------------------------------------------------------------------
3028
3029/// Set wide character array (from utils.c set_widearray) - no-op, Rust uses native UTF-8
3030pub fn set_widearray(_s: &str) {}
3031
3032/// Warning with va_list formatting (from utils.c zwarning)
3033pub fn zwarning(cmd: &str, msg: &str) {
3034    if cmd.is_empty() {
3035        eprintln!("zsh: {}", msg);
3036    } else {
3037        eprintln!("{}: {}", cmd, msg);
3038    }
3039}
3040
3041/// Plural helper (from utils.c zz_plural_z_alpha) - returns 's' for plural
3042pub fn zz_plural_z_alpha() -> &'static str {
3043    "s"
3044}
3045
3046/// Check if a character needs nice formatting (from utils.c is_nicechar)
3047pub fn is_nicechar(c: char) -> bool {
3048    c.is_ascii_control() || !c.is_ascii()
3049}
3050
3051/// Free a string (from utils.c freestr) - no-op in Rust
3052pub fn freestr(_s: String) {
3053    // Rust Drop handles this
3054}
3055
3056/// Create a temporary file (from utils.c gettempfile)
3057pub fn gettempfile(prefix: &str, suffix: &str) -> Option<String> {
3058    let dir = std::env::var("TMPDIR")
3059        .or_else(|_| std::env::var("TMP"))
3060        .unwrap_or_else(|_| "/tmp".to_string());
3061    let name = format!("{}/{}{}{}", dir, prefix, std::process::id(), suffix);
3062    Some(name)
3063}
3064
3065/// Copy string with upper/lower case (from utils.c strucpy)
3066pub fn strucpy(s: &str, upper: bool) -> String {
3067    if upper {
3068        s.to_uppercase()
3069    } else {
3070        s.to_string()
3071    }
3072}
3073
3074/// Copy n chars with upper/lower case (from utils.c struncpy)
3075pub fn struncpy(s: &str, n: usize, upper: bool) -> String {
3076    let s: String = s.chars().take(n).collect();
3077    if upper {
3078        s.to_uppercase()
3079    } else {
3080        s
3081    }
3082}
3083
3084/// Check if array length >= n (from utils.c arrlen_ge)
3085pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
3086    arr.len() >= n
3087}
3088
3089/// Check if array length > n (from utils.c arrlen_gt)
3090pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
3091    arr.len() > n
3092}
3093
3094/// Check if array length < n (from utils.c arrlen_lt)
3095pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
3096    arr.len() < n
3097}
3098
3099/// Set stdin to blocking mode (from utils.c setblock_stdin)
3100pub fn setblock_stdin() {
3101    setblock_fd(0, true);
3102}
3103
3104/// Buffer size helper for time formatting (from utils.c ztrftimebuf)
3105pub fn ztrftimebuf(needed: usize) -> usize {
3106    // Return a reasonable buffer size for time formatting
3107    needed.max(256)
3108}
3109
3110/// Call shell function by name (from utils.c subst_string_by_func)
3111pub fn subst_string_by_func(_func_name: &str, _arg: &str, _orig: &str) -> Option<String> {
3112    // This would require exec engine access - return None to indicate no substitution
3113    None
3114}
3115
3116/// Make bang character special/non-special (from utils.c makebangspecial)
3117pub fn makebangspecial(_yes: bool) {
3118    // Character type table manipulation - handled by the lexer in Rust
3119}
3120
3121/// Check if wide character is blank (from utils.c wcsiblank)
3122pub fn wcsiblank(c: char) -> bool {
3123    c == ' ' || c == '\t' || c.is_whitespace()
3124}
3125
3126/// Get wide character type (from utils.c wcsitype)
3127pub fn wcsitype(c: char, itype: u32) -> bool {
3128    const IALPHA: u32 = 1;
3129    const IALNUM: u32 = 2;
3130    const IDIGIT: u32 = 3;
3131    const IIDENT: u32 = 4;
3132    const IWORD: u32 = 5;
3133    const IBLANK: u32 = 6;
3134    const ISPACE: u32 = 7;
3135
3136    match itype {
3137        IALPHA => c.is_alphabetic(),
3138        IALNUM => c.is_alphanumeric(),
3139        IDIGIT => c.is_ascii_digit(),
3140        IALPHA | IIDENT => c.is_alphanumeric() || c == '_',
3141        IWORD => c.is_alphanumeric() || c == '_',
3142        IBLANK => c == ' ' || c == '\t',
3143        ISPACE => c.is_whitespace(),
3144        _ => false,
3145    }
3146}
3147
3148/// Duplicate array of wide strings (from utils.c wcs_zarrdup) - same as zarrdup in Rust
3149pub fn wcs_zarrdup(arr: &[String]) -> Vec<String> {
3150    arr.to_vec()
3151}
3152
3153/// Set terminal to cbreak mode (from utils.c setcbreak)
3154#[cfg(unix)]
3155pub fn setcbreak() -> bool {
3156    if let Some(mut ti) = gettyinfo(0) {
3157        ti.c_lflag &= !(libc::ICANON | libc::ECHO);
3158        ti.c_cc[libc::VMIN] = 1;
3159        ti.c_cc[libc::VTIME] = 0;
3160        settyinfo(0, &ti)
3161    } else {
3162        false
3163    }
3164}
3165
3166#[cfg(not(unix))]
3167pub fn setcbreak() -> bool {
3168    false
3169}
3170
3171/// Metafy and duplicate string (from utils.c ztrdup_metafy)
3172pub fn ztrdup_metafy(s: &str) -> String {
3173    metafy(s)
3174}
3175
3176/// Unmetafy a single character (from utils.c unmeta_one)
3177pub fn unmeta_one(s: &str) -> (char, usize) {
3178    let bytes = s.as_bytes();
3179    if bytes.is_empty() {
3180        return ('\0', 0);
3181    }
3182    if bytes[0] == 0x83 && bytes.len() > 1 {
3183        ((bytes[1] ^ 32) as char, 2)
3184    } else {
3185        (bytes[0] as char, 1)
3186    }
3187}
3188
3189/// Get string length counting to end pointer (from utils.c ztrlenend)
3190pub fn ztrlenend(s: &str, end: usize) -> usize {
3191    s[..end.min(s.len())].chars().count()
3192}
3193
3194/// Multibyte metachar length with conversion (from utils.c mb_metacharlenconv_r)
3195pub fn mb_metacharlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
3196    if let Some(c) = s[pos..].chars().next() {
3197        (c.len_utf8(), Some(c))
3198    } else {
3199        (0, None)
3200    }
3201}
3202
3203/// Multibyte metastring length to end (from utils.c mb_metastrlenend)
3204pub fn mb_metastrlenend(s: &str, width: bool, end: usize) -> usize {
3205    if width {
3206        s[..end.min(s.len())]
3207            .chars()
3208            .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(1))
3209            .sum()
3210    } else {
3211        s[..end.min(s.len())].chars().count()
3212    }
3213}
3214
3215/// Multibyte char length with conversion (from utils.c mb_charlenconv_r)
3216pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
3217    mb_metacharlenconv_r(s, pos)
3218}
3219
3220/// Multibyte char length (from utils.c mb_charlenconv)
3221pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
3222    s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
3223}
3224
3225/// Single-byte nice format (from utils.c sb_niceformat)
3226pub fn sb_niceformat(s: &str) -> String {
3227    niceformat(s)
3228}
3229
3230/// Check if single-byte needs nice format (from utils.c is_sb_niceformat)
3231pub fn is_sb_niceformat(s: &str) -> bool {
3232    is_niceformat(s)
3233}
3234
3235/// Expand tabs to spaces (from utils.c zexpandtabs)
3236pub fn zexpandtabs(s: &str, tabstop: usize) -> String {
3237    let tabstop = if tabstop == 0 { 8 } else { tabstop };
3238    let mut result = String::with_capacity(s.len());
3239    let mut col = 0;
3240    for c in s.chars() {
3241        if c == '\t' {
3242            let spaces = tabstop - (col % tabstop);
3243            for _ in 0..spaces {
3244                result.push(' ');
3245            }
3246            col += spaces;
3247        } else if c == '\n' {
3248            result.push(c);
3249            col = 0;
3250        } else {
3251            result.push(c);
3252            col += unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
3253        }
3254    }
3255    result
3256}
3257
3258/// Add unprintable character representation (from utils.c addunprintable)
3259pub fn addunprintable(c: char) -> String {
3260    if c.is_ascii_control() {
3261        if (c as u8) < 32 {
3262            format!("^{}", (c as u8 + 64) as char)
3263        } else {
3264            format!("^?")
3265        }
3266    } else if !c.is_ascii() {
3267        format!("\\u{:04x}", c as u32)
3268    } else {
3269        c.to_string()
3270    }
3271}
3272
3273/// Double-quote and print string (from utils.c dquotedzputs)
3274pub fn dquotedzputs(s: &str) -> String {
3275    let mut result = String::with_capacity(s.len() + 2);
3276    result.push('"');
3277    for c in s.chars() {
3278        match c {
3279            '$' | '`' | '"' | '\\' => {
3280                result.push('\\');
3281                result.push(c);
3282            }
3283            '\n' => result.push_str("\\n"),
3284            _ => result.push(c),
3285        }
3286    }
3287    result.push('"');
3288    result
3289}
3290
3291/// Initialize directory save struct (from utils.c init_dirsav)
3292#[derive(Debug, Clone)]
3293pub struct DirSav {
3294    pub dirfd: i32,
3295    pub dirname: Option<String>,
3296    pub level: i32,
3297}
3298
3299pub fn init_dirsav() -> DirSav {
3300    DirSav {
3301        dirfd: -1,
3302        dirname: std::env::current_dir()
3303            .ok()
3304            .map(|p| p.to_string_lossy().to_string()),
3305        level: 0,
3306    }
3307}
3308
3309/// Debug printf (from utils.c dputs) - only active in debug builds
3310pub fn dputs(msg: &str) {
3311    #[cfg(debug_assertions)]
3312    {
3313        eprintln!("BUG: {}", msg);
3314    }
3315    #[cfg(not(debug_assertions))]
3316    {
3317        let _ = msg;
3318    }
3319}
3320
3321/// Remove character from string (from utils.c chuck)
3322pub fn chuck(s: &mut String, pos: usize) {
3323    if pos < s.len() {
3324        s.remove(pos);
3325    }
3326}
3327
3328/// Check if array length <= n (from utils.c arrlen_le)
3329pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
3330    arr.len() <= n
3331}
3332
3333/// Skip balanced parentheses (from utils.c skipparens)
3334pub fn skipparens(s: &str, open: char, close: char) -> usize {
3335    let mut depth = 0;
3336    for (i, c) in s.char_indices() {
3337        if c == open {
3338            depth += 1;
3339        } else if c == close {
3340            depth -= 1;
3341            if depth == 0 {
3342                return i + c.len_utf8();
3343            }
3344        }
3345    }
3346    s.len()
3347}
3348
3349/// Call hook function by name (from utils.c subst_string_by_hook)
3350pub fn subst_string_by_hook(_hook: &str, _arg: &str, _orig: &str) -> Option<String> {
3351    // Hook functions require access to the exec engine
3352    None
3353}
3354
3355/// Make single-element array on heap (from utils.c hmkarray)
3356pub fn hmkarray(s: &str) -> Vec<String> {
3357    if s.is_empty() {
3358        Vec::new()
3359    } else {
3360        vec![s.to_string()]
3361    }
3362}
3363
3364/// Nice-format and duplicate string (from utils.c nicedupstring)
3365pub fn nicedupstring(s: &str) -> String {
3366    niceformat(s)
3367}
3368
3369/// Check mail file status (from utils.c mailstat)
3370pub fn mailstat(path: &str) -> Option<std::fs::Metadata> {
3371    // Check for strstrstrstrstrstrstrstr/strstrstrstrstrstrstrstr format (strstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstr)
3372    // First try the path as a Strstrdir
3373    let strstrdir = format!("{}/.strstrdir/strstrstr", path);
3374    if let Ok(meta) = std::fs::metadata(&strstrdir) {
3375        return Some(meta);
3376    }
3377    // Then try direct file
3378    std::fs::metadata(path).ok()
3379}