1use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10
11pub static mut SCRIPT_NAME: Option<String> = None;
13pub static mut SCRIPT_FILENAME: Option<String> = None;
15
16pub fn zerr(msg: &str) {
18 eprintln!("zsh: {}", msg);
19}
20
21pub fn zerrnam(cmd: &str, msg: &str) {
23 eprintln!("{}: {}", cmd, msg);
24}
25
26pub fn zwarn(msg: &str) {
28 eprintln!("zsh: warning: {}", msg);
29}
30
31pub fn zwarnnam(cmd: &str, msg: &str) {
33 eprintln!("{}: warning: {}", cmd, msg);
34}
35
36pub 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
46pub fn is_directory(path: &str) -> bool {
48 Path::new(path).is_dir()
49}
50
51pub 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
68pub 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
91pub 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
133pub 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
150pub fn nicezputs(s: &str) -> String {
152 s.chars().map(nicechar).collect()
153}
154
155pub fn is_word_char(c: char, wordchars: &str) -> bool {
157 c.is_alphanumeric() || wordchars.contains(c)
158}
159
160pub fn is_ifs_char(c: char, ifs: &str) -> bool {
162 ifs.contains(c)
163}
164
165pub fn tulower(c: char) -> char {
167 c.to_lowercase().next().unwrap_or(c)
168}
169
170pub fn tuupper(c: char) -> char {
172 c.to_uppercase().next().unwrap_or(c)
173}
174
175pub 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
185pub 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
201pub fn is_float(s: &str) -> bool {
203 s.parse::<f64>().is_ok()
204}
205
206pub 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
214pub fn zsleep(seconds: f64) {
216 let duration = std::time::Duration::from_secs_f64(seconds);
217 std::thread::sleep(duration);
218}
219
220pub 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); Ok(())
229 }
230 #[cfg(not(unix))]
231 {
232 let _ = (fd, data);
233 Err(io::Error::new(io::ErrorKind::Unsupported, "Not supported"))
234 }
235}
236
237pub 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
258pub fn zclose(fd: i32) {
260 #[cfg(unix)]
261 unsafe {
262 libc::close(fd);
263 }
264}
265
266pub 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
279pub 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
296pub 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#[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 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
342fn 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
375fn is_pattern(c: char) -> bool {
378 matches!(
379 c,
380 '*' | '?' | '[' | ']' | '<' | '>' | '(' | ')' | '|' | '#' | '^' | '~'
381 )
382}
383
384pub 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 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 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 let mut result = String::with_capacity(s.len() + 4);
429 result.push('\'');
430 for c in s.chars() {
431 if c == '\'' {
432 result.push_str("'\\''");
434 } else if c == '\n' {
435 result.push_str("'$'\\n''");
437 } else {
438 result.push(c);
439 }
440 }
441 result.push('\'');
442 result
443 }
444
445 QuoteType::SingleOptional => {
446 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 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 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 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 s.replace('`', "\\`")
528 }
529 }
530}
531
532pub 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
547pub 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
582pub fn sepsplit(s: &str, sep: Option<&str>, allownull: bool) -> Vec<String> {
588 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 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
619pub 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
633pub 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
644pub 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
676pub 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
696pub 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
707pub 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
734pub 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
756pub 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
782pub 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
813pub 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
836pub fn has_token(s: &str) -> bool {
838 s.bytes().any(|b| b == 0x83) }
840
841pub fn arrlen<T>(arr: &[T]) -> usize {
843 arr.len()
844}
845
846pub fn dupstrpfx(s: &str, len: usize) -> String {
848 s.chars().take(len).collect()
849}
850
851const META_CHAR: char = '\u{83}';
852
853pub 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
871pub 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
886pub 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
902pub fn ztrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
904 unmeta(s1).cmp(&unmeta(s2))
905}
906
907pub fn ztrsub(t: &str, s: &str) -> usize {
909 ztrlen(&t[..t.len().saturating_sub(s.len())])
910}
911
912pub 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
932pub 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
950pub 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
968pub fn zstricmp(s1: &str, s2: &str) -> std::cmp::Ordering {
970 s1.to_lowercase().cmp(&s2.to_lowercase())
971}
972
973pub fn zstrstr(haystack: &str, needle: &str) -> Option<usize> {
975 haystack.find(needle)
976}
977
978pub fn ztrdup(s: &str) -> String {
980 s.to_string()
981}
982
983pub fn ztrncpy(s: &str, n: usize) -> String {
985 s.chars().take(n).collect()
986}
987
988pub fn dyncat(s1: &str, s2: &str) -> String {
990 format!("{}{}", s1, s2)
991}
992
993pub fn tricat(s1: &str, s2: &str, s3: &str) -> String {
995 format!("{}{}{}", s1, s2, s3)
996}
997
998pub fn bicat(s1: &str, s2: &str) -> String {
1000 format!("{}{}", s1, s2)
1001}
1002
1003pub 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
1010pub fn invnstrcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
1012 nstrcmp(s2, s1)
1013}
1014
1015pub fn str_ends_with(s: &str, suffix: &str) -> bool {
1017 s.ends_with(suffix)
1018}
1019
1020pub fn str_starts_with(s: &str, prefix: &str) -> bool {
1022 s.starts_with(prefix)
1023}
1024
1025pub 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
1033pub 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
1041pub fn is_word_char_simple(c: char) -> bool {
1043 c.is_alphanumeric() || c == '_'
1044}
1045
1046pub 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
1060pub 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
1074pub 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
1103pub 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
1121pub fn wordcount(s: &str) -> usize {
1123 s.split_whitespace().count()
1124}
1125
1126pub fn charcount(s: &str) -> usize {
1128 s.chars().count()
1129}
1130
1131pub fn linecount(s: &str) -> usize {
1133 s.lines().count()
1134}
1135
1136pub fn zjoin(arr: &[String], delim: char) -> String {
1138 arr.join(&delim.to_string())
1139}
1140
1141pub 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
1155pub fn skipwsep(s: &str) -> &str {
1157 s.trim_start()
1158}
1159
1160pub fn iwsep(c: char) -> bool {
1162 c == ' ' || c == '\t'
1163}
1164
1165pub fn imeta(c: char) -> bool {
1167 (c as u32) < 32 || c == '\x7f' || c == '\u{83}'
1168}
1169
1170pub 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
1182pub 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
1220pub fn current_time_fmt(fmt: &str) -> String {
1222 ztrftime(fmt, std::time::SystemTime::now())
1223}
1224
1225pub 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
1246pub 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
1267pub 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
1312pub fn isprintable(s: &str) -> bool {
1314 s.chars().all(|c| !c.is_control() || c == '\n' || c == '\t')
1315}
1316
1317pub 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
1338pub 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
1359pub fn zsleep_ms(ms: u64) {
1361 std::thread::sleep(std::time::Duration::from_millis(ms));
1362}
1363
1364pub 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
1379pub fn zgetcwd() -> Option<String> {
1381 std::env::current_dir()
1382 .ok()
1383 .map(|p| p.to_string_lossy().to_string())
1384}
1385
1386pub fn zchdir(path: &str) -> bool {
1388 std::env::set_current_dir(path).is_ok()
1389}
1390
1391pub fn isabspath(path: &str) -> bool {
1393 path.starts_with('/')
1394}
1395
1396pub 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
1408pub fn realpath(path: &str) -> Option<String> {
1410 std::fs::canonicalize(path)
1411 .ok()
1412 .map(|p| p.to_string_lossy().to_string())
1413}
1414
1415pub fn file_exists(path: &str) -> bool {
1417 std::path::Path::new(path).exists()
1418}
1419
1420pub fn is_file(path: &str) -> bool {
1422 std::path::Path::new(path).is_file()
1423}
1424
1425pub fn is_dir(path: &str) -> bool {
1427 std::path::Path::new(path).is_dir()
1428}
1429
1430pub 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
1437pub fn file_size(path: &str) -> Option<u64> {
1439 std::fs::metadata(path).ok().map(|m| m.len())
1440}
1441
1442pub 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
1451pub fn read_file(path: &str) -> Option<String> {
1453 std::fs::read_to_string(path).ok()
1454}
1455
1456pub 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
1463pub fn write_file(path: &str, contents: &str) -> bool {
1465 std::fs::write(path, contents).is_ok()
1466}
1467
1468pub 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
1479pub 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
1489pub fn mkdir(path: &str) -> bool {
1491 std::fs::create_dir(path).is_ok()
1492}
1493
1494pub fn mkdir_p(path: &str) -> bool {
1496 std::fs::create_dir_all(path).is_ok()
1497}
1498
1499pub fn rm_file(path: &str) -> bool {
1501 std::fs::remove_file(path).is_ok()
1502}
1503
1504pub fn rm_dir(path: &str) -> bool {
1506 std::fs::remove_dir(path).is_ok()
1507}
1508
1509pub fn rm_dir_all(path: &str) -> bool {
1511 std::fs::remove_dir_all(path).is_ok()
1512}
1513
1514pub fn copy_file(src: &str, dst: &str) -> bool {
1516 std::fs::copy(src, dst).is_ok()
1517}
1518
1519pub fn rename_file(src: &str, dst: &str) -> bool {
1521 std::fs::rename(src, dst).is_ok()
1522}
1523
1524pub 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
1537pub 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
1544pub fn getenv(name: &str) -> Option<String> {
1546 std::env::var(name).ok()
1547}
1548
1549pub fn setenv(name: &str, value: &str) {
1551 std::env::set_var(name, value);
1552}
1553
1554pub fn unsetenv(name: &str) {
1556 std::env::remove_var(name);
1557}
1558
1559pub fn environ() -> Vec<(String, String)> {
1561 std::env::vars().collect()
1562}
1563
1564pub fn getuid() -> u32 {
1566 #[cfg(unix)]
1567 unsafe {
1568 libc::getuid()
1569 }
1570 #[cfg(not(unix))]
1571 0
1572}
1573
1574pub fn geteuid() -> u32 {
1576 #[cfg(unix)]
1577 unsafe {
1578 libc::geteuid()
1579 }
1580 #[cfg(not(unix))]
1581 0
1582}
1583
1584pub fn getgid() -> u32 {
1586 #[cfg(unix)]
1587 unsafe {
1588 libc::getgid()
1589 }
1590 #[cfg(not(unix))]
1591 0
1592}
1593
1594pub fn getegid() -> u32 {
1596 #[cfg(unix)]
1597 unsafe {
1598 libc::getegid()
1599 }
1600 #[cfg(not(unix))]
1601 0
1602}
1603
1604pub fn getpid() -> i32 {
1606 std::process::id() as i32
1607}
1608
1609pub fn getppid() -> i32 {
1611 #[cfg(unix)]
1612 unsafe {
1613 libc::getppid()
1614 }
1615 #[cfg(not(unix))]
1616 0
1617}
1618
1619pub fn is_root() -> bool {
1621 geteuid() == 0
1622}
1623
1624pub 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
1636pub 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
1649pub 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
1657pub 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
1665pub 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
1677pub 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
1697pub 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
1704pub fn random_range(max: u32) -> u32 {
1706 if max == 0 {
1707 0
1708 } else {
1709 random_int() % max
1710 }
1711}
1712
1713pub 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
1722pub fn slashsplit(s: &str) -> Vec<String> {
1728 s.split('/')
1729 .filter(|s| !s.is_empty())
1730 .map(String::from)
1731 .collect()
1732}
1733
1734pub 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
1740pub fn mkarray(s: Option<&str>) -> Vec<String> {
1742 match s {
1743 Some(val) => vec![val.to_string()],
1744 None => Vec::new(),
1745 }
1746}
1747
1748pub fn freearray(_arr: Vec<String>) {
1750 }
1752
1753pub fn strpfx(s: &str, t: &str) -> bool {
1755 t.starts_with(s)
1756}
1757
1758pub fn strsfx(s: &str, t: &str) -> bool {
1760 t.ends_with(s)
1761}
1762
1763pub fn zbeep() {
1765 eprint!("\x07");
1766}
1767
1768pub fn mode_to_octal(mode: u32) -> String {
1770 format!("{:04o}", mode & 0o7777)
1771}
1772
1773pub 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
1786pub 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
1798pub fn adjustwinsize() -> (usize, usize) {
1800 let cols = get_term_width();
1801 let rows = get_term_height();
1802 (cols, rows)
1803}
1804
1805pub 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
1820pub 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
1839pub 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
1852pub 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
1864pub 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
1872pub 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
1884pub 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
1891pub 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
1900pub 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
1911pub fn arrdup(arr: &[String]) -> Vec<String> {
1913 arr.to_vec()
1914}
1915
1916pub fn arrdup_max(arr: &[String], max: usize) -> Vec<String> {
1918 arr.iter().take(max).cloned().collect()
1919}
1920
1921pub 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
1988pub 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
2005pub 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
2030pub fn inittyptab() {
2033 }
2035
2036pub 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
2050pub 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 s.find(|c: char| c.is_ascii_whitespace())
2058 }
2059 }
2060}
2061
2062pub 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
2070pub 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
2094pub 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 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
2246pub fn ucs4toutf8(codepoint: u32) -> Option<String> {
2248 char::from_u32(codepoint).map(|c| c.to_string())
2249}
2250
2251pub 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
2269pub 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
2282pub fn is_niceformat(s: &str) -> bool {
2284 s.chars().any(|c| c.is_ascii_control())
2285}
2286
2287pub fn hasspecial(s: &str) -> bool {
2289 s.chars().any(|c| is_special(c))
2290}
2291
2292pub 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
2312pub 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#[cfg(unix)]
2328pub fn attachtty(pgrp: i32) {
2329 unsafe {
2330 libc::tcsetpgrp(0, pgrp);
2331 }
2332}
2333
2334#[cfg(unix)]
2336pub fn gettygrp() -> i32 {
2337 unsafe { libc::tcgetpgrp(0) }
2338}
2339
2340pub 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
2352pub fn zsetupterm() -> bool {
2354 is_tty(1)
2357}
2358
2359pub fn zdeleteterm() {
2361 }
2363
2364pub fn putraw(c: char) {
2366 print!("{}", c);
2367}
2368
2369pub fn putshout(c: char) {
2371 print!("{}", c);
2372}
2373
2374pub 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
2383pub fn mb_charinit() {
2385 }
2387
2388pub fn wcs_nicechar_sel(c: char, quotable: bool) -> String {
2390 nicechar_sel(c, quotable)
2391}
2392
2393pub fn wcs_nicechar(c: char) -> String {
2395 nicechar(c)
2396}
2397
2398pub fn is_wcs_nicechar(c: char) -> bool {
2400 c.is_ascii_control()
2401}
2402
2403pub fn zwcwidth(c: char) -> usize {
2405 unicode_width::UnicodeWidthChar::width(c).unwrap_or(1)
2406}
2407
2408pub 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
2417pub 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
2425pub fn substnamedir(
2427 path: &str,
2428 home: &str,
2429 named_dirs: &std::collections::HashMap<String, String>,
2430) -> String {
2431 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 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
2457pub 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
2476pub 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
2491pub 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
2500pub fn getnameddir(
2502 name: &str,
2503 named_dirs: &std::collections::HashMap<String, String>,
2504) -> Option<String> {
2505 named_dirs.get(name).cloned()
2506}
2507
2508pub 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
2515pub type PrepromptFn = Box<dyn Fn()>;
2517
2518pub 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
2558pub struct TimedFn {
2560 pub func: String,
2561 pub when: i64,
2562}
2563
2564pub fn preprompt_actions() {
2566 }
2572
2573pub fn checkmailpath(paths: &[String]) -> Vec<String> {
2575 let mut messages = Vec::new();
2576 for path in paths {
2577 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
2598pub fn printprompt4(ps4: &str) -> String {
2600 ps4.replace("%N", "").replace("%i", "").replace("%_", "")
2603}
2604
2605#[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#[cfg(unix)]
2618pub fn settyinfo(fd: i32, ti: &libc::termios) -> bool {
2619 unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, ti) == 0 }
2620}
2621
2622pub fn adjustlines() -> usize {
2624 get_term_height()
2625}
2626
2627pub fn adjustcolumns() -> usize {
2629 get_term_width()
2630}
2631
2632pub 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
2645pub 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 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
2666pub fn addmodulefd(fd: i32) {
2668 #[cfg(unix)]
2669 {
2670 unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) };
2672 }
2673 #[cfg(not(unix))]
2674 {
2675 let _ = fd;
2676 }
2677}
2678
2679pub 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
2693pub fn zcloselockfd(fd: i32) {
2695 zclose(fd);
2696}
2697
2698pub 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
2708pub 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
2717pub fn zmonotime() -> i64 {
2719 std::time::Instant::now().elapsed().as_secs() as i64
2720}
2721
2722pub 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
2728pub fn noquery(_purge: bool) -> bool {
2730 false
2731}
2732
2733pub 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
2747pub fn getshfunc(
2749 name: &str,
2750 functions: &std::collections::HashMap<String, String>,
2751) -> Option<String> {
2752 functions.get(name).cloned()
2753}
2754
2755pub fn makecommaspecial(_yes: bool) {
2757 }
2759
2760pub fn zarrdup(arr: &[String]) -> Vec<String> {
2762 arr.to_vec()
2763}
2764
2765pub 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; 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
2787pub 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
2809pub 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 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
2827pub 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
2843pub fn nicedup(s: &str) -> String {
2845 niceformat(s)
2846}
2847
2848pub fn niceztrlen(s: &str) -> usize {
2850 niceformat(s).len()
2851}
2852
2853pub 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
2865pub fn restoredir(saved: &str) -> bool {
2867 std::env::set_current_dir(saved).is_ok()
2868}
2869
2870pub fn convfloat(dval: f64, digits: i32, flags: u32) -> String {
2872 crate::params::format_float(dval, digits, flags)
2873}
2874
2875pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
2877 crate::params::convfloat_underscore(dval, underscore)
2878}
2879
2880pub 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 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
3025pub fn set_widearray(_s: &str) {}
3031
3032pub fn zwarning(cmd: &str, msg: &str) {
3034 if cmd.is_empty() {
3035 eprintln!("zsh: {}", msg);
3036 } else {
3037 eprintln!("{}: {}", cmd, msg);
3038 }
3039}
3040
3041pub fn zz_plural_z_alpha() -> &'static str {
3043 "s"
3044}
3045
3046pub fn is_nicechar(c: char) -> bool {
3048 c.is_ascii_control() || !c.is_ascii()
3049}
3050
3051pub fn freestr(_s: String) {
3053 }
3055
3056pub 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
3065pub fn strucpy(s: &str, upper: bool) -> String {
3067 if upper {
3068 s.to_uppercase()
3069 } else {
3070 s.to_string()
3071 }
3072}
3073
3074pub 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
3084pub fn arrlen_ge<T>(arr: &[T], n: usize) -> bool {
3086 arr.len() >= n
3087}
3088
3089pub fn arrlen_gt<T>(arr: &[T], n: usize) -> bool {
3091 arr.len() > n
3092}
3093
3094pub fn arrlen_lt<T>(arr: &[T], n: usize) -> bool {
3096 arr.len() < n
3097}
3098
3099pub fn setblock_stdin() {
3101 setblock_fd(0, true);
3102}
3103
3104pub fn ztrftimebuf(needed: usize) -> usize {
3106 needed.max(256)
3108}
3109
3110pub fn subst_string_by_func(_func_name: &str, _arg: &str, _orig: &str) -> Option<String> {
3112 None
3114}
3115
3116pub fn makebangspecial(_yes: bool) {
3118 }
3120
3121pub fn wcsiblank(c: char) -> bool {
3123 c == ' ' || c == '\t' || c.is_whitespace()
3124}
3125
3126pub 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
3148pub fn wcs_zarrdup(arr: &[String]) -> Vec<String> {
3150 arr.to_vec()
3151}
3152
3153#[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
3171pub fn ztrdup_metafy(s: &str) -> String {
3173 metafy(s)
3174}
3175
3176pub 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
3189pub fn ztrlenend(s: &str, end: usize) -> usize {
3191 s[..end.min(s.len())].chars().count()
3192}
3193
3194pub 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
3203pub 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
3215pub fn mb_charlenconv_r(s: &str, pos: usize) -> (usize, Option<char>) {
3217 mb_metacharlenconv_r(s, pos)
3218}
3219
3220pub fn mb_charlenconv(s: &str, pos: usize) -> usize {
3222 s[pos..].chars().next().map(|c| c.len_utf8()).unwrap_or(0)
3223}
3224
3225pub fn sb_niceformat(s: &str) -> String {
3227 niceformat(s)
3228}
3229
3230pub fn is_sb_niceformat(s: &str) -> bool {
3232 is_niceformat(s)
3233}
3234
3235pub 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
3258pub 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
3273pub 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#[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
3309pub 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
3321pub fn chuck(s: &mut String, pos: usize) {
3323 if pos < s.len() {
3324 s.remove(pos);
3325 }
3326}
3327
3328pub fn arrlen_le<T>(arr: &[T], n: usize) -> bool {
3330 arr.len() <= n
3331}
3332
3333pub 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
3349pub fn subst_string_by_hook(_hook: &str, _arg: &str, _orig: &str) -> Option<String> {
3351 None
3353}
3354
3355pub fn hmkarray(s: &str) -> Vec<String> {
3357 if s.is_empty() {
3358 Vec::new()
3359 } else {
3360 vec![s.to_string()]
3361 }
3362}
3363
3364pub fn nicedupstring(s: &str) -> String {
3366 niceformat(s)
3367}
3368
3369pub fn mailstat(path: &str) -> Option<std::fs::Metadata> {
3371 let strstrdir = format!("{}/.strstrdir/strstrstr", path);
3374 if let Ok(meta) = std::fs::metadata(&strstrdir) {
3375 return Some(meta);
3376 }
3377 std::fs::metadata(path).ok()
3379}