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 }