package config import ( "errors" "fmt" "strings" "github.com/gobwas/glob" ) var errBadExcludePattern = errors.New("is not a valid exclude pattern") // CompileExcludePattern 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 CompileExcludePattern(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("%w: %w", 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 } // validateExcludes refuses an exclude pattern, global or in a snapshot, // that does not compile, naming the setting and quoting the pattern. func (c *Config) validateExcludes() error { err := validateExcludeList("exclude", c.Exclude) if err != nil { return err } for _, name := range c.SnapshotNames() { err = validateExcludeList("snapshots."+name+".exclude", c.Snapshots[name].Exclude) if err != nil { return err } } return nil } // validateExcludeList refuses the first pattern in patterns that does not // compile. setting is the name of the list, as in "exclude". func validateExcludeList(setting string, patterns []string) error { for i, pattern := range patterns { _, _, err := CompileExcludePattern(pattern) if err != nil { return fmt.Errorf("%s[%d]: %q %w", setting, i, pattern, err) } } return nil }