pax_global_header00006660000000000000000000000064140262037250014513gustar00rootroot0000000000000052 comment=fe9680600dc19b55e87461807fbb5105e29dbbcf editorconfig-0.2.0/000077500000000000000000000000001402620372500141665ustar00rootroot00000000000000editorconfig-0.2.0/.github/000077500000000000000000000000001402620372500155265ustar00rootroot00000000000000editorconfig-0.2.0/.github/FUNDING.yml000066400000000000000000000000161402620372500173400ustar00rootroot00000000000000github: mvdan editorconfig-0.2.0/.github/workflows/000077500000000000000000000000001402620372500175635ustar00rootroot00000000000000editorconfig-0.2.0/.github/workflows/test.yml000066400000000000000000000007451402620372500212730ustar00rootroot00000000000000on: [push, pull_request] name: Test jobs: test: strategy: matrix: go-version: [1.15.x, 1.16.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - name: Test run: go test ./... - name: Test with -race run: go test -race ./... editorconfig-0.2.0/.gitmodules000066400000000000000000000001501402620372500163370ustar00rootroot00000000000000[submodule "core-test"] path = core-test url = https://github.com/editorconfig/editorconfig-core-test editorconfig-0.2.0/CMakeLists.txt000066400000000000000000000002121402620372500167210ustar00rootroot00000000000000# This file is only used as part of 'go test'. enable_testing() set(EDITORCONFIG_CMD $ENV{EDITORCONFIG_CMD}) add_subdirectory(core-test) editorconfig-0.2.0/LICENSE000066400000000000000000000027201402620372500151740ustar00rootroot00000000000000Copyright (c) 2019, Daniel Martí. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. editorconfig-0.2.0/README.md000066400000000000000000000017151402620372500154510ustar00rootroot00000000000000# editorconfig [![GoDoc](https://godoc.org/mvdan.cc/editorconfig?status.svg)](https://godoc.org/mvdan.cc/editorconfig) A small package to parse and use [EditorConfig][1] files. Currently passes all of the official [test cases][2], which are run via `go test`. ```go props, err := editorconfig.Find("path/to/file.go") if err != nil { ... } // Print all the properties fmt.Println(props) // Query specific ones fmt.Println(props.Get("indent_style")) fmt.Println(props.IndentSize()) ``` Note that an [official library][3] exists for Go. This alternative implementation started with a different design: * Specialised INI parser, for full compatibility with the spec * Ability to cache parsing files and compiling pattern matches * Storing and querying all properties equally * Minimising pointers and maps to store data [1]: https://editorconfig.org/ [2]: https://github.com/editorconfig/editorconfig-core-test [3]: https://github.com/editorconfig/editorconfig-core-go editorconfig-0.2.0/_sample/000077500000000000000000000000001402620372500156065ustar00rootroot00000000000000editorconfig-0.2.0/_sample/.editorconfig000066400000000000000000000000761402620372500202660ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true editorconfig-0.2.0/_sample/subdir/000077500000000000000000000000001402620372500170765ustar00rootroot00000000000000editorconfig-0.2.0/_sample/subdir/.editorconfig000066400000000000000000000000521402620372500215500ustar00rootroot00000000000000[*.go] indent_style = tab indent_size = 8 editorconfig-0.2.0/_sample/subdir/code.go000066400000000000000000000000171402620372500203350ustar00rootroot00000000000000package subdir editorconfig-0.2.0/cmd_test.go000066400000000000000000000014651402620372500163250ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information package editorconfig import ( "flag" "fmt" "log" ) func cmd() { var ( configName = flag.String("f", DefaultName, "") emulateVersion = flag.String("b", "", "") version = flag.Bool("v", false, "") versionLong = flag.Bool("version", false, "") ) flag.Parse() if *version || *versionLong { fmt.Printf("EditorConfig Go mvdan.cc/editorconfig, Version 0.0.0-devel\n") return } args := flag.Args() if len(args) == 0 { flag.Usage() } query := Query{ ConfigName: *configName, Version: *emulateVersion, } for _, arg := range args { result, err := query.Find(arg) if err != nil { log.Fatal(err) } if len(args) > 1 { result.Name = arg } fmt.Printf("%s", result) } } editorconfig-0.2.0/core-test/000077500000000000000000000000001402620372500160735ustar00rootroot00000000000000editorconfig-0.2.0/editorconfig.go000066400000000000000000000227701402620372500172010ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information // Package editorconfig allows parsing and using EditorConfig files, as defined // in https://editorconfig.org/. package editorconfig import ( "bufio" "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" ) const DefaultName = ".editorconfig" // File is an EditorConfig file with a number of sections. type File struct { Root bool Sections []Section } // Section is a single EditorConfig section, which applies a number of // properties to the filenames matching it. type Section struct { // Name is the section's name. Usually, this will be a valid pattern // matching string, such as "[*.go]". Name string // Properties is the list of name-value properties contained by a // section. It is kept in increasing order, to allow binary searches. Properties []Property } // Property is a single property with a name and a value, which can be // represented as a single line like "indent_size=8". type Property struct { // Name is always lowercase and allows identifying a property. Name string // Value holds data for a property. Value string } // String turns a property into its INI format. func (p Property) String() string { return fmt.Sprintf("%s=%s", p.Name, p.Value) } // String turns a file into its INI format. func (f *File) String() string { var b strings.Builder if f.Root { fmt.Fprintf(&b, "root=true\n\n") } for i, section := range f.Sections { if i > 0 { fmt.Fprintln(&b) } fmt.Fprintf(&b, "[%s]\n", section.Name) for _, prop := range section.Properties { fmt.Fprintf(&b, "%s=%s\n", prop.Name, prop.Value) } } return b.String() } // Lookup finds a property by its name within a section and returns a pointer to // it, or nil if no such property exists. // // Note that most of the time, Get should be used instead. func (s Section) Lookup(name string) *Property { // TODO: binary search for i, prop := range s.Properties { if prop.Name == name { return &s.Properties[i] } } return nil } // Get returns the value of a property found by its name. If no such property // exists, an empty string is returned. func (s Section) Get(name string) string { if prop := s.Lookup(name); prop != nil { return prop.Value } return "" } // IndentSize is a shortcut for Get("indent_size") as an int. func (s Section) IndentSize() int { n, _ := strconv.Atoi(s.Get("indent_size")) return n } // IndentSize is a shortcut for Get("trim_trailing_whitespace") as a bool. func (s Section) TrimTrailingWhitespace() bool { return s.Get("trim_trailing_whitespace") == "true" } // IndentSize is a shortcut for Get("insert_final_newline") as a bool. func (s Section) InsertFinalNewline() bool { return s.Get("insert_final_newline") == "true" } // IndentSize is similar to Get("indent_size"), but it handles the "tab" default // and returns an int. When unset, it returns 0. func (s Section) TabWidth() int { value := s.Get("indent_size") if value == "tab" { value = s.Get("tab_width") } n, _ := strconv.Atoi(value) return n } // Add introduces a number of properties to the section. Properties that were // already part of the section are ignored. func (s *Section) Add(properties ...Property) { for _, prop := range properties { if s.Lookup(prop.Name) == nil { s.Properties = append(s.Properties, prop) } } } // String turns a section into its INI format. func (s Section) String() string { var b strings.Builder if s.Name != "" { fmt.Fprintf(&b, "[%s]\n", s.Name) } for _, prop := range s.Properties { fmt.Fprintf(&b, "%s=%s\n", prop.Name, prop.Value) } return b.String() } // Filter returns a set of properties from a file that apply to a file name. // Properties from later sections take precedence. The name should be a path // relative to the directory holding the EditorConfig. // // If cache is non-nil, the map will be used to reuse patterns translated and // compiled to regular expressions. // // Note that this function doesn't apply defaults; for that, see Find. // // Note that, since the EditorConfig spec doesn't allow backslashes as path // separators, backslashes in name are converted to forward slashes. func (f *File) Filter(name string, cache map[string]*regexp.Regexp) Section { name = filepath.ToSlash(name) result := Section{} for i := len(f.Sections) - 1; i >= 0; i-- { section := f.Sections[i] rx := cache[section.Name] if rx == nil { rx = toRegexp(section.Name) if cache != nil { cache[section.Name] = rx } } if rx.MatchString(name) { result.Add(section.Properties...) } } return result } // Find figures out the properties that apply to a file name on disk, and // returns them as a section. The name doesn't need to be an absolute path. // // It is equivalent to Query{}.Find; please note that no caching at all takes // place in this mode. func Find(name string) (Section, error) { return Query{}.Find(name) } // Query allows fine-grained control of how EditorConfig files are found and // used. It also attempts to cache and reuse work, which makes its Find method // significantly faster when used on many files. type Query struct { // ConfigName specifies what EditorConfig file name to use when // searching for files on disk. If empty, it defaults to DefaultName. ConfigName string // FileCache keeps track of which directories are known to contain an // EditorConfig. Existing entries which are nil mean that the directory // is known to not contain an EditorConfig. // // If nil, no caching takes place. FileCache map[string]*File // RegexpCache keeps track of patterns which have already been // translated to a regular expression and compiled, to save repeating // the work. // // If nil, no caching takes place. RegexpCache map[string]*regexp.Regexp // Version specifies an EditorConfig version to use when applying its // spec. When empty, it defaults to the latest version. This field // should generally be left untouched. Version string } // Find figures out the properties that apply to a file name on disk, and // returns them as a section. The name doesn't need to be an absolute path. // // Any relevant EditorConfig files are parsed and used as necessary. Parsing the // files can be cached in Query. // // The defaults for supported properties are applied before returning. func (q Query) Find(name string) (Section, error) { name, err := filepath.Abs(name) if err != nil { return Section{}, err } configName := q.ConfigName if configName == "" { configName = DefaultName } result := Section{} dir := name for { if d := filepath.Dir(dir); d != dir { dir = d } else { break } file, e := q.FileCache[dir] if !e { f, err := os.Open(filepath.Join(dir, configName)) if os.IsNotExist(err) { // continue below, caching the nil file } else if err != nil { return Section{}, err } else { var err error file, err = Parse(f) f.Close() if err != nil { return Section{}, err } } if q.FileCache != nil { q.FileCache[dir] = file } } if file == nil { continue } relative := name[len(dir)+1:] result.Add(file.Filter(relative, q.RegexpCache).Properties...) if file.Root { break } } if result.Get("indent_style") == "tab" { if value := result.Get("tab_width"); value != "" { // When indent_style is "tab" and tab_width is set, // indent_size should default to tab_width. result.Add(Property{Name: "indent_size", Value: value}) } if q.Version != "" && q.Version < "0.9.0" { } else if result.Get("indent_size") == "" { // When indent_style is "tab", indent_size defaults to // "tab". Only on 0.9.0 and later. result.Add(Property{Name: "indent_size", Value: "tab"}) } } else if result.Get("tab_width") == "" { if value := result.Get("indent_size"); value != "" && value != "tab" { // tab_width defaults to the value of indent_size. result.Add(Property{Name: "tab_width", Value: value}) } } return result, nil } func toRegexp(pat string) *regexp.Regexp { if i := strings.IndexByte(pat, '/'); i == 0 { pat = pat[1:] } else if i < 0 { pat = "**/" + pat } rxStr, err := patternRegexp(pat, patternFilenames|patternBraces) if err != nil { panic(err) } return regexp.MustCompile("^" + rxStr + "$") } func Parse(r io.Reader) (*File, error) { f := &File{} scanner := bufio.NewScanner(r) var section *Section for scanner.Scan() { line := scanner.Text() if i := strings.Index(line, " #"); i >= 0 { line = line[:i] } else if i := strings.Index(line, " ;"); i >= 0 { line = line[:i] } line = strings.TrimSpace(line) if len(line) > 2 && line[0] == '[' && line[len(line)-1] == ']' { name := line[1 : len(line)-1] if len(name) > 4096 { section = &Section{} // ignore continue } f.Sections = append(f.Sections, Section{Name: name}) section = &f.Sections[len(f.Sections)-1] continue } i := strings.IndexAny(line, "=:") if i < 0 { continue } key := strings.ToLower(strings.TrimSpace(line[:i])) value := strings.TrimSpace(line[i+1:]) switch key { case "root", "indent_style", "indent_size", "tab_width", "end_of_line", "charset", "trim_trailing_whitespace", "insert_final_newline": value = strings.ToLower(value) } // The spec tests require supporting at least these lengths. // Larger lengths rarely make sense, // and they could mean holding onto lots of memory, // so use them as limits. if len(key) > 1024 || len(value) > 4096 { continue } if section != nil { section.Add(Property{Name: key, Value: value}) } else if key == "root" { f.Root = value == "true" } } return f, nil } editorconfig-0.2.0/editorconfig_test.go000066400000000000000000000035341402620372500202350ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information package editorconfig import ( "fmt" "os" "os/exec" "regexp" "sync" "testing" ) func TestMain(m *testing.M) { if os.Getenv("EDITORCONFIG_CMD") != "" { cmd() os.Exit(0) } // call flag.Parse() here if TestMain uses flags os.Exit(m.Run()) } func run(dir, command string, args ...string) (string, error) { cmd := exec.Command(command, args...) cmd.Dir = dir out, err := cmd.CombinedOutput() return string(out), err } func mustRun(t *testing.T, dir, command string, args ...string) { out, err := run(dir, command, args...) if err != nil { fmt.Println(out) t.Fatal(err) } } func TestViaCmake(t *testing.T) { os.Setenv("EDITORCONFIG_CMD", os.Args[0]) mustRun(t, "core-test", "cmake", "..") // Run with a high number of parallel jobs, and with a reduced sleep // before exit when using -race, as we have a lot of test cases to run. os.Setenv("GORACE", "atexit_sleep_ms=10") out, err := run("core-test", "ctest", "-j8") if err != nil { rxFailed := regexp.MustCompile(` - ([a-zA-Z0-9_]+) \((.*)\)`) matches := rxFailed.FindAllStringSubmatch(out, -1) if len(matches) == 0 { // something went very wrong fmt.Println(out) t.Fatal(err) } for _, match := range matches { name, reason := match[1], match[2] t.Errorf("%s failed: %s", name, reason) } } } func TestConcurrentQuery(t *testing.T) { q := Query{} n := 100 name := "_sample/subdir/code.go" many := func(fn func()) { var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { fn() wg.Done() }() } wg.Wait() } many(func() { section, err := q.Find(name) if err != nil { t.Error(err) } if exp, got := 4, len(section.Properties); exp != got { t.Errorf("wanted %d properties, got %d", exp, got) } _ = section.String() }) } editorconfig-0.2.0/example_test.go000066400000000000000000000024531402620372500172130ustar00rootroot00000000000000// Copyright (c) 2019, Daniel Martí // See LICENSE for licensing information package editorconfig_test import ( "fmt" "strings" "mvdan.cc/editorconfig" ) func Example() { props, err := editorconfig.Find("_sample/subdir/code.go") if err != nil { panic(err) } fmt.Println(props) fmt.Println(props.Get("indent_style")) fmt.Println(props.IndentSize()) fmt.Println(props.TrimTrailingWhitespace()) fmt.Println(props.InsertFinalNewline()) // Output: // indent_style=tab // indent_size=8 // end_of_line=lf // insert_final_newline=true // // tab // 8 // false // true } func ExampleParse() { config := ` root = true [*] # match all files end_of_line = lf insert_final_newline = true [*.go] # only match Go indent_style = tab indent_size = 8 ` file, err := editorconfig.Parse(strings.NewReader(config)) if err != nil { panic(err) } fmt.Println(file) // Output: // root=true // // [*] // end_of_line=lf // insert_final_newline=true // // [*.go] // indent_style=tab // indent_size=8 } func ExampleFile_Filter() { config := ` [*] end_of_line = lf [*.go] indent_style = tab ` file, err := editorconfig.Parse(strings.NewReader(config)) if err != nil { panic(err) } fmt.Println(file.Filter("main.go", nil)) // Output: // indent_style=tab // end_of_line=lf } editorconfig-0.2.0/go.mod000066400000000000000000000000461402620372500152740ustar00rootroot00000000000000module mvdan.cc/editorconfig go 1.15 editorconfig-0.2.0/go.sum000066400000000000000000000000001402620372500153070ustar00rootroot00000000000000editorconfig-0.2.0/pattern_bundle.go000066400000000000000000000164671402620372500175410ustar00rootroot00000000000000// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. //go:generate bundle -o pattern_bundle.go -prefix pattern mvdan.cc/sh/v3/pattern // Package pattern allows working with shell pattern matching notation, also // known as wildcards or globbing. // // For reference, see // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_13. // package editorconfig import ( "bytes" "fmt" "regexp" "strconv" "strings" ) // TODO: support Mode in the other APIs too type patternMode uint const ( patternShortest patternMode = 1 << iota // prefer the shortest match. patternFilenames // "*" and "?" don't match slashes; only "**" does patternBraces // support "{a,b}" and "{1..4}" ) var patternnumRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`) // Regexp turns a shell pattern into a regular expression that can be used with // regexp.Compile. It will return an error if the input pattern was incorrect. // Otherwise, the returned expression can be passed to regexp.MustCompile. // // For example, Regexp(`foo*bar?`, true) returns `foo.*bar.`. // // Note that this function (and QuoteMeta) should not be directly used with file // paths if Windows is supported, as the path separator on that platform is the // same character as the escaping character for shell patterns. func patternRegexp(pat string, mode patternMode) (string, error) { any := false noopLoop: for _, r := range pat { switch r { // including those that need escaping since they are // regular expression metacharacters case '*', '?', '[', '\\', '.', '+', '(', ')', '|', ']', '{', '}', '^', '$': any = true break noopLoop } } if !any { // short-cut without a string copy return pat, nil } closingBraces := []int{} var buf bytes.Buffer writeLoop: for i := 0; i < len(pat); i++ { switch c := pat[i]; c { case '*': if mode&patternFilenames != 0 { if i++; i < len(pat) && pat[i] == '*' { if i++; i < len(pat) && pat[i] == '/' { buf.WriteString("(.*/|)") } else { buf.WriteString(".*") i-- } } else { buf.WriteString("[^/]*") i-- } } else { buf.WriteString(".*") } if mode&patternShortest != 0 { buf.WriteByte('?') } case '?': if mode&patternFilenames != 0 { buf.WriteString("[^/]") } else { buf.WriteByte('.') } case '\\': if i++; i >= len(pat) { return "", fmt.Errorf(`\ at end of pattern`) } buf.WriteString(regexp.QuoteMeta(string(pat[i]))) case '[': name, err := patterncharClass(pat[i:]) if err != nil { return "", err } if name != "" { buf.WriteString(name) i += len(name) - 1 break } if mode&patternFilenames != 0 { for _, c := range pat[i:] { if c == ']' { break } else if c == '/' { buf.WriteString("\\[") continue writeLoop } } } buf.WriteByte(c) if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } switch c = pat[i]; c { case '!', '^': buf.WriteByte('^') if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } c = pat[i] } if c == ']' { buf.WriteByte(']') if i++; i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } c = pat[i] } rangeStart := byte(0) loopBracket: for ; i < len(pat); i++ { c = pat[i] buf.WriteByte(c) switch c { case '\\': if i++; i < len(pat) { buf.WriteByte(pat[i]) } continue case ']': break loopBracket } if rangeStart != 0 && rangeStart > c { return "", fmt.Errorf("invalid range: %c-%c", rangeStart, c) } if c == '-' { rangeStart = pat[i-1] } else { rangeStart = 0 } } if i >= len(pat) { return "", fmt.Errorf("[ was not matched with a closing ]") } case '{': if mode&patternBraces == 0 { buf.WriteString(regexp.QuoteMeta(string(c))) break } innerLevel := 1 commas := false peekBrace: for j := i + 1; j < len(pat); j++ { switch c := pat[j]; c { case '{': innerLevel++ case ',': commas = true case '\\': j++ case '}': if innerLevel--; innerLevel > 0 { continue } if !commas { break peekBrace } closingBraces = append(closingBraces, j) buf.WriteString("(?:") continue writeLoop } } if match := patternnumRange.FindStringSubmatch(pat[i+1:]); len(match) == 3 { start, err1 := strconv.Atoi(match[1]) end, err2 := strconv.Atoi(match[2]) if err1 != nil || err2 != nil || start > end { return "", fmt.Errorf("invalid range: %q", match[0]) } // TODO: can we do better here? buf.WriteString("(?:") for n := start; n <= end; n++ { if n > start { buf.WriteByte('|') } fmt.Fprintf(&buf, "%d", n) } buf.WriteByte(')') i += len(match[0]) break } buf.WriteString(regexp.QuoteMeta(string(c))) case ',': if len(closingBraces) == 0 { buf.WriteString(regexp.QuoteMeta(string(c))) } else { buf.WriteByte('|') } case '}': if len(closingBraces) > 0 && closingBraces[len(closingBraces)-1] == i { buf.WriteByte(')') closingBraces = closingBraces[:len(closingBraces)-1] } else { buf.WriteString(regexp.QuoteMeta(string(c))) } default: if c > 128 { buf.WriteByte(c) } else { buf.WriteString(regexp.QuoteMeta(string(c))) } } } return buf.String(), nil } func patterncharClass(s string) (string, error) { if strings.HasPrefix(s, "[[.") || strings.HasPrefix(s, "[[=") { return "", fmt.Errorf("collating features not available") } if !strings.HasPrefix(s, "[[:") { return "", nil } name := s[3:] end := strings.Index(name, ":]]") if end < 0 { return "", fmt.Errorf("[[: was not matched with a closing :]]") } name = name[:end] switch name { case "alnum", "alpha", "ascii", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", "upper", "word", "xdigit": default: return "", fmt.Errorf("invalid character class: %q", name) } return s[:len(name)+6], nil } // HasMeta returns whether a string contains any unescaped pattern // metacharacters: '*', '?', or '['. When the function returns false, the given // pattern can only match at most one string. // // For example, HasMeta(`foo\*bar`) returns false, but HasMeta(`foo*bar`) // returns true. // // This can be useful to avoid extra work, like TranslatePattern. Note that this // function cannot be used to avoid QuotePattern, as backslashes are quoted by // that function but ignored here. func patternHasMeta(pat string) bool { for i := 0; i < len(pat); i++ { switch pat[i] { case '\\': i++ case '*', '?', '[': return true } } return false } // QuoteMeta returns a string that quotes all pattern metacharacters in the // given text. The returned string is a pattern that matches the literal text. // // For example, QuoteMeta(`foo*bar?`) returns `foo\*bar\?`. func patternQuoteMeta(pat string) string { any := false loop: for _, r := range pat { switch r { case '*', '?', '[', '\\': any = true break loop } } if !any { // short-cut without a string copy return pat } var buf bytes.Buffer for _, r := range pat { switch r { case '*', '?', '[', '\\': buf.WriteByte('\\') } buf.WriteRune(r) } return buf.String() }