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