Merge pull request #22621 from hashicorp/f-lang-funcs-fileset-enhancements

lang/funcs: Various fileset() function updates for usability and consistency
This commit is contained in:
Brian Flad 2019-09-03 23:10:37 -04:00 committed by GitHub
commit d9704d40bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 879 additions and 48 deletions

1
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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"
@ -212,6 +213,10 @@ func MakeFileExistsFunc(baseDir string) function.Function {
func MakeFileSetFunc(baseDir string) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
{
Name: "pattern",
Type: cty.String,
@ -219,20 +224,19 @@ func MakeFileSetFunc(baseDir string) function.Function {
},
Type: function.StaticReturnType(cty.Set(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
pattern := args[0].AsString()
pattern, err := homedir.Expand(pattern)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to expand ~: %s", err)
path := args[0].AsString()
pattern := args[1].AsString()
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
}
if !filepath.IsAbs(pattern) {
pattern = filepath.Join(baseDir, pattern)
}
// Join the path to the glob pattern, while ensuring the full
// pattern is canonical for the host OS. The joined path is
// automatically cleaned during this operation.
pattern = filepath.Join(path, pattern)
// Ensure that the path is canonical for the host OS
pattern = filepath.Clean(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)
}
@ -249,6 +253,17 @@ func MakeFileSetFunc(baseDir string) function.Function {
continue
}
// Remove the path and file separator from matches.
match, err = filepath.Rel(path, match)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err)
}
// Replace any remaining file separators with forward slash (/)
// separators for cross-system compatibility.
match = filepath.ToSlash(match)
matchVals = append(matchVals, cty.StringVal(match))
}
@ -375,9 +390,9 @@ func FileExists(baseDir string, path cty.Value) (cty.Value, error) {
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func FileSet(baseDir string, pattern cty.Value) (cty.Value, error) {
func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) {
fn := MakeFileSetFunc(baseDir)
return fn.Call([]cty.Value{pattern})
return fn.Call([]cty.Value{path, pattern})
}
// FileBase64 reads the contents of the file at the given path.

View File

@ -226,36 +226,55 @@ func TestFileExists(t *testing.T) {
func TestFileSet(t *testing.T) {
tests := []struct {
Path cty.Value
Pattern cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata/missing*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("*/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("{testdata,missing}"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/missing*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("*/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("**/missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
@ -263,6 +282,7 @@ func TestFileSet(t *testing.T) {
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
@ -270,6 +290,7 @@ func TestFileSet(t *testing.T) {
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/hello.???"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
@ -277,6 +298,7 @@ func TestFileSet(t *testing.T) {
false,
},
{
cty.StringVal("."),
cty.StringVal("testdata/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.tmpl"),
@ -285,6 +307,16 @@ 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"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
@ -292,6 +324,7 @@ func TestFileSet(t *testing.T) {
false,
},
{
cty.StringVal("."),
cty.StringVal("*/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
@ -299,6 +332,7 @@ func TestFileSet(t *testing.T) {
false,
},
{
cty.StringVal("."),
cty.StringVal("*/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.tmpl"),
@ -307,20 +341,85 @@ 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("["),
cty.SetValEmpty(cty.String),
true,
},
{
cty.StringVal("."),
cty.StringVal("\\"),
cty.SetValEmpty(cty.String),
true,
},
{
cty.StringVal("testdata"),
cty.StringVal("missing"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata"),
cty.StringVal("missing*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata"),
cty.StringVal("*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata"),
cty.StringVal("hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata"),
cty.StringVal("hello.???"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.txt"),
}),
false,
},
{
cty.StringVal("testdata"),
cty.StringVal("hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("hello.tmpl"),
cty.StringVal("hello.txt"),
}),
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileSet(\".\", %#v)", test.Pattern), func(t *testing.T) {
got, err := FileSet(".", test.Pattern)
t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) {
got, err := FileSet(".", test.Path, test.Pattern)
if test.Err {
if err == nil {

View File

@ -281,10 +281,31 @@ func TestFunctions(t *testing.T) {
"fileset": {
{
`fileset("hello.*")`,
`fileset(".", "*/hello.*")`,
cty.SetVal([]cty.Value{
cty.StringVal("testdata/functions-test/hello.tmpl"),
cty.StringVal("testdata/functions-test/hello.txt"),
cty.StringVal("subdirectory/hello.tmpl"),
cty.StringVal("subdirectory/hello.txt"),
}),
},
{
`fileset(".", "subdirectory/hello.*")`,
cty.SetVal([]cty.Value{
cty.StringVal("subdirectory/hello.tmpl"),
cty.StringVal("subdirectory/hello.txt"),
}),
},
{
`fileset(".", "hello.*")`,
cty.SetVal([]cty.Value{
cty.StringVal("hello.tmpl"),
cty.StringVal("hello.txt"),
}),
},
{
`fileset("subdirectory", "hello.*")`,
cty.SetVal([]cty.Value{
cty.StringVal("hello.tmpl"),
cty.StringVal("hello.txt"),
}),
},
},

View File

@ -0,0 +1 @@
Hello, ${name}!

View File

@ -0,0 +1 @@
hello!

32
vendor/github.com/bmatcuk/doublestar/.gitignore generated vendored Normal file
View File

@ -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/

15
vendor/github.com/bmatcuk/doublestar/.travis.yml generated vendored Normal file
View File

@ -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)

22
vendor/github.com/bmatcuk/doublestar/LICENSE generated vendored Normal file
View File

@ -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.

109
vendor/github.com/bmatcuk/doublestar/README.md generated vendored Normal file
View File

@ -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

476
vendor/github.com/bmatcuk/doublestar/doublestar.go generated vendored Normal file
View File

@ -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
}

3
vendor/github.com/bmatcuk/doublestar/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module github.com/bmatcuk/doublestar
go 1.12

6
vendor/modules.txt vendored
View File

@ -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

View File

@ -12,18 +12,28 @@ description: |-
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
`fileset` enumerates a set of regular file names given a pattern.
`fileset` enumerates a set of regular file names given a path and pattern.
The path is automatically removed from the resulting set of file names and any
result still containing path separators always returns forward slash (`/`) as
the path separator for cross-system compatibility.
```hcl
fileset(pattern)
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
@ -32,16 +42,38 @@ before Terraform takes any actions.
## Examples
```
> fileset("${path.module}/*.txt")
> fileset(path.module, "files/*.txt")
[
"path/to/module/hello.txt",
"path/to/module/world.txt",
"files/hello.txt",
"files/world.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
[the `for_each` meta-argument](/docs/configuration/resources.html#for_each-multiple-resource-instances-defined-by-a-map-or-set-of-strings):
```hcl
resource "example_thing" "example" {
for_each = fileset("${path.module}/files/*")
for_each = fileset(path.module, "files/*")
# other configuration using each.value
}