Home | History | Annotate | Download | only in pathtools
      1 // Copyright 2014 Google Inc. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package pathtools
     16 
     17 import (
     18 	"errors"
     19 	"os"
     20 	"path/filepath"
     21 	"strings"
     22 )
     23 
     24 var GlobMultipleRecursiveErr = errors.New("pattern contains multiple **")
     25 var GlobLastRecursiveErr = errors.New("pattern ** as last path element")
     26 
     27 // Glob returns the list of files that match the given pattern along with the
     28 // list of directories that were searched to construct the file list.
     29 // The supported glob patterns are equivalent to filepath.Glob, with an
     30 // extension that recursive glob (** matching zero or more complete path
     31 // entries) is supported.
     32 func Glob(pattern string) (matches, dirs []string, err error) {
     33 	return GlobWithExcludes(pattern, nil)
     34 }
     35 
     36 // GlobWithExcludes returns the list of files that match the given pattern but
     37 // do not match the given exclude patterns, along with the list of directories
     38 // that were searched to construct the file list.  The supported glob and
     39 // exclude patterns are equivalent to filepath.Glob, with an extension that
     40 // recursive glob (** matching zero or more complete path entries) is supported.
     41 func GlobWithExcludes(pattern string, excludes []string) (matches, dirs []string, err error) {
     42 	if filepath.Base(pattern) == "**" {
     43 		return nil, nil, GlobLastRecursiveErr
     44 	} else {
     45 		matches, dirs, err = glob(pattern, false)
     46 	}
     47 
     48 	if err != nil {
     49 		return nil, nil, err
     50 	}
     51 
     52 	matches, err = filterExcludes(matches, excludes)
     53 	if err != nil {
     54 		return nil, nil, err
     55 	}
     56 
     57 	return matches, dirs, nil
     58 }
     59 
     60 // glob is a recursive helper function to handle globbing each level of the pattern individually,
     61 // allowing searched directories to be tracked.  Also handles the recursive glob pattern, **.
     62 func glob(pattern string, hasRecursive bool) (matches, dirs []string, err error) {
     63 	if !isWild(pattern) {
     64 		// If there are no wilds in the pattern, check whether the file exists or not.
     65 		// Uses filepath.Glob instead of manually statting to get consistent results.
     66 		pattern = filepath.Clean(pattern)
     67 		matches, err = filepath.Glob(pattern)
     68 		if err != nil {
     69 			return matches, dirs, err
     70 		}
     71 
     72 		if len(matches) == 0 {
     73 			// Some part of the non-wild pattern didn't exist.  Add the last existing directory
     74 			// as a dependency.
     75 			var matchDirs []string
     76 			for len(matchDirs) == 0 {
     77 				pattern, _ = saneSplit(pattern)
     78 				matchDirs, err = filepath.Glob(pattern)
     79 				if err != nil {
     80 					return matches, dirs, err
     81 				}
     82 			}
     83 			dirs = append(dirs, matchDirs...)
     84 		}
     85 		return matches, dirs, err
     86 	}
     87 
     88 	dir, file := saneSplit(pattern)
     89 
     90 	if file == "**" {
     91 		if hasRecursive {
     92 			return matches, dirs, GlobMultipleRecursiveErr
     93 		}
     94 		hasRecursive = true
     95 	}
     96 
     97 	dirMatches, dirs, err := glob(dir, hasRecursive)
     98 	if err != nil {
     99 		return nil, nil, err
    100 	}
    101 
    102 	for _, m := range dirMatches {
    103 		if info, _ := os.Stat(m); info.IsDir() {
    104 			if file == "**" {
    105 				recurseDirs, err := walkAllDirs(m)
    106 				if err != nil {
    107 					return nil, nil, err
    108 				}
    109 				matches = append(matches, recurseDirs...)
    110 			} else {
    111 				dirs = append(dirs, m)
    112 				newMatches, err := filepath.Glob(filepath.Join(m, file))
    113 				if err != nil {
    114 					return nil, nil, err
    115 				}
    116 				matches = append(matches, newMatches...)
    117 			}
    118 		}
    119 	}
    120 
    121 	return matches, dirs, nil
    122 }
    123 
    124 // Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
    125 // Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
    126 // not "/".  Returns ".", "" if path is "."
    127 func saneSplit(path string) (dir, file string) {
    128 	if path == "." {
    129 		return ".", ""
    130 	}
    131 	dir, file = filepath.Split(path)
    132 	switch dir {
    133 	case "":
    134 		dir = "."
    135 	case "/":
    136 		// Nothing
    137 	default:
    138 		dir = dir[:len(dir)-1]
    139 	}
    140 	return dir, file
    141 }
    142 
    143 func isWild(pattern string) bool {
    144 	return strings.ContainsAny(pattern, "*?[")
    145 }
    146 
    147 // Returns a list of all directories under dir
    148 func walkAllDirs(dir string) (dirs []string, err error) {
    149 	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    150 		if err != nil {
    151 			return err
    152 		}
    153 
    154 		if info.Mode().IsDir() {
    155 			dirs = append(dirs, path)
    156 		}
    157 		return nil
    158 	})
    159 
    160 	return dirs, err
    161 }
    162 
    163 // Filters the strings in matches based on the glob patterns in excludes.  Hierarchical (a/*) and
    164 // recursive (**) glob patterns are supported.
    165 func filterExcludes(matches []string, excludes []string) ([]string, error) {
    166 	if len(excludes) == 0 {
    167 		return matches, nil
    168 	}
    169 
    170 	var ret []string
    171 matchLoop:
    172 	for _, m := range matches {
    173 		for _, e := range excludes {
    174 			exclude, err := match(e, m)
    175 			if err != nil {
    176 				return nil, err
    177 			}
    178 			if exclude {
    179 				continue matchLoop
    180 			}
    181 		}
    182 		ret = append(ret, m)
    183 	}
    184 
    185 	return ret, nil
    186 }
    187 
    188 // match returns true if name matches pattern using the same rules as filepath.Match, but supporting
    189 // hierarchical patterns (a/*) and recursive globs (**).
    190 func match(pattern, name string) (bool, error) {
    191 	if filepath.Base(pattern) == "**" {
    192 		return false, GlobLastRecursiveErr
    193 	}
    194 
    195 	for {
    196 		var patternFile, nameFile string
    197 		pattern, patternFile = saneSplit(pattern)
    198 		name, nameFile = saneSplit(name)
    199 
    200 		if patternFile == "**" {
    201 			return matchPrefix(pattern, filepath.Join(name, nameFile))
    202 		}
    203 
    204 		if nameFile == "" && patternFile == "" {
    205 			return true, nil
    206 		} else if nameFile == "" || patternFile == "" {
    207 			return false, nil
    208 		}
    209 
    210 		match, err := filepath.Match(patternFile, nameFile)
    211 		if err != nil || !match {
    212 			return match, err
    213 		}
    214 	}
    215 }
    216 
    217 // matchPrefix returns true if the beginning of name matches pattern using the same rules as
    218 // filepath.Match, but supporting hierarchical patterns (a/*).  Recursive globs (**) are not
    219 // supported, they should have been handled in match().
    220 func matchPrefix(pattern, name string) (bool, error) {
    221 	if len(pattern) > 0 && pattern[0] == '/' {
    222 		if len(name) > 0 && name[0] == '/' {
    223 			pattern = pattern[1:]
    224 			name = name[1:]
    225 		} else {
    226 			return false, nil
    227 		}
    228 	}
    229 
    230 	for {
    231 		var patternElem, nameElem string
    232 		patternElem, pattern = saneSplitFirst(pattern)
    233 		nameElem, name = saneSplitFirst(name)
    234 
    235 		if patternElem == "." {
    236 			patternElem = ""
    237 		}
    238 		if nameElem == "." {
    239 			nameElem = ""
    240 		}
    241 
    242 		if patternElem == "**" {
    243 			return false, GlobMultipleRecursiveErr
    244 		}
    245 
    246 		if patternElem == "" {
    247 			return true, nil
    248 		} else if nameElem == "" {
    249 			return false, nil
    250 		}
    251 
    252 		match, err := filepath.Match(patternElem, nameElem)
    253 		if err != nil || !match {
    254 			return match, err
    255 		}
    256 	}
    257 }
    258 
    259 func saneSplitFirst(path string) (string, string) {
    260 	i := strings.IndexRune(path, filepath.Separator)
    261 	if i < 0 {
    262 		return path, ""
    263 	}
    264 	return path[:i], path[i+1:]
    265 }
    266 
    267 func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
    268 	var (
    269 		matches []string
    270 		deps    []string
    271 	)
    272 
    273 	globedList = make([]string, 0)
    274 	depDirs = make([]string, 0)
    275 
    276 	for _, pattern := range patterns {
    277 		if isWild(pattern) {
    278 			matches, deps, err = Glob(filepath.Join(prefix, pattern))
    279 			if err != nil {
    280 				return nil, nil, err
    281 			}
    282 			globedList = append(globedList, matches...)
    283 			depDirs = append(depDirs, deps...)
    284 		} else {
    285 			globedList = append(globedList, filepath.Join(prefix, pattern))
    286 		}
    287 	}
    288 	return globedList, depDirs, nil
    289 }
    290