// Package exclude compiles exclude patterns into globs. package exclude import ( "errors" "fmt" "strings" "github.com/gobwas/glob" ) var errBadExcludePattern = errors.New("is not a valid exclude pattern") // Compile compiles one exclude pattern into the globs that match it. // anchored reports whether the pattern matches only from the root of the // source directory. An empty pattern yields no globs. Config.Validate and // the scanner both compile patterns with it, so a pattern that loads is one // the scanner can use. func Compile(pattern string) ([]glob.Glob, bool, error) { if pattern == "" { return nil, false, nil } // A leading "/" anchors the pattern to the source directory. trimmed, anchored := strings.CutPrefix(pattern, "/") // A trailing "/" marks a directory and is not matched. trimmed = strings.TrimSuffix(trimmed, "/") // A leading "**/" demands a "/" before the name and so misses an // entry directly under the source path. It means "at every depth", // which is what an unanchored pattern already does, so drop the // prefix and match unanchored: as in gitignore, "**/x" and // "/**/x" then also match a top-level "x". if rest, found := strings.CutPrefix(trimmed, "**/"); found { trimmed = rest anchored = false } // Likewise "/**/" inside a pattern demands two slashes, so // "a/**/*.log" misses "a/x.log". As in gitignore it stands for // zero or more directories, so also compile the pattern with it // made a single "/". The glob library's {a,b} is no help: with a // "**" inside it in mid-pattern, it misses paths it should match. var globs []glob.Glob for _, variant := range doubleStarVariants(trimmed) { g, err := glob.Compile(variant, '/') if err != nil { return nil, false, fmt.Errorf("%q %w: %w", pattern, errBadExcludePattern, err) } globs = append(globs, g) } return globs, anchored, nil } // doubleStarVariants returns pattern once for every way of keeping each // "/**/" in it or making it a single "/". func doubleStarVariants(pattern string) []string { const doubleStar = "/**/" before, after, found := strings.Cut(pattern, doubleStar) if !found { return []string{pattern} } var variants []string for _, rest := range doubleStarVariants(after) { variants = append(variants, before+doubleStar+rest, before+"/"+rest) } return variants }