Skip to main content

zsh/
glob.rs

1//! Filename generation (globbing) for zshrs
2//!
3//! Direct port from zsh/Src/glob.c
4//!
5//! Supports:
6//! - Basic glob patterns (*, ?, [...])
7//! - Extended glob patterns (#, ##, ~, ^)
8//! - Recursive globbing (**/*)
9//! - Glob qualifiers (., /, @, etc.)
10//! - Brace expansion ({a,b,c}, {1..10})
11//! - Sorting and filtering matches
12
13use std::cmp::Ordering;
14use std::collections::HashSet;
15use std::fs::{self, Metadata};
16use std::os::unix::fs::{MetadataExt, PermissionsExt};
17use std::path::{Path, PathBuf};
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// Sort specifier flags
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum GlobSort {
23    Name,
24    Depth,
25    Size,
26    Atime,
27    Mtime,
28    Ctime,
29    Links,
30    None,
31    Exec(usize), // index into exec sort strings
32}
33
34/// Sort order
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SortOrder {
37    Ascending,
38    Descending,
39}
40
41/// A single sort specification
42#[derive(Debug, Clone)]
43pub struct SortSpec {
44    pub sort_type: GlobSort,
45    pub order: SortOrder,
46    pub follow_links: bool,
47}
48
49/// Time units for qualifiers
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum TimeUnit {
52    Seconds,
53    Minutes,
54    Hours,
55    Days,
56    Weeks,
57    Months,
58}
59
60/// Size units for qualifiers
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum SizeUnit {
63    Bytes,
64    PosixBlocks,
65    Kilobytes,
66    Megabytes,
67    Gigabytes,
68    Terabytes,
69}
70
71/// Range comparison
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum RangeOp {
74    Less,
75    Equal,
76    Greater,
77}
78
79/// A glob qualifier function
80#[derive(Debug, Clone)]
81pub enum Qualifier {
82    /// File type qualifiers
83    IsRegular,
84    IsDirectory,
85    IsSymlink,
86    IsSocket,
87    IsFifo,
88    IsBlockDev,
89    IsCharDev,
90    IsDevice,
91    IsExecutable,
92
93    /// Permission qualifiers
94    Readable,
95    Writable,
96    Executable,
97    WorldReadable,
98    WorldWritable,
99    WorldExecutable,
100    GroupReadable,
101    GroupWritable,
102    GroupExecutable,
103    Setuid,
104    Setgid,
105    Sticky,
106
107    /// Ownership qualifiers
108    OwnedByEuid,
109    OwnedByEgid,
110    OwnedByUid(u32),
111    OwnedByGid(u32),
112
113    /// Numeric qualifiers with range
114    Size {
115        value: u64,
116        unit: SizeUnit,
117        op: RangeOp,
118    },
119    Links {
120        value: u64,
121        op: RangeOp,
122    },
123    Atime {
124        value: i64,
125        unit: TimeUnit,
126        op: RangeOp,
127    },
128    Mtime {
129        value: i64,
130        unit: TimeUnit,
131        op: RangeOp,
132    },
133    Ctime {
134        value: i64,
135        unit: TimeUnit,
136        op: RangeOp,
137    },
138
139    /// Mode specification
140    Mode {
141        yes: u32,
142        no: u32,
143    },
144
145    /// Device number
146    Device(u64),
147
148    /// Non-empty directory
149    NonEmptyDir,
150
151    /// Shell evaluation
152    Eval(String),
153}
154
155/// A glob match with metadata for sorting
156#[derive(Debug, Clone)]
157pub struct GlobMatch {
158    pub name: String,
159    pub path: PathBuf,
160    pub size: u64,
161    pub atime: i64,
162    pub mtime: i64,
163    pub ctime: i64,
164    pub links: u64,
165    pub mode: u32,
166    pub uid: u32,
167    pub gid: u32,
168    pub dev: u64,
169    pub ino: u64,
170    // For symlink targets (when following)
171    pub target_size: u64,
172    pub target_atime: i64,
173    pub target_mtime: i64,
174    pub target_ctime: i64,
175    pub target_links: u64,
176    // For exec sort strings
177    pub sort_strings: Vec<String>,
178}
179
180impl GlobMatch {
181    pub fn from_path(path: &Path) -> Option<Self> {
182        let meta = fs::symlink_metadata(path).ok()?;
183        let name = path.file_name()?.to_string_lossy().to_string();
184
185        let (target_size, target_atime, target_mtime, target_ctime, target_links) =
186            if meta.file_type().is_symlink() {
187                if let Ok(target_meta) = fs::metadata(path) {
188                    (
189                        target_meta.size(),
190                        target_meta.atime(),
191                        target_meta.mtime(),
192                        target_meta.ctime(),
193                        target_meta.nlink(),
194                    )
195                } else {
196                    (
197                        meta.size(),
198                        meta.atime(),
199                        meta.mtime(),
200                        meta.ctime(),
201                        meta.nlink(),
202                    )
203                }
204            } else {
205                (
206                    meta.size(),
207                    meta.atime(),
208                    meta.mtime(),
209                    meta.ctime(),
210                    meta.nlink(),
211                )
212            };
213
214        Some(GlobMatch {
215            name,
216            path: path.to_path_buf(),
217            size: meta.size(),
218            atime: meta.atime(),
219            mtime: meta.mtime(),
220            ctime: meta.ctime(),
221            links: meta.nlink(),
222            mode: meta.mode(),
223            uid: meta.uid(),
224            gid: meta.gid(),
225            dev: meta.dev(),
226            ino: meta.ino(),
227            target_size,
228            target_atime,
229            target_mtime,
230            target_ctime,
231            target_links,
232            sort_strings: Vec::new(),
233        })
234    }
235
236    pub fn compare(&self, other: &Self, specs: &[SortSpec], numeric_sort: bool) -> Ordering {
237        for spec in specs {
238            let cmp = match spec.sort_type {
239                GlobSort::Name => {
240                    if numeric_sort {
241                        numeric_string_cmp(&self.name, &other.name)
242                    } else {
243                        self.name.cmp(&other.name)
244                    }
245                }
246                GlobSort::Depth => {
247                    let self_depth = self.path.components().count();
248                    let other_depth = other.path.components().count();
249                    self_depth.cmp(&other_depth)
250                }
251                GlobSort::Size => {
252                    if spec.follow_links {
253                        self.target_size.cmp(&other.target_size)
254                    } else {
255                        self.size.cmp(&other.size)
256                    }
257                }
258                GlobSort::Atime => {
259                    if spec.follow_links {
260                        other.target_atime.cmp(&self.target_atime)
261                    } else {
262                        other.atime.cmp(&self.atime)
263                    }
264                }
265                GlobSort::Mtime => {
266                    if spec.follow_links {
267                        other.target_mtime.cmp(&self.target_mtime)
268                    } else {
269                        other.mtime.cmp(&self.mtime)
270                    }
271                }
272                GlobSort::Ctime => {
273                    if spec.follow_links {
274                        other.target_ctime.cmp(&self.target_ctime)
275                    } else {
276                        other.ctime.cmp(&self.ctime)
277                    }
278                }
279                GlobSort::Links => {
280                    if spec.follow_links {
281                        other.target_links.cmp(&self.target_links)
282                    } else {
283                        other.links.cmp(&self.links)
284                    }
285                }
286                GlobSort::None => Ordering::Equal,
287                GlobSort::Exec(idx) => {
288                    let a = self.sort_strings.get(idx).map(|s| s.as_str()).unwrap_or("");
289                    let b = other
290                        .sort_strings
291                        .get(idx)
292                        .map(|s| s.as_str())
293                        .unwrap_or("");
294                    if numeric_sort {
295                        numeric_string_cmp(a, b)
296                    } else {
297                        a.cmp(b)
298                    }
299                }
300            };
301
302            if cmp != Ordering::Equal {
303                return match spec.order {
304                    SortOrder::Ascending => cmp,
305                    SortOrder::Descending => cmp.reverse(),
306                };
307            }
308        }
309        Ordering::Equal
310    }
311}
312
313/// Numeric string comparison (for numeric glob sort)
314fn numeric_string_cmp(a: &str, b: &str) -> Ordering {
315    let mut ai = a.chars().peekable();
316    let mut bi = b.chars().peekable();
317
318    loop {
319        match (ai.peek(), bi.peek()) {
320            (None, None) => return Ordering::Equal,
321            (None, Some(_)) => return Ordering::Less,
322            (Some(_), None) => return Ordering::Greater,
323            (Some(&ac), Some(&bc)) => {
324                if ac.is_ascii_digit() && bc.is_ascii_digit() {
325                    // Compare numeric segments
326                    let mut an = String::new();
327                    let mut bn = String::new();
328                    while let Some(&c) = ai.peek() {
329                        if c.is_ascii_digit() {
330                            an.push(c);
331                            ai.next();
332                        } else {
333                            break;
334                        }
335                    }
336                    while let Some(&c) = bi.peek() {
337                        if c.is_ascii_digit() {
338                            bn.push(c);
339                            bi.next();
340                        } else {
341                            break;
342                        }
343                    }
344                    let av: u64 = an.parse().unwrap_or(0);
345                    let bv: u64 = bn.parse().unwrap_or(0);
346                    match av.cmp(&bv) {
347                        Ordering::Equal => continue,
348                        other => return other,
349                    }
350                } else {
351                    match ac.cmp(&bc) {
352                        Ordering::Equal => {
353                            ai.next();
354                            bi.next();
355                        }
356                        other => return other,
357                    }
358                }
359            }
360        }
361    }
362}
363
364/// Glob options
365#[derive(Debug, Clone, Default)]
366pub struct GlobOptions {
367    pub null_glob: bool,
368    pub mark_dirs: bool,
369    pub no_glob_dots: bool,
370    pub list_types: bool,
371    pub numeric_sort: bool,
372    pub follow_links: bool,
373    pub extended_glob: bool,
374    pub case_glob: bool,
375    pub glob_star_short: bool,
376    pub bare_glob_qual: bool,
377    pub brace_ccl: bool,
378}
379
380/// Parsed glob qualifier set
381#[derive(Debug, Clone, Default)]
382pub struct QualifierSet {
383    pub qualifiers: Vec<Qualifier>,
384    pub alternatives: Vec<Vec<Qualifier>>,
385    pub negated: bool,
386    pub follow_links: bool,
387    pub sorts: Vec<SortSpec>,
388    pub first: Option<i32>,
389    pub last: Option<i32>,
390    pub colon_mods: Option<String>,
391    pub pre_words: Vec<String>,
392    pub post_words: Vec<String>,
393}
394
395/// Main glob state
396pub struct GlobState {
397    pub options: GlobOptions,
398    pub matches: Vec<GlobMatch>,
399    pub qualifiers: Option<QualifierSet>,
400    pathbuf: String,
401    pathpos: usize,
402}
403
404impl GlobState {
405    pub fn new(options: GlobOptions) -> Self {
406        GlobState {
407            options,
408            matches: Vec::new(),
409            qualifiers: None,
410            pathbuf: String::with_capacity(4096),
411            pathpos: 0,
412        }
413    }
414
415    /// Main entry point: expand a glob pattern
416    pub fn glob(&mut self, pattern: &str) -> Vec<String> {
417        self.matches.clear();
418        self.pathbuf.clear();
419        self.pathpos = 0;
420
421        // Check if globbing is enabled and pattern has wildcards
422        if !has_wildcards(pattern) {
423            return vec![pattern.to_string()];
424        }
425
426        // Parse qualifiers if present
427        let (pat, quals) = self.parse_qualifiers(pattern);
428        self.qualifiers = quals;
429
430        // Parse the pattern into components
431        if let Some(complist) = self.parse_pattern(&pat) {
432            // Handle absolute vs relative paths
433            if pat.starts_with('/') {
434                self.pathbuf.push('/');
435                self.pathpos = 1;
436            }
437
438            // Do the actual globbing
439            self.scanner(&complist, 0);
440        }
441
442        // Sort results
443        self.sort_matches();
444
445        // Apply subscript selection
446        self.apply_selection();
447
448        // Extract filenames
449        let mut results: Vec<String> = self
450            .matches
451            .iter()
452            .map(|m| {
453                let mut s = m.path.to_string_lossy().to_string();
454                if self.options.mark_dirs || self.options.list_types {
455                    if let Ok(meta) = fs::symlink_metadata(&m.path) {
456                        let ch = file_type_char(meta.mode());
457                        if self.options.list_types || (self.options.mark_dirs && ch == '/') {
458                            s.push(ch);
459                        }
460                    }
461                }
462                s
463            })
464            .collect();
465
466        // Handle no matches
467        if results.is_empty() && !self.options.null_glob {
468            results.push(pattern.to_string());
469        }
470
471        results
472    }
473
474    fn parse_qualifiers(&self, pattern: &str) -> (String, Option<QualifierSet>) {
475        if !pattern.ends_with(')') {
476            return (pattern.to_string(), None);
477        }
478
479        // Find matching open paren
480        let bytes = pattern.as_bytes();
481        let mut depth = 0;
482        let mut qual_start = None;
483
484        for i in (0..bytes.len()).rev() {
485            match bytes[i] {
486                b')' => depth += 1,
487                b'(' => {
488                    depth -= 1;
489                    if depth == 0 {
490                        qual_start = Some(i);
491                        break;
492                    }
493                }
494                _ => {}
495            }
496        }
497
498        let start = match qual_start {
499            Some(s) => s,
500            None => return (pattern.to_string(), None),
501        };
502
503        // Check for (#q...) explicit qualifier syntax
504        let qual_str = &pattern[start + 1..pattern.len() - 1];
505        let (is_explicit, qual_content) = if qual_str.starts_with("#q") {
506            (true, &qual_str[2..])
507        } else if self.options.bare_glob_qual {
508            (false, qual_str)
509        } else {
510            return (pattern.to_string(), None);
511        };
512
513        // Don't parse as qualifiers if it contains | or ~ (alternatives/exclusions)
514        if !is_explicit && (qual_content.contains('|') || qual_content.contains('~')) {
515            return (pattern.to_string(), None);
516        }
517
518        // Parse the qualifiers
519        let qs = self.parse_qualifier_string(qual_content);
520        (pattern[..start].to_string(), Some(qs))
521    }
522
523    fn parse_qualifier_string(&self, s: &str) -> QualifierSet {
524        let mut qs = QualifierSet::default();
525        let mut chars = s.chars().peekable();
526        let mut negated = false;
527        let mut follow = false;
528
529        while let Some(c) = chars.next() {
530            match c {
531                '^' => negated = !negated,
532                '-' => follow = !follow,
533                ',' => {
534                    // Start new alternative
535                    if !qs.qualifiers.is_empty() {
536                        qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
537                    }
538                    negated = false;
539                    follow = false;
540                }
541                ':' => {
542                    // Colon modifiers - rest of string
543                    let rest: String = chars.collect();
544                    qs.colon_mods = Some(format!(":{}", rest));
545                    break;
546                }
547                // File type qualifiers
548                '/' => qs.qualifiers.push(Qualifier::IsDirectory),
549                '.' => qs.qualifiers.push(Qualifier::IsRegular),
550                '@' => qs.qualifiers.push(Qualifier::IsSymlink),
551                '=' => qs.qualifiers.push(Qualifier::IsSocket),
552                'p' => qs.qualifiers.push(Qualifier::IsFifo),
553                '%' => match chars.peek() {
554                    Some('b') => {
555                        chars.next();
556                        qs.qualifiers.push(Qualifier::IsBlockDev);
557                    }
558                    Some('c') => {
559                        chars.next();
560                        qs.qualifiers.push(Qualifier::IsCharDev);
561                    }
562                    _ => qs.qualifiers.push(Qualifier::IsDevice),
563                },
564                '*' => qs.qualifiers.push(Qualifier::IsExecutable),
565                // Permission qualifiers
566                'r' => qs.qualifiers.push(Qualifier::Readable),
567                'w' => qs.qualifiers.push(Qualifier::Writable),
568                'x' => qs.qualifiers.push(Qualifier::Executable),
569                'R' => qs.qualifiers.push(Qualifier::WorldReadable),
570                'W' => qs.qualifiers.push(Qualifier::WorldWritable),
571                'X' => qs.qualifiers.push(Qualifier::WorldExecutable),
572                'A' => qs.qualifiers.push(Qualifier::GroupReadable),
573                'I' => qs.qualifiers.push(Qualifier::GroupWritable),
574                'E' => qs.qualifiers.push(Qualifier::GroupExecutable),
575                's' => qs.qualifiers.push(Qualifier::Setuid),
576                'S' => qs.qualifiers.push(Qualifier::Setgid),
577                't' => qs.qualifiers.push(Qualifier::Sticky),
578                // Ownership
579                'U' => qs.qualifiers.push(Qualifier::OwnedByEuid),
580                'G' => qs.qualifiers.push(Qualifier::OwnedByEgid),
581                'u' => {
582                    let uid = self.parse_uid_gid(&mut chars);
583                    qs.qualifiers.push(Qualifier::OwnedByUid(uid));
584                }
585                'g' => {
586                    let gid = self.parse_uid_gid(&mut chars);
587                    qs.qualifiers.push(Qualifier::OwnedByGid(gid));
588                }
589                // Size
590                'L' => {
591                    let (unit, op, val) = self.parse_size_spec(&mut chars);
592                    qs.qualifiers.push(Qualifier::Size {
593                        value: val,
594                        unit,
595                        op,
596                    });
597                }
598                // Link count
599                'l' => {
600                    let (op, val) = self.parse_range_spec(&mut chars);
601                    qs.qualifiers.push(Qualifier::Links { value: val, op });
602                }
603                // Times
604                'a' => {
605                    let (unit, op, val) = self.parse_time_spec(&mut chars);
606                    qs.qualifiers.push(Qualifier::Atime {
607                        value: val as i64,
608                        unit,
609                        op,
610                    });
611                }
612                'm' => {
613                    let (unit, op, val) = self.parse_time_spec(&mut chars);
614                    qs.qualifiers.push(Qualifier::Mtime {
615                        value: val as i64,
616                        unit,
617                        op,
618                    });
619                }
620                'c' => {
621                    let (unit, op, val) = self.parse_time_spec(&mut chars);
622                    qs.qualifiers.push(Qualifier::Ctime {
623                        value: val as i64,
624                        unit,
625                        op,
626                    });
627                }
628                // Sort
629                'o' | 'O' => {
630                    let desc = c == 'O';
631                    if let Some(&sc) = chars.peek() {
632                        let sort_type = match sc {
633                            'n' => {
634                                chars.next();
635                                GlobSort::Name
636                            }
637                            'L' => {
638                                chars.next();
639                                GlobSort::Size
640                            }
641                            'l' => {
642                                chars.next();
643                                GlobSort::Links
644                            }
645                            'a' => {
646                                chars.next();
647                                GlobSort::Atime
648                            }
649                            'm' => {
650                                chars.next();
651                                GlobSort::Mtime
652                            }
653                            'c' => {
654                                chars.next();
655                                GlobSort::Ctime
656                            }
657                            'd' => {
658                                chars.next();
659                                GlobSort::Depth
660                            }
661                            'N' => {
662                                chars.next();
663                                GlobSort::None
664                            }
665                            _ => GlobSort::Name,
666                        };
667                        qs.sorts.push(SortSpec {
668                            sort_type,
669                            order: if desc {
670                                SortOrder::Descending
671                            } else {
672                                SortOrder::Ascending
673                            },
674                            follow_links: follow,
675                        });
676                    }
677                }
678                // Flags
679                'N' => { /* nullglob handled elsewhere */ }
680                'D' => { /* dotglob handled elsewhere */ }
681                'n' => { /* numsort handled elsewhere */ }
682                'M' => { /* markdirs handled elsewhere */ }
683                'T' => { /* listtypes handled elsewhere */ }
684                'F' => qs.qualifiers.push(Qualifier::NonEmptyDir),
685                // Subscript
686                '[' => {
687                    let (first, last) = self.parse_subscript(&mut chars);
688                    qs.first = first;
689                    qs.last = last;
690                }
691                _ => {}
692            }
693        }
694
695        if !qs.qualifiers.is_empty() {
696            qs.alternatives.push(std::mem::take(&mut qs.qualifiers));
697        }
698
699        qs.negated = negated;
700        qs.follow_links = follow;
701        qs
702    }
703
704    fn parse_uid_gid(&self, chars: &mut std::iter::Peekable<std::str::Chars>) -> u32 {
705        // Check for numeric or delimited string
706        if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
707            let mut num = String::new();
708            while let Some(&c) = chars.peek() {
709                if c.is_ascii_digit() {
710                    num.push(c);
711                    chars.next();
712                } else {
713                    break;
714                }
715            }
716            num.parse().unwrap_or(0)
717        } else {
718            // Delimited name - skip for now
719            0
720        }
721    }
722
723    fn parse_size_spec(
724        &self,
725        chars: &mut std::iter::Peekable<std::str::Chars>,
726    ) -> (SizeUnit, RangeOp, u64) {
727        let unit = match chars.peek() {
728            Some('p') | Some('P') => {
729                chars.next();
730                SizeUnit::PosixBlocks
731            }
732            Some('k') | Some('K') => {
733                chars.next();
734                SizeUnit::Kilobytes
735            }
736            Some('m') | Some('M') => {
737                chars.next();
738                SizeUnit::Megabytes
739            }
740            Some('g') | Some('G') => {
741                chars.next();
742                SizeUnit::Gigabytes
743            }
744            Some('t') | Some('T') => {
745                chars.next();
746                SizeUnit::Terabytes
747            }
748            _ => SizeUnit::Bytes,
749        };
750        let (op, val) = self.parse_range_spec(chars);
751        (unit, op, val)
752    }
753
754    fn parse_time_spec(
755        &self,
756        chars: &mut std::iter::Peekable<std::str::Chars>,
757    ) -> (TimeUnit, RangeOp, u64) {
758        let unit = match chars.peek() {
759            Some('s') => {
760                chars.next();
761                TimeUnit::Seconds
762            }
763            Some('m') => {
764                chars.next();
765                TimeUnit::Minutes
766            }
767            Some('h') => {
768                chars.next();
769                TimeUnit::Hours
770            }
771            Some('d') => {
772                chars.next();
773                TimeUnit::Days
774            }
775            Some('w') => {
776                chars.next();
777                TimeUnit::Weeks
778            }
779            Some('M') => {
780                chars.next();
781                TimeUnit::Months
782            }
783            _ => TimeUnit::Days,
784        };
785        let (op, val) = self.parse_range_spec(chars);
786        (unit, op, val)
787    }
788
789    fn parse_range_spec(&self, chars: &mut std::iter::Peekable<std::str::Chars>) -> (RangeOp, u64) {
790        let op = match chars.peek() {
791            Some('+') => {
792                chars.next();
793                RangeOp::Greater
794            }
795            Some('-') => {
796                chars.next();
797                RangeOp::Less
798            }
799            _ => RangeOp::Equal,
800        };
801        let mut num = String::new();
802        while let Some(&c) = chars.peek() {
803            if c.is_ascii_digit() {
804                num.push(c);
805                chars.next();
806            } else {
807                break;
808            }
809        }
810        let val = num.parse().unwrap_or(0);
811        (op, val)
812    }
813
814    fn parse_subscript(
815        &self,
816        chars: &mut std::iter::Peekable<std::str::Chars>,
817    ) -> (Option<i32>, Option<i32>) {
818        let mut first_str = String::new();
819        let mut last_str = String::new();
820        let mut in_last = false;
821
822        while let Some(&c) = chars.peek() {
823            chars.next();
824            if c == ']' {
825                break;
826            } else if c == ',' {
827                in_last = true;
828            } else if in_last {
829                last_str.push(c);
830            } else {
831                first_str.push(c);
832            }
833        }
834
835        let first = first_str.parse().ok();
836        let last = if in_last {
837            last_str.parse().ok()
838        } else {
839            first
840        };
841        (first, last)
842    }
843
844    fn parse_pattern(&self, pattern: &str) -> Option<Vec<PatternComponent>> {
845        let mut components = Vec::new();
846        let mut current = String::new();
847        let mut chars = pattern.chars().peekable();
848        let mut in_bracket = false;
849
850        // Skip leading slash for absolute paths
851        if chars.peek() == Some(&'/') {
852            chars.next();
853        }
854
855        while let Some(c) = chars.next() {
856            match c {
857                '/' if !in_bracket => {
858                    if !current.is_empty() {
859                        components.push(PatternComponent::Pattern(current.clone()));
860                        current.clear();
861                    }
862                }
863                '[' => {
864                    in_bracket = true;
865                    current.push(c);
866                }
867                ']' => {
868                    in_bracket = false;
869                    current.push(c);
870                }
871                '*' if !in_bracket && chars.peek() == Some(&'*') => {
872                    chars.next();
873                    // Check for ***
874                    let follow = chars.peek() == Some(&'*');
875                    if follow {
876                        chars.next();
877                    }
878                    // Skip trailing /
879                    if chars.peek() == Some(&'/') {
880                        chars.next();
881                    }
882                    if !current.is_empty() {
883                        components.push(PatternComponent::Pattern(current.clone()));
884                        current.clear();
885                    }
886                    components.push(PatternComponent::Recursive {
887                        follow_links: follow,
888                    });
889                }
890                _ => current.push(c),
891            }
892        }
893
894        if !current.is_empty() {
895            components.push(PatternComponent::Pattern(current));
896        }
897
898        if components.is_empty() {
899            None
900        } else {
901            Some(components)
902        }
903    }
904
905    fn scanner(&mut self, components: &[PatternComponent], depth: usize) {
906        if components.is_empty() {
907            return;
908        }
909
910        let base_path = if self.pathbuf.is_empty() {
911            ".".to_string()
912        } else {
913            self.pathbuf.clone()
914        };
915
916        match &components[0] {
917            PatternComponent::Pattern(pat) => {
918                self.scan_pattern(&base_path, pat, &components[1..], depth);
919            }
920            PatternComponent::Recursive { follow_links } => {
921                // Match zero directories first
922                self.scanner(&components[1..], depth);
923                // Then recurse into subdirectories
924                self.scan_recursive(&base_path, &components[1..], *follow_links, depth);
925            }
926        }
927    }
928
929    fn scan_pattern(&mut self, base: &str, pattern: &str, rest: &[PatternComponent], depth: usize) {
930        let dir = match fs::read_dir(base) {
931            Ok(d) => d,
932            Err(_) => return,
933        };
934
935        for entry in dir.flatten() {
936            let name = entry.file_name().to_string_lossy().to_string();
937
938            // Skip hidden files unless pattern starts with .
939            if self.options.no_glob_dots && name.starts_with('.') && !pattern.starts_with('.') {
940                continue;
941            }
942
943            if pattern_match(
944                pattern,
945                &name,
946                self.options.extended_glob,
947                self.options.case_glob,
948            ) {
949                let path = entry.path();
950
951                if rest.is_empty() {
952                    // Final component - add to matches if qualifiers pass
953                    if self.check_qualifiers(&path) {
954                        if let Some(m) = GlobMatch::from_path(&path) {
955                            self.matches.push(m);
956                        }
957                    }
958                } else {
959                    // More components to match - must be a directory
960                    if path.is_dir() {
961                        let old_pos = self.pathbuf.len();
962                        if !self.pathbuf.is_empty() && !self.pathbuf.ends_with('/') {
963                            self.pathbuf.push('/');
964                        }
965                        self.pathbuf.push_str(&name);
966                        self.scanner(rest, depth + 1);
967                        self.pathbuf.truncate(old_pos);
968                    }
969                }
970            }
971        }
972    }
973
974    fn scan_recursive(
975        &mut self,
976        base: &str,
977        rest: &[PatternComponent],
978        follow_links: bool,
979        depth: usize,
980    ) {
981        let dir = match fs::read_dir(base) {
982            Ok(d) => d,
983            Err(_) => return,
984        };
985
986        for entry in dir.flatten() {
987            let name = entry.file_name().to_string_lossy().to_string();
988
989            // Skip hidden files
990            if self.options.no_glob_dots && name.starts_with('.') {
991                continue;
992            }
993
994            let path = entry.path();
995            let is_dir = if follow_links {
996                path.is_dir()
997            } else {
998                entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
999            };
1000
1001            if is_dir {
1002                let old_pos = self.pathbuf.len();
1003                if !self.pathbuf.is_empty() && !self.pathbuf.ends_with('/') {
1004                    self.pathbuf.push('/');
1005                }
1006                self.pathbuf.push_str(&name);
1007
1008                // Try matching rest from this directory
1009                self.scanner(rest, depth + 1);
1010
1011                // Continue recursing
1012                self.scan_recursive(&self.pathbuf.clone(), rest, follow_links, depth + 1);
1013
1014                self.pathbuf.truncate(old_pos);
1015            }
1016        }
1017    }
1018
1019    fn check_qualifiers(&self, path: &Path) -> bool {
1020        let qs = match &self.qualifiers {
1021            Some(q) => q,
1022            None => return true,
1023        };
1024
1025        if qs.alternatives.is_empty() {
1026            return true;
1027        }
1028
1029        let meta = match if qs.follow_links {
1030            fs::metadata(path)
1031        } else {
1032            fs::symlink_metadata(path)
1033        } {
1034            Ok(m) => m,
1035            Err(_) => return false,
1036        };
1037
1038        // Check each alternative (OR)
1039        for alt in &qs.alternatives {
1040            if self.check_qualifier_list(alt, path, &meta) {
1041                return !qs.negated;
1042            }
1043        }
1044
1045        qs.negated
1046    }
1047
1048    fn check_qualifier_list(&self, quals: &[Qualifier], path: &Path, meta: &Metadata) -> bool {
1049        for q in quals {
1050            if !self.check_single_qualifier(q, path, meta) {
1051                return false;
1052            }
1053        }
1054        true
1055    }
1056
1057    fn check_single_qualifier(&self, qual: &Qualifier, path: &Path, meta: &Metadata) -> bool {
1058        let mode = meta.mode();
1059        let ft = meta.file_type();
1060
1061        match qual {
1062            Qualifier::IsRegular => ft.is_file(),
1063            Qualifier::IsDirectory => ft.is_dir(),
1064            Qualifier::IsSymlink => ft.is_symlink(),
1065            Qualifier::IsSocket => mode & libc::S_IFMT as u32 == libc::S_IFSOCK as u32,
1066            Qualifier::IsFifo => mode & libc::S_IFMT as u32 == libc::S_IFIFO as u32,
1067            Qualifier::IsBlockDev => mode & libc::S_IFMT as u32 == libc::S_IFBLK as u32,
1068            Qualifier::IsCharDev => mode & libc::S_IFMT as u32 == libc::S_IFCHR as u32,
1069            Qualifier::IsDevice => {
1070                let fmt = mode & libc::S_IFMT as u32;
1071                fmt == libc::S_IFBLK as u32 || fmt == libc::S_IFCHR as u32
1072            }
1073            Qualifier::IsExecutable => ft.is_file() && (mode & 0o111 != 0),
1074            Qualifier::Readable => mode & 0o400 != 0,
1075            Qualifier::Writable => mode & 0o200 != 0,
1076            Qualifier::Executable => mode & 0o100 != 0,
1077            Qualifier::WorldReadable => mode & 0o004 != 0,
1078            Qualifier::WorldWritable => mode & 0o002 != 0,
1079            Qualifier::WorldExecutable => mode & 0o001 != 0,
1080            Qualifier::GroupReadable => mode & 0o040 != 0,
1081            Qualifier::GroupWritable => mode & 0o020 != 0,
1082            Qualifier::GroupExecutable => mode & 0o010 != 0,
1083            Qualifier::Setuid => mode & libc::S_ISUID as u32 != 0,
1084            Qualifier::Setgid => mode & libc::S_ISGID as u32 != 0,
1085            Qualifier::Sticky => mode & libc::S_ISVTX as u32 != 0,
1086            Qualifier::OwnedByEuid => meta.uid() == unsafe { libc::geteuid() },
1087            Qualifier::OwnedByEgid => meta.gid() == unsafe { libc::getegid() },
1088            Qualifier::OwnedByUid(uid) => meta.uid() == *uid,
1089            Qualifier::OwnedByGid(gid) => meta.gid() == *gid,
1090            Qualifier::Size { value, unit, op } => {
1091                let size = meta.size();
1092                let scaled = scale_size(size, *unit);
1093                compare_range(scaled, *value, *op)
1094            }
1095            Qualifier::Links { value, op } => compare_range(meta.nlink(), *value, *op),
1096            Qualifier::Atime { value, unit, op } => {
1097                let now = SystemTime::now()
1098                    .duration_since(UNIX_EPOCH)
1099                    .unwrap()
1100                    .as_secs() as i64;
1101                let diff = now - meta.atime();
1102                let scaled = scale_time(diff, *unit);
1103                compare_range(scaled as u64, *value as u64, *op)
1104            }
1105            Qualifier::Mtime { value, unit, op } => {
1106                let now = SystemTime::now()
1107                    .duration_since(UNIX_EPOCH)
1108                    .unwrap()
1109                    .as_secs() as i64;
1110                let diff = now - meta.mtime();
1111                let scaled = scale_time(diff, *unit);
1112                compare_range(scaled as u64, *value as u64, *op)
1113            }
1114            Qualifier::Ctime { value, unit, op } => {
1115                let now = SystemTime::now()
1116                    .duration_since(UNIX_EPOCH)
1117                    .unwrap()
1118                    .as_secs() as i64;
1119                let diff = now - meta.ctime();
1120                let scaled = scale_time(diff, *unit);
1121                compare_range(scaled as u64, *value as u64, *op)
1122            }
1123            Qualifier::Mode { yes, no } => {
1124                let m = mode & 0o7777;
1125                (m & yes) == *yes && (m & no) == 0
1126            }
1127            Qualifier::Device(dev) => meta.dev() == *dev,
1128            Qualifier::NonEmptyDir => {
1129                if !ft.is_dir() {
1130                    return false;
1131                }
1132                if let Ok(mut entries) = fs::read_dir(path) {
1133                    entries.any(|e| {
1134                        e.ok()
1135                            .map(|e| {
1136                                let name = e.file_name();
1137                                name != "." && name != ".."
1138                            })
1139                            .unwrap_or(false)
1140                    })
1141                } else {
1142                    false
1143                }
1144            }
1145            Qualifier::Eval(_) => true, // Would need shell integration
1146        }
1147    }
1148
1149    fn sort_matches(&mut self) {
1150        let specs = self
1151            .qualifiers
1152            .as_ref()
1153            .map(|q| q.sorts.clone())
1154            .unwrap_or_else(|| {
1155                vec![SortSpec {
1156                    sort_type: GlobSort::Name,
1157                    order: SortOrder::Ascending,
1158                    follow_links: false,
1159                }]
1160            });
1161
1162        if specs.iter().any(|s| s.sort_type == GlobSort::None) {
1163            return;
1164        }
1165
1166        let numeric = self.options.numeric_sort;
1167        self.matches.sort_by(|a, b| a.compare(b, &specs, numeric));
1168    }
1169
1170    fn apply_selection(&mut self) {
1171        let (first, last) = match &self.qualifiers {
1172            Some(q) => (q.first, q.last),
1173            None => return,
1174        };
1175
1176        let len = self.matches.len() as i32;
1177        if len == 0 {
1178            return;
1179        }
1180
1181        let start = match first {
1182            Some(f) if f < 0 => (len + f).max(0) as usize,
1183            Some(f) => (f - 1).max(0) as usize,
1184            None => 0,
1185        };
1186
1187        let end = match last {
1188            Some(l) if l < 0 => (len + l + 1).max(0) as usize,
1189            Some(l) => l.min(len) as usize,
1190            None => len as usize,
1191        };
1192
1193        if start < end && start < self.matches.len() {
1194            self.matches = self.matches[start..end.min(self.matches.len())].to_vec();
1195        } else {
1196            self.matches.clear();
1197        }
1198    }
1199}
1200
1201/// Pattern component
1202#[derive(Debug, Clone)]
1203enum PatternComponent {
1204    Pattern(String),
1205    Recursive { follow_links: bool },
1206}
1207
1208/// Check if string has glob wildcards
1209pub fn has_wildcards(s: &str) -> bool {
1210    let mut in_bracket = false;
1211    let mut escape = false;
1212
1213    for c in s.chars() {
1214        if escape {
1215            escape = false;
1216            continue;
1217        }
1218        match c {
1219            '\\' => escape = true,
1220            '[' => {
1221                in_bracket = true;
1222                return true; // brackets themselves are wildcards
1223            }
1224            ']' => in_bracket = false,
1225            '*' | '?' if !in_bracket => return true,
1226            '#' | '^' | '~' if !in_bracket => return true,
1227            _ => {}
1228        }
1229    }
1230    false
1231}
1232
1233/// Simple glob pattern matching
1234pub fn pattern_match(pattern: &str, text: &str, extended: bool, case_sensitive: bool) -> bool {
1235    let pat = if case_sensitive {
1236        pattern.to_string()
1237    } else {
1238        pattern.to_lowercase()
1239    };
1240    let txt = if case_sensitive {
1241        text.to_string()
1242    } else {
1243        text.to_lowercase()
1244    };
1245
1246    glob_match_impl(&pat, &txt, extended)
1247}
1248
1249fn glob_match_impl(pattern: &str, text: &str, extended: bool) -> bool {
1250    let mut pi = pattern.chars().peekable();
1251    let mut ti = text.chars().peekable();
1252
1253    while let Some(pc) = pi.next() {
1254        match pc {
1255            '*' => {
1256                // ** is handled at higher level
1257                if pi.peek().is_none() {
1258                    return true; // * at end matches everything
1259                }
1260                // Try matching rest of pattern from each position
1261                let rest: String = pi.collect();
1262                let mut pos = 0;
1263                for (i, _) in text
1264                    .char_indices()
1265                    .skip(ti.clone().count().saturating_sub(text.len()))
1266                {
1267                    if i >= pos {
1268                        if glob_match_impl(&rest, &text[i..], extended) {
1269                            return true;
1270                        }
1271                        pos = i + 1;
1272                    }
1273                }
1274                // Also try matching at end
1275                return glob_match_impl(&rest, "", extended);
1276            }
1277            '?' => {
1278                if ti.next().is_none() {
1279                    return false;
1280                }
1281            }
1282            '[' => {
1283                let tc = match ti.next() {
1284                    Some(c) => c,
1285                    None => return false,
1286                };
1287                if !match_bracket_expr(&mut pi, tc) {
1288                    return false;
1289                }
1290            }
1291            '#' if extended => {
1292                // Zero or more of previous - simplified
1293                continue;
1294            }
1295            '^' if extended => {
1296                // Negation - simplified
1297                continue;
1298            }
1299            '~' if extended => {
1300                // Exclusion - simplified
1301                continue;
1302            }
1303            '\\' => {
1304                let escaped = pi.next();
1305                let tc = ti.next();
1306                if escaped != tc {
1307                    return false;
1308                }
1309            }
1310            _ => {
1311                if ti.next() != Some(pc) {
1312                    return false;
1313                }
1314            }
1315        }
1316    }
1317
1318    ti.peek().is_none()
1319}
1320
1321fn match_bracket_expr(pi: &mut std::iter::Peekable<std::str::Chars>, tc: char) -> bool {
1322    let mut chars_in_class = Vec::new();
1323    let mut negate = false;
1324    let mut first = true;
1325
1326    while let Some(c) = pi.next() {
1327        if first && (c == '!' || c == '^') {
1328            negate = true;
1329            first = false;
1330            continue;
1331        }
1332        first = false;
1333
1334        if c == ']' && !chars_in_class.is_empty() {
1335            break;
1336        }
1337
1338        if pi.peek() == Some(&'-') {
1339            pi.next();
1340            if let Some(&end) = pi.peek() {
1341                if end != ']' {
1342                    pi.next();
1343                    for ch in c..=end {
1344                        chars_in_class.push(ch);
1345                    }
1346                    continue;
1347                }
1348            }
1349            // '-' at end is literal
1350            chars_in_class.push(c);
1351            chars_in_class.push('-');
1352            continue;
1353        }
1354
1355        chars_in_class.push(c);
1356    }
1357
1358    let matched = chars_in_class.contains(&tc);
1359    if negate {
1360        !matched
1361    } else {
1362        matched
1363    }
1364}
1365
1366/// File type character for -F style listing
1367pub fn file_type_char(mode: u32) -> char {
1368    let fmt = mode & libc::S_IFMT as u32;
1369    if fmt == libc::S_IFBLK as u32 {
1370        '#'
1371    } else if fmt == libc::S_IFCHR as u32 {
1372        '%'
1373    } else if fmt == libc::S_IFDIR as u32 {
1374        '/'
1375    } else if fmt == libc::S_IFIFO as u32 {
1376        '|'
1377    } else if fmt == libc::S_IFLNK as u32 {
1378        '@'
1379    } else if fmt == libc::S_IFREG as u32 {
1380        if mode & 0o111 != 0 {
1381            '*'
1382        } else {
1383            ' '
1384        }
1385    } else if fmt == libc::S_IFSOCK as u32 {
1386        '='
1387    } else {
1388        '?'
1389    }
1390}
1391
1392fn scale_size(bytes: u64, unit: SizeUnit) -> u64 {
1393    match unit {
1394        SizeUnit::Bytes => bytes,
1395        SizeUnit::PosixBlocks => (bytes + 511) / 512,
1396        SizeUnit::Kilobytes => (bytes + 1023) / 1024,
1397        SizeUnit::Megabytes => (bytes + 1048575) / 1048576,
1398        SizeUnit::Gigabytes => (bytes + 1073741823) / 1073741824,
1399        SizeUnit::Terabytes => (bytes + 1099511627775) / 1099511627776,
1400    }
1401}
1402
1403fn scale_time(secs: i64, unit: TimeUnit) -> i64 {
1404    match unit {
1405        TimeUnit::Seconds => secs,
1406        TimeUnit::Minutes => secs / 60,
1407        TimeUnit::Hours => secs / 3600,
1408        TimeUnit::Days => secs / 86400,
1409        TimeUnit::Weeks => secs / 604800,
1410        TimeUnit::Months => secs / 2592000,
1411    }
1412}
1413
1414fn compare_range(value: u64, target: u64, op: RangeOp) -> bool {
1415    match op {
1416        RangeOp::Less => value < target,
1417        RangeOp::Equal => value == target,
1418        RangeOp::Greater => value > target,
1419    }
1420}
1421
1422// ============================================================================
1423// Brace expansion
1424// ============================================================================
1425
1426/// Check if string has brace expansion
1427pub fn has_braces(s: &str, brace_ccl: bool) -> bool {
1428    let mut depth = 0;
1429    let mut has_comma = false;
1430    let mut has_dotdot = false;
1431
1432    let chars: Vec<char> = s.chars().collect();
1433    let len = chars.len();
1434
1435    for i in 0..len {
1436        match chars[i] {
1437            '{' => {
1438                if brace_ccl && depth == 0 {
1439                    // Check for {a-z} style
1440                    if i + 2 < len && chars[i + 2] == '}' {
1441                        return true;
1442                    }
1443                }
1444                depth += 1;
1445            }
1446            '}' => {
1447                if depth > 0 {
1448                    depth -= 1;
1449                    if depth == 0 && (has_comma || has_dotdot) {
1450                        return true;
1451                    }
1452                }
1453            }
1454            ',' if depth == 1 => has_comma = true,
1455            '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => has_dotdot = true,
1456            _ => {}
1457        }
1458    }
1459
1460    false
1461}
1462
1463/// Expand braces in a string
1464pub fn expand_braces(s: &str, brace_ccl: bool) -> Vec<String> {
1465    if !has_braces(s, brace_ccl) {
1466        return vec![s.to_string()];
1467    }
1468
1469    let mut results = vec![s.to_string()];
1470    let mut changed = true;
1471
1472    while changed {
1473        changed = false;
1474        let mut new_results = Vec::new();
1475
1476        for item in &results {
1477            if let Some(expanded) = expand_single_brace(item, brace_ccl) {
1478                new_results.extend(expanded);
1479                changed = true;
1480            } else {
1481                new_results.push(item.clone());
1482            }
1483        }
1484
1485        results = new_results;
1486    }
1487
1488    results
1489}
1490
1491fn expand_single_brace(s: &str, brace_ccl: bool) -> Option<Vec<String>> {
1492    let chars: Vec<char> = s.chars().collect();
1493    let len = chars.len();
1494
1495    // Find the first brace
1496    let mut brace_start = None;
1497    for i in 0..len {
1498        if chars[i] == '{' {
1499            brace_start = Some(i);
1500            break;
1501        }
1502    }
1503
1504    let start = brace_start?;
1505
1506    // Find matching close brace and contents
1507    let mut depth = 1;
1508    let mut comma_positions = Vec::new();
1509    let mut dotdot_pos = None;
1510
1511    for i in (start + 1)..len {
1512        match chars[i] {
1513            '{' => depth += 1,
1514            '}' => {
1515                depth -= 1;
1516                if depth == 0 {
1517                    let prefix: String = chars[..start].iter().collect();
1518                    let suffix: String = chars[i + 1..].iter().collect();
1519                    let content: String = chars[start + 1..i].iter().collect();
1520
1521                    // Check for range expansion
1522                    if let Some(dp) = dotdot_pos {
1523                        if comma_positions.is_empty() {
1524                            return expand_range(&prefix, &content, dp, &suffix);
1525                        }
1526                    }
1527
1528                    // Comma expansion
1529                    if !comma_positions.is_empty() {
1530                        return expand_comma(&prefix, &content, &comma_positions, &suffix);
1531                    }
1532
1533                    // brace_ccl expansion
1534                    if brace_ccl && content.len() > 0 {
1535                        return expand_ccl(&prefix, &content, &suffix);
1536                    }
1537
1538                    return None;
1539                }
1540            }
1541            ',' if depth == 1 => comma_positions.push(i - start - 1),
1542            '.' if depth == 1 && i + 1 < len && chars[i + 1] == '.' => {
1543                if dotdot_pos.is_none() {
1544                    dotdot_pos = Some(i - start - 1);
1545                }
1546            }
1547            _ => {}
1548        }
1549    }
1550
1551    None
1552}
1553
1554fn expand_range(
1555    prefix: &str,
1556    content: &str,
1557    dotdot_pos: usize,
1558    suffix: &str,
1559) -> Option<Vec<String>> {
1560    let left = &content[..dotdot_pos];
1561    let right_start = dotdot_pos + 2;
1562
1563    // Check for second ..
1564    let (right, incr) = if let Some(pos) = content[right_start..].find("..") {
1565        let r = &content[right_start..right_start + pos];
1566        let i: i64 = content[right_start + pos + 2..].parse().unwrap_or(1);
1567        (r, i.abs() as u64)
1568    } else {
1569        (&content[right_start..], 1u64)
1570    };
1571
1572    // Try numeric range
1573    if let (Ok(start), Ok(end)) = (left.parse::<i64>(), right.parse::<i64>()) {
1574        let mut results = Vec::new();
1575        let (start, end, reverse) = if start <= end {
1576            (start, end, false)
1577        } else {
1578            (end, start, true)
1579        };
1580
1581        // Determine padding width
1582        let width = left.len().max(right.len());
1583        let pad = left.starts_with('0') || right.starts_with('0');
1584
1585        let mut vals: Vec<i64> = (start..=end).step_by(incr as usize).collect();
1586        if reverse {
1587            vals.reverse();
1588        }
1589
1590        for v in vals {
1591            let s = if pad {
1592                format!("{}{:0>width$}{}", prefix, v, suffix, width = width)
1593            } else {
1594                format!("{}{}{}", prefix, v, suffix)
1595            };
1596            results.push(s);
1597        }
1598        return Some(results);
1599    }
1600
1601    // Try character range
1602    if left.len() == 1 && right.len() == 1 {
1603        let start = left.chars().next()?;
1604        let end = right.chars().next()?;
1605        let (start, end, reverse) = if start <= end {
1606            (start, end, false)
1607        } else {
1608            (end, start, true)
1609        };
1610
1611        let mut results = Vec::new();
1612        let mut chars: Vec<char> = (start..=end).collect();
1613        if reverse {
1614            chars.reverse();
1615        }
1616
1617        for c in chars {
1618            results.push(format!("{}{}{}", prefix, c, suffix));
1619        }
1620        return Some(results);
1621    }
1622
1623    None
1624}
1625
1626fn expand_comma(
1627    prefix: &str,
1628    content: &str,
1629    positions: &[usize],
1630    suffix: &str,
1631) -> Option<Vec<String>> {
1632    let mut results = Vec::new();
1633    let mut last = 0;
1634
1635    for &pos in positions {
1636        let part = &content[last..pos];
1637        results.push(format!("{}{}{}", prefix, part, suffix));
1638        last = pos + 1;
1639    }
1640    results.push(format!("{}{}{}", prefix, &content[last..], suffix));
1641
1642    Some(results)
1643}
1644
1645fn expand_ccl(prefix: &str, content: &str, suffix: &str) -> Option<Vec<String>> {
1646    let mut chars_set = HashSet::new();
1647    let chars: Vec<char> = content.chars().collect();
1648    let mut i = 0;
1649
1650    while i < chars.len() {
1651        if i + 2 < chars.len() && chars[i + 1] == '-' {
1652            let start = chars[i];
1653            let end = chars[i + 2];
1654            for c in start..=end {
1655                chars_set.insert(c);
1656            }
1657            i += 3;
1658        } else {
1659            chars_set.insert(chars[i]);
1660            i += 1;
1661        }
1662    }
1663
1664    let mut results: Vec<String> = chars_set
1665        .iter()
1666        .map(|c| format!("{}{}{}", prefix, c, suffix))
1667        .collect();
1668    results.sort();
1669    Some(results)
1670}
1671
1672// ============================================================================
1673// Convenience functions
1674// ============================================================================
1675
1676/// Glob with default options
1677pub fn glob(pattern: &str) -> Vec<String> {
1678    let mut state = GlobState::new(GlobOptions {
1679        null_glob: false,
1680        mark_dirs: false,
1681        no_glob_dots: true,
1682        list_types: false,
1683        numeric_sort: false,
1684        follow_links: false,
1685        extended_glob: true,
1686        case_glob: true,
1687        glob_star_short: false,
1688        bare_glob_qual: true,
1689        brace_ccl: false,
1690    });
1691    state.glob(pattern)
1692}
1693
1694/// Glob with custom options
1695pub fn glob_with_options(pattern: &str, options: GlobOptions) -> Vec<String> {
1696    let mut state = GlobState::new(options);
1697    state.glob(pattern)
1698}
1699
1700/// Add path component (from glob.c addpath lines 263-274)
1701pub fn addpath(buf: &mut String, component: &str) {
1702    buf.push_str(component);
1703    if !buf.ends_with('/') {
1704        buf.push('/');
1705    }
1706}
1707
1708/// Stat full path (from glob.c statfullpath lines 282-347)
1709pub fn statfullpath(pathbuf: &str, name: &str, follow: bool) -> Option<std::fs::Metadata> {
1710    let full = if name.is_empty() {
1711        if pathbuf.is_empty() {
1712            ".".to_string()
1713        } else {
1714            pathbuf.to_string()
1715        }
1716    } else {
1717        format!("{}{}", pathbuf, name)
1718    };
1719
1720    if follow {
1721        std::fs::metadata(&full).ok()
1722    } else {
1723        std::fs::symlink_metadata(&full).ok()
1724    }
1725}
1726
1727/// Check if path is a directory (from glob.c)
1728pub fn is_directory(path: &str) -> bool {
1729    std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
1730}
1731
1732/// Check if path is a symlink
1733pub fn is_symlink(path: &str) -> bool {
1734    std::fs::symlink_metadata(path)
1735        .map(|m| m.file_type().is_symlink())
1736        .unwrap_or(false)
1737}
1738
1739/// Match minimum distance for spelling correction (from glob.c mindist lines 3523-3575)
1740pub fn mindist(dir: &str, name: &str, best: &mut String, exact: bool) -> usize {
1741    let Ok(entries) = std::fs::read_dir(dir) else {
1742        return usize::MAX;
1743    };
1744
1745    let mut min_dist = usize::MAX;
1746
1747    for entry in entries.flatten() {
1748        let entry_name = entry.file_name().to_string_lossy().to_string();
1749        if exact && entry_name == name {
1750            *best = entry_name;
1751            return 0;
1752        }
1753
1754        let dist = crate::utils::spdist(name, &entry_name, min_dist);
1755        if dist < min_dist {
1756            min_dist = dist;
1757            *best = entry_name.clone();
1758        }
1759    }
1760
1761    min_dist
1762}
1763
1764/// Parse qualifier (from glob.c qgetnum)
1765pub fn qgetnum(s: &str) -> Option<(i64, &str)> {
1766    let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
1767    if end == 0 {
1768        return None;
1769    }
1770    let num = s[..end].parse::<i64>().ok()?;
1771    Some((num, &s[end..]))
1772}
1773
1774/// Parse time modifier (from glob.c qualtime)
1775pub fn qualtime(s: &str, units: char) -> Option<(i64, &str)> {
1776    let (mut num, rest) = qgetnum(s)?;
1777
1778    match units {
1779        'h' => num *= 3600,
1780        'd' => num *= 86400,
1781        'w' => num *= 604800,
1782        'M' => num *= 2592000,
1783        _ => {}
1784    }
1785
1786    Some((num, rest))
1787}
1788
1789/// Parse size modifier (from glob.c qualsize)
1790pub fn qualsize(s: &str, units: char) -> Option<(i64, &str)> {
1791    let (mut num, rest) = qgetnum(s)?;
1792
1793    match units {
1794        'k' | 'K' => num *= 1024,
1795        'm' | 'M' => num *= 1024 * 1024,
1796        'g' | 'G' => num *= 1024 * 1024 * 1024,
1797        't' | 'T' => num *= 1024 * 1024 * 1024 * 1024,
1798        'p' | 'P' => num *= 512,
1799        _ => {}
1800    }
1801
1802    Some((num, rest))
1803}
1804
1805/// Sort glob matches by type (from glob.c gmatchcmp lines 3595-3680)
1806pub fn sort_matches_by_type(matches: &mut [String], sort_type: GlobSort, reverse: bool) {
1807    match sort_type {
1808        GlobSort::Name => {
1809            matches.sort();
1810        }
1811        GlobSort::Size => {
1812            matches.sort_by(|a, b| {
1813                let size_a = std::fs::metadata(a).map(|m| m.len()).unwrap_or(0);
1814                let size_b = std::fs::metadata(b).map(|m| m.len()).unwrap_or(0);
1815                size_a.cmp(&size_b)
1816            });
1817        }
1818        GlobSort::Mtime => {
1819            matches.sort_by(|a, b| {
1820                let time_a = std::fs::metadata(a).and_then(|m| m.modified()).ok();
1821                let time_b = std::fs::metadata(b).and_then(|m| m.modified()).ok();
1822                time_a.cmp(&time_b)
1823            });
1824        }
1825        GlobSort::Atime => {
1826            matches.sort_by(|a, b| {
1827                let time_a = std::fs::metadata(a).and_then(|m| m.accessed()).ok();
1828                let time_b = std::fs::metadata(b).and_then(|m| m.accessed()).ok();
1829                time_a.cmp(&time_b)
1830            });
1831        }
1832        GlobSort::Depth => {
1833            matches.sort_by(|a, b| {
1834                let depth_a = a.matches('/').count();
1835                let depth_b = b.matches('/').count();
1836                depth_a.cmp(&depth_b)
1837            });
1838        }
1839        GlobSort::Links => {
1840            matches.sort_by(|a, b| {
1841                let links_a = std::fs::metadata(a).map(|m| m.nlink()).unwrap_or(0);
1842                let links_b = std::fs::metadata(b).map(|m| m.nlink()).unwrap_or(0);
1843                links_a.cmp(&links_b)
1844            });
1845        }
1846        _ => {}
1847    }
1848
1849    if reverse {
1850        matches.reverse();
1851    }
1852}
1853
1854/// File qualifier test functions (from glob.c qual* functions)
1855pub mod qualifiers {
1856    use std::os::unix::fs::MetadataExt;
1857    use std::os::unix::fs::PermissionsExt;
1858
1859    pub fn is_regular(path: &str) -> bool {
1860        std::fs::metadata(path)
1861            .map(|m| m.is_file())
1862            .unwrap_or(false)
1863    }
1864
1865    pub fn is_directory(path: &str) -> bool {
1866        std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
1867    }
1868
1869    pub fn is_symlink(path: &str) -> bool {
1870        std::fs::symlink_metadata(path)
1871            .map(|m| m.file_type().is_symlink())
1872            .unwrap_or(false)
1873    }
1874
1875    pub fn is_fifo(path: &str) -> bool {
1876        std::fs::metadata(path)
1877            .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFIFO as u32)
1878            .unwrap_or(false)
1879    }
1880
1881    pub fn is_socket(path: &str) -> bool {
1882        std::fs::metadata(path)
1883            .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFSOCK as u32)
1884            .unwrap_or(false)
1885    }
1886
1887    pub fn is_block_device(path: &str) -> bool {
1888        std::fs::metadata(path)
1889            .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFBLK as u32)
1890            .unwrap_or(false)
1891    }
1892
1893    pub fn is_char_device(path: &str) -> bool {
1894        std::fs::metadata(path)
1895            .map(|m| (m.mode() & libc::S_IFMT as u32) == libc::S_IFCHR as u32)
1896            .unwrap_or(false)
1897    }
1898
1899    pub fn is_setuid(path: &str) -> bool {
1900        std::fs::metadata(path)
1901            .map(|m| (m.mode() & libc::S_ISUID as u32) != 0)
1902            .unwrap_or(false)
1903    }
1904
1905    pub fn is_setgid(path: &str) -> bool {
1906        std::fs::metadata(path)
1907            .map(|m| (m.mode() & libc::S_ISGID as u32) != 0)
1908            .unwrap_or(false)
1909    }
1910
1911    pub fn is_sticky(path: &str) -> bool {
1912        std::fs::metadata(path)
1913            .map(|m| (m.mode() & libc::S_ISVTX as u32) != 0)
1914            .unwrap_or(false)
1915    }
1916
1917    pub fn is_readable(path: &str) -> bool {
1918        std::fs::metadata(path).is_ok() && std::fs::File::open(path).is_ok()
1919    }
1920
1921    pub fn is_writable(path: &str) -> bool {
1922        std::fs::OpenOptions::new().write(true).open(path).is_ok()
1923    }
1924
1925    pub fn is_executable(path: &str) -> bool {
1926        std::fs::metadata(path)
1927            .map(|m| (m.mode() & 0o111) != 0)
1928            .unwrap_or(false)
1929    }
1930
1931    pub fn size_matches(path: &str, size: u64, cmp: std::cmp::Ordering) -> bool {
1932        std::fs::metadata(path)
1933            .map(|m| m.len().cmp(&size) == cmp)
1934            .unwrap_or(false)
1935    }
1936
1937    pub fn mtime_matches(path: &str, secs: i64, cmp: std::cmp::Ordering) -> bool {
1938        std::fs::metadata(path)
1939            .and_then(|m| m.modified())
1940            .map(|t| {
1941                let elapsed = t.elapsed().map(|d| d.as_secs() as i64).unwrap_or(0);
1942                elapsed.cmp(&secs) == cmp
1943            })
1944            .unwrap_or(false)
1945    }
1946
1947    pub fn uid_matches(path: &str, uid: u32) -> bool {
1948        std::fs::metadata(path)
1949            .map(|m| m.uid() == uid)
1950            .unwrap_or(false)
1951    }
1952
1953    pub fn gid_matches(path: &str, gid: u32) -> bool {
1954        std::fs::metadata(path)
1955            .map(|m| m.gid() == gid)
1956            .unwrap_or(false)
1957    }
1958
1959    pub fn nlinks_matches(path: &str, nlinks: u64, cmp: std::cmp::Ordering) -> bool {
1960        std::fs::metadata(path)
1961            .map(|m| m.nlink().cmp(&nlinks) == cmp)
1962            .unwrap_or(false)
1963    }
1964
1965    /// Check if file is an executable command (from glob.c qualiscom)
1966    pub fn is_command(path: &str) -> bool {
1967        let meta = match std::fs::metadata(path) {
1968            Ok(m) => m,
1969            Err(_) => return false,
1970        };
1971
1972        if !meta.is_file() {
1973            return false;
1974        }
1975
1976        // Check if executable
1977        let mode = meta.mode();
1978        if mode & 0o111 == 0 {
1979            return false;
1980        }
1981
1982        // Check if in PATH would make it a command
1983        // For now just check executable bit
1984        true
1985    }
1986}
1987
1988// ============================================================================
1989// Pattern matching with replacement (from glob.c getmatch family)
1990// ============================================================================
1991
1992/// Match flags for getmatch
1993#[derive(Debug, Clone, Copy)]
1994pub struct MatchFlags {
1995    /// Match at start
1996    pub anchored_start: bool,
1997    /// Match at end
1998    pub anchored_end: bool,
1999    /// Shortest match
2000    pub shortest: bool,
2001    /// Subexpression matching
2002    pub subexpr: bool,
2003}
2004
2005impl Default for MatchFlags {
2006    fn default() -> Self {
2007        MatchFlags {
2008            anchored_start: false,
2009            anchored_end: false,
2010            shortest: false,
2011            subexpr: false,
2012        }
2013    }
2014}
2015
2016/// Internal match data
2017#[derive(Debug, Clone)]
2018pub struct MatchData {
2019    pub str: String,
2020    pub pattern: String,
2021    pub match_start: usize,
2022    pub match_end: usize,
2023    pub replacement: Option<String>,
2024}
2025
2026/// Get match return value (from glob.c get_match_ret lines 2338-2420)
2027pub fn get_match_ret(data: &MatchData, start: usize, end: usize) -> String {
2028    if start >= end || start >= data.str.len() {
2029        return String::new();
2030    }
2031
2032    let end = end.min(data.str.len());
2033    data.str[start..end].to_string()
2034}
2035
2036/// Compile pattern and get match info (from glob.c compgetmatch lines 2430-2510)
2037pub fn compgetmatch(pat: &str) -> Option<(String, MatchFlags)> {
2038    let mut flags = MatchFlags::default();
2039    let mut pattern = pat.to_string();
2040
2041    // Check for anchors
2042    if pattern.starts_with('#') {
2043        flags.anchored_start = true;
2044        pattern = pattern[1..].to_string();
2045    }
2046    if pattern.starts_with("##") {
2047        flags.anchored_start = true;
2048        flags.shortest = false;
2049        pattern = pattern[2..].to_string();
2050    }
2051    if pattern.ends_with('%') {
2052        flags.anchored_end = true;
2053        pattern.pop();
2054    }
2055    if pattern.ends_with("%%") {
2056        flags.anchored_end = true;
2057        flags.shortest = false;
2058        pattern.truncate(pattern.len().saturating_sub(2));
2059    }
2060
2061    Some((pattern, flags))
2062}
2063
2064/// Get pattern match with optional replacement (from glob.c getmatch lines 2520-2680)
2065///
2066/// This implements ${var#pat}, ${var##pat}, ${var%pat}, ${var%%pat},
2067/// ${var/pat/repl}, ${var//pat/repl}
2068pub fn getmatch(s: &str, pat: &str, flags: MatchFlags, n: i32, replstr: Option<&str>) -> String {
2069    let chars: Vec<char> = s.chars().collect();
2070    let len = chars.len();
2071
2072    if len == 0 {
2073        return s.to_string();
2074    }
2075
2076    // Find match
2077    let (match_start, match_end) = if flags.anchored_start && flags.anchored_end {
2078        // Full match
2079        if pattern_match(pat, s, true, true) {
2080            (0, len)
2081        } else {
2082            return s.to_string();
2083        }
2084    } else if flags.anchored_start {
2085        // Match from start (# or ##)
2086        let mut best_end = 0;
2087        for end in 1..=len {
2088            let substr: String = chars[..end].iter().collect();
2089            if pattern_match(pat, &substr, true, true) {
2090                if flags.shortest {
2091                    return match replstr {
2092                        Some(r) => format!("{}{}", r, chars[end..].iter().collect::<String>()),
2093                        None => chars[end..].iter().collect(),
2094                    };
2095                }
2096                best_end = end;
2097            }
2098        }
2099        if best_end > 0 {
2100            (0, best_end)
2101        } else {
2102            return s.to_string();
2103        }
2104    } else if flags.anchored_end {
2105        // Match from end (% or %%)
2106        let mut best_start = len;
2107        for start in (0..len).rev() {
2108            let substr: String = chars[start..].iter().collect();
2109            if pattern_match(pat, &substr, true, true) {
2110                if flags.shortest {
2111                    return match replstr {
2112                        Some(r) => format!("{}{}", chars[..start].iter().collect::<String>(), r),
2113                        None => chars[..start].iter().collect(),
2114                    };
2115                }
2116                best_start = start;
2117            }
2118        }
2119        if best_start < len {
2120            (best_start, len)
2121        } else {
2122            return s.to_string();
2123        }
2124    } else {
2125        // Floating match (/ or //)
2126        for start in 0..len {
2127            for end in (start + 1)..=len {
2128                let substr: String = chars[start..end].iter().collect();
2129                if pattern_match(pat, &substr, true, true) {
2130                    let prefix: String = chars[..start].iter().collect();
2131                    let suffix: String = chars[end..].iter().collect();
2132                    return match replstr {
2133                        Some(r) => format!("{}{}{}", prefix, r, suffix),
2134                        None => format!("{}{}", prefix, suffix),
2135                    };
2136                }
2137            }
2138        }
2139        return s.to_string();
2140    };
2141
2142    // Apply replacement
2143    let prefix: String = chars[..match_start].iter().collect();
2144    let suffix: String = chars[match_end..].iter().collect();
2145
2146    match replstr {
2147        Some(r) => format!("{}{}{}", prefix, r, suffix),
2148        None => format!("{}{}", prefix, suffix),
2149    }
2150}
2151
2152/// Get match for array elements (from glob.c getmatcharr lines 2690-2750)
2153pub fn getmatcharr(
2154    arr: &[String],
2155    pat: &str,
2156    flags: MatchFlags,
2157    n: i32,
2158    replstr: Option<&str>,
2159) -> Vec<String> {
2160    arr.iter()
2161        .map(|s| getmatch(s, pat, flags, n, replstr))
2162        .collect()
2163}
2164
2165/// Get match list for global replacement (from glob.c getmatchlist lines 2760-2850)
2166pub fn getmatchlist(s: &str, pat: &str) -> Vec<(usize, usize)> {
2167    let mut matches = Vec::new();
2168    let chars: Vec<char> = s.chars().collect();
2169    let len = chars.len();
2170
2171    let mut pos = 0;
2172    while pos < len {
2173        for end in (pos + 1)..=len {
2174            let substr: String = chars[pos..end].iter().collect();
2175            if pattern_match(pat, &substr, true, true) {
2176                matches.push((pos, end));
2177                pos = end;
2178                break;
2179            }
2180        }
2181        if matches.last().map(|&(_, e)| e) != Some(pos) {
2182            pos += 1;
2183        }
2184    }
2185
2186    matches
2187}
2188
2189/// Set pattern start offset (from glob.c set_pat_start)
2190pub fn set_pat_start(pattern: &str, offset: usize) -> String {
2191    if offset == 0 || offset >= pattern.len() {
2192        return pattern.to_string();
2193    }
2194    pattern[offset..].to_string()
2195}
2196
2197/// Set pattern end (from glob.c set_pat_end)
2198pub fn set_pat_end(pattern: &str, end: usize) -> String {
2199    if end >= pattern.len() {
2200        return pattern.to_string();
2201    }
2202    pattern[..end].to_string()
2203}
2204
2205// ============================================================================
2206// Tokenization (from glob.c tokenize family)
2207// ============================================================================
2208
2209/// Token types for glob tokenization
2210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2211pub enum GlobToken {
2212    Literal(char),
2213    Star,         // *
2214    Question,     // ?
2215    BracketOpen,  // [
2216    BracketClose, // ]
2217    ParenOpen,    // (
2218    ParenClose,   // )
2219    Pipe,         // |
2220    Hash,         // # (extended)
2221    Tilde,        // ~ (extended)
2222    Caret,        // ^ (extended)
2223    BraceOpen,    // {
2224    BraceClose,   // }
2225    Comma,        // , (in braces)
2226    Range,        // .. (in braces)
2227}
2228
2229/// Tokenize a glob pattern (from glob.c tokenize lines 3100-3180)
2230pub fn tokenize(s: &str) -> Vec<GlobToken> {
2231    let mut tokens = Vec::new();
2232    let mut chars = s.chars().peekable();
2233
2234    while let Some(c) = chars.next() {
2235        let token = match c {
2236            '\\' => {
2237                // Escaped character
2238                if let Some(next) = chars.next() {
2239                    GlobToken::Literal(next)
2240                } else {
2241                    GlobToken::Literal('\\')
2242                }
2243            }
2244            '*' => GlobToken::Star,
2245            '?' => GlobToken::Question,
2246            '[' => GlobToken::BracketOpen,
2247            ']' => GlobToken::BracketClose,
2248            '(' => GlobToken::ParenOpen,
2249            ')' => GlobToken::ParenClose,
2250            '|' => GlobToken::Pipe,
2251            '#' => GlobToken::Hash,
2252            '~' => GlobToken::Tilde,
2253            '^' => GlobToken::Caret,
2254            '{' => GlobToken::BraceOpen,
2255            '}' => GlobToken::BraceClose,
2256            ',' => GlobToken::Comma,
2257            '.' if chars.peek() == Some(&'.') => {
2258                chars.next();
2259                GlobToken::Range
2260            }
2261            _ => GlobToken::Literal(c),
2262        };
2263        tokens.push(token);
2264    }
2265
2266    tokens
2267}
2268
2269/// Tokenize for shell (from glob.c shtokenize lines 3190-3250)
2270/// Handles shell-specific quoting
2271pub fn shtokenize(s: &str) -> Vec<GlobToken> {
2272    let mut tokens = Vec::new();
2273    let mut chars = s.chars().peekable();
2274    let mut in_single_quote = false;
2275    let mut in_double_quote = false;
2276
2277    while let Some(c) = chars.next() {
2278        if in_single_quote {
2279            if c == '\'' {
2280                in_single_quote = false;
2281            } else {
2282                tokens.push(GlobToken::Literal(c));
2283            }
2284            continue;
2285        }
2286
2287        if in_double_quote {
2288            if c == '"' {
2289                in_double_quote = false;
2290            } else if c == '\\' {
2291                if let Some(next) = chars.next() {
2292                    tokens.push(GlobToken::Literal(next));
2293                }
2294            } else {
2295                tokens.push(GlobToken::Literal(c));
2296            }
2297            continue;
2298        }
2299
2300        match c {
2301            '\'' => in_single_quote = true,
2302            '"' => in_double_quote = true,
2303            '\\' => {
2304                if let Some(next) = chars.next() {
2305                    tokens.push(GlobToken::Literal(next));
2306                }
2307            }
2308            '*' => tokens.push(GlobToken::Star),
2309            '?' => tokens.push(GlobToken::Question),
2310            '[' => tokens.push(GlobToken::BracketOpen),
2311            ']' => tokens.push(GlobToken::BracketClose),
2312            _ => tokens.push(GlobToken::Literal(c)),
2313        }
2314    }
2315
2316    tokens
2317}
2318
2319/// Tokenize with zsh-specific flags (from glob.c zshtokenize lines 3260-3380)
2320pub fn zshtokenize(s: &str, extended_glob: bool, sh_glob: bool) -> Vec<GlobToken> {
2321    let mut tokens = Vec::new();
2322    let mut chars = s.chars().peekable();
2323
2324    while let Some(c) = chars.next() {
2325        let token = match c {
2326            '\\' => {
2327                if let Some(next) = chars.next() {
2328                    GlobToken::Literal(next)
2329                } else {
2330                    GlobToken::Literal('\\')
2331                }
2332            }
2333            '*' => GlobToken::Star,
2334            '?' => GlobToken::Question,
2335            '[' => GlobToken::BracketOpen,
2336            ']' => GlobToken::BracketClose,
2337            '#' if extended_glob => GlobToken::Hash,
2338            '^' if extended_glob => GlobToken::Caret,
2339            '~' if extended_glob => GlobToken::Tilde,
2340            '(' if extended_glob => GlobToken::ParenOpen,
2341            ')' if extended_glob => GlobToken::ParenClose,
2342            '|' if extended_glob => GlobToken::Pipe,
2343            '{' if !sh_glob => GlobToken::BraceOpen,
2344            '}' if !sh_glob => GlobToken::BraceClose,
2345            ',' if !sh_glob => GlobToken::Comma,
2346            _ => GlobToken::Literal(c),
2347        };
2348        tokens.push(token);
2349    }
2350
2351    tokens
2352}
2353
2354/// Remove null arguments from token list (from glob.c remnulargs lines 3390-3420)
2355pub fn remnulargs(tokens: &mut Vec<GlobToken>) {
2356    tokens.retain(|t| {
2357        if let GlobToken::Literal(c) = t {
2358            *c != '\0'
2359        } else {
2360            true
2361        }
2362    });
2363}
2364
2365// ============================================================================
2366// Mode specification parsing (from glob.c qgetmodespec)
2367// ============================================================================
2368
2369/// Parsed mode specification
2370#[derive(Debug, Clone, Copy, Default)]
2371pub struct ModeSpec {
2372    pub who: u32,  // u, g, o, a masks
2373    pub op: char,  // +, -, =
2374    pub perm: u32, // r, w, x, s, t masks
2375}
2376
2377/// Parse mode specification like chmod (from glob.c qgetmodespec lines 790-920)
2378/// Examples: u+x, go-w, a=r, 755
2379pub fn qgetmodespec(s: &str) -> Option<(ModeSpec, &str)> {
2380    let mut chars = s.chars().peekable();
2381    let mut spec = ModeSpec::default();
2382
2383    // Check for octal mode
2384    if chars.peek().map(|c| c.is_ascii_digit()).unwrap_or(false) {
2385        let mut mode_str = String::new();
2386        while let Some(&c) = chars.peek() {
2387            if c.is_ascii_digit() && c < '8' {
2388                mode_str.push(c);
2389                chars.next();
2390            } else {
2391                break;
2392            }
2393        }
2394        if let Ok(mode) = u32::from_str_radix(&mode_str, 8) {
2395            spec.perm = mode;
2396            spec.op = '=';
2397            spec.who = 0o7777;
2398            let rest_pos = s.len() - chars.collect::<String>().len();
2399            return Some((spec, &s[rest_pos..]));
2400        }
2401        return None;
2402    }
2403
2404    // Parse symbolic mode
2405    // Who: u, g, o, a
2406    let mut who = 0u32;
2407    while let Some(&c) = chars.peek() {
2408        match c {
2409            'u' => {
2410                who |= 0o4700;
2411                chars.next();
2412            }
2413            'g' => {
2414                who |= 0o2070;
2415                chars.next();
2416            }
2417            'o' => {
2418                who |= 0o1007;
2419                chars.next();
2420            }
2421            'a' => {
2422                who |= 0o7777;
2423                chars.next();
2424            }
2425            _ => break,
2426        }
2427    }
2428    if who == 0 {
2429        who = 0o7777; // Default to all
2430    }
2431    spec.who = who;
2432
2433    // Op: +, -, =
2434    spec.op = match chars.next() {
2435        Some('+') => '+',
2436        Some('-') => '-',
2437        Some('=') => '=',
2438        _ => return None,
2439    };
2440
2441    // Perm: r, w, x, X, s, t
2442    let mut perm = 0u32;
2443    while let Some(&c) = chars.peek() {
2444        match c {
2445            'r' => {
2446                perm |= 0o444;
2447                chars.next();
2448            }
2449            'w' => {
2450                perm |= 0o222;
2451                chars.next();
2452            }
2453            'x' => {
2454                perm |= 0o111;
2455                chars.next();
2456            }
2457            'X' => {
2458                perm |= 0o111;
2459                chars.next();
2460            } // Conditional execute
2461            's' => {
2462                perm |= 0o6000;
2463                chars.next();
2464            }
2465            't' => {
2466                perm |= 0o1000;
2467                chars.next();
2468            }
2469            _ => break,
2470        }
2471    }
2472    spec.perm = perm & who;
2473
2474    let rest_pos = s.len() - chars.collect::<String>().len();
2475    Some((spec, &s[rest_pos..]))
2476}
2477
2478/// Apply mode spec to existing mode
2479pub fn apply_modespec(mode: u32, spec: &ModeSpec) -> u32 {
2480    match spec.op {
2481        '+' => mode | spec.perm,
2482        '-' => mode & !spec.perm,
2483        '=' => (mode & !spec.who) | spec.perm,
2484        _ => mode,
2485    }
2486}
2487
2488// ============================================================================
2489// Brace char range parsing (from glob.c bracechardots)
2490// ============================================================================
2491
2492/// Parse character range in braces like {a..z} (from glob.c bracechardots lines 1780-1850)
2493pub fn bracechardots(s: &str) -> Option<(char, char, i32)> {
2494    let chars: Vec<char> = s.chars().collect();
2495
2496    // Must be at least "a..b"
2497    if chars.len() < 4 {
2498        return None;
2499    }
2500
2501    // Find ..
2502    let dotdot_pos = s.find("..")?;
2503    if dotdot_pos == 0 {
2504        return None;
2505    }
2506
2507    let left = &s[..dotdot_pos];
2508    let right = &s[dotdot_pos + 2..];
2509
2510    // Check for increment
2511    let (end_str, incr) = if let Some(pos) = right.find("..") {
2512        let end = &right[..pos];
2513        let inc: i32 = right[pos + 2..].parse().unwrap_or(1);
2514        (end, inc)
2515    } else {
2516        (right, 1)
2517    };
2518
2519    // Single character range
2520    if left.chars().count() == 1 && end_str.chars().count() == 1 {
2521        let c1 = left.chars().next()?;
2522        let c2 = end_str.chars().next()?;
2523        return Some((c1, c2, incr));
2524    }
2525
2526    None
2527}
2528
2529// ============================================================================
2530// Redirect expansion (from glob.c xpandredir)
2531// ============================================================================
2532
2533/// Redirect types
2534#[derive(Debug, Clone)]
2535pub struct Redirect {
2536    pub fd: i32,
2537    pub target: String,
2538    pub rtype: RedirectType,
2539}
2540
2541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2542pub enum RedirectType {
2543    Read,      // <
2544    Write,     // >
2545    Append,    // >>
2546    ReadWrite, // <>
2547    Clobber,   // >|
2548    Here,      // <<
2549    HereStr,   // <<<
2550    Dup,       // >&, <&
2551    Pipe,      // |
2552}
2553
2554/// Expand redirections with glob patterns (from glob.c xpandredir lines 1690-1770)
2555pub fn xpandredir(redir: &Redirect, options: &GlobOptions) -> Vec<Redirect> {
2556    // Check if target has wildcards
2557    if !has_wildcards(&redir.target) {
2558        return vec![redir.clone()];
2559    }
2560
2561    // Glob expand the target
2562    let mut state = GlobState::new(options.clone());
2563    let matches = state.glob(&redir.target);
2564
2565    if matches.is_empty() {
2566        return vec![redir.clone()];
2567    }
2568
2569    // For redirections, we usually only want one match
2570    if matches.len() > 1 {
2571        // Ambiguous redirect - return original
2572        return vec![redir.clone()];
2573    }
2574
2575    vec![Redirect {
2576        fd: redir.fd,
2577        target: matches[0].clone(),
2578        rtype: redir.rtype,
2579    }]
2580}
2581
2582// ============================================================================
2583// Exec string for sorting (from glob.c glob_exec_string)
2584// ============================================================================
2585
2586/// Execute a command and capture output for sorting (from glob.c glob_exec_string lines 920-1020)
2587/// This is used for the `e` glob qualifier: *(e:'cmd':)
2588pub fn glob_exec_string(cmd: &str, filename: &str) -> Option<String> {
2589    use std::process::Command;
2590
2591    // Replace $REPLY or {} with filename
2592    let cmd = cmd.replace("$REPLY", filename).replace("{}", filename);
2593
2594    let output = Command::new("sh").arg("-c").arg(&cmd).output().ok()?;
2595
2596    if output.status.success() {
2597        Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
2598    } else {
2599        None
2600    }
2601}
2602
2603/// Execute a qualifier expression (from glob.c qualsheval full impl)
2604pub fn qualsheval(filename: &str, expr: &str) -> bool {
2605    use std::process::Command;
2606
2607    // Set REPLY to filename and evaluate expression
2608    let script = format!("REPLY='{}'; {}", filename.replace("'", "'\\''"), expr);
2609
2610    Command::new("sh")
2611        .arg("-c")
2612        .arg(&script)
2613        .status()
2614        .map(|s| s.success())
2615        .unwrap_or(false)
2616}
2617
2618#[cfg(test)]
2619mod tests {
2620    use super::*;
2621    use std::fs::{self, File};
2622    use std::io::Write;
2623    use tempfile::TempDir;
2624
2625    fn setup_test_dir() -> TempDir {
2626        let dir = TempDir::new().unwrap();
2627        let base = dir.path();
2628
2629        // Create test files
2630        File::create(base.join("file1.txt")).unwrap();
2631        File::create(base.join("file2.txt")).unwrap();
2632        File::create(base.join("file3.rs")).unwrap();
2633        File::create(base.join(".hidden")).unwrap();
2634
2635        // Create subdirectory
2636        fs::create_dir(base.join("subdir")).unwrap();
2637        File::create(base.join("subdir/nested.txt")).unwrap();
2638
2639        dir
2640    }
2641
2642    #[test]
2643    fn test_has_wildcards() {
2644        assert!(has_wildcards("*.txt"));
2645        assert!(has_wildcards("file?.txt"));
2646        assert!(has_wildcards("file[12].txt"));
2647        assert!(!has_wildcards("file.txt"));
2648        assert!(!has_wildcards("path/to/file.txt"));
2649    }
2650
2651    #[test]
2652    fn test_pattern_match() {
2653        assert!(pattern_match("*.txt", "file.txt", false, true));
2654        assert!(pattern_match("file?.txt", "file1.txt", false, true));
2655        assert!(!pattern_match("*.txt", "file.rs", false, true));
2656        assert!(pattern_match("file[12].txt", "file1.txt", false, true));
2657        assert!(!pattern_match("file[12].txt", "file3.txt", false, true));
2658    }
2659
2660    #[test]
2661    fn test_brace_expansion() {
2662        let result = expand_braces("{a,b,c}", false);
2663        assert_eq!(result, vec!["a", "b", "c"]);
2664
2665        let result = expand_braces("file{1,2,3}.txt", false);
2666        assert_eq!(result, vec!["file1.txt", "file2.txt", "file3.txt"]);
2667
2668        let result = expand_braces("{1..5}", false);
2669        assert_eq!(result, vec!["1", "2", "3", "4", "5"]);
2670
2671        let result = expand_braces("{a..e}", false);
2672        assert_eq!(result, vec!["a", "b", "c", "d", "e"]);
2673    }
2674
2675    #[test]
2676    fn test_glob_simple() {
2677        let dir = setup_test_dir();
2678        let pattern = format!("{}/*.txt", dir.path().display());
2679
2680        let mut state = GlobState::new(GlobOptions::default());
2681        let results = state.glob(&pattern);
2682
2683        assert_eq!(results.len(), 2);
2684        assert!(results.iter().any(|s| s.ends_with("file1.txt")));
2685        assert!(results.iter().any(|s| s.ends_with("file2.txt")));
2686    }
2687
2688    #[test]
2689    fn test_glob_hidden() {
2690        let dir = setup_test_dir();
2691        let pattern = format!("{}/*", dir.path().display());
2692
2693        // With no_glob_dots = true (default)
2694        let mut state = GlobState::new(GlobOptions {
2695            no_glob_dots: true,
2696            ..Default::default()
2697        });
2698        let results = state.glob(&pattern);
2699        assert!(!results.iter().any(|s| s.contains(".hidden")));
2700
2701        // With no_glob_dots = false
2702        let mut state = GlobState::new(GlobOptions {
2703            no_glob_dots: false,
2704            ..Default::default()
2705        });
2706        let results = state.glob(&pattern);
2707        assert!(results.iter().any(|s| s.contains(".hidden")));
2708    }
2709
2710    #[test]
2711    fn test_file_type_char() {
2712        assert_eq!(file_type_char(libc::S_IFDIR as u32), '/');
2713        assert_eq!(file_type_char(libc::S_IFREG as u32), ' ');
2714        assert_eq!(file_type_char(libc::S_IFREG as u32 | 0o111), '*');
2715        assert_eq!(file_type_char(libc::S_IFLNK as u32), '@');
2716    }
2717
2718    #[test]
2719    fn test_numeric_string_cmp() {
2720        assert_eq!(numeric_string_cmp("file1", "file2"), Ordering::Less);
2721        assert_eq!(numeric_string_cmp("file10", "file2"), Ordering::Greater);
2722        assert_eq!(numeric_string_cmp("file10", "file10"), Ordering::Equal);
2723    }
2724}