1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package pkgpattern 6 7 import ( 8 "regexp" 9 "strings" 10 ) 11 12 // Note: most of this code was originally part of the cmd/go/internal/search 13 // package; it was migrated here in order to support the use case of 14 // commands other than cmd/go that need to accept package pattern args. 15 16 // TreeCanMatchPattern(pattern)(name) reports whether 17 // name or children of name can possibly match pattern. 18 // Pattern is the same limited glob accepted by MatchPattern. 19 func TreeCanMatchPattern(pattern string) func(name string) bool { 20 wildCard := false 21 if i := strings.Index(pattern, "..."); i >= 0 { 22 wildCard = true 23 pattern = pattern[:i] 24 } 25 return func(name string) bool { 26 return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || 27 wildCard && strings.HasPrefix(name, pattern) 28 } 29 } 30 31 // MatchPattern(pattern)(name) reports whether 32 // name matches pattern. Pattern is a limited glob 33 // pattern in which '...' means 'any string' and there 34 // is no other special syntax. 35 // Unfortunately, there are two special cases. Quoting "go help packages": 36 // 37 // First, /... at the end of the pattern can match an empty string, 38 // so that net/... matches both net and packages in its subdirectories, like net/http. 39 // Second, any slash-separated pattern element containing a wildcard never 40 // participates in a match of the "vendor" element in the path of a vendored 41 // package, so that ./... does not match packages in subdirectories of 42 // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. 43 // Note, however, that a directory named vendor that itself contains code 44 // is not a vendored package: cmd/vendor would be a command named vendor, 45 // and the pattern cmd/... matches it. 46 func MatchPattern(pattern string) func(name string) bool { 47 return matchPatternInternal(pattern, true) 48 } 49 50 // MatchSimplePattern returns a function that can be used to check 51 // whether a given name matches a pattern, where pattern is a limited 52 // glob pattern in which '...' means 'any string', with no other 53 // special syntax. There is one special case for MatchPatternSimple: 54 // according to the rules in "go help packages": a /... at the end of 55 // the pattern can match an empty string, so that net/... matches both 56 // net and packages in its subdirectories, like net/http. 57 func MatchSimplePattern(pattern string) func(name string) bool { 58 return matchPatternInternal(pattern, false) 59 } 60 61 func matchPatternInternal(pattern string, vendorExclude bool) func(name string) bool { 62 // Convert pattern to regular expression. 63 // The strategy for the trailing /... is to nest it in an explicit ? expression. 64 // The strategy for the vendor exclusion is to change the unmatchable 65 // vendor strings to a disallowed code point (vendorChar) and to use 66 // "(anything but that codepoint)*" as the implementation of the ... wildcard. 67 // This is a bit complicated but the obvious alternative, 68 // namely a hand-written search like in most shell glob matchers, 69 // is too easy to make accidentally exponential. 70 // Using package regexp guarantees linear-time matching. 71 72 const vendorChar = "\x00" 73 74 if vendorExclude && strings.Contains(pattern, vendorChar) { 75 return func(name string) bool { return false } 76 } 77 78 re := regexp.QuoteMeta(pattern) 79 wild := `.*` 80 if vendorExclude { 81 wild = `[^` + vendorChar + `]*` 82 re = replaceVendor(re, vendorChar) 83 switch { 84 case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): 85 re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` 86 case re == vendorChar+`/\.\.\.`: 87 re = `(/vendor|/` + vendorChar + `/\.\.\.)` 88 } 89 } 90 if strings.HasSuffix(re, `/\.\.\.`) { 91 re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` 92 } 93 re = strings.ReplaceAll(re, `\.\.\.`, wild) 94 95 reg := regexp.MustCompile(`^` + re + `$`) 96 97 return func(name string) bool { 98 if vendorExclude { 99 if strings.Contains(name, vendorChar) { 100 return false 101 } 102 name = replaceVendor(name, vendorChar) 103 } 104 return reg.MatchString(name) 105 } 106 } 107 108 // hasPathPrefix reports whether the path s begins with the 109 // elements in prefix. 110 func hasPathPrefix(s, prefix string) bool { 111 switch { 112 default: 113 return false 114 case len(s) == len(prefix): 115 return s == prefix 116 case len(s) > len(prefix): 117 if prefix != "" && prefix[len(prefix)-1] == '/' { 118 return strings.HasPrefix(s, prefix) 119 } 120 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix 121 } 122 } 123 124 // replaceVendor returns the result of replacing 125 // non-trailing vendor path elements in x with repl. 126 func replaceVendor(x, repl string) string { 127 if !strings.Contains(x, "vendor") { 128 return x 129 } 130 elem := strings.Split(x, "/") 131 for i := 0; i < len(elem)-1; i++ { 132 if elem[i] == "vendor" { 133 elem[i] = repl 134 } 135 } 136 return strings.Join(elem, "/") 137 } 138