// Package glob implements globbing for elvish.
package glob import ( ) // TODO: Use native path separators instead of always using /. // PathInfo keeps a path resulting from glob expansion and its FileInfo. The // FileInfo is useful for efficiently determining if a given pathname satisfies // a particular constraint without doing an extra stat. type PathInfo struct { // The generated path, consistent with the original glob pattern. It cannot // be replaced by Info.Name(), which is just the final path component. Path string Info os.FileInfo } // Glob returns a list of file names satisfying the given pattern. func ( string, func(PathInfo) bool) bool { return Parse().Glob() } // Glob returns a list of file names satisfying the Pattern. func ( Pattern) ( func(PathInfo) bool) bool { := .Segments := "" // TODO(xiaq): This is a hack solely for supporting globs that start with // ~ (tilde) in the eval package. if .DirOverride != "" { = .DirOverride } if len() > 0 && IsSlash([0]) { = [1:] += "/" } else if runtime.GOOS == "windows" && len() > 1 && IsLiteral([0]) && IsSlash([1]) { // TODO: Handle UNC. := [0].(Literal).Data if isDrive() { = [2:] = + "/" } } return glob(, , ) } func ( string) bool { return len() == 2 && [1] == ':' && (('a' <= [0] && [1] <= 'z') || ('A' <= [0] && [0] <= 'Z')) } // glob finds all filenames matching the given Segments in the given dir, and // calls the callback on all of them. If the callback returns false, globbing is // interrupted, and glob returns false. Otherwise it returns true. func ( []Segment, string, func(PathInfo) bool) bool { // Consume non-wildcard path elements simply by following the path. This may // seem like an optimization, but is actually required for "." and ".." to // be used as path elements, as they do not appear in the result of ReadDir. for len() > 1 && IsLiteral([0]) && IsSlash([1]) { := [0].(Literal).Data = [2:] += + "/" if , := os.Stat(); != nil || !.IsDir() { return true } } if len() == 0 { if , := os.Stat(); == nil { return (PathInfo{, }) } return true } else if len() == 1 && IsLiteral([0]) { := + [0].(Literal).Data if , := os.Stat(); == nil { return (PathInfo{, }) } return true } , := readDir() if != nil { // TODO(xiaq): Silently drop the error. return true } := -1 // nexti moves i to the next index in segs that is either / or ** (in other // words, something that matches /). := func() { for ++; < len(); ++ { if IsSlash([]) || IsWild1([], StarStar) { break } } } () // Enumerate the position of the first slash. In the presence of multiple // **'s in the pattern, the first slash may be in any of those. // // For instance, in x**y**z, the first slash may be in the first ** or the // second: // 1) If it is in the first, then pattern is equivalent to x*/**y**z. We // match directories with x* and recurse in each subdirectory with the // pattern **y**z. // 2) If it is the in the second, we know that since the first ** can no // longer contain any slashes, we treat it as * (this is done in // matchElement). The pattern is now equivalent to x*y*/**z. We match // directories with x*y* and recurse in each subdirectory with the // pattern **z. // // The rules are: // 1) For each **, we treat it as */** and all previous ones as *. We match // subdirectories with the part before /, and recurse in subdirectories // with the pattern after /. // 2) If a literal / is encountered, we return after recursing in the // subdirectories. for < len() { := IsSlash([]) var , []Segment if { // segs = x/y. Match dir with x, recurse on y. , = [:], [+1:] } else { // segs = x**y. Match dir with x*, recurse on **y. , = [:+1], [:] } for , := range { := .Name() if matchElement(, ) && .IsDir() { if !(, ++"/", ) { return false } } } if { // First slash cannot appear later than a slash in the pattern. return true } () } // If we reach here, it is possible to have no slashes at all. Simply match // the entire pattern with all files. for , := range { := .Name() if matchElement(, ) { := + , := os.Stat() if != nil { return true } if !(PathInfo{, }) { return false } } } return true } // readDir is just like ioutil.ReadDir except that it treats an argument of "" // as ".". func ( string) ([]os.FileInfo, error) { if == "" { = "." } return ioutil.ReadDir() } // matchElement matches a path element against segments, which may not contain // any Slash segments. It treats StarStar segments as they are Star segments. func ( []Segment, string) bool { if len() == 0 { return == "" } // If the name start with "." and the first segment is a Wild, only match // when MatchHidden is true. if len() > 0 && [0] == '.' && IsWild([0]) && ![0].(Wild).MatchHidden { return false } : for len() > 0 { // Find a chunk. A chunk is an optional Star followed by a run of // fixed-length segments (Literal and Question). var int for = 1; < len(); ++ { if IsWild2([], Star, StarStar) { break } } := [:] := IsWild2([0], Star, StarStar) var Wild if { = [0].(Wild) = [1:] } = [:] // NOTE A quick path when len(segs) == 0 can be implemented: match // backwards. // Match at the current position. If this is the last chunk, we need to // make sure name is exhausted by the matching. , := matchFixedLength(, ) if && ( == "" || len() > 0) { = continue } if { // NOTE An optimization is to make the upper bound not len(names), // but rather len(names) - LB(# bytes segs can match) for , := range { := + len(string()) // Match name[:j] with the starting *, and the rest with chunk. if !.Match() { break } , := matchFixedLength(, [:]) if && ( == "" || len() > 0) { = continue } } } return false } return == "" } // matchFixedLength returns whether a run of fixed-length segments (Literal and // Question) matches a prefix of name. It returns whether the match is // successful and if if it is, the remaining part of name. func ( []Segment, string) (bool, string) { for , := range { if == "" { return false, "" } switch seg := .(type) { case Literal: := len(.Data) if len() < || [:] != .Data { return false, "" } = [:] case Wild: if .Type == Question { , := utf8.DecodeRuneInString() if !.Match() { return false, "" } = [:] } else { panic("matchFixedLength given non-question wild segment") } default: panic("matchFixedLength given non-literal non-wild segment") } } return true, }