github.com/tiagovtristao/plz@v13.4.0+incompatible/src/fs/glob.go (about) 1 package fs 2 3 import ( 4 "os" 5 "path" 6 "path/filepath" 7 "regexp" 8 "strings" 9 "sync" 10 11 "github.com/karrick/godirwalk" 12 ) 13 14 // Used to identify the fixed part at the start of a glob pattern. 15 var initialFixedPart = regexp.MustCompile(`([^\*]+)/(.*)`) 16 17 // IsGlob returns true if the given pattern requires globbing (i.e. contains characters that would be expanded by it) 18 func IsGlob(pattern string) bool { 19 return strings.ContainsAny(pattern, "*?[") 20 } 21 22 // Glob implements matching using Go's built-in filepath.Glob, but extends it to support 23 // Ant-style patterns using **. 24 func Glob(buildFileNames []string, rootPath string, includes, prefixedExcludes, excludes []string, includeHidden bool) []string { 25 filenames := []string{} 26 for _, include := range includes { 27 matches, err := glob(buildFileNames, rootPath, include, includeHidden, prefixedExcludes) 28 if err != nil { 29 panic(err) 30 } 31 for _, filename := range matches { 32 if !includeHidden { 33 // Exclude hidden & temporary files 34 _, file := path.Split(filename) 35 if strings.HasPrefix(file, ".") || (strings.HasPrefix(file, "#") && strings.HasSuffix(file, "#")) { 36 continue 37 } 38 } 39 if strings.HasPrefix(filename, rootPath) && rootPath != "" { 40 filename = filename[len(rootPath)+1:] // +1 to strip the slash too 41 } 42 if !shouldExcludeMatch(filename, excludes) { 43 filenames = append(filenames, filename) 44 } 45 } 46 } 47 return filenames 48 } 49 50 func shouldExcludeMatch(match string, excludes []string) bool { 51 for _, excl := range excludes { 52 if strings.ContainsRune(match, '/') && !strings.ContainsRune(excl, '/') { 53 match = path.Base(match) 54 } 55 if matches, err := filepath.Match(excl, match); matches || err != nil { 56 return true 57 } 58 } 59 return false 60 } 61 62 func glob(buildFileNames []string, rootPath, pattern string, includeHidden bool, excludes []string) ([]string, error) { 63 // Go's Glob function doesn't handle Ant-style ** patterns. Do it ourselves if we have to, 64 // but we prefer not since our solution will have to do a potentially inefficient walk. 65 if !strings.Contains(pattern, "*") { 66 return []string{path.Join(rootPath, pattern)}, nil 67 } else if !strings.Contains(pattern, "**") { 68 return filepath.Glob(path.Join(rootPath, pattern)) 69 } 70 71 // Optimisation: when we have a fixed part at the start, add that to the root path. 72 // e.g. glob(["src/**/*"]) should start walking in src and not at the current directory, 73 // because it can't possibly match anything else at that level. 74 // Can be quite important in cases where it would descend into a massive node_modules tree 75 // or similar, which leads to a big slowdown since it's synchronous with parsing 76 // (ideally it would not be of course, but that's a more complex change and this is useful anyway). 77 submatches := initialFixedPart.FindStringSubmatch(pattern) 78 if submatches != nil { 79 rootPath = path.Join(rootPath, submatches[1]) 80 pattern = submatches[2] 81 } 82 if !PathExists(rootPath) { 83 return nil, nil 84 } 85 86 matches := []string{} 87 // Turn the pattern into a regex. Oh dear... 88 pattern = "^" + path.Join(rootPath, pattern) + "$" 89 pattern = strings.Replace(pattern, "*", "[^/]*", -1) // handle single (all) * components 90 pattern = strings.Replace(pattern, "[^/]*[^/]*", ".*", -1) // handle ** components 91 pattern = strings.Replace(pattern, "/.*/", "/(?:.*/)?", -1) // allow /**/ to match nothing 92 pattern = strings.Replace(pattern, "+", "\\+", -1) // escape + 93 regex, err := regexp.Compile(pattern) 94 if err != nil { 95 return matches, err 96 } 97 98 err = Walk(rootPath, func(name string, isDir bool) error { 99 if isDir { 100 if name != rootPath && IsPackage(buildFileNames, name) { 101 return filepath.SkipDir // Can't glob past a package boundary 102 } else if !includeHidden && strings.HasPrefix(path.Base(name), ".") { 103 return filepath.SkipDir // Don't descend into hidden directories 104 } else if shouldExcludeMatch(name, excludes) { 105 return filepath.SkipDir 106 } 107 } else if regex.MatchString(name) && !shouldExcludeMatch(name, excludes) { 108 matches = append(matches, name) 109 } 110 return nil 111 }) 112 return matches, err 113 } 114 115 // Walk implements an equivalent to filepath.Walk. 116 // It's implemented over github.com/karrick/godirwalk but the provided interface doesn't use that 117 // to make it a little easier to handle. 118 func Walk(rootPath string, callback func(name string, isDir bool) error) error { 119 return WalkMode(rootPath, func(name string, isDir bool, mode os.FileMode) error { 120 return callback(name, isDir) 121 }) 122 } 123 124 // WalkMode is like Walk but the callback receives an additional type specifying the file mode type. 125 // N.B. This only includes the bits of the mode that determine the mode type, not the permissions. 126 func WalkMode(rootPath string, callback func(name string, isDir bool, mode os.FileMode) error) error { 127 // Compatibility with filepath.Walk which allows passing a file as the root argument. 128 if info, err := os.Lstat(rootPath); err != nil { 129 return err 130 } else if !info.IsDir() { 131 return callback(rootPath, false, info.Mode()) 132 } 133 return godirwalk.Walk(rootPath, &godirwalk.Options{Callback: func(name string, info *godirwalk.Dirent) error { 134 return callback(name, info.IsDir(), info.ModeType()) 135 }}) 136 } 137 138 // Memoize this to cut down on filesystem operations 139 var isPackageMemo = map[string]bool{} 140 var isPackageMutex sync.RWMutex 141 142 // IsPackage returns true if the given directory name is a package (i.e. contains a build file) 143 func IsPackage(buildFileNames []string, name string) bool { 144 isPackageMutex.RLock() 145 ret, present := isPackageMemo[name] 146 isPackageMutex.RUnlock() 147 if present { 148 return ret 149 } 150 ret = isPackageInternal(buildFileNames, name) 151 isPackageMutex.Lock() 152 isPackageMemo[name] = ret 153 isPackageMutex.Unlock() 154 return ret 155 } 156 157 func isPackageInternal(buildFileNames []string, name string) bool { 158 for _, buildFileName := range buildFileNames { 159 if FileExists(path.Join(name, buildFileName)) { 160 return true 161 } 162 } 163 return false 164 }