github.com/wrdlbrnft/go-gitignore@v0.0.0-20201129201858-74ef740b8b77/pattern.go (about)

     1  package gitignore
     2  
     3  import (
     4  	"path/filepath"
     5  	"strings"
     6  
     7  	"github.com/danwakefield/fnmatch"
     8  )
     9  
    10  // Pattern represents per-line patterns within a .gitignore file
    11  type Pattern interface {
    12  	Match
    13  
    14  	// Match returns true if the given path matches the name pattern. If the
    15  	// pattern is meant for directories only, and the path is not a directory,
    16  	// Match will return false. The matching is performed by fnmatch(). It
    17  	// is assumed path is relative to the base path of the owning GitIgnore.
    18  	Match(string, bool) bool
    19  }
    20  
    21  // pattern is the base implementation of a .gitignore pattern
    22  type pattern struct {
    23  	_negated   bool
    24  	_anchored  bool
    25  	_directory bool
    26  	_string    string
    27  	_fnmatch   string
    28  	_position  Position
    29  } // pattern()
    30  
    31  // name represents patterns matching a file or path name (i.e. the last
    32  // component of a path)
    33  type name struct {
    34  	pattern
    35  } // name{}
    36  
    37  // path represents a pattern that contains at least one path separator within
    38  // the pattern (i.e. not at the start or end of the pattern)
    39  type path struct {
    40  	pattern
    41  	_depth int
    42  } // path{}
    43  
    44  // any represents a pattern that contains at least one "any" token "**"
    45  // allowing for recursive matching.
    46  type any struct {
    47  	pattern
    48  	_tokens []*Token
    49  } // any{}
    50  
    51  // NewPattern returns a Pattern from the ordered slice of Tokens. The tokens are
    52  // assumed to represent a well-formed .gitignore pattern. A Pattern may be
    53  // negated, anchored to the start of the path (relative to the base directory
    54  // of tie containing .gitignore), or match directories only.
    55  func NewPattern(tokens []*Token) Pattern {
    56  	// if we have no tokens there is no pattern
    57  	if len(tokens) == 0 {
    58  		return nil
    59  	}
    60  
    61  	// extract the pattern position from first token
    62  	_position := tokens[0].Position
    63  	_string := tokenset(tokens).String()
    64  
    65  	// is this a negated pattern?
    66  	_negated := false
    67  	if tokens[0].Type == NEGATION {
    68  		_negated = true
    69  		tokens = tokens[1:]
    70  	}
    71  
    72  	// is this pattern anchored to the start of the path?
    73  	_anchored := false
    74  	if tokens[0].Type == SEPARATOR {
    75  		_anchored = true
    76  		tokens = tokens[1:]
    77  	}
    78  
    79  	// is this pattern for directories only?
    80  	_directory := false
    81  	_last := len(tokens) - 1
    82  	if tokens[_last].Type == SEPARATOR {
    83  		_directory = true
    84  		tokens = tokens[:_last]
    85  	}
    86  
    87  	// build the pattern expression
    88  	_fnmatch := tokenset(tokens).String()
    89  	_pattern := &pattern{
    90  		_negated:   _negated,
    91  		_anchored:  _anchored,
    92  		_position:  _position,
    93  		_directory: _directory,
    94  		_string:    _string,
    95  		_fnmatch:   _fnmatch,
    96  	}
    97  	return _pattern.compile(tokens)
    98  } // NewPattern()
    99  
   100  // compile generates a specific Pattern (i.e. name, path or any)
   101  // represented by the list of tokens.
   102  func (p *pattern) compile(tokens []*Token) Pattern {
   103  	// what tokens do we have in this pattern?
   104  	//      - ANY token means we can match to any depth
   105  	//      - SEPARATOR means we have path rather than file matching
   106  	_separator := false
   107  	for _, _token := range tokens {
   108  		switch _token.Type {
   109  		case ANY:
   110  			return p.any(tokens)
   111  		case SEPARATOR:
   112  			_separator = true
   113  		}
   114  	}
   115  
   116  	// should we perform path or name/file matching?
   117  	if _separator {
   118  		return p.path(tokens)
   119  	} else {
   120  		return p.name(tokens)
   121  	}
   122  } // compile()
   123  
   124  // Ignore returns true if the pattern describes files or paths that should be
   125  // ignored.
   126  func (p *pattern) Ignore() bool { return !p._negated }
   127  
   128  // Include returns true if the pattern describes files or paths that should be
   129  // included (i.e. not ignored)
   130  func (p *pattern) Include() bool { return p._negated }
   131  
   132  // Position returns the position of the first token of this pattern.
   133  func (p *pattern) Position() Position { return p._position }
   134  
   135  // String returns the string representation of the pattern.
   136  func (p *pattern) String() string { return p._string }
   137  
   138  //
   139  // name patterns
   140  //      - designed to match trailing file/directory names only
   141  //
   142  
   143  // name returns a Pattern designed to match file or directory names, with no
   144  // path elements.
   145  func (p *pattern) name(tokens []*Token) Pattern {
   146  	return &name{*p}
   147  } // name()
   148  
   149  // Match returns true if the given path matches the name pattern. If the
   150  // pattern is meant for directories only, and the path is not a directory,
   151  // Match will return false. The matching is performed by fnmatch(). It
   152  // is assumed path is relative to the base path of the owning GitIgnore.
   153  func (n *name) Match(path string, isdir bool) bool {
   154  	// are we expecting a directory?
   155  	if n._directory && !isdir {
   156  		return false
   157  	}
   158  
   159  	// should we match the whole path, or just the last component?
   160  	if n._anchored {
   161  		return fnmatch.Match(n._fnmatch, path, 0)
   162  	} else {
   163  		_, _base := filepath.Split(path)
   164  		return fnmatch.Match(n._fnmatch, _base, 0)
   165  	}
   166  } // Match()
   167  
   168  //
   169  // path patterns
   170  //      - designed to match complete or partial paths (not just filenames)
   171  //
   172  
   173  // path returns a Pattern designed to match paths that include at least one
   174  // path separator '/' neither at the end nor the start of the pattern.
   175  func (p *pattern) path(tokens []*Token) Pattern {
   176  	// how many directory components are we expecting?
   177  	_depth := 0
   178  	for _, _token := range tokens {
   179  		if _token.Type == SEPARATOR {
   180  			_depth++
   181  		}
   182  	}
   183  
   184  	// return the pattern instance
   185  	return &path{pattern: *p, _depth: _depth}
   186  } // path()
   187  
   188  // Match returns true if the given path matches the path pattern. If the
   189  // pattern is meant for directories only, and the path is not a directory,
   190  // Match will return false. The matching is performed by fnmatch()
   191  // with flags set to FNM_PATHNAME. It is assumed path is relative to the
   192  // base path of the owning GitIgnore.
   193  func (p *path) Match(path string, isdir bool) bool {
   194  	// are we expecting a directory
   195  	if p._directory && !isdir {
   196  		return false
   197  	}
   198  
   199  	if fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME) {
   200  		return true
   201  	} else if p._anchored {
   202  		return false
   203  	}
   204  
   205  	// match against the trailing path elements
   206  	return fnmatch.Match(p._fnmatch, path, fnmatch.FNM_PATHNAME)
   207  } // Match()
   208  
   209  //
   210  // "any" patterns
   211  //
   212  
   213  // any returns a Pattern designed to match paths that include at least one
   214  // any pattern '**', specifying recursive matching.
   215  func (p *pattern) any(tokens []*Token) Pattern {
   216  	// consider only the non-SEPARATOR tokens, as these will be matched
   217  	// against the path components
   218  	_tokens := make([]*Token, 0)
   219  	for _, _token := range tokens {
   220  		if _token.Type != SEPARATOR {
   221  			_tokens = append(_tokens, _token)
   222  		}
   223  	}
   224  
   225  	return &any{*p, _tokens}
   226  } // any()
   227  
   228  // Match returns true if the given path matches the any pattern. If the
   229  // pattern is meant for directories only, and the path is not a directory,
   230  // Match will return false. The matching is performed by recursively applying
   231  // fnmatch() with flags set to FNM_PATHNAME. It is assumed path is relative to
   232  // the base path of the owning GitIgnore.
   233  func (a *any) Match(path string, isdir bool) bool {
   234  	// are we expecting a directory?
   235  	if a._directory && !isdir {
   236  		return false
   237  	}
   238  
   239  	// split the path into components
   240  	_parts := strings.Split(path, string(_SEPARATOR))
   241  
   242  	// attempt to match the parts against the pattern tokens
   243  	return a.match(_parts, a._tokens)
   244  } // Match()
   245  
   246  // match performs the recursive matching for 'any' patterns. An 'any'
   247  // token '**' may match any path component, or no path component.
   248  func (a *any) match(path []string, tokens []*Token) bool {
   249  	// if we have no more tokens, then we have matched this path
   250  	// if there are also no more path elements, otherwise there's no match
   251  	if len(tokens) == 0 {
   252  		return len(path) == 0
   253  	}
   254  
   255  	// what token are we trying to match?
   256  	_token := tokens[0]
   257  	switch _token.Type {
   258  	case ANY:
   259  		if len(path) == 0 {
   260  			return a.match(path, tokens[1:])
   261  		} else {
   262  			return a.match(path, tokens[1:]) || a.match(path[1:], tokens)
   263  		}
   264  
   265  	default:
   266  		// if we have a non-ANY token, then we must have a non-empty path
   267  		if len(path) != 0 {
   268  			// if the current path element matches this token,
   269  			// we match if the remainder of the path matches the
   270  			// remaining tokens
   271  			if fnmatch.Match(_token.Token(), path[0], fnmatch.FNM_PATHNAME) {
   272  				return a.match(path[1:], tokens[1:])
   273  			}
   274  		}
   275  	}
   276  
   277  	// if we are here, then we have no match
   278  	return false
   279  } // match()
   280  
   281  // ensure the patterns confirm to the Pattern interface
   282  var _ Pattern = &name{}
   283  var _ Pattern = &path{}
   284  var _ Pattern = &any{}