├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── cmd └── zglob │ └── main.go ├── fastwalk ├── fastwalk.go ├── fastwalk_dirent_fileno.go ├── fastwalk_dirent_ino.go ├── fastwalk_portable.go ├── fastwalk_test.go └── fastwalk_unix.go ├── go.mod ├── zglob.go └── zglob_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: mattn # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -v . 35 | 36 | - name: Test 37 | run: go test -v . 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Yasuhiro Matsumoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-zglob 2 | 3 | [![Build Status](https://github.com/mattn/go-zglob/actions/workflows/go.yml/badge.svg)](https://github.com/mattn/go-zglob/actions/workflows/go.yml) 4 | 5 | zglob 6 | 7 | ## Usage 8 | 9 | ```go 10 | matches, err := zglob.Glob(`./foo/b*/**/z*.txt`) 11 | ``` 12 | 13 | ## Installation 14 | 15 | For using library: 16 | 17 | ```console 18 | $ go get github.com/mattn/go-zglob 19 | ``` 20 | 21 | For using command: 22 | 23 | ```console 24 | $ go install github.com/mattn/go-zglob/cmd/zglob@latest 25 | ``` 26 | 27 | ## License 28 | 29 | MIT 30 | 31 | ## Author 32 | 33 | Yasuhiro Matsumoto (a.k.a mattn) 34 | -------------------------------------------------------------------------------- /cmd/zglob/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/mattn/go-zglob" 9 | ) 10 | 11 | func main() { 12 | var d bool 13 | flag.BoolVar(&d, "d", false, "with directory") 14 | flag.Parse() 15 | for _, arg := range os.Args[1:] { 16 | matches, err := zglob.Glob(arg) 17 | if err != nil { 18 | continue 19 | } 20 | for _, m := range matches { 21 | if !d { 22 | if fi, err := os.Stat(m); err == nil && fi.Mode().IsDir() { 23 | continue 24 | } 25 | } 26 | fmt.Println(m) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fastwalk/fastwalk.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // A faster implementation of filepath.Walk. 6 | // 7 | // filepath.Walk's design necessarily calls os.Lstat on each file, 8 | // even if the caller needs less info. And goimports only need to know 9 | // the type of each file. The kernel interface provides the type in 10 | // the Readdir call but the standard library ignored it. 11 | // fastwalk_unix.go contains a fork of the syscall routines. 12 | // 13 | // See golang.org/issue/16399 14 | 15 | package fastwalk 16 | 17 | import ( 18 | "errors" 19 | "os" 20 | "path/filepath" 21 | "runtime" 22 | "sync" 23 | ) 24 | 25 | // TraverseLink is a sentinel error for fastWalk, similar to filepath.SkipDir. 26 | var TraverseLink = errors.New("traverse symlink, assuming target is a directory") 27 | 28 | // FastWalk walks the file tree rooted at root, calling walkFn for 29 | // each file or directory in the tree, including root. 30 | // 31 | // If fastWalk returns filepath.SkipDir, the directory is skipped. 32 | // 33 | // Unlike filepath.Walk: 34 | // * file stat calls must be done by the user. 35 | // The only provided metadata is the file type, which does not include 36 | // any permission bits. 37 | // * multiple goroutines stat the filesystem concurrently. The provided 38 | // walkFn must be safe for concurrent use. 39 | // * fastWalk can follow symlinks if walkFn returns the TraverseLink 40 | // sentinel error. It is the walkFn's responsibility to prevent 41 | // fastWalk from going into symlink cycles. 42 | func FastWalk(root string, walkFn func(path string, typ os.FileMode) error) error { 43 | // Check if "root" is actually a file, not a directory. 44 | stat, err := os.Stat(root) 45 | if err != nil { 46 | return err 47 | } 48 | if !stat.IsDir() { 49 | // If it is, just directly pass it to walkFn and return. 50 | return walkFn(root, stat.Mode()) 51 | } 52 | 53 | // TODO(bradfitz): make numWorkers configurable? We used a 54 | // minimum of 4 to give the kernel more info about multiple 55 | // things we want, in hopes its I/O scheduling can take 56 | // advantage of that. Hopefully most are in cache. Maybe 4 is 57 | // even too low of a minimum. Profile more. 58 | numWorkers := 4 59 | if n := runtime.NumCPU(); n > numWorkers { 60 | numWorkers = n 61 | } 62 | w := &walker{ 63 | fn: walkFn, 64 | enqueuec: make(chan walkItem, numWorkers), // buffered for performance 65 | workc: make(chan walkItem, numWorkers), // buffered for performance 66 | donec: make(chan struct{}), 67 | 68 | // buffered for correctness & not leaking goroutines: 69 | resc: make(chan error, numWorkers), 70 | } 71 | 72 | // TODO(bradfitz): start the workers as needed? maybe not worth it. 73 | var wg sync.WaitGroup 74 | for i := 0; i < numWorkers; i++ { 75 | wg.Add(1) 76 | go w.doWork(&wg) 77 | } 78 | 79 | todo := []walkItem{{dir: root}} 80 | out := 0 81 | for { 82 | workc := w.workc 83 | var workItem walkItem 84 | if len(todo) == 0 { 85 | workc = nil 86 | } else { 87 | workItem = todo[len(todo)-1] 88 | } 89 | select { 90 | case workc <- workItem: 91 | todo = todo[:len(todo)-1] 92 | out++ 93 | case it := <-w.enqueuec: 94 | todo = append(todo, it) 95 | case err := <-w.resc: 96 | if err != nil { 97 | // Signal to the workers to close. 98 | close(w.donec) 99 | 100 | // Drain the results channel from the other workers which 101 | // haven't returned yet. 102 | go func() { 103 | for { 104 | select { 105 | case _, ok := <-w.resc: 106 | if !ok { 107 | return 108 | } 109 | } 110 | } 111 | }() 112 | 113 | wg.Wait() 114 | return err 115 | } 116 | 117 | out-- 118 | if out == 0 && len(todo) == 0 { 119 | // It's safe to quit here, as long as the buffered 120 | // enqueue channel isn't also readable, which might 121 | // happen if the worker sends both another unit of 122 | // work and its result before the other select was 123 | // scheduled and both w.resc and w.enqueuec were 124 | // readable. 125 | select { 126 | case it := <-w.enqueuec: 127 | todo = append(todo, it) 128 | default: 129 | // Signal to the workers to close, and wait for all of 130 | // them to return. 131 | close(w.donec) 132 | wg.Wait() 133 | return nil 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | // doWork reads directories as instructed (via workc) and runs the 141 | // user's callback function. 142 | func (w *walker) doWork(wg *sync.WaitGroup) { 143 | for { 144 | select { 145 | case <-w.donec: 146 | wg.Done() 147 | return 148 | case it := <-w.workc: 149 | w.resc <- w.walk(it.dir, !it.callbackDone) 150 | } 151 | } 152 | } 153 | 154 | type walker struct { 155 | fn func(path string, typ os.FileMode) error 156 | 157 | donec chan struct{} // closed on fastWalk's return 158 | workc chan walkItem // to workers 159 | enqueuec chan walkItem // from workers 160 | resc chan error // from workers 161 | } 162 | 163 | type walkItem struct { 164 | dir string 165 | callbackDone bool // callback already called; don't do it again 166 | } 167 | 168 | func (w *walker) enqueue(it walkItem) { 169 | select { 170 | case w.enqueuec <- it: 171 | case <-w.donec: 172 | } 173 | } 174 | 175 | func (w *walker) onDirEnt(dirName, baseName string, typ os.FileMode) error { 176 | joined := dirName + string(os.PathSeparator) + baseName 177 | if typ == os.ModeDir { 178 | w.enqueue(walkItem{dir: joined}) 179 | return nil 180 | } 181 | 182 | err := w.fn(joined, typ) 183 | if typ == os.ModeSymlink { 184 | if err == TraverseLink { 185 | // Set callbackDone so we don't call it twice for both the 186 | // symlink-as-symlink and the symlink-as-directory later: 187 | w.enqueue(walkItem{dir: joined, callbackDone: true}) 188 | return nil 189 | } 190 | if err == filepath.SkipDir { 191 | // Permit SkipDir on symlinks too. 192 | return nil 193 | } 194 | } 195 | return err 196 | } 197 | func (w *walker) walk(root string, runUserCallback bool) error { 198 | if runUserCallback { 199 | err := w.fn(root, os.ModeDir) 200 | if err == filepath.SkipDir { 201 | return nil 202 | } 203 | if err != nil { 204 | return err 205 | } 206 | } 207 | 208 | return readDir(root, w.onDirEnt) 209 | } 210 | -------------------------------------------------------------------------------- /fastwalk/fastwalk_dirent_fileno.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build freebsd openbsd netbsd 6 | 7 | package fastwalk 8 | 9 | import "syscall" 10 | 11 | func direntInode(dirent *syscall.Dirent) uint64 { 12 | return uint64(dirent.Fileno) 13 | } 14 | -------------------------------------------------------------------------------- /fastwalk/fastwalk_dirent_ino.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux,!appengine darwin 6 | 7 | package fastwalk 8 | 9 | import "syscall" 10 | 11 | func direntInode(dirent *syscall.Dirent) uint64 { 12 | return uint64(dirent.Ino) 13 | } 14 | -------------------------------------------------------------------------------- /fastwalk/fastwalk_portable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build appengine !linux,!darwin,!freebsd,!openbsd,!netbsd 6 | 7 | package fastwalk 8 | 9 | import ( 10 | "io/ioutil" 11 | "os" 12 | ) 13 | 14 | // readDir calls fn for each directory entry in dirName. 15 | // It does not descend into directories or follow symlinks. 16 | // If fn returns a non-nil error, readDir returns with that error 17 | // immediately. 18 | func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { 19 | fis, err := ioutil.ReadDir(dirName) 20 | if err != nil { 21 | return nil 22 | } 23 | for _, fi := range fis { 24 | if err := fn(dirName, fi.Name(), fi.Mode()&os.ModeType); err != nil { 25 | return err 26 | } 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /fastwalk/fastwalk_test.go: -------------------------------------------------------------------------------- 1 | package fastwalk 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestFastWalk(t *testing.T) { 11 | var tmpdir string 12 | var err error 13 | 14 | if tmpdir, err = ioutil.TempDir("", "zglob"); err != nil { 15 | t.Fatal(err) 16 | } 17 | defer os.RemoveAll(tmpdir) 18 | 19 | if err = os.Chdir(tmpdir); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | os.MkdirAll(filepath.Join(tmpdir, "foo/bar/baz"), 0755) 24 | ioutil.WriteFile(filepath.Join(tmpdir, "foo/bar/baz.txt"), []byte{}, 0644) 25 | ioutil.WriteFile(filepath.Join(tmpdir, "foo/bar/baz/noo.txt"), []byte{}, 0644) 26 | 27 | cases := []struct { 28 | path string 29 | dir bool 30 | triggered bool 31 | }{ 32 | {path: "foo/bar", dir: true, triggered: false}, 33 | {path: "foo/bar/baz", dir: true, triggered: false}, 34 | {path: "foo/bar/baz.txt", dir: false, triggered: false}, 35 | {path: "foo/bar/baz/noo.txt", dir: false, triggered: false}, 36 | } 37 | 38 | for i, tt := range cases { 39 | err = FastWalk(tt.path, func(path string, mode os.FileMode) error { 40 | if path != tt.path { 41 | return nil 42 | } 43 | 44 | if tt.dir != mode.IsDir() { 45 | t.Errorf("expected path %q to be: dir:%v, but got dir:%v", tt.path, tt.dir, mode.IsDir()) 46 | } 47 | cases[i].triggered = true 48 | return nil 49 | }) 50 | if err != nil { 51 | t.Errorf("error running FastWalk on %q: %v", tt.path, err) 52 | continue 53 | } 54 | 55 | if !cases[i].triggered { 56 | t.Errorf("expected %q to be triggered, but got %v", tt.path, cases[i].triggered) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /fastwalk/fastwalk_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux,!appengine darwin freebsd openbsd netbsd 6 | 7 | package fastwalk 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "os" 13 | "syscall" 14 | "unsafe" 15 | ) 16 | 17 | const blockSize = 8 << 10 18 | 19 | // unknownFileMode is a sentinel (and bogus) os.FileMode 20 | // value used to represent a syscall.DT_UNKNOWN Dirent.Type. 21 | const unknownFileMode os.FileMode = os.ModeNamedPipe | os.ModeSocket | os.ModeDevice 22 | 23 | func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { 24 | fd, err := syscall.Open(dirName, 0, 0) 25 | if err != nil { 26 | return err 27 | } 28 | defer syscall.Close(fd) 29 | 30 | // The buffer must be at least a block long. 31 | buf := make([]byte, blockSize) // stack-allocated; doesn't escape 32 | bufp := 0 // starting read position in buf 33 | nbuf := 0 // end valid data in buf 34 | for { 35 | if bufp >= nbuf { 36 | bufp = 0 37 | nbuf, err = syscall.ReadDirent(fd, buf) 38 | if err != nil { 39 | return os.NewSyscallError("readdirent", err) 40 | } 41 | if nbuf <= 0 { 42 | return nil 43 | } 44 | } 45 | consumed, name, typ := parseDirEnt(buf[bufp:nbuf]) 46 | bufp += consumed 47 | if name == "" || name == "." || name == ".." { 48 | continue 49 | } 50 | // Fallback for filesystems (like old XFS) that don't 51 | // support Dirent.Type and have DT_UNKNOWN (0) there 52 | // instead. 53 | if typ == unknownFileMode { 54 | fi, err := os.Lstat(dirName + "/" + name) 55 | if err != nil { 56 | // It got deleted in the meantime. 57 | if os.IsNotExist(err) { 58 | continue 59 | } 60 | return err 61 | } 62 | typ = fi.Mode() & os.ModeType 63 | } 64 | if err := fn(dirName, name, typ); err != nil { 65 | return err 66 | } 67 | } 68 | } 69 | 70 | func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode) { 71 | // golang.org/issue/15653 72 | dirent := (*syscall.Dirent)(unsafe.Pointer(&buf[0])) 73 | if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v { 74 | panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v)) 75 | } 76 | if len(buf) < int(dirent.Reclen) { 77 | panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen)) 78 | } 79 | consumed = int(dirent.Reclen) 80 | if direntInode(dirent) == 0 { // File absent in directory. 81 | return 82 | } 83 | switch dirent.Type { 84 | case syscall.DT_REG: 85 | typ = 0 86 | case syscall.DT_DIR: 87 | typ = os.ModeDir 88 | case syscall.DT_LNK: 89 | typ = os.ModeSymlink 90 | case syscall.DT_BLK: 91 | typ = os.ModeDevice 92 | case syscall.DT_FIFO: 93 | typ = os.ModeNamedPipe 94 | case syscall.DT_SOCK: 95 | typ = os.ModeSocket 96 | case syscall.DT_UNKNOWN: 97 | typ = unknownFileMode 98 | default: 99 | // Skip weird things. 100 | // It's probably a DT_WHT (http://lwn.net/Articles/325369/) 101 | // or something. Revisit if/when this package is moved outside 102 | // of goimports. goimports only cares about regular files, 103 | // symlinks, and directories. 104 | return 105 | } 106 | 107 | nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0])) 108 | nameLen := bytes.IndexByte(nameBuf[:], 0) 109 | if nameLen < 0 { 110 | panic("failed to find terminating 0 byte in dirent") 111 | } 112 | 113 | // Special cases for common things: 114 | if nameLen == 1 && nameBuf[0] == '.' { 115 | name = "." 116 | } else if nameLen == 2 && nameBuf[0] == '.' && nameBuf[1] == '.' { 117 | name = ".." 118 | } else { 119 | name = string(nameBuf[:nameLen]) 120 | } 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/go-zglob 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /zglob.go: -------------------------------------------------------------------------------- 1 | package zglob 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "regexp" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/mattn/go-zglob/fastwalk" 15 | ) 16 | 17 | var ( 18 | envre = regexp.MustCompile(`^(\$[a-zA-Z][a-zA-Z0-9_]+|\$\([a-zA-Z][a-zA-Z0-9_]+\))$`) 19 | mu sync.Mutex 20 | ) 21 | 22 | type zenv struct { 23 | dirmask string 24 | fre *regexp.Regexp 25 | pattern string 26 | root string 27 | } 28 | 29 | func toSlash(path string) string { 30 | if filepath.Separator == '/' { 31 | return path 32 | } 33 | var buf bytes.Buffer 34 | cc := []rune(path) 35 | for i := 0; i < len(cc); i++ { 36 | if i < len(cc)-2 && cc[i] == '\\' && (cc[i+1] == '{' || cc[i+1] == '}') { 37 | buf.WriteRune(cc[i]) 38 | buf.WriteRune(cc[i+1]) 39 | i++ 40 | } else if cc[i] == '\\' { 41 | buf.WriteRune('/') 42 | } else { 43 | buf.WriteRune(cc[i]) 44 | } 45 | } 46 | return buf.String() 47 | } 48 | 49 | func New(pattern string) (*zenv, error) { 50 | globmask := "" 51 | root := "" 52 | for n, i := range strings.Split(toSlash(pattern), "/") { 53 | if root == "" && strings.ContainsAny(i, "*{") { 54 | if globmask == "" { 55 | root = "." 56 | } else { 57 | root = toSlash(globmask) 58 | } 59 | } 60 | if n == 0 && i == "~" { 61 | if runtime.GOOS == "windows" { 62 | i = os.Getenv("USERPROFILE") 63 | } else { 64 | i = os.Getenv("HOME") 65 | } 66 | } 67 | if envre.MatchString(i) { 68 | i = strings.Trim(strings.Trim(os.Getenv(i[1:]), "()"), `"`) 69 | } 70 | 71 | globmask = path.Join(globmask, i) 72 | if n == 0 { 73 | if runtime.GOOS == "windows" && filepath.VolumeName(i) != "" { 74 | globmask = i + "/" 75 | } else if len(globmask) == 0 { 76 | globmask = "/" 77 | } 78 | } 79 | } 80 | if root == "" { 81 | return &zenv{ 82 | dirmask: "", 83 | fre: nil, 84 | pattern: pattern, 85 | root: "", 86 | }, nil 87 | } 88 | if globmask == "" { 89 | globmask = "." 90 | } 91 | globmask = toSlash(path.Clean(globmask)) 92 | 93 | cc := []rune(globmask) 94 | var dirmask strings.Builder 95 | var filemask strings.Builder 96 | staticDir := true 97 | for i := 0; i < len(cc); i++ { 98 | if i < len(cc)-2 && cc[i] == '\\' { 99 | i++ 100 | fmt.Fprintf(&filemask, "[\\x%02X]", cc[i]) 101 | if staticDir { 102 | dirmask.WriteRune(cc[i]) 103 | } 104 | } else if cc[i] == '*' { 105 | staticDir = false 106 | if i < len(cc)-2 && cc[i+1] == '*' && cc[i+2] == '/' { 107 | filemask.WriteString("(.*/)?") 108 | i += 2 109 | } else { 110 | filemask.WriteString("[^/]*") 111 | } 112 | } else if cc[i] == '[' { // range 113 | staticDir = false 114 | var b strings.Builder 115 | for j := i + 1; j < len(cc); j++ { 116 | if cc[j] == ']' { 117 | i = j 118 | break 119 | } else { 120 | b.WriteRune(cc[j]) 121 | } 122 | } 123 | if pattern := b.String(); pattern != "" { 124 | filemask.WriteByte('[') 125 | filemask.WriteString(pattern) 126 | filemask.WriteByte(']') 127 | continue 128 | } 129 | } else { 130 | if cc[i] == '{' { 131 | staticDir = false 132 | var b strings.Builder 133 | for j := i + 1; j < len(cc); j++ { 134 | if cc[j] == ',' { 135 | b.WriteByte('|') 136 | } else if cc[j] == '}' { 137 | i = j 138 | break 139 | } else { 140 | c := cc[j] 141 | if c == '/' { 142 | b.WriteRune(c) 143 | } else if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c { 144 | b.WriteRune(c) 145 | } else { 146 | fmt.Fprintf(&b, "[\\x%02X]", c) 147 | } 148 | } 149 | } 150 | if pattern := b.String(); pattern != "" { 151 | filemask.WriteByte('(') 152 | filemask.WriteString(pattern) 153 | filemask.WriteByte(')') 154 | continue 155 | } 156 | } else if i < len(cc)-1 && cc[i] == '!' && cc[i+1] == '(' { 157 | i++ 158 | var b strings.Builder 159 | for j := i + 1; j < len(cc); j++ { 160 | if cc[j] == ')' { 161 | i = j 162 | break 163 | } else { 164 | c := cc[j] 165 | fmt.Fprintf(&b, "[^\\x%02X/]*", c) 166 | } 167 | } 168 | if pattern := b.String(); pattern != "" { 169 | if dirmask.Len() == 0 { 170 | m := filemask.String() 171 | dirmask.WriteString(m) 172 | root = m 173 | } 174 | filemask.WriteString(pattern) 175 | continue 176 | } 177 | } 178 | c := cc[i] 179 | if c == '/' || ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c { 180 | filemask.WriteRune(c) 181 | } else { 182 | fmt.Fprintf(&filemask, "[\\x%02X]", c) 183 | } 184 | if staticDir { 185 | dirmask.WriteRune(c) 186 | } 187 | } 188 | } 189 | if m := filemask.String(); len(m) > 0 && m[len(m)-1] == '/' { 190 | if root == "" { 191 | root = m 192 | } 193 | filemask.WriteString("[^/]*") 194 | } 195 | var pat string 196 | if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { 197 | pat = "^(?i:" + filemask.String() + ")$" 198 | } else { 199 | pat = "^" + filemask.String() + "$" 200 | } 201 | fre, err := regexp.Compile(pat) 202 | if err != nil { 203 | return nil, err 204 | } 205 | return &zenv{ 206 | dirmask: path.Dir(dirmask.String()) + "/", 207 | fre: fre, 208 | pattern: pattern, 209 | root: filepath.Clean(root), 210 | }, nil 211 | } 212 | 213 | func Glob(pattern string) ([]string, error) { 214 | return glob(pattern, false) 215 | } 216 | 217 | func GlobFollowSymlinks(pattern string) ([]string, error) { 218 | return glob(pattern, true) 219 | } 220 | 221 | func glob(pattern string, followSymlinks bool) ([]string, error) { 222 | zenv, err := New(pattern) 223 | if err != nil { 224 | return nil, err 225 | } 226 | if zenv.root == "" { 227 | _, err := os.Stat(pattern) 228 | if err != nil { 229 | return nil, os.ErrNotExist 230 | } 231 | return []string{pattern}, nil 232 | } 233 | relative := !filepath.IsAbs(pattern) 234 | matches := []string{} 235 | 236 | err = fastwalk.FastWalk(zenv.root, func(path string, info os.FileMode) error { 237 | if zenv.root == "." && len(zenv.root) < len(path) { 238 | path = path[len(zenv.root)+1:] 239 | } 240 | path = filepath.ToSlash(path) 241 | 242 | if followSymlinks && info == os.ModeSymlink { 243 | followedPath, err := filepath.EvalSymlinks(path) 244 | if err == nil { 245 | fi, err := os.Lstat(followedPath) 246 | if err == nil && fi.IsDir() { 247 | return fastwalk.TraverseLink 248 | } 249 | } 250 | } 251 | 252 | if info.IsDir() { 253 | if path == "." || len(path) <= len(zenv.root) { 254 | return nil 255 | } 256 | if zenv.fre.MatchString(path) { 257 | mu.Lock() 258 | matches = append(matches, path) 259 | mu.Unlock() 260 | return nil 261 | } 262 | if len(path) < len(zenv.dirmask) && !strings.HasPrefix(zenv.dirmask, path+"/") { 263 | return filepath.SkipDir 264 | } 265 | } 266 | 267 | if zenv.fre.MatchString(path) { 268 | if relative && filepath.IsAbs(path) { 269 | path = path[len(zenv.root)+1:] 270 | } 271 | mu.Lock() 272 | matches = append(matches, path) 273 | mu.Unlock() 274 | } 275 | return nil 276 | }) 277 | 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | return matches, nil 283 | } 284 | 285 | func Match(pattern, name string) (matched bool, err error) { 286 | zenv, err := New(pattern) 287 | if err != nil { 288 | return false, err 289 | } 290 | return zenv.Match(name), nil 291 | } 292 | 293 | func (z *zenv) Match(name string) bool { 294 | if z.root == "" { 295 | return z.pattern == name 296 | } 297 | 298 | name = filepath.ToSlash(name) 299 | 300 | if name == "." || len(name) <= len(z.root) { 301 | return false 302 | } 303 | 304 | if z.fre.MatchString(name) { 305 | return true 306 | } 307 | return false 308 | } 309 | -------------------------------------------------------------------------------- /zglob_test.go: -------------------------------------------------------------------------------- 1 | package zglob 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func check(got []string, expected []string) bool { 16 | sort.Strings(got) 17 | sort.Strings(expected) 18 | return reflect.DeepEqual(expected, got) 19 | } 20 | 21 | type testZGlob struct { 22 | pattern string 23 | expected []string 24 | err string 25 | } 26 | 27 | var testGlobs = []testZGlob{ 28 | {`fo*`, []string{`foo`}, ""}, 29 | {`foo`, []string{`foo`}, ""}, 30 | {`foo/*`, []string{`foo/bar`, `foo/baz`}, ""}, 31 | {`foo/b[a]*`, []string{`foo/bar`, `foo/baz`}, ""}, 32 | {`foo/b[a][r]*`, []string{`foo/bar`}, ""}, 33 | {`foo/b[a-z]*`, []string{`foo/bar`, `foo/baz`}, ""}, 34 | {`foo/b[c-z]*`, []string{}, ""}, 35 | {`foo/b[z-c]*`, []string{}, "error parsing regexp"}, 36 | {`foo/**`, []string{`foo/bar`, `foo/baz`}, ""}, 37 | {`f*o/**`, []string{`foo/bar`, `foo/baz`}, ""}, 38 | {`*oo/**`, []string{`foo/bar`, `foo/baz`, `hoo/bar`}, ""}, 39 | {`*oo/b*`, []string{`foo/bar`, `foo/baz`, `hoo/bar`}, ""}, 40 | {`*oo/bar`, []string{`foo/bar`, `hoo/bar`}, ""}, 41 | {`*oo/*z`, []string{`foo/baz`}, ""}, 42 | {`foo/**/*`, []string{`foo/bar`, `foo/bar/baz`, `foo/bar/baz.txt`, `foo/bar/baz/noo.txt`, `foo/baz`}, ""}, 43 | {`*oo/**/*`, []string{`foo/bar`, `foo/bar/baz`, `foo/bar/baz.txt`, `foo/bar/baz/noo.txt`, `foo/baz`, `hoo/bar`}, ""}, 44 | {`*oo/*.txt`, []string{}, ""}, 45 | {`*oo/*/*.txt`, []string{`foo/bar/baz.txt`}, ""}, 46 | {`*oo/**/*.txt`, []string{`foo/bar/baz.txt`, `foo/bar/baz/noo.txt`}, ""}, 47 | {`doo`, nil, "file does not exist"}, 48 | {`./f*`, []string{`foo`}, ""}, 49 | {`**/bar/**/*.txt`, []string{`foo/bar/baz.txt`, `foo/bar/baz/noo.txt`}, ""}, 50 | {`**/bar/**/*.{jpg,png}`, []string{`zzz/bar/baz/joo.png`, `zzz/bar/baz/zoo.jpg`}, ""}, 51 | {`zzz/bar/baz/zoo.{jpg,png}`, []string{`zzz/bar/baz/zoo.jpg`}, ""}, 52 | {`zzz/bar/{baz,z}/zoo.jpg`, []string{`zzz/bar/baz/zoo.jpg`}, ""}, 53 | {`zzz/nar/\{noo,x\}/joo.png`, []string{`zzz/nar/{noo,x}/joo.png`}, ""}, 54 | } 55 | 56 | func fatalIf(err error) { 57 | if err != nil { 58 | panic(err.Error()) 59 | } 60 | } 61 | 62 | func setup() (string, string) { 63 | tmpdir, err := ioutil.TempDir("", "zglob") 64 | fatalIf(err) 65 | 66 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "foo/baz"), 0755)) 67 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "foo/bar"), 0755)) 68 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "foo/bar/baz.txt"), []byte{}, 0644)) 69 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "foo/bar/baz"), 0755)) 70 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "foo/bar/baz/noo.txt"), []byte{}, 0644)) 71 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "hoo/bar"), 0755)) 72 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "foo/bar/baz.txt"), []byte{}, 0644)) 73 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "zzz/bar/baz"), 0755)) 74 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "zzz/bar/baz/zoo.jpg"), []byte{}, 0644)) 75 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "zzz/bar/baz/joo.png"), []byte{}, 0644)) 76 | fatalIf(os.MkdirAll(filepath.Join(tmpdir, "zzz/nar/{noo,x}"), 0755)) 77 | fatalIf(ioutil.WriteFile(filepath.Join(tmpdir, "zzz/nar/{noo,x}/joo.png"), []byte{}, 0644)) 78 | 79 | curdir, err := os.Getwd() 80 | fatalIf(err) 81 | fatalIf(os.Chdir(tmpdir)) 82 | 83 | return tmpdir, curdir 84 | } 85 | 86 | func TestGlob(t *testing.T) { 87 | tmpdir, savedCwd := setup() 88 | defer os.RemoveAll(tmpdir) 89 | defer os.Chdir(savedCwd) 90 | 91 | tmpdir = "." 92 | for _, test := range testGlobs { 93 | expected := make([]string, len(test.expected)) 94 | for i, e := range test.expected { 95 | expected[i] = e 96 | } 97 | got, err := Glob(test.pattern) 98 | if err != nil { 99 | if !strings.Contains(err.Error(), test.err) { 100 | t.Error(err) 101 | } 102 | continue 103 | } 104 | if !check(expected, got) { 105 | t.Errorf(`zglob failed: pattern %q(%q): expected %v but got %v`, test.pattern, tmpdir, expected, got) 106 | } 107 | } 108 | } 109 | 110 | func TestGlobAbs(t *testing.T) { 111 | tmpdir, savedCwd := setup() 112 | defer os.RemoveAll(tmpdir) 113 | defer os.Chdir(savedCwd) 114 | 115 | for _, test := range testGlobs { 116 | pattern := toSlash(path.Join(tmpdir, test.pattern)) 117 | expected := make([]string, len(test.expected)) 118 | for i, e := range test.expected { 119 | expected[i] = filepath.ToSlash(filepath.Join(tmpdir, e)) 120 | } 121 | got, err := Glob(pattern) 122 | if err != nil { 123 | if !strings.Contains(err.Error(), test.err) { 124 | t.Error(err) 125 | } 126 | continue 127 | } 128 | if !check(expected, got) { 129 | t.Errorf(`zglob failed: pattern %q(%q): expected %v but got %v`, pattern, tmpdir, expected, got) 130 | } 131 | } 132 | } 133 | 134 | func TestMatch(t *testing.T) { 135 | for _, test := range testGlobs { 136 | for _, f := range test.expected { 137 | got, err := Match(test.pattern, f) 138 | if err != nil { 139 | t.Error(err) 140 | continue 141 | } 142 | if !got { 143 | t.Errorf("%q should match with %q", f, test.pattern) 144 | } 145 | } 146 | } 147 | } 148 | 149 | func TestFollowSymlinks(t *testing.T) { 150 | tmpdir, err := ioutil.TempDir("", "zglob") 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | os.MkdirAll(filepath.Join(tmpdir, "foo"), 0755) 156 | ioutil.WriteFile(filepath.Join(tmpdir, "foo/baz.txt"), []byte{}, 0644) 157 | defer os.RemoveAll(tmpdir) 158 | 159 | err = os.Symlink(filepath.Join(tmpdir, "foo"), filepath.Join(tmpdir, "bar")) 160 | if err != nil { 161 | t.Skip(err.Error()) 162 | } 163 | 164 | curdir, err := os.Getwd() 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | err = os.Chdir(tmpdir) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | defer os.Chdir(curdir) 173 | 174 | got, err := GlobFollowSymlinks("**/*") 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | expected := []string{"foo", "foo/baz.txt", "bar/baz.txt"} 179 | 180 | if !check(expected, got) { 181 | t.Errorf(`zglob failed: expected %v but got %v`, expected, got) 182 | } 183 | } 184 | 185 | func TestGlobError(t *testing.T) { 186 | tmpdir, err := ioutil.TempDir("", "zglob") 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | defer os.RemoveAll(tmpdir) 191 | 192 | err = os.MkdirAll(filepath.Join(tmpdir, "foo"), 0222) 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | 197 | curdir, err := os.Getwd() 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | err = os.Chdir(tmpdir) 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | defer os.Chdir(curdir) 206 | 207 | got, err := Glob("**/*") 208 | if !errors.Is(err, os.ErrPermission) { 209 | t.Errorf(`zglob failed: expected %v but got %v`, os.ErrPermission, err) 210 | } 211 | if !check(nil, got) { 212 | t.Errorf(`zglob failed: expected %v but got %v`, nil, got) 213 | } 214 | } 215 | 216 | func BenchmarkGlob(b *testing.B) { 217 | tmpdir, savedCwd := setup() 218 | defer os.RemoveAll(tmpdir) 219 | defer os.Chdir(savedCwd) 220 | 221 | for i := 0; i < b.N; i++ { 222 | for _, test := range testGlobs { 223 | if test.err != "" { 224 | continue 225 | } 226 | got, err := Glob(test.pattern) 227 | if err != nil { 228 | b.Fatal(err) 229 | } 230 | if len(got) != len(test.expected) { 231 | b.Fatalf(`zglob failed: pattern %q: expected %v but got %v`, test.pattern, test.expected, got) 232 | } 233 | } 234 | } 235 | } 236 | --------------------------------------------------------------------------------