| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package pkgpattern | |
| import ( | |
| "regexp" | |
| "strings" | |
| "unicode/utf8" | |
| ) | |
| // Note: most of this code was originally part of the cmd/go/internal/search | |
| // package; it was migrated here in order to support the use case of | |
| // commands other than cmd/go that need to accept package pattern args. | |
| // TreeCanMatchPattern(pattern)(name) reports whether | |
| // name or children of name can possibly match pattern. | |
| // Pattern is the same limited glob accepted by MatchPattern. | |
| func TreeCanMatchPattern(pattern string) func(name string) bool { | |
| wildCard := false | |
| if i := strings.Index(pattern, "..."); i >= 0 { | |
| wildCard = true | |
| pattern = pattern[:i] | |
| } | |
| return func(name string) bool { | |
| return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || | |
| wildCard && strings.HasPrefix(name, pattern) | |
| } | |
| } | |
| // MatchPattern(pattern)(name) reports whether | |
| // name matches pattern. Pattern is a limited glob | |
| // pattern in which '...' means 'any string' and there | |
| // is no other special syntax. | |
| // Unfortunately, there are two special cases. Quoting "go help packages": | |
| // | |
| // First, /... at the end of the pattern can match an empty string, | |
| // so that net/... matches both net and packages in its subdirectories, like net/http. | |
| // Second, any slash-separated pattern element containing a wildcard never | |
| // participates in a match of the "vendor" element in the path of a vendored | |
| // package, so that ./... does not match packages in subdirectories of | |
| // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. | |
| // Note, however, that a directory named vendor that itself contains code | |
| // is not a vendored package: cmd/vendor would be a command named vendor, | |
| // and the pattern cmd/... matches it. | |
| func MatchPattern(pattern string) func(name string) bool { | |
| return matchPatternInternal(pattern, true) | |
| } | |
| // MatchSimplePattern returns a function that can be used to check | |
| // whether a given name matches a pattern, where pattern is a limited | |
| // glob pattern in which '...' means 'any string', with no other | |
| // special syntax. There is one special case for MatchPatternSimple: | |
| // according to the rules in "go help packages": a /... at the end of | |
| // the pattern can match an empty string, so that net/... matches both | |
| // net and packages in its subdirectories, like net/http. | |
| func MatchSimplePattern(pattern string) func(name string) bool { | |
| return matchPatternInternal(pattern, false) | |
| } | |
| func matchPatternInternal(pattern string, vendorExclude bool) func(name string) bool { | |
| // Convert pattern to regular expression. | |
| // The strategy for the trailing /... is to nest it in an explicit ? expression. | |
| // The strategy for the vendor exclusion is to change the unmatchable | |
| // vendor strings to a disallowed code point (vendorChar) and to use | |
| // "(anything but that codepoint)*" as the implementation of the ... wildcard. | |
| // This is a bit complicated but the obvious alternative, | |
| // namely a hand-written search like in most shell glob matchers, | |
| // is too easy to make accidentally exponential. | |
| // Using package regexp guarantees linear-time matching. | |
| const vendorChar = "\x00" | |
| if vendorExclude && strings.Contains(pattern, vendorChar) || !utf8.ValidString(pattern) { | |
| return func(name string) bool { return false } | |
| } | |
| re := regexp.QuoteMeta(pattern) | |
| wild := `.*` | |
| if vendorExclude { | |
| wild = `[^` + vendorChar + `]*` | |
| re = replaceVendor(re, vendorChar) | |
| switch { | |
| case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): | |
| re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` | |
| case re == vendorChar+`/\.\.\.`: | |
| re = `(/vendor|/` + vendorChar + `/\.\.\.)` | |
| } | |
| } | |
| if strings.HasSuffix(re, `/\.\.\.`) { | |
| re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` | |
| } | |
| re = strings.ReplaceAll(re, `\.\.\.`, wild) | |
| reg := regexp.MustCompile(`^` + re + `$`) | |
| return func(name string) bool { | |
| if vendorExclude { | |
| if strings.Contains(name, vendorChar) { | |
| return false | |
| } | |
| name = replaceVendor(name, vendorChar) | |
| } | |
| return reg.MatchString(name) | |
| } | |
| } | |
| // hasPathPrefix reports whether the path s begins with the | |
| // elements in prefix. | |
| func hasPathPrefix(s, prefix string) bool { | |
| switch { | |
| default: | |
| return false | |
| case len(s) == len(prefix): | |
| return s == prefix | |
| case len(s) > len(prefix): | |
| if prefix != "" && prefix[len(prefix)-1] == '/' { | |
| return strings.HasPrefix(s, prefix) | |
| } | |
| return s[len(prefix)] == '/' && s[:len(prefix)] == prefix | |
| } | |
| } | |
| // replaceVendor returns the result of replacing | |
| // non-trailing vendor path elements in x with repl. | |
| func replaceVendor(x, repl string) string { | |
| if !strings.Contains(x, "vendor") { | |
| return x | |
| } | |
| elem := strings.Split(x, "/") | |
| for i := 0; i < len(elem)-1; i++ { | |
| if elem[i] == "vendor" { | |
| elem[i] = repl | |
| } | |
| } | |
| return strings.Join(elem, "/") | |
| } | |