feat(ignore): add .stignore escaping on Windows (#10205)
Based on the discussion in https://forum.syncthing.net/t/towards-syncthing-2-0/24072/35 This PR adds the ability for Windows users to use the pipe character (|) to escape the metacharacters *, ?, [, and { in .stignore files. Additionally, this PR adds the ability for the user to set the escape character to backslash, or any character they want, by adding a line in the form: #escape=X (where X is any single rune), to the top of an .stignore file. This would allow users to use the same .stignore file across platforms, by simply adding #escape=\ to the top of the file. ### Testing All tests pass in CI. ### Documentation See https://github.com/syncthing/docs/pull/919 Fixes #10057: Support escaping in .stignore files on Windows Fixes #7547: Ignore pattern with \[ and \] does not work
This commit is contained in:
+55
-1
@@ -13,9 +13,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
@@ -27,6 +29,17 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
const escapePrefix = "#escape"
|
||||
|
||||
var defaultEscapeChar = '\\'
|
||||
|
||||
func init() {
|
||||
if os.PathSeparator == defaultEscapeChar {
|
||||
// The pipe character (|) is not allowed in filenames on Windows
|
||||
defaultEscapeChar = '|'
|
||||
}
|
||||
}
|
||||
|
||||
// A ParseError signifies an error with contents of an ignore file,
|
||||
// including I/O errors on included files. An I/O error on the root level
|
||||
// ignore file is not a ParseError.
|
||||
@@ -238,6 +251,7 @@ func (m *Matcher) Match(file string) (result ignoreresult.R) {
|
||||
return ignoreresult.NotIgnored
|
||||
}
|
||||
|
||||
// Change backslashes to slashes (on Windows only)
|
||||
file = filepath.ToSlash(file)
|
||||
|
||||
if m.matches != nil {
|
||||
@@ -504,8 +518,36 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
escapeChar := defaultEscapeChar
|
||||
|
||||
var err error
|
||||
escapePrefixSeen := false
|
||||
includedPatterns := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, escapePrefix) {
|
||||
if escapePrefixSeen {
|
||||
return nil, nil, errors.New("mutiple #escape= lines found in ignore file")
|
||||
}
|
||||
|
||||
if len(patterns)-includedPatterns > 0 {
|
||||
return nil, nil, errors.New("#escape= line found after patterns in ignore file")
|
||||
}
|
||||
|
||||
escapePrefixSeen = true
|
||||
trimmed := strings.TrimSpace(strings.TrimPrefix(line, escapePrefix))
|
||||
before, esc, ok := strings.Cut(trimmed, "=")
|
||||
if ok && before == "" {
|
||||
esc = strings.TrimSpace(esc)
|
||||
// avoids allocation of a new slice.
|
||||
if utf8.RuneCountInString(esc) == 1 {
|
||||
escapeChar, _ = utf8.DecodeRuneInString(esc)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("failed to parse #escape= line in ignore file: %q", line)
|
||||
}
|
||||
|
||||
if _, ok := linesSeen[line]; ok {
|
||||
continue
|
||||
}
|
||||
@@ -517,7 +559,18 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
|
||||
continue
|
||||
}
|
||||
|
||||
line = filepath.ToSlash(line)
|
||||
if escapeChar != '\\' {
|
||||
// ToSlash changes backslashes to forward slashes on Windows only,
|
||||
// so we only need to do this, if escapeChar is not a backslash.
|
||||
// If escapeChar is a backslash, then the user is using forward
|
||||
// slashes for path separators, and we leave backslashes alone.
|
||||
line = filepath.ToSlash(line)
|
||||
// Replace all escapeChars with backslashes
|
||||
line = strings.ReplaceAll(line, string(escapeChar), `\`)
|
||||
// Now restore double escapeChars to actually escape the escapeChar.
|
||||
line = strings.ReplaceAll(line, `\\`, `\`+string(escapeChar))
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "#include"):
|
||||
fields := strings.SplitN(line, " ", 2)
|
||||
@@ -536,6 +589,7 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
|
||||
var includePatterns []Pattern
|
||||
if includePatterns, err = loadParseIncludeFile(fs, includeFile, cd, linesSeen); err == nil {
|
||||
patterns = append(patterns, includePatterns...)
|
||||
includedPatterns += len(includePatterns)
|
||||
} else {
|
||||
// Wrap the error, as if the include does not exist, we get a
|
||||
// IsNotExists(err) == true error, which we use to check
|
||||
|
||||
Reference in New Issue
Block a user