github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/internal/ignore/rules.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package ignore 18 19 import ( 20 "bufio" 21 "bytes" 22 "io" 23 "log" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/pkg/errors" 29 ) 30 31 // HelmIgnore default name of an ignorefile. 32 const HelmIgnore = ".helmignore" 33 34 // Rules is a collection of path matching rules. 35 // 36 // Parse() and ParseFile() will construct and populate new Rules. 37 // Empty() will create an immutable empty ruleset. 38 type Rules struct { 39 patterns []*pattern 40 } 41 42 // Empty builds an empty ruleset. 43 func Empty() *Rules { 44 return &Rules{patterns: []*pattern{}} 45 } 46 47 // AddDefaults adds default ignore patterns. 48 // 49 // Ignore all dotfiles in "templates/" 50 func (r *Rules) AddDefaults() { 51 r.parseRule(`templates/.?*`) 52 } 53 54 // ParseFile parses a helmignore file and returns the *Rules. 55 func ParseFile(file string) (*Rules, error) { 56 f, err := os.Open(file) 57 if err != nil { 58 return nil, err 59 } 60 defer f.Close() 61 return Parse(f) 62 } 63 64 // Parse parses a rules file 65 func Parse(file io.Reader) (*Rules, error) { 66 r := &Rules{patterns: []*pattern{}} 67 68 s := bufio.NewScanner(file) 69 currentLine := 0 70 utf8bom := []byte{0xEF, 0xBB, 0xBF} 71 for s.Scan() { 72 scannedBytes := s.Bytes() 73 // We trim UTF8 BOM 74 if currentLine == 0 { 75 scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) 76 } 77 line := string(scannedBytes) 78 currentLine++ 79 80 if err := r.parseRule(line); err != nil { 81 return r, err 82 } 83 } 84 return r, s.Err() 85 } 86 87 // Ignore evaluates the file at the given path, and returns true if it should be ignored. 88 // 89 // Ignore evaluates path against the rules in order. Evaluation stops when a match 90 // is found. Matching a negative rule will stop evaluation. 91 func (r *Rules) Ignore(path string, fi os.FileInfo) bool { 92 // Don't match on empty dirs. 93 if path == "" { 94 return false 95 } 96 97 // Disallow ignoring the current working directory. 98 // See issue: 99 // 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?" 100 if path == "." || path == "./" { 101 return false 102 } 103 for _, p := range r.patterns { 104 if p.match == nil { 105 log.Printf("ignore: no matcher supplied for %q", p.raw) 106 return false 107 } 108 109 // For negative rules, we need to capture and return non-matches, 110 // and continue for matches. 111 if p.negate { 112 if p.mustDir && !fi.IsDir() { 113 return true 114 } 115 if !p.match(path, fi) { 116 return true 117 } 118 continue 119 } 120 121 // If the rule is looking for directories, and this is not a directory, 122 // skip it. 123 if p.mustDir && !fi.IsDir() { 124 continue 125 } 126 if p.match(path, fi) { 127 return true 128 } 129 } 130 return false 131 } 132 133 // parseRule parses a rule string and creates a pattern, which is then stored in the Rules object. 134 func (r *Rules) parseRule(rule string) error { 135 rule = strings.TrimSpace(rule) 136 137 // Ignore blank lines 138 if rule == "" { 139 return nil 140 } 141 // Comment 142 if strings.HasPrefix(rule, "#") { 143 return nil 144 } 145 146 // Fail any rules that contain ** 147 if strings.Contains(rule, "**") { 148 return errors.New("double-star (**) syntax is not supported") 149 } 150 151 // Fail any patterns that can't compile. A non-empty string must be 152 // given to Match() to avoid optimization that skips rule evaluation. 153 if _, err := filepath.Match(rule, "abc"); err != nil { 154 return err 155 } 156 157 p := &pattern{raw: rule} 158 159 // Negation is handled at a higher level, so strip the leading ! from the 160 // string. 161 if strings.HasPrefix(rule, "!") { 162 p.negate = true 163 rule = rule[1:] 164 } 165 166 // Directory verification is handled by a higher level, so the trailing / 167 // is removed from the rule. That way, a directory named "foo" matches, 168 // even if the supplied string does not contain a literal slash character. 169 if strings.HasSuffix(rule, "/") { 170 p.mustDir = true 171 rule = strings.TrimSuffix(rule, "/") 172 } 173 174 if strings.HasPrefix(rule, "/") { 175 // Require path matches the root path. 176 p.match = func(n string, fi os.FileInfo) bool { 177 rule = strings.TrimPrefix(rule, "/") 178 ok, err := filepath.Match(rule, n) 179 if err != nil { 180 log.Printf("Failed to compile %q: %s", rule, err) 181 return false 182 } 183 return ok 184 } 185 } else if strings.Contains(rule, "/") { 186 // require structural match. 187 p.match = func(n string, fi os.FileInfo) bool { 188 ok, err := filepath.Match(rule, n) 189 if err != nil { 190 log.Printf("Failed to compile %q: %s", rule, err) 191 return false 192 } 193 return ok 194 } 195 } else { 196 p.match = func(n string, fi os.FileInfo) bool { 197 // When there is no slash in the pattern, we evaluate ONLY the 198 // filename. 199 n = filepath.Base(n) 200 ok, err := filepath.Match(rule, n) 201 if err != nil { 202 log.Printf("Failed to compile %q: %s", rule, err) 203 return false 204 } 205 return ok 206 } 207 } 208 209 r.patterns = append(r.patterns, p) 210 return nil 211 } 212 213 // matcher is a function capable of computing a match. 214 // 215 // It returns true if the rule matches. 216 type matcher func(name string, fi os.FileInfo) bool 217 218 // pattern describes a pattern to be matched in a rule set. 219 type pattern struct { 220 // raw is the unparsed string, with nothing stripped. 221 raw string 222 // match is the matcher function. 223 match matcher 224 // negate indicates that the rule's outcome should be negated. 225 negate bool 226 // mustDir indicates that the matched file must be a directory. 227 mustDir bool 228 }