mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-05 03:17:50 +08:00
216 lines
4.5 KiB
Go
216 lines
4.5 KiB
Go
// Package glob implements globbing for elvish.
|
|
package glob
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Pattern is a glob pattern.
|
|
type Pattern struct {
|
|
Segments []Segment
|
|
DirOverride string
|
|
}
|
|
|
|
// Segment is the constituent unit of a Pattern.
|
|
type Segment struct {
|
|
// Type of the Segment.
|
|
Type SegmentType
|
|
Data string // For Literal, the literal string. For Question, Star and StarStar, nonempty if they should match all files.
|
|
}
|
|
|
|
// SegmentType is the type of a Segment.
|
|
type SegmentType int
|
|
|
|
// Values for SegmentType.
|
|
const (
|
|
Literal SegmentType = iota
|
|
Slash
|
|
Question
|
|
Star
|
|
StarStar
|
|
)
|
|
|
|
// Glob returns a list of file names satisfying the given pattern.
|
|
func Glob(p string) []string {
|
|
return Parse(p).Glob()
|
|
}
|
|
|
|
// Glob returns a list of file names satisfying the Pattern.
|
|
func (p Pattern) Glob() []string {
|
|
segs := p.Segments
|
|
dir := ""
|
|
if len(segs) > 0 && segs[0].Type == Slash {
|
|
segs = segs[1:]
|
|
dir = "/"
|
|
}
|
|
|
|
if p.DirOverride != "" {
|
|
dir = p.DirOverride
|
|
}
|
|
|
|
results := []string{}
|
|
ch := make(chan string)
|
|
go func() {
|
|
glob(segs, dir, ch)
|
|
close(ch)
|
|
}()
|
|
|
|
for s := range ch {
|
|
results = append(results, s)
|
|
}
|
|
return results
|
|
}
|
|
|
|
func glob(segs []Segment, dir string, results chan<- string) {
|
|
// Empty segment, resulting from a trailing slash. Generate the starting
|
|
// directory.
|
|
if len(segs) == 0 {
|
|
results <- dir + "/"
|
|
return
|
|
}
|
|
|
|
var prefix string
|
|
if dir == "" {
|
|
prefix = ""
|
|
dir = "."
|
|
} else if dir == "/" {
|
|
prefix = "/"
|
|
} else {
|
|
// dir never has a trailing slash unless it is /.
|
|
prefix = dir + "/"
|
|
}
|
|
|
|
i := -1
|
|
nexti := func() {
|
|
for i++; i < len(segs); i++ {
|
|
if segs[i].Type == Slash || segs[i].Type == StarStar {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
nexti()
|
|
|
|
infos, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
// XXX Silently drop the error
|
|
return
|
|
}
|
|
|
|
// Enumerate the position of the first slash.
|
|
for i < len(segs) {
|
|
slash := segs[i].Type == Slash
|
|
var first, rest []Segment
|
|
if slash {
|
|
// segs = x/y. Match dir with x, recurse on y.
|
|
first, rest = segs[:i], segs[i+1:]
|
|
} else {
|
|
// segs = x**y. Match dir with x*, recurse on **y.
|
|
first, rest = segs[:i+1], segs[i:]
|
|
}
|
|
|
|
for _, info := range infos {
|
|
name := info.Name()
|
|
if match(first, name) && info.IsDir() {
|
|
glob(rest, prefix+name, results)
|
|
}
|
|
}
|
|
|
|
if slash {
|
|
// First slash cannot appear later than a slash in the pattern.
|
|
return
|
|
}
|
|
nexti()
|
|
}
|
|
|
|
// If we reach here, it is possible to have no slashes at all.
|
|
for _, info := range infos {
|
|
name := info.Name()
|
|
if match(segs, name) {
|
|
results <- prefix + name
|
|
}
|
|
}
|
|
}
|
|
|
|
// match matches a name against segments. It treats StarStar segments as they
|
|
// are Star segments. The segments may not contain Slash'es.
|
|
func match(segs []Segment, name string) bool {
|
|
if len(segs) == 0 {
|
|
return name == ""
|
|
}
|
|
// Question, Star and StarAll only match leading dot when their Data field
|
|
// is nonempty.
|
|
if len(name) > 0 && name[0] == '.' && segs[0].Data == "" {
|
|
switch segs[0].Type {
|
|
case Question, Star, StarStar:
|
|
return false
|
|
}
|
|
}
|
|
segs:
|
|
for len(segs) > 0 {
|
|
// Find a chunk. A chunk is a run of Literal and Question, with an
|
|
// optional leading Star.
|
|
var i int
|
|
for i = 1; i < len(segs); i++ {
|
|
if segs[i].Type == Star || segs[i].Type == StarStar {
|
|
break
|
|
}
|
|
}
|
|
|
|
chunk := segs[:i]
|
|
star := chunk[0].Type == Star || chunk[0].Type == StarStar
|
|
if star {
|
|
chunk = chunk[1:]
|
|
}
|
|
segs = segs[i:]
|
|
|
|
// NOTE A quick path when len(segs) == 0 can be implemented: match
|
|
// backwards.
|
|
|
|
// Match at the current position. If this is the last chunk, we need to
|
|
// make sure name is exhausted by the matching.
|
|
ok, rest := matchChunk(chunk, name)
|
|
if ok && (rest == "" || len(segs) > 0) {
|
|
name = rest
|
|
continue
|
|
}
|
|
|
|
if star {
|
|
// NOTE An optimization is to make the upper bound not len(names),
|
|
// but rather len(names) - LB(# bytes segs can match)
|
|
for i := 1; i <= len(name); i++ {
|
|
// Match after skipping i bytes.
|
|
ok, rest := matchChunk(chunk, name[i:])
|
|
if ok && (rest == "" || len(segs) > 0) {
|
|
name = rest
|
|
continue segs
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return name == ""
|
|
}
|
|
|
|
// matchChunk returns whether a chunk matches a prefix of name. If suceeded, it
|
|
// also returns the remaining part of name.
|
|
func matchChunk(chunk []Segment, name string) (bool, string) {
|
|
for _, seg := range chunk {
|
|
if name == "" {
|
|
return false, ""
|
|
}
|
|
switch seg.Type {
|
|
case Literal:
|
|
n := len(seg.Data)
|
|
if len(name) < n || name[:n] != seg.Data {
|
|
return false, ""
|
|
}
|
|
name = name[n:]
|
|
case Question:
|
|
_, n := utf8.DecodeRuneInString(name)
|
|
name = name[n:]
|
|
}
|
|
}
|
|
return true, name
|
|
}
|