| package sftp |
| |
| import ( |
| "path" |
| "strings" |
| "unicode/utf8" |
| ) |
| |
| // ErrBadPattern indicates a globbing pattern was malformed. |
| var ErrBadPattern = path.ErrBadPattern |
| |
| // Unix separator |
| const separator = "/" |
| |
| // Match reports whether name matches the shell file name pattern. |
| // The pattern syntax is: |
| // |
| // pattern: |
| // { term } |
| // term: |
| // '*' matches any sequence of non-Separator characters |
| // '?' matches any single non-Separator character |
| // '[' [ '^' ] { character-range } ']' |
| // character class (must be non-empty) |
| // c matches character c (c != '*', '?', '\\', '[') |
| // '\\' c matches character c |
| // |
| // character-range: |
| // c matches character c (c != '\\', '-', ']') |
| // '\\' c matches character c |
| // lo '-' hi matches character c for lo <= c <= hi |
| // |
| // Match requires pattern to match all of name, not just a substring. |
| // The only possible returned error is ErrBadPattern, when pattern |
| // is malformed. |
| // |
| // |
| func Match(pattern, name string) (matched bool, err error) { |
| return path.Match(pattern, name) |
| } |
| |
| // detect if byte(char) is path separator |
| func isPathSeparator(c byte) bool { |
| return string(c) == "/" |
| } |
| |
| // scanChunk gets the next segment of pattern, which is a non-star string |
| // possibly preceded by a star. |
| func scanChunk(pattern string) (star bool, chunk, rest string) { |
| for len(pattern) > 0 && pattern[0] == '*' { |
| pattern = pattern[1:] |
| star = true |
| } |
| inrange := false |
| var i int |
| Scan: |
| for i = 0; i < len(pattern); i++ { |
| switch pattern[i] { |
| case '\\': |
| |
| // error check handled in matchChunk: bad pattern. |
| if i+1 < len(pattern) { |
| i++ |
| } |
| case '[': |
| inrange = true |
| case ']': |
| inrange = false |
| case '*': |
| if !inrange { |
| break Scan |
| } |
| } |
| } |
| return star, pattern[0:i], pattern[i:] |
| } |
| |
| // matchChunk checks whether chunk matches the beginning of s. |
| // If so, it returns the remainder of s (after the match). |
| // Chunk is all single-character operators: literals, char classes, and ?. |
| func matchChunk(chunk, s string) (rest string, ok bool, err error) { |
| for len(chunk) > 0 { |
| if len(s) == 0 { |
| return |
| } |
| switch chunk[0] { |
| case '[': |
| // character class |
| r, n := utf8.DecodeRuneInString(s) |
| s = s[n:] |
| chunk = chunk[1:] |
| // We can't end right after '[', we're expecting at least |
| // a closing bracket and possibly a caret. |
| if len(chunk) == 0 { |
| err = ErrBadPattern |
| return |
| } |
| // possibly negated |
| negated := chunk[0] == '^' |
| if negated { |
| chunk = chunk[1:] |
| } |
| // parse all ranges |
| match := false |
| nrange := 0 |
| for { |
| if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { |
| chunk = chunk[1:] |
| break |
| } |
| var lo, hi rune |
| if lo, chunk, err = getEsc(chunk); err != nil { |
| return |
| } |
| hi = lo |
| if chunk[0] == '-' { |
| if hi, chunk, err = getEsc(chunk[1:]); err != nil { |
| return |
| } |
| } |
| if lo <= r && r <= hi { |
| match = true |
| } |
| nrange++ |
| } |
| if match == negated { |
| return |
| } |
| |
| case '?': |
| if isPathSeparator(s[0]) { |
| return |
| } |
| _, n := utf8.DecodeRuneInString(s) |
| s = s[n:] |
| chunk = chunk[1:] |
| |
| case '\\': |
| chunk = chunk[1:] |
| if len(chunk) == 0 { |
| err = ErrBadPattern |
| return |
| } |
| fallthrough |
| |
| default: |
| if chunk[0] != s[0] { |
| return |
| } |
| s = s[1:] |
| chunk = chunk[1:] |
| } |
| } |
| return s, true, nil |
| } |
| |
| // getEsc gets a possibly-escaped character from chunk, for a character class. |
| func getEsc(chunk string) (r rune, nchunk string, err error) { |
| if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { |
| err = ErrBadPattern |
| return |
| } |
| if chunk[0] == '\\' { |
| chunk = chunk[1:] |
| if len(chunk) == 0 { |
| err = ErrBadPattern |
| return |
| } |
| } |
| r, n := utf8.DecodeRuneInString(chunk) |
| if r == utf8.RuneError && n == 1 { |
| err = ErrBadPattern |
| } |
| nchunk = chunk[n:] |
| if len(nchunk) == 0 { |
| err = ErrBadPattern |
| } |
| return |
| } |
| |
| // Split splits path immediately following the final Separator, |
| // separating it into a directory and file name component. |
| // If there is no Separator in path, Split returns an empty dir |
| // and file set to path. |
| // The returned values have the property that path = dir+file. |
| func Split(path string) (dir, file string) { |
| i := len(path) - 1 |
| for i >= 0 && !isPathSeparator(path[i]) { |
| i-- |
| } |
| return path[:i+1], path[i+1:] |
| } |
| |
| // Glob returns the names of all files matching pattern or nil |
| // if there is no matching file. The syntax of patterns is the same |
| // as in Match. The pattern may describe hierarchical names such as |
| // /usr/*/bin/ed (assuming the Separator is '/'). |
| // |
| // Glob ignores file system errors such as I/O errors reading directories. |
| // The only possible returned error is ErrBadPattern, when pattern |
| // is malformed. |
| func (c *Client) Glob(pattern string) (matches []string, err error) { |
| if !hasMeta(pattern) { |
| file, err := c.Lstat(pattern) |
| if err != nil { |
| return nil, nil |
| } |
| dir, _ := Split(pattern) |
| dir = cleanGlobPath(dir) |
| return []string{Join(dir, file.Name())}, nil |
| } |
| |
| dir, file := Split(pattern) |
| dir = cleanGlobPath(dir) |
| |
| if !hasMeta(dir) { |
| return c.glob(dir, file, nil) |
| } |
| |
| // Prevent infinite recursion. See issue 15879. |
| if dir == pattern { |
| return nil, ErrBadPattern |
| } |
| |
| var m []string |
| m, err = c.Glob(dir) |
| if err != nil { |
| return |
| } |
| for _, d := range m { |
| matches, err = c.glob(d, file, matches) |
| if err != nil { |
| return |
| } |
| } |
| return |
| } |
| |
| // cleanGlobPath prepares path for glob matching. |
| func cleanGlobPath(path string) string { |
| switch path { |
| case "": |
| return "." |
| case string(separator): |
| // do nothing to the path |
| return path |
| default: |
| return path[0 : len(path)-1] // chop off trailing separator |
| } |
| } |
| |
| // glob searches for files matching pattern in the directory dir |
| // and appends them to matches. If the directory cannot be |
| // opened, it returns the existing matches. New matches are |
| // added in lexicographical order. |
| func (c *Client) glob(dir, pattern string, matches []string) (m []string, e error) { |
| m = matches |
| fi, err := c.Stat(dir) |
| if err != nil { |
| return |
| } |
| if !fi.IsDir() { |
| return |
| } |
| names, err := c.ReadDir(dir) |
| if err != nil { |
| return |
| } |
| //sort.Strings(names) |
| |
| for _, n := range names { |
| matched, err := Match(pattern, n.Name()) |
| if err != nil { |
| return m, err |
| } |
| if matched { |
| m = append(m, Join(dir, n.Name())) |
| } |
| } |
| return |
| } |
| |
| // Join joins any number of path elements into a single path, adding |
| // a Separator if necessary. |
| // all empty strings are ignored. |
| func Join(elem ...string) string { |
| return path.Join(elem...) |
| } |
| |
| // hasMeta reports whether path contains any of the magic characters |
| // recognized by Match. |
| func hasMeta(path string) bool { |
| // TODO(niemeyer): Should other magic characters be added here? |
| return strings.ContainsAny(path, "*?[") |
| } |