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/aws/aws-sdk-go v1.22.0
|
||||||
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect
|
||||||
github.com/blang/semver v3.5.1+incompatible
|
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/boltdb/bolt v1.3.1 // indirect
|
||||||
github.com/chzyer/logex v1.1.10 // indirect
|
github.com/chzyer/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
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/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 h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
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 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
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=
|
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"
|
"path/filepath"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar"
|
||||||
"github.com/hashicorp/hcl2/hcl"
|
"github.com/hashicorp/hcl2/hcl"
|
||||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
homedir "github.com/mitchellh/go-homedir"
|
||||||
|
@ -235,7 +236,7 @@ func MakeFileSetFunc(baseDir string) function.Function {
|
||||||
// automatically cleaned during this operation.
|
// automatically cleaned during this operation.
|
||||||
pattern = filepath.Join(path, pattern)
|
pattern = filepath.Join(path, pattern)
|
||||||
|
|
||||||
matches, err := filepath.Glob(pattern)
|
matches, err := doublestar.Glob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
|
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),
|
cty.SetValEmpty(cty.String),
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cty.StringVal("."),
|
||||||
|
cty.StringVal("{testdata,missing}"),
|
||||||
|
cty.SetValEmpty(cty.String),
|
||||||
|
false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cty.StringVal("."),
|
cty.StringVal("."),
|
||||||
cty.StringVal("testdata/missing"),
|
cty.StringVal("testdata/missing"),
|
||||||
|
@ -261,6 +267,12 @@ func TestFileSet(t *testing.T) {
|
||||||
cty.SetValEmpty(cty.String),
|
cty.SetValEmpty(cty.String),
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
cty.StringVal("."),
|
||||||
|
cty.StringVal("**/missing"),
|
||||||
|
cty.SetValEmpty(cty.String),
|
||||||
|
false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cty.StringVal("."),
|
cty.StringVal("."),
|
||||||
cty.StringVal("testdata/*.txt"),
|
cty.StringVal("testdata/*.txt"),
|
||||||
|
@ -294,6 +306,15 @@ func TestFileSet(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
false,
|
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("."),
|
||||||
cty.StringVal("*/hello.txt"),
|
cty.StringVal("*/hello.txt"),
|
||||||
|
@ -319,6 +340,24 @@ func TestFileSet(t *testing.T) {
|
||||||
}),
|
}),
|
||||||
false,
|
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("."),
|
||||||
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/bgentry/speakeasy
|
||||||
# github.com/blang/semver v3.5.1+incompatible
|
# github.com/blang/semver v3.5.1+incompatible
|
||||||
github.com/blang/semver
|
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 v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/chzyer/readline
|
github.com/chzyer/readline
|
||||||
# github.com/coreos/etcd v3.3.10+incompatible
|
# 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/hcl/strconv
|
||||||
github.com/hashicorp/hcl/json/scanner
|
github.com/hashicorp/hcl/json/scanner
|
||||||
github.com/hashicorp/hcl/json/token
|
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
|
||||||
github.com/hashicorp/hcl2/hcl/hclsyntax
|
github.com/hashicorp/hcl2/hcl/hclsyntax
|
||||||
github.com/hashicorp/hcl2/hcldec
|
github.com/hashicorp/hcl2/hcldec
|
||||||
|
@ -353,7 +355,7 @@ github.com/hashicorp/hil/scanner
|
||||||
github.com/hashicorp/logutils
|
github.com/hashicorp/logutils
|
||||||
# github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb
|
# github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb
|
||||||
github.com/hashicorp/serf/coordinate
|
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/terraform-config-inspect/tfconfig
|
||||||
# github.com/hashicorp/vault v0.10.4
|
# github.com/hashicorp/vault v0.10.4
|
||||||
github.com/hashicorp/vault/helper/pgpkeys
|
github.com/hashicorp/vault/helper/pgpkeys
|
||||||
|
|
|
@ -24,9 +24,16 @@ fileset(path, pattern)
|
||||||
Supported pattern matches:
|
Supported pattern matches:
|
||||||
|
|
||||||
- `*` - matches any sequence of non-separator characters
|
- `*` - matches any sequence of non-separator characters
|
||||||
|
- `**` - matches any sequence of characters, including separator characters
|
||||||
- `?` - matches any single non-separator character
|
- `?` - matches any single non-separator character
|
||||||
- `[RANGE]` - matches a range of characters
|
- `{alternative1,...}` - matches a sequence of characters if one of the comma-separated alternatives matches
|
||||||
- `[^RANGE]` - matches outside the range of characters
|
- `[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,
|
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
|
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",
|
"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",
|
"hello.txt",
|
||||||
"world.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
|
A common use of `fileset` is to create one resource instance per matched file, using
|
||||||
|
|
Loading…
Reference in New Issue