blob: 1faf8b381e64f024a749da9c1569fca8c24c2b65 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package glob defines a globbing syntax and implements matching routines.
//
// Globs match a slash separated series of glob expressions.
//
// // Patterns:
// term ['/' term]*
// term:
// '*' matches any sequence of non-Separator characters
// '?' matches any single non-Separator character
// '[' [ '^' ] { character-range } ']'
// // Character classes (must be non-empty):
// c matches character c (c != '*', '?', '\\', '[', '/')
// '\\' c matches character c
// // Character-ranges:
// c matches character c (c != '\\', '-', ']')
// '\\' c matches character c
// lo '-' hi matches character c for lo <= c <= hi
package glob
import (
"path/filepath"
"strings"
)
// Glob represents a slash separated path glob expression.
type Glob struct {
elems []string
recursive bool
restricted bool
}
// Parse returns a new Glob.
func Parse(pattern string) (*Glob, error) {
if len(pattern) > 0 && pattern[0] == '/' {
return nil, filepath.ErrBadPattern
}
g := &Glob{}
if pattern != "" {
g.elems = strings.Split(pattern, "/")
}
if last := len(g.elems) - 1; last >= 0 {
if g.elems[last] == "..." {
g.elems = g.elems[:last]
g.recursive = true
} else if g.elems[last] == "***" {
g.elems = g.elems[:last]
g.recursive = true
g.restricted = true
}
}
// The only error we can get from the filepath library is badpattern.
// A future implementation would most likely recognize that here, so for now
// I'll just check every part to make sure it's error free.
// Note: Match never returns an error when matching against an empty string.
for _, elem := range g.elems {
if _, err := filepath.Match(elem, "test"); err != nil {
return nil, err
}
}
return g, nil
}
// Len returns the number of path elements represented by the glob expression.
func (g *Glob) Len() int {
return len(g.elems)
}
// Finished returns true if the pattern cannot match anything.
func (g *Glob) Finished() bool {
return !g.recursive && len(g.elems) == 0
}
// Recursive returns true if the pattern is recursive.
func (g *Glob) Recursive() bool {
return g.recursive
}
// Restricted returns true if recursion is restricted (up to the caller to
// know what that means).
func (g *Glob) Restricted() bool {
return g.restricted
}
// Split returns the suffix of g starting at the path element corresponding to
// start.
func (g *Glob) Split(start int) *Glob {
if start >= len(g.elems) {
return &Glob{elems: nil, recursive: g.recursive, restricted: g.restricted}
}
return &Glob{elems: g.elems[start:], recursive: g.recursive, restricted: g.restricted}
}
// MatchInitialSegment tries to match segment against the initial element of g.
// Returns:
// matched, a boolean indicating whether the match was successful;
// exact, a boolean indicating whether segment matched a fixed string pattern;
// remainder, a Glob representing the unmatched remainder of g.
func (g *Glob) MatchInitialSegment(segment string) (matched bool, exact bool, remainder *Glob) {
if len(g.elems) == 0 {
if !g.recursive {
return false, false, nil
}
// The segment matches "...". This is not an exact match.
return true, false, g
}
if matches, err := filepath.Match(g.elems[0], segment); err != nil {
panic("Error in glob pattern found.")
} else if matches {
_, fixed := isFixed(g.elems[0])
return true, fixed, g.Split(1)
}
return false, false, nil
}
// PartialMatch tries matching elems against part of a glob pattern.
// Returns:
// matched, a boolean indicating whether each element e_i of elems matches the
// (start + i)th element of the glob pattern;
// exact, a boolean indicating whether elems matched a fixed string pattern.
// <path> is considered an exact match for pattern <path>/...;
// remainder, a Glob representing the unmatched remainder of g. remainder will
// be empty if the pattern is completely matched.
//
// Note that if the glob is recursive elems can have more elements then
// the glob pattern and still get a true result.
func (g *Glob) PartialMatch(start int, elems []string) (matched bool, exact bool, remainder *Glob) {
g = g.Split(start)
allExact := true
for i := 0; i < len(elems); i++ {
var matched, exact bool
if matched, exact, g = g.MatchInitialSegment(elems[i]); !matched {
return false, false, nil
} else if !exact {
allExact = false
}
}
return true, allExact, g
}
// isFixed returns the unescaped string and true if 's' is a pattern specifying
// a fixed string. Otherwise it returns the original string and false.
func isFixed(s string) (string, bool) {
// No special characters.
if !strings.ContainsAny(s, "*?[") {
return s, true
}
// Special characters and no backslash.
if !strings.ContainsAny(s, "\\") {
return "", false
}
unescaped := ""
escape := false
for _, c := range s {
if escape {
escape = false
unescaped += string(c)
} else if strings.ContainsRune("*?[", c) {
// S contains an unescaped special character.
return s, false
} else if c == '\\' {
escape = true
} else {
unescaped += string(c)
}
}
return unescaped, true
}
func (g *Glob) SplitFixedPrefix() ([]string, *Glob) {
var prefix []string
start := 0
for _, elem := range g.elems {
if u, q := isFixed(elem); q {
prefix = append(prefix, u)
start++
} else {
break
}
}
return prefix, g.Split(start)
}
func (g *Glob) String() string {
e := g.elems
if g.recursive {
if g.restricted {
e = append(e, "***")
} else {
e = append(e, "...")
}
}
return filepath.Join(e...)
}