github.com/flant/helm@v2.8.1+incompatible/pkg/ignore/rules.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     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  	"errors"
    22  	"io"
    23  	"log"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  )
    28  
    29  // HelmIgnore default name of an ignorefile.
    30  const HelmIgnore = ".helmignore"
    31  
    32  // Rules is a collection of path matching rules.
    33  //
    34  // Parse() and ParseFile() will construct and populate new Rules.
    35  // Empty() will create an immutable empty ruleset.
    36  type Rules struct {
    37  	patterns []*pattern
    38  }
    39  
    40  // Empty builds an empty ruleset.
    41  func Empty() *Rules {
    42  	return &Rules{patterns: []*pattern{}}
    43  }
    44  
    45  // AddDefaults adds default ignore patterns.
    46  //
    47  // Ignore all dotfiles in "templates/"
    48  func (r *Rules) AddDefaults() {
    49  	r.parseRule(`templates/.?*`)
    50  }
    51  
    52  // ParseFile parses a helmignore file and returns the *Rules.
    53  func ParseFile(file string) (*Rules, error) {
    54  	f, err := os.Open(file)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	defer f.Close()
    59  	return Parse(f)
    60  }
    61  
    62  // Parse parses a rules file
    63  func Parse(file io.Reader) (*Rules, error) {
    64  	r := &Rules{patterns: []*pattern{}}
    65  
    66  	s := bufio.NewScanner(file)
    67  	for s.Scan() {
    68  		if err := r.parseRule(s.Text()); err != nil {
    69  			return r, err
    70  		}
    71  	}
    72  	return r, s.Err()
    73  }
    74  
    75  // Len returns the number of patterns in this rule set.
    76  func (r *Rules) Len() int {
    77  	return len(r.patterns)
    78  }
    79  
    80  // Ignore evalutes the file at the given path, and returns true if it should be ignored.
    81  //
    82  // Ignore evaluates path against the rules in order. Evaluation stops when a match
    83  // is found. Matching a negative rule will stop evaluation.
    84  func (r *Rules) Ignore(path string, fi os.FileInfo) bool {
    85  	// Don't match on empty dirs.
    86  	if path == "" {
    87  		return false
    88  	}
    89  
    90  	// Disallow ignoring the current working directory.
    91  	// See issue:
    92  	// 1776 (New York City) Hamilton: "Pardon me, are you Aaron Burr, sir?"
    93  	if path == "." || path == "./" {
    94  		return false
    95  	}
    96  	for _, p := range r.patterns {
    97  		if p.match == nil {
    98  			log.Printf("ignore: no matcher supplied for %q", p.raw)
    99  			return false
   100  		}
   101  
   102  		// For negative rules, we need to capture and return non-matches,
   103  		// and continue for matches.
   104  		if p.negate {
   105  			if p.mustDir && !fi.IsDir() {
   106  				return true
   107  			}
   108  			if !p.match(path, fi) {
   109  				return true
   110  			}
   111  			continue
   112  		}
   113  
   114  		// If the rule is looking for directories, and this is not a directory,
   115  		// skip it.
   116  		if p.mustDir && !fi.IsDir() {
   117  			continue
   118  		}
   119  		if p.match(path, fi) {
   120  			return true
   121  		}
   122  	}
   123  	return false
   124  }
   125  
   126  // parseRule parses a rule string and creates a pattern, which is then stored in the Rules object.
   127  func (r *Rules) parseRule(rule string) error {
   128  	rule = strings.TrimSpace(rule)
   129  
   130  	// Ignore blank lines
   131  	if rule == "" {
   132  		return nil
   133  	}
   134  	// Comment
   135  	if strings.HasPrefix(rule, "#") {
   136  		return nil
   137  	}
   138  
   139  	// Fail any rules that contain **
   140  	if strings.Contains(rule, "**") {
   141  		return errors.New("double-star (**) syntax is not supported")
   142  	}
   143  
   144  	// Fail any patterns that can't compile. A non-empty string must be
   145  	// given to Match() to avoid optimization that skips rule evaluation.
   146  	if _, err := filepath.Match(rule, "abc"); err != nil {
   147  		return err
   148  	}
   149  
   150  	p := &pattern{raw: rule}
   151  
   152  	// Negation is handled at a higher level, so strip the leading ! from the
   153  	// string.
   154  	if strings.HasPrefix(rule, "!") {
   155  		p.negate = true
   156  		rule = rule[1:]
   157  	}
   158  
   159  	// Directory verification is handled by a higher level, so the trailing /
   160  	// is removed from the rule. That way, a directory named "foo" matches,
   161  	// even if the supplied string does not contain a literal slash character.
   162  	if strings.HasSuffix(rule, "/") {
   163  		p.mustDir = true
   164  		rule = strings.TrimSuffix(rule, "/")
   165  	}
   166  
   167  	if strings.HasPrefix(rule, "/") {
   168  		// Require path matches the root path.
   169  		p.match = func(n string, fi os.FileInfo) bool {
   170  			rule = strings.TrimPrefix(rule, "/")
   171  			ok, err := filepath.Match(rule, n)
   172  			if err != nil {
   173  				log.Printf("Failed to compile %q: %s", rule, err)
   174  				return false
   175  			}
   176  			return ok
   177  		}
   178  	} else if strings.Contains(rule, "/") {
   179  		// require structural match.
   180  		p.match = func(n string, fi os.FileInfo) bool {
   181  			ok, err := filepath.Match(rule, n)
   182  			if err != nil {
   183  				log.Printf("Failed to compile %q: %s", rule, err)
   184  				return false
   185  			}
   186  			return ok
   187  		}
   188  	} else {
   189  		p.match = func(n string, fi os.FileInfo) bool {
   190  			// When there is no slash in the pattern, we evaluate ONLY the
   191  			// filename.
   192  			n = filepath.Base(n)
   193  			ok, err := filepath.Match(rule, n)
   194  			if err != nil {
   195  				log.Printf("Failed to compile %q: %s", rule, err)
   196  				return false
   197  			}
   198  			return ok
   199  		}
   200  	}
   201  
   202  	r.patterns = append(r.patterns, p)
   203  	return nil
   204  }
   205  
   206  // matcher is a function capable of computing a match.
   207  //
   208  // It returns true if the rule matches.
   209  type matcher func(name string, fi os.FileInfo) bool
   210  
   211  // pattern describes a pattern to be matched in a rule set.
   212  type pattern struct {
   213  	// raw is the unparsed string, with nothing stripped.
   214  	raw string
   215  	// match is the matcher function.
   216  	match matcher
   217  	// negate indicates that the rule's outcome should be negated.
   218  	negate bool
   219  	// mustDir indicates that the matched file must be a directory.
   220  	mustDir bool
   221  }