glob.go (3496B)
1 // Copyright 2021 The Hugo Authors. 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 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package glob 15 16 import ( 17 "os" 18 "path" 19 "path/filepath" 20 "runtime" 21 "strings" 22 "sync" 23 24 "github.com/gobwas/glob" 25 "github.com/gobwas/glob/syntax" 26 ) 27 28 const filepathSeparator = string(os.PathSeparator) 29 30 var ( 31 isWindows = runtime.GOOS == "windows" 32 defaultGlobCache = &globCache{ 33 isCaseSensitive: false, 34 isWindows: isWindows, 35 cache: make(map[string]globErr), 36 } 37 38 filenamesGlobCache = &globCache{ 39 isCaseSensitive: false, // As long as the search strings are all lower case, this does not allocate. 40 isWindows: isWindows, 41 cache: make(map[string]globErr), 42 } 43 ) 44 45 type globErr struct { 46 glob glob.Glob 47 err error 48 } 49 50 type globCache struct { 51 // Config 52 isCaseSensitive bool 53 isWindows bool 54 55 // Cache 56 sync.RWMutex 57 cache map[string]globErr 58 } 59 60 func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) { 61 var eg globErr 62 63 gc.RLock() 64 var found bool 65 eg, found = gc.cache[pattern] 66 gc.RUnlock() 67 if found { 68 return eg.glob, eg.err 69 } 70 71 var g glob.Glob 72 var err error 73 74 pattern = filepath.ToSlash(pattern) 75 76 if gc.isCaseSensitive { 77 g, err = glob.Compile(pattern, '/') 78 } else { 79 g, err = glob.Compile(strings.ToLower(pattern), '/') 80 81 } 82 83 eg = globErr{ 84 globDecorator{ 85 g: g, 86 isCaseSensitive: gc.isCaseSensitive, 87 isWindows: gc.isWindows}, 88 err, 89 } 90 91 gc.Lock() 92 gc.cache[pattern] = eg 93 gc.Unlock() 94 95 return eg.glob, eg.err 96 } 97 98 type globDecorator struct { 99 // Whether both pattern and the strings to match will be matched 100 // by their original case. 101 isCaseSensitive bool 102 103 // On Windows we may get filenames with Windows slashes to match, 104 // which wee need to normalize. 105 isWindows bool 106 107 g glob.Glob 108 } 109 110 func (g globDecorator) Match(s string) bool { 111 if g.isWindows { 112 s = filepath.ToSlash(s) 113 } 114 if !g.isCaseSensitive { 115 s = strings.ToLower(s) 116 } 117 return g.g.Match(s) 118 } 119 120 func GetGlob(pattern string) (glob.Glob, error) { 121 return defaultGlobCache.GetGlob(pattern) 122 } 123 124 func NormalizePath(p string) string { 125 return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.") 126 } 127 128 // ResolveRootDir takes a normalized path on the form "assets/**.json" and 129 // determines any root dir, i.e. any start path without any wildcards. 130 func ResolveRootDir(p string) string { 131 parts := strings.Split(path.Dir(p), "/") 132 var roots []string 133 for _, part := range parts { 134 if HasGlobChar(part) { 135 break 136 } 137 roots = append(roots, part) 138 } 139 140 if len(roots) == 0 { 141 return "" 142 } 143 144 return strings.Join(roots, "/") 145 } 146 147 // FilterGlobParts removes any string with glob wildcard. 148 func FilterGlobParts(a []string) []string { 149 b := a[:0] 150 for _, x := range a { 151 if !HasGlobChar(x) { 152 b = append(b, x) 153 } 154 } 155 return b 156 } 157 158 // HasGlobChar returns whether s contains any glob wildcards. 159 func HasGlobChar(s string) bool { 160 for i := 0; i < len(s); i++ { 161 if syntax.Special(s[i]) { 162 return true 163 } 164 } 165 return false 166 }