ignore.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
  2. //
  3. // This program is free software: you can redistribute it and/or modify it
  4. // under the terms of the GNU General Public License as published by the Free
  5. // Software Foundation, either version 3 of the License, or (at your option)
  6. // any later version.
  7. //
  8. // This program is distributed in the hope that it will be useful, but WITHOUT
  9. // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  10. // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  11. // more details.
  12. //
  13. // You should have received a copy of the GNU General Public License along
  14. // with this program. If not, see <http://www.gnu.org/licenses/>.
  15. package ignore
  16. import (
  17. "bufio"
  18. "fmt"
  19. "io"
  20. "os"
  21. "path/filepath"
  22. "regexp"
  23. "strings"
  24. "github.com/syncthing/syncthing/internal/fnmatch"
  25. )
  26. type Pattern struct {
  27. match *regexp.Regexp
  28. include bool
  29. }
  30. type Patterns []Pattern
  31. func Load(file string) (Patterns, error) {
  32. seen := make(map[string]bool)
  33. return loadIgnoreFile(file, seen)
  34. }
  35. func Parse(r io.Reader, file string) (Patterns, error) {
  36. seen := map[string]bool{
  37. file: true,
  38. }
  39. return parseIgnoreFile(r, file, seen)
  40. }
  41. func (l Patterns) Match(file string) bool {
  42. for _, pattern := range l {
  43. if pattern.match.MatchString(file) {
  44. return pattern.include
  45. }
  46. }
  47. return false
  48. }
  49. func loadIgnoreFile(file string, seen map[string]bool) (Patterns, error) {
  50. if seen[file] {
  51. return nil, fmt.Errorf("Multiple include of ignore file %q", file)
  52. }
  53. seen[file] = true
  54. fd, err := os.Open(file)
  55. if err != nil {
  56. return nil, err
  57. }
  58. defer fd.Close()
  59. return parseIgnoreFile(fd, file, seen)
  60. }
  61. func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Patterns, error) {
  62. var exps Patterns
  63. addPattern := func(line string) error {
  64. include := true
  65. if strings.HasPrefix(line, "!") {
  66. line = line[1:]
  67. include = false
  68. }
  69. if strings.HasPrefix(line, "/") {
  70. // Pattern is rooted in the current dir only
  71. exp, err := fnmatch.Convert(line[1:], fnmatch.FNM_PATHNAME)
  72. if err != nil {
  73. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  74. }
  75. exps = append(exps, Pattern{exp, include})
  76. } else if strings.HasPrefix(line, "**/") {
  77. // Add the pattern as is, and without **/ so it matches in current dir
  78. exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
  79. if err != nil {
  80. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  81. }
  82. exps = append(exps, Pattern{exp, include})
  83. exp, err = fnmatch.Convert(line[3:], fnmatch.FNM_PATHNAME)
  84. if err != nil {
  85. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  86. }
  87. exps = append(exps, Pattern{exp, include})
  88. } else if strings.HasPrefix(line, "#include ") {
  89. includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
  90. includes, err := loadIgnoreFile(includeFile, seen)
  91. if err != nil {
  92. return err
  93. } else {
  94. exps = append(exps, includes...)
  95. }
  96. } else {
  97. // Path name or pattern, add it so it matches files both in
  98. // current directory and subdirs.
  99. exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
  100. if err != nil {
  101. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  102. }
  103. exps = append(exps, Pattern{exp, include})
  104. exp, err = fnmatch.Convert("**/"+line, fnmatch.FNM_PATHNAME)
  105. if err != nil {
  106. return fmt.Errorf("Invalid pattern %q in ignore file", line)
  107. }
  108. exps = append(exps, Pattern{exp, include})
  109. }
  110. return nil
  111. }
  112. scanner := bufio.NewScanner(fd)
  113. var err error
  114. for scanner.Scan() {
  115. line := strings.TrimSpace(scanner.Text())
  116. switch {
  117. case line == "":
  118. continue
  119. case strings.HasPrefix(line, "//"):
  120. continue
  121. case strings.HasPrefix(line, "#"):
  122. err = addPattern(line)
  123. case strings.HasSuffix(line, "/**"):
  124. err = addPattern(line)
  125. case strings.HasSuffix(line, "/"):
  126. err = addPattern(line)
  127. if err == nil {
  128. err = addPattern(line + "**")
  129. }
  130. default:
  131. err = addPattern(line)
  132. if err == nil {
  133. err = addPattern(line + "/**")
  134. }
  135. }
  136. if err != nil {
  137. return nil, err
  138. }
  139. }
  140. return exps, nil
  141. }