...

Source file src/helm.sh/helm/v3/pkg/ignore/rules.go

Documentation: helm.sh/helm/v3/pkg/ignore

     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, _ 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, _ 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, _ 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  }
   229  

View as plain text