github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/glob/glob.go (about) 1 // Package glob implements globbing for elvish. 2 package glob 3 4 import ( 5 "io/ioutil" 6 "os" 7 "unicode/utf8" 8 ) 9 10 // Glob returns a list of file names satisfying the given pattern. 11 func Glob(p string, cb func(string) bool) bool { 12 return Parse(p).Glob(cb) 13 } 14 15 // Glob returns a list of file names satisfying the Pattern. 16 func (p Pattern) Glob(cb func(string) bool) bool { 17 segs := p.Segments 18 dir := "" 19 if len(segs) > 0 && IsSlash(segs[0]) { 20 segs = segs[1:] 21 dir = "/" 22 } 23 24 if p.DirOverride != "" { 25 dir = p.DirOverride 26 } 27 28 return glob(segs, dir, cb) 29 } 30 31 func glob(segs []Segment, dir string, cb func(string) bool) bool { 32 // Consume the non-wildcard prefix. This is required so that "." and "..", 33 // which doesn't appear in the result of ReadDir, can appear as standalone 34 // path components in the pattern. 35 for len(segs) > 0 && IsLiteral(segs[0]) { 36 seg0 := segs[0].(Literal).Data 37 var path string 38 switch dir { 39 case "": 40 path = seg0 41 case "/": 42 path = "/" + seg0 43 default: 44 path = dir + "/" + seg0 45 } 46 if len(segs) == 1 { 47 // A lone literal. Generate it if the named file exists, and return. 48 if _, err := os.Stat(path); err == nil { 49 return cb(path) 50 } 51 return true 52 } else if IsSlash(segs[1]) { 53 // A lone literal followed by a slash. Change the directory if it 54 // exists, otherwise return. 55 if info, err := os.Stat(path); err == nil && info.IsDir() { 56 dir = path 57 } else { 58 return true 59 } 60 segs = segs[2:] 61 } else { 62 break 63 } 64 } 65 66 // Empty segment, resulting from a trailing slash. Generate the starting 67 // directory. 68 if len(segs) == 0 { 69 return cb(dir + "/") 70 } 71 72 var prefix string 73 if dir == "" { 74 prefix = "" 75 dir = "." 76 } else if dir == "/" { 77 prefix = "/" 78 } else { 79 // dir never has a trailing slash unless it is /. 80 prefix = dir + "/" 81 } 82 83 i := -1 84 nexti := func() { 85 for i++; i < len(segs); i++ { 86 if IsSlash(segs[i]) || IsWild1(segs[i], StarStar) { 87 break 88 } 89 } 90 } 91 nexti() 92 93 infos, err := ioutil.ReadDir(dir) 94 if err != nil { 95 // XXX Silently drop the error 96 return true 97 } 98 99 // Enumerate the position of the first slash. 100 for i < len(segs) { 101 slash := IsSlash(segs[i]) 102 var first, rest []Segment 103 if slash { 104 // segs = x/y. Match dir with x, recurse on y. 105 first, rest = segs[:i], segs[i+1:] 106 } else { 107 // segs = x**y. Match dir with x*, recurse on **y. 108 first, rest = segs[:i+1], segs[i:] 109 } 110 111 for _, info := range infos { 112 name := info.Name() 113 if match(first, name) && info.IsDir() { 114 if !glob(rest, prefix+name, cb) { 115 return false 116 } 117 } 118 } 119 120 if slash { 121 // First slash cannot appear later than a slash in the pattern. 122 return true 123 } 124 nexti() 125 } 126 127 // If we reach here, it is possible to have no slashes at all. 128 for _, info := range infos { 129 name := info.Name() 130 if match(segs, name) { 131 if !cb(prefix + name) { 132 return false 133 } 134 } 135 } 136 return true 137 } 138 139 // match matches a name against segments. It treats StarStar segments as they 140 // are Star segments. The segments may not contain Slash'es. 141 func match(segs []Segment, name string) bool { 142 if len(segs) == 0 { 143 return name == "" 144 } 145 // If the name start with "." and the first segment is a Wild, only match 146 // when MatchHidden is true. 147 if len(name) > 0 && name[0] == '.' && IsWild(segs[0]) { 148 seg := segs[0].(Wild) 149 if !seg.MatchHidden { 150 return false 151 } 152 } 153 segs: 154 for len(segs) > 0 { 155 // Find a chunk. A chunk is a run of Literal and Question, with an 156 // optional leading Star. 157 var i int 158 for i = 1; i < len(segs); i++ { 159 if IsWild2(segs[i], Star, StarStar) { 160 break 161 } 162 } 163 164 chunk := segs[:i] 165 startsWithStar := IsWild2(chunk[0], Star, StarStar) 166 var startingStar Wild 167 if startsWithStar { 168 startingStar = chunk[0].(Wild) 169 chunk = chunk[1:] 170 } 171 segs = segs[i:] 172 173 // NOTE A quick path when len(segs) == 0 can be implemented: match 174 // backwards. 175 176 // Match at the current position. If this is the last chunk, we need to 177 // make sure name is exhausted by the matching. 178 ok, rest := matchChunk(chunk, name) 179 if ok && (rest == "" || len(segs) > 0) { 180 name = rest 181 continue 182 } 183 184 if startsWithStar { 185 // NOTE An optimization is to make the upper bound not len(names), 186 // but rather len(names) - LB(# bytes segs can match) 187 for i, r := range name { 188 j := i + len(string(r)) 189 // Match name[:j] with the starting *, and the rest with chunk. 190 if !startingStar.Match(r) { 191 break 192 } 193 ok, rest := matchChunk(chunk, name[j:]) 194 if ok && (rest == "" || len(segs) > 0) { 195 name = rest 196 continue segs 197 } 198 } 199 } 200 return false 201 } 202 return name == "" 203 } 204 205 // matchChunk returns whether a chunk matches a prefix of name. If succeeded, it 206 // also returns the remaining part of name. 207 func matchChunk(chunk []Segment, name string) (bool, string) { 208 for _, seg := range chunk { 209 if name == "" { 210 return false, "" 211 } 212 switch seg := seg.(type) { 213 case Literal: 214 n := len(seg.Data) 215 if len(name) < n || name[:n] != seg.Data { 216 return false, "" 217 } 218 name = name[n:] 219 case Wild: 220 if seg.Type == Question { 221 r, n := utf8.DecodeRuneInString(name) 222 if !seg.Match(r) { 223 return false, "" 224 } 225 name = name[n:] 226 } else { 227 panic("chunk has non-question wild segment") 228 } 229 default: 230 panic("chunk has non-literal non-wild segment") 231 } 232 } 233 return true, name 234 }