// 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" ) // 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) { 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, "/") }