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  }