github.com/boyter/gocodewalker@v1.3.2/go-gitignore/pattern.go (about)

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