278 lines
6 KiB
Go
278 lines
6 KiB
Go
|
package glob
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
//"log"
|
||
|
"os"
|
||
|
gpath "path"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"unicode/utf8"
|
||
|
|
||
|
"github.com/MichaelTJones/walk"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
// NotSlash is any rune but path separator.
|
||
|
notSlash = "[^/]"
|
||
|
// AnyRune is zero or more non-path separators.
|
||
|
anyRune = notSlash + "*"
|
||
|
// ZeroOrMoreDirectories is used by ** patterns.
|
||
|
zeroOrMoreDirectories = `(?:[.{}\w\-\ ]+\/)*`
|
||
|
// TrailingStarStar matches everything inside directory.
|
||
|
trailingStarStar = "/**"
|
||
|
// SlashStarStarSlash maches zero or more directories.
|
||
|
slashStarStarSlash = "/**/"
|
||
|
)
|
||
|
|
||
|
// RegexpInfo contains additional info about the Regexp created by a glob pattern.
|
||
|
type RegexpInfo struct {
|
||
|
Regexp *regexp.Regexp
|
||
|
Negate bool
|
||
|
Path string
|
||
|
Glob string
|
||
|
}
|
||
|
|
||
|
// MatchString matches a string with either a regexp or direct string match
|
||
|
func (ri *RegexpInfo) MatchString(s string) bool {
|
||
|
if ri.Regexp != nil {
|
||
|
return ri.Regexp.MatchString(s)
|
||
|
} else if ri.Path != "" {
|
||
|
return strings.HasSuffix(s, ri.Path)
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Globexp builds a regular express from from extended glob pattern and then
|
||
|
// returns a Regexp object.
|
||
|
func Globexp(glob string) *regexp.Regexp {
|
||
|
var re bytes.Buffer
|
||
|
|
||
|
re.WriteString("^")
|
||
|
|
||
|
i, inGroup, L := 0, false, len(glob)
|
||
|
|
||
|
for i < L {
|
||
|
r, w := utf8.DecodeRuneInString(glob[i:])
|
||
|
|
||
|
switch r {
|
||
|
default:
|
||
|
re.WriteRune(r)
|
||
|
|
||
|
case '\\', '$', '^', '+', '.', '(', ')', '=', '!', '|':
|
||
|
re.WriteRune('\\')
|
||
|
re.WriteRune(r)
|
||
|
|
||
|
case '/':
|
||
|
// TODO optimize later, string could be long
|
||
|
rest := glob[i:]
|
||
|
re.WriteRune('/')
|
||
|
if strings.HasPrefix(rest, "/**/") {
|
||
|
re.WriteString(zeroOrMoreDirectories)
|
||
|
w *= 4
|
||
|
} else if rest == "/**" {
|
||
|
re.WriteString(".*")
|
||
|
w *= 3
|
||
|
}
|
||
|
|
||
|
case '?':
|
||
|
re.WriteRune('.')
|
||
|
|
||
|
case '[', ']':
|
||
|
re.WriteRune(r)
|
||
|
|
||
|
case '{':
|
||
|
if i < L-1 {
|
||
|
if glob[i+1:i+2] == "{" {
|
||
|
re.WriteString("\\{")
|
||
|
w *= 2
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
inGroup = true
|
||
|
re.WriteRune('(')
|
||
|
|
||
|
case '}':
|
||
|
if inGroup {
|
||
|
inGroup = false
|
||
|
re.WriteRune(')')
|
||
|
} else {
|
||
|
re.WriteRune('}')
|
||
|
}
|
||
|
|
||
|
case ',':
|
||
|
if inGroup {
|
||
|
re.WriteRune('|')
|
||
|
} else {
|
||
|
re.WriteRune('\\')
|
||
|
re.WriteRune(r)
|
||
|
}
|
||
|
|
||
|
case '*':
|
||
|
rest := glob[i:]
|
||
|
if strings.HasPrefix(rest, "**/") {
|
||
|
re.WriteString(zeroOrMoreDirectories)
|
||
|
w *= 3
|
||
|
} else {
|
||
|
re.WriteString(anyRune)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
i += w
|
||
|
}
|
||
|
|
||
|
re.WriteString("$")
|
||
|
//log.Printf("regex string %s", re.String())
|
||
|
return regexp.MustCompile(re.String())
|
||
|
}
|
||
|
|
||
|
// Glob returns files and dirctories that match patterns. Patterns must use
|
||
|
// slashes, even Windows.
|
||
|
//
|
||
|
// Special chars.
|
||
|
//
|
||
|
// /**/ - match zero or more directories
|
||
|
// {a,b} - match a or b, no spaces
|
||
|
// * - match any non-separator char
|
||
|
// ? - match a single non-separator char
|
||
|
// **/ - match any directory, start of pattern only
|
||
|
// /** - match any this directory, end of pattern only
|
||
|
// ! - removes files from resultset, start of pattern only
|
||
|
//
|
||
|
func Glob(patterns []string) ([]*FileAsset, []*RegexpInfo, error) {
|
||
|
// TODO very inefficient and unintelligent, optimize later
|
||
|
|
||
|
m := map[string]*FileAsset{}
|
||
|
regexps := []*RegexpInfo{}
|
||
|
|
||
|
for _, pattern := range patterns {
|
||
|
remove := strings.HasPrefix(pattern, "!")
|
||
|
if remove {
|
||
|
pattern = pattern[1:]
|
||
|
if hasMeta(pattern) {
|
||
|
re := Globexp(pattern)
|
||
|
regexps = append(regexps, &RegexpInfo{Regexp: re, Glob: pattern, Negate: true})
|
||
|
for path := range m {
|
||
|
if re.MatchString(path) {
|
||
|
m[path] = nil
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
path := gpath.Clean(pattern)
|
||
|
m[path] = nil
|
||
|
regexps = append(regexps, &RegexpInfo{Path: path, Glob: pattern, Negate: true})
|
||
|
}
|
||
|
} else {
|
||
|
if hasMeta(pattern) {
|
||
|
re := Globexp(pattern)
|
||
|
regexps = append(regexps, &RegexpInfo{Regexp: re, Glob: pattern})
|
||
|
root := PatternRoot(pattern)
|
||
|
if root == "" {
|
||
|
return nil, nil, fmt.Errorf("Cannot get root from pattern: %s", pattern)
|
||
|
}
|
||
|
fileAssets, err := walkFiles(root)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
for _, file := range fileAssets {
|
||
|
if re.MatchString(file.Path) {
|
||
|
// TODO closure problem assigning &file
|
||
|
tmp := file
|
||
|
m[file.Path] = tmp
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
path := gpath.Clean(pattern)
|
||
|
info, err := os.Stat(path)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
regexps = append(regexps, &RegexpInfo{Path: path, Glob: pattern, Negate: false})
|
||
|
fa := &FileAsset{Path: path, FileInfo: info}
|
||
|
m[path] = fa
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//log.Printf("m %v", m)
|
||
|
keys := []*FileAsset{}
|
||
|
for _, it := range m {
|
||
|
if it != nil {
|
||
|
keys = append(keys, it)
|
||
|
}
|
||
|
}
|
||
|
return keys, regexps, nil
|
||
|
}
|
||
|
|
||
|
// hasMeta determines if a path has special chars used to build a Regexp.
|
||
|
func hasMeta(path string) bool {
|
||
|
return strings.IndexAny(path, "*?[{") >= 0
|
||
|
}
|
||
|
|
||
|
func isDir(path string) bool {
|
||
|
st, err := os.Stat(path)
|
||
|
if os.IsNotExist(err) {
|
||
|
return false
|
||
|
}
|
||
|
return st.IsDir()
|
||
|
}
|
||
|
|
||
|
// PatternRoot gets a real directory root from a pattern. The directory
|
||
|
// returned is used as the start location for globbing.
|
||
|
func PatternRoot(s string) string {
|
||
|
if isDir(s) {
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// No directory in pattern
|
||
|
parts := strings.Split(s, "/")
|
||
|
if len(parts) == 1 {
|
||
|
return "."
|
||
|
}
|
||
|
// parts returns an empty string at positio 0 if the s starts with "/"
|
||
|
root := ""
|
||
|
|
||
|
// Build path until a dirname has a char used to build regex
|
||
|
for i, part := range parts {
|
||
|
if hasMeta(part) {
|
||
|
break
|
||
|
}
|
||
|
if i > 0 {
|
||
|
root += "/"
|
||
|
}
|
||
|
root += part
|
||
|
}
|
||
|
// Default to cwd
|
||
|
if root == "" {
|
||
|
root = "."
|
||
|
}
|
||
|
return root
|
||
|
}
|
||
|
|
||
|
// walkFiles walks a directory starting at root returning all directories and files
|
||
|
// include those found in subdirectories.
|
||
|
func walkFiles(root string) ([]*FileAsset, error) {
|
||
|
fileAssets := []*FileAsset{}
|
||
|
var lock sync.Mutex
|
||
|
visitor := func(path string, info os.FileInfo, err error) error {
|
||
|
// if err != nil {
|
||
|
// fmt.Println("visitor err", err.Error(), "root", root)
|
||
|
// }
|
||
|
if err == nil {
|
||
|
lock.Lock()
|
||
|
fileAssets = append(fileAssets, &FileAsset{FileInfo: info, Path: filepath.ToSlash(path)})
|
||
|
lock.Unlock()
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
err := walk.Walk(root, visitor)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return fileAssets, nil
|
||
|
}
|