lang/funcs: Switch fileset() function glob implementation to github.com/bmatcuk/doublestar to support additional glob patterns
This allows the usage of the glob patterns `**` and `{alternative1,...}` to simplify Terraform configuration logic for more complex file matching.
This commit is contained in:
parent
769f626a0e
commit
19cf34114f
1
go.mod
1
go.mod
|
@ -19,6 +19,7 @@ require (
|
|||
github.com/aws/aws-sdk-go v1.22.0
|
||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/bmatcuk/doublestar v1.1.5
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
|
|
2
go.sum
2
go.sum
|
@ -74,6 +74,8 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ
|
|||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk=
|
||||
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bmatcuk/doublestar"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
|
@ -235,7 +236,7 @@ func MakeFileSetFunc(baseDir string) function.Function {
|
|||
// automatically cleaned during this operation.
|
||||
pattern = filepath.Join(path, pattern)
|
||||
|
||||
matches, err := filepath.Glob(pattern)
|
||||
matches, err := doublestar.Glob(pattern)
|
||||
if err != nil {
|
||||
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
|
||||
}
|
||||
|
|
|
@ -243,6 +243,12 @@ func TestFileSet(t *testing.T) {
|
|||
cty.SetValEmpty(cty.String),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("{testdata,missing}"),
|
||||
cty.SetValEmpty(cty.String),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("testdata/missing"),
|
||||
|
@ -261,6 +267,12 @@ func TestFileSet(t *testing.T) {
|
|||
cty.SetValEmpty(cty.String),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("**/missing"),
|
||||
cty.SetValEmpty(cty.String),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("testdata/*.txt"),
|
||||
|
@ -294,6 +306,15 @@ func TestFileSet(t *testing.T) {
|
|||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("testdata/hello.{tmpl,txt}"),
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.StringVal("testdata/hello.tmpl"),
|
||||
cty.StringVal("testdata/hello.txt"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("*/hello.txt"),
|
||||
|
@ -319,6 +340,24 @@ func TestFileSet(t *testing.T) {
|
|||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("**/hello*"),
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.StringVal("testdata/hello.tmpl"),
|
||||
cty.StringVal("testdata/hello.txt"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("**/hello.{tmpl,txt}"),
|
||||
cty.SetVal([]cty.Value{
|
||||
cty.StringVal("testdata/hello.tmpl"),
|
||||
cty.StringVal("testdata/hello.txt"),
|
||||
}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
cty.StringVal("."),
|
||||
cty.StringVal("["),
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# vi
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# test directory
|
||||
test/
|
|
@ -0,0 +1,15 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.11
|
||||
- 1.12
|
||||
|
||||
before_install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- go test -race -coverprofile=coverage.txt -covermode=atomic
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Bob Matcuk
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
![Release](https://img.shields.io/github/release/bmatcuk/doublestar.svg?branch=master)
|
||||
[![Build Status](https://travis-ci.org/bmatcuk/doublestar.svg?branch=master)](https://travis-ci.org/bmatcuk/doublestar)
|
||||
[![codecov.io](https://img.shields.io/codecov/c/github/bmatcuk/doublestar.svg?branch=master)](https://codecov.io/github/bmatcuk/doublestar?branch=master)
|
||||
|
||||
# doublestar
|
||||
|
||||
**doublestar** is a [golang](http://golang.org/) implementation of path pattern
|
||||
matching and globbing with support for "doublestar" (aka globstar: `**`)
|
||||
patterns.
|
||||
|
||||
doublestar patterns match files and directories recursively. For example, if
|
||||
you had the following directory structure:
|
||||
|
||||
```
|
||||
grandparent
|
||||
`-- parent
|
||||
|-- child1
|
||||
`-- child2
|
||||
```
|
||||
|
||||
You could find the children with patterns such as: `**/child*`,
|
||||
`grandparent/**/child?`, `**/parent/*`, or even just `**` by itself (which will
|
||||
return all files and directories recursively).
|
||||
|
||||
Bash's globstar is doublestar's inspiration and, as such, works similarly.
|
||||
Note that the doublestar must appear as a path component by itself. A pattern
|
||||
such as `/path**` is invalid and will be treated the same as `/path*`, but
|
||||
`/path*/**` should achieve the desired result. Additionally, `/path/**` will
|
||||
match all directories and files under the path directory, but `/path/**/` will
|
||||
only match directories.
|
||||
|
||||
## Installation
|
||||
|
||||
**doublestar** can be installed via `go get`:
|
||||
|
||||
```bash
|
||||
go get github.com/bmatcuk/doublestar
|
||||
```
|
||||
|
||||
To use it in your code, you must import it:
|
||||
|
||||
```go
|
||||
import "github.com/bmatcuk/doublestar"
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### Match
|
||||
```go
|
||||
func Match(pattern, name string) (bool, error)
|
||||
```
|
||||
|
||||
Match returns true if `name` matches the file name `pattern`
|
||||
([see below](#patterns)). `name` and `pattern` are split on forward slash (`/`)
|
||||
characters and may be relative or absolute.
|
||||
|
||||
Note: `Match()` is meant to be a drop-in replacement for `path.Match()`. As
|
||||
such, it always uses `/` as the path separator. If you are writing code that
|
||||
will run on systems where `/` is not the path separator (such as Windows), you
|
||||
want to use `PathMatch()` (below) instead.
|
||||
|
||||
|
||||
### PathMatch
|
||||
```go
|
||||
func PathMatch(pattern, name string) (bool, error)
|
||||
```
|
||||
|
||||
PathMatch returns true if `name` matches the file name `pattern`
|
||||
([see below](#patterns)). The difference between Match and PathMatch is that
|
||||
PathMatch will automatically use your system's path separator to split `name`
|
||||
and `pattern`.
|
||||
|
||||
`PathMatch()` is meant to be a drop-in replacement for `filepath.Match()`.
|
||||
|
||||
### Glob
|
||||
```go
|
||||
func Glob(pattern string) ([]string, error)
|
||||
```
|
||||
|
||||
Glob finds all files and directories in the filesystem that match `pattern`
|
||||
([see below](#patterns)). `pattern` may be relative (to the current working
|
||||
directory), or absolute.
|
||||
|
||||
`Glob()` is meant to be a drop-in replacement for `filepath.Glob()`.
|
||||
|
||||
## Patterns
|
||||
|
||||
**doublestar** supports the following special terms in the patterns:
|
||||
|
||||
Special Terms | Meaning
|
||||
------------- | -------
|
||||
`*` | matches any sequence of non-path-separators
|
||||
`**` | matches any sequence of characters, including path separators
|
||||
`?` | matches any single non-path-separator character
|
||||
`[class]` | matches any single non-path-separator character against a class of characters ([see below](#character-classes))
|
||||
`{alt1,...}` | matches a sequence of characters if one of the comma-separated alternatives matches
|
||||
|
||||
Any character with a special meaning can be escaped with a backslash (`\`).
|
||||
|
||||
### Character Classes
|
||||
|
||||
Character classes support the following:
|
||||
|
||||
Class | Meaning
|
||||
---------- | -------
|
||||
`[abc]` | matches any single character within the set
|
||||
`[a-z]` | matches any single character in the range
|
||||
`[^class]` | matches any single character which does *not* match the class
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
package doublestar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ErrBadPattern indicates a pattern was malformed.
|
||||
var ErrBadPattern = path.ErrBadPattern
|
||||
|
||||
// Split a path on the given separator, respecting escaping.
|
||||
func splitPathOnSeparator(path string, separator rune) (ret []string) {
|
||||
idx := 0
|
||||
if separator == '\\' {
|
||||
// if the separator is '\\', then we can just split...
|
||||
ret = strings.Split(path, string(separator))
|
||||
idx = len(ret)
|
||||
} else {
|
||||
// otherwise, we need to be careful of situations where the separator was escaped
|
||||
cnt := strings.Count(path, string(separator))
|
||||
if cnt == 0 {
|
||||
return []string{path}
|
||||
}
|
||||
|
||||
ret = make([]string, cnt+1)
|
||||
pathlen := len(path)
|
||||
separatorLen := utf8.RuneLen(separator)
|
||||
emptyEnd := false
|
||||
for start := 0; start < pathlen; {
|
||||
end := indexRuneWithEscaping(path[start:], separator)
|
||||
if end == -1 {
|
||||
emptyEnd = false
|
||||
end = pathlen
|
||||
} else {
|
||||
emptyEnd = true
|
||||
end += start
|
||||
}
|
||||
ret[idx] = path[start:end]
|
||||
start = end + separatorLen
|
||||
idx++
|
||||
}
|
||||
|
||||
// If the last rune is a path separator, we need to append an empty string to
|
||||
// represent the last, empty path component. By default, the strings from
|
||||
// make([]string, ...) will be empty, so we just need to icrement the count
|
||||
if emptyEnd {
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
return ret[:idx]
|
||||
}
|
||||
|
||||
// Find the first index of a rune in a string,
|
||||
// ignoring any times the rune is escaped using "\".
|
||||
func indexRuneWithEscaping(s string, r rune) int {
|
||||
end := strings.IndexRune(s, r)
|
||||
if end == -1 {
|
||||
return -1
|
||||
}
|
||||
if end > 0 && s[end-1] == '\\' {
|
||||
start := end + utf8.RuneLen(r)
|
||||
end = indexRuneWithEscaping(s[start:], r)
|
||||
if end != -1 {
|
||||
end += start
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
// Match returns true if name matches the shell file name pattern.
|
||||
// The pattern syntax is:
|
||||
//
|
||||
// pattern:
|
||||
// { term }
|
||||
// term:
|
||||
// '*' matches any sequence of non-path-separators
|
||||
// '**' matches any sequence of characters, including
|
||||
// path separators.
|
||||
// '?' matches any single non-path-separator character
|
||||
// '[' [ '^' ] { character-range } ']'
|
||||
// character class (must be non-empty)
|
||||
// '{' { term } [ ',' { term } ... ] '}'
|
||||
// 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 path-separator defaults to the '/' character. The only possible
|
||||
// returned error is ErrBadPattern, when pattern is malformed.
|
||||
//
|
||||
// Note: this is meant as a drop-in replacement for path.Match() which
|
||||
// always uses '/' as the path separator. If you want to support systems
|
||||
// which use a different path separator (such as Windows), what you want
|
||||
// is the PathMatch() function below.
|
||||
//
|
||||
func Match(pattern, name string) (bool, error) {
|
||||
return matchWithSeparator(pattern, name, '/')
|
||||
}
|
||||
|
||||
// PathMatch is like Match except that it uses your system's path separator.
|
||||
// For most systems, this will be '/'. However, for Windows, it would be '\\'.
|
||||
// Note that for systems where the path separator is '\\', escaping is
|
||||
// disabled.
|
||||
//
|
||||
// Note: this is meant as a drop-in replacement for filepath.Match().
|
||||
//
|
||||
func PathMatch(pattern, name string) (bool, error) {
|
||||
return matchWithSeparator(pattern, name, os.PathSeparator)
|
||||
}
|
||||
|
||||
// Match returns true if name matches the shell file name pattern.
|
||||
// The pattern syntax is:
|
||||
//
|
||||
// pattern:
|
||||
// { term }
|
||||
// term:
|
||||
// '*' matches any sequence of non-path-separators
|
||||
// '**' matches any sequence of characters, including
|
||||
// path separators.
|
||||
// '?' matches any single non-path-separator character
|
||||
// '[' [ '^' ] { character-range } ']'
|
||||
// character class (must be non-empty)
|
||||
// '{' { term } [ ',' { term } ... ] '}'
|
||||
// c matches character c (c != '*', '?', '\\', '[')
|
||||
// '\\' c matches character c
|
||||
//
|
||||
// character-range:
|
||||
// c matches character c (c != '\\', '-', ']')
|
||||
// '\\' c matches character c, unless separator is '\\'
|
||||
// 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 matchWithSeparator(pattern, name string, separator rune) (bool, error) {
|
||||
patternComponents := splitPathOnSeparator(pattern, separator)
|
||||
nameComponents := splitPathOnSeparator(name, separator)
|
||||
return doMatching(patternComponents, nameComponents)
|
||||
}
|
||||
|
||||
func doMatching(patternComponents, nameComponents []string) (matched bool, err error) {
|
||||
// check for some base-cases
|
||||
patternLen, nameLen := len(patternComponents), len(nameComponents)
|
||||
if patternLen == 0 && nameLen == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if patternLen == 0 || nameLen == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
patIdx, nameIdx := 0, 0
|
||||
for patIdx < patternLen && nameIdx < nameLen {
|
||||
if patternComponents[patIdx] == "**" {
|
||||
// if our last pattern component is a doublestar, we're done -
|
||||
// doublestar will match any remaining name components, if any.
|
||||
if patIdx++; patIdx >= patternLen {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// otherwise, try matching remaining components
|
||||
for ; nameIdx < nameLen; nameIdx++ {
|
||||
if m, _ := doMatching(patternComponents[patIdx:], nameComponents[nameIdx:]); m {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// try matching components
|
||||
matched, err = matchComponent(patternComponents[patIdx], nameComponents[nameIdx])
|
||||
if !matched || err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
patIdx++
|
||||
nameIdx++
|
||||
}
|
||||
return patIdx >= patternLen && nameIdx >= nameLen, nil
|
||||
}
|
||||
|
||||
// Glob returns the names of all files matching pattern or nil
|
||||
// if there is no matching file. The syntax of pattern 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.
|
||||
//
|
||||
// Your system path separator is automatically used. This means on
|
||||
// systems where the separator is '\\' (Windows), escaping will be
|
||||
// disabled.
|
||||
//
|
||||
// Note: this is meant as a drop-in replacement for filepath.Glob().
|
||||
//
|
||||
func Glob(pattern string) (matches []string, err error) {
|
||||
patternComponents := splitPathOnSeparator(filepath.ToSlash(pattern), '/')
|
||||
if len(patternComponents) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// On Windows systems, this will return the drive name ('C:') for filesystem
|
||||
// paths, or \\<server>\<share> for UNC paths. On other systems, it will
|
||||
// return an empty string. Since absolute paths on non-Windows systems start
|
||||
// with a slash, patternComponent[0] == volumeName will return true for both
|
||||
// absolute Windows paths and absolute non-Windows paths, but we need a
|
||||
// separate check for UNC paths.
|
||||
volumeName := filepath.VolumeName(pattern)
|
||||
isWindowsUNC := strings.HasPrefix(pattern, `\\`)
|
||||
if isWindowsUNC || patternComponents[0] == volumeName {
|
||||
startComponentIndex := 1
|
||||
if isWindowsUNC {
|
||||
startComponentIndex = 4
|
||||
}
|
||||
return doGlob(fmt.Sprintf("%s%s", volumeName, string(os.PathSeparator)), patternComponents[startComponentIndex:], matches)
|
||||
}
|
||||
|
||||
// otherwise, it's a relative pattern
|
||||
return doGlob(".", patternComponents, matches)
|
||||
}
|
||||
|
||||
// Perform a glob
|
||||
func doGlob(basedir string, components, matches []string) (m []string, e error) {
|
||||
m = matches
|
||||
e = nil
|
||||
|
||||
// figure out how many components we don't need to glob because they're
|
||||
// just names without patterns - we'll use os.Lstat below to check if that
|
||||
// path actually exists
|
||||
patLen := len(components)
|
||||
patIdx := 0
|
||||
for ; patIdx < patLen; patIdx++ {
|
||||
if strings.IndexAny(components[patIdx], "*?[{\\") >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if patIdx > 0 {
|
||||
basedir = filepath.Join(basedir, filepath.Join(components[0:patIdx]...))
|
||||
}
|
||||
|
||||
// Lstat will return an error if the file/directory doesn't exist
|
||||
fi, err := os.Lstat(basedir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// if there are no more components, we've found a match
|
||||
if patIdx >= patLen {
|
||||
m = append(m, basedir)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, we need to check each item in the directory...
|
||||
// first, if basedir is a symlink, follow it...
|
||||
if (fi.Mode() & os.ModeSymlink) != 0 {
|
||||
fi, err = os.Stat(basedir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// confirm it's a directory...
|
||||
if !fi.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
// read directory
|
||||
dir, err := os.Open(basedir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
files, _ := dir.Readdir(-1)
|
||||
lastComponent := (patIdx + 1) >= patLen
|
||||
if components[patIdx] == "**" {
|
||||
// if the current component is a doublestar, we'll try depth-first
|
||||
for _, file := range files {
|
||||
// if symlink, we may want to follow
|
||||
if (file.Mode() & os.ModeSymlink) != 0 {
|
||||
file, err = os.Stat(filepath.Join(basedir, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if file.IsDir() {
|
||||
// recurse into directories
|
||||
if lastComponent {
|
||||
m = append(m, filepath.Join(basedir, file.Name()))
|
||||
}
|
||||
m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx:], m)
|
||||
} else if lastComponent {
|
||||
// if the pattern's last component is a doublestar, we match filenames, too
|
||||
m = append(m, filepath.Join(basedir, file.Name()))
|
||||
}
|
||||
}
|
||||
if lastComponent {
|
||||
return // we're done
|
||||
}
|
||||
patIdx++
|
||||
lastComponent = (patIdx + 1) >= patLen
|
||||
}
|
||||
|
||||
// check items in current directory and recurse
|
||||
var match bool
|
||||
for _, file := range files {
|
||||
match, e = matchComponent(components[patIdx], file.Name())
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
if match {
|
||||
if lastComponent {
|
||||
m = append(m, filepath.Join(basedir, file.Name()))
|
||||
} else {
|
||||
m, e = doGlob(filepath.Join(basedir, file.Name()), components[patIdx+1:], m)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to match a single pattern component with a path component
|
||||
func matchComponent(pattern, name string) (bool, error) {
|
||||
// check some base cases
|
||||
patternLen, nameLen := len(pattern), len(name)
|
||||
if patternLen == 0 && nameLen == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if patternLen == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if nameLen == 0 && pattern != "*" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check for matches one rune at a time
|
||||
patIdx, nameIdx := 0, 0
|
||||
for patIdx < patternLen && nameIdx < nameLen {
|
||||
patRune, patAdj := utf8.DecodeRuneInString(pattern[patIdx:])
|
||||
nameRune, nameAdj := utf8.DecodeRuneInString(name[nameIdx:])
|
||||
if patRune == '\\' {
|
||||
// handle escaped runes
|
||||
patIdx += patAdj
|
||||
patRune, patAdj = utf8.DecodeRuneInString(pattern[patIdx:])
|
||||
if patRune == utf8.RuneError {
|
||||
return false, ErrBadPattern
|
||||
} else if patRune == nameRune {
|
||||
patIdx += patAdj
|
||||
nameIdx += nameAdj
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
} else if patRune == '*' {
|
||||
// handle stars
|
||||
if patIdx += patAdj; patIdx >= patternLen {
|
||||
// a star at the end of a pattern will always
|
||||
// match the rest of the path
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// check if we can make any matches
|
||||
for ; nameIdx < nameLen; nameIdx += nameAdj {
|
||||
if m, _ := matchComponent(pattern[patIdx:], name[nameIdx:]); m {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else if patRune == '[' {
|
||||
// handle character sets
|
||||
patIdx += patAdj
|
||||
endClass := indexRuneWithEscaping(pattern[patIdx:], ']')
|
||||
if endClass == -1 {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
endClass += patIdx
|
||||
classRunes := []rune(pattern[patIdx:endClass])
|
||||
classRunesLen := len(classRunes)
|
||||
if classRunesLen > 0 {
|
||||
classIdx := 0
|
||||
matchClass := false
|
||||
if classRunes[0] == '^' {
|
||||
classIdx++
|
||||
}
|
||||
for classIdx < classRunesLen {
|
||||
low := classRunes[classIdx]
|
||||
if low == '-' {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
classIdx++
|
||||
if low == '\\' {
|
||||
if classIdx < classRunesLen {
|
||||
low = classRunes[classIdx]
|
||||
classIdx++
|
||||
} else {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
}
|
||||
high := low
|
||||
if classIdx < classRunesLen && classRunes[classIdx] == '-' {
|
||||
// we have a range of runes
|
||||
if classIdx++; classIdx >= classRunesLen {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
high = classRunes[classIdx]
|
||||
if high == '-' {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
classIdx++
|
||||
if high == '\\' {
|
||||
if classIdx < classRunesLen {
|
||||
high = classRunes[classIdx]
|
||||
classIdx++
|
||||
} else {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
}
|
||||
}
|
||||
if low <= nameRune && nameRune <= high {
|
||||
matchClass = true
|
||||
}
|
||||
}
|
||||
if matchClass == (classRunes[0] == '^') {
|
||||
return false, nil
|
||||
}
|
||||
} else {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
patIdx = endClass + 1
|
||||
nameIdx += nameAdj
|
||||
} else if patRune == '{' {
|
||||
// handle alternatives such as {alt1,alt2,...}
|
||||
patIdx += patAdj
|
||||
endOptions := indexRuneWithEscaping(pattern[patIdx:], '}')
|
||||
if endOptions == -1 {
|
||||
return false, ErrBadPattern
|
||||
}
|
||||
endOptions += patIdx
|
||||
options := splitPathOnSeparator(pattern[patIdx:endOptions], ',')
|
||||
patIdx = endOptions + 1
|
||||
for _, o := range options {
|
||||
m, e := matchComponent(o+pattern[patIdx:], name[nameIdx:])
|
||||
if e != nil {
|
||||
return false, e
|
||||
}
|
||||
if m {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
} else if patRune == '?' || patRune == nameRune {
|
||||
// handle single-rune wildcard
|
||||
patIdx += patAdj
|
||||
nameIdx += nameAdj
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if patIdx >= patternLen && nameIdx >= nameLen {
|
||||
return true, nil
|
||||
}
|
||||
if nameIdx >= nameLen && pattern[patIdx:] == "*" || pattern[patIdx:] == "**" {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/bmatcuk/doublestar
|
||||
|
||||
go 1.12
|
|
@ -128,6 +128,8 @@ github.com/bgentry/go-netrc/netrc
|
|||
github.com/bgentry/speakeasy
|
||||
# github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/blang/semver
|
||||
# github.com/bmatcuk/doublestar v1.1.5
|
||||
github.com/bmatcuk/doublestar
|
||||
# github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/chzyer/readline
|
||||
# github.com/coreos/etcd v3.3.10+incompatible
|
||||
|
@ -332,7 +334,7 @@ github.com/hashicorp/hcl/hcl/scanner
|
|||
github.com/hashicorp/hcl/hcl/strconv
|
||||
github.com/hashicorp/hcl/json/scanner
|
||||
github.com/hashicorp/hcl/json/token
|
||||
# github.com/hashicorp/hcl2 v0.0.0-20190809210004-72d32879a5c5
|
||||
# github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6
|
||||
github.com/hashicorp/hcl2/hcl
|
||||
github.com/hashicorp/hcl2/hcl/hclsyntax
|
||||
github.com/hashicorp/hcl2/hcldec
|
||||
|
@ -353,7 +355,7 @@ github.com/hashicorp/hil/scanner
|
|||
github.com/hashicorp/logutils
|
||||
# github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb
|
||||
github.com/hashicorp/serf/coordinate
|
||||
# github.com/hashicorp/terraform-config-inspect v0.0.0-20190327195015-8022a2663a70
|
||||
# github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4
|
||||
github.com/hashicorp/terraform-config-inspect/tfconfig
|
||||
# github.com/hashicorp/vault v0.10.4
|
||||
github.com/hashicorp/vault/helper/pgpkeys
|
||||
|
|
|
@ -24,9 +24,16 @@ fileset(path, pattern)
|
|||
Supported pattern matches:
|
||||
|
||||
- `*` - matches any sequence of non-separator characters
|
||||
- `**` - matches any sequence of characters, including separator characters
|
||||
- `?` - matches any single non-separator character
|
||||
- `[RANGE]` - matches a range of characters
|
||||
- `[^RANGE]` - matches outside the range of characters
|
||||
- `{alternative1,...}` - matches a sequence of characters if one of the comma-separated alternatives matches
|
||||
- `[CLASS]` - matches any single non-separator character inside a class of characters (see below)
|
||||
- `[^CLASS]` - matches any single non-separator character outside a class of characters (see below)
|
||||
|
||||
Character classes support the following:
|
||||
|
||||
- `[abc]` - matches any single character within the set
|
||||
- `[a-z]` - matches any single character within the range
|
||||
|
||||
Functions are evaluated during configuration parsing rather than at apply time,
|
||||
so this function can only be used with files that are already present on disk
|
||||
|
@ -41,11 +48,24 @@ before Terraform takes any actions.
|
|||
"files/world.txt",
|
||||
]
|
||||
|
||||
> fileset("${path.module}/files", "*.txt")
|
||||
> fileset(path.module, "files/{hello,world}.txt")
|
||||
[
|
||||
"files/hello.txt",
|
||||
"files/world.txt",
|
||||
]
|
||||
|
||||
> fileset("${path.module}/files", "*")
|
||||
[
|
||||
"hello.txt",
|
||||
"world.txt",
|
||||
]
|
||||
|
||||
> fileset("${path.module}/files", "**")
|
||||
[
|
||||
"hello.txt",
|
||||
"world.txt",
|
||||
"subdirectory/anotherfile.txt",
|
||||
]
|
||||
```
|
||||
|
||||
A common use of `fileset` is to create one resource instance per matched file, using
|
||||
|
|
Loading…
Reference in New Issue