Merge pull request #22523 from hashicorp/f-lang-funcs-filelist

lang/funcs: Add fileset function
This commit is contained in:
Pam Selle 2019-08-26 12:30:13 -04:00 committed by GitHub
commit cf687a94b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 241 additions and 0 deletions

View File

@ -207,6 +207,60 @@ func MakeFileExistsFunc(baseDir string) function.Function {
}) })
} }
// MakeFileSetFunc constructs a function that takes a glob pattern
// and enumerates a file set from that pattern
func MakeFileSetFunc(baseDir string) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "pattern",
Type: cty.String,
},
},
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)
}
if !filepath.IsAbs(pattern) {
pattern = filepath.Join(baseDir, pattern)
}
// Ensure that the path is canonical for the host OS
pattern = filepath.Clean(pattern)
matches, err := filepath.Glob(pattern)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
}
var matchVals []cty.Value
for _, match := range matches {
fi, err := os.Stat(match)
if err != nil {
return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err)
}
if !fi.Mode().IsRegular() {
continue
}
matchVals = append(matchVals, cty.StringVal(match))
}
if len(matchVals) == 0 {
return cty.SetValEmpty(cty.String), nil
}
return cty.SetVal(matchVals), nil
},
})
}
// BasenameFunc constructs a function that takes a string containing a filesystem path // BasenameFunc constructs a function that takes a string containing a filesystem path
// and removes all except the last portion from it. // and removes all except the last portion from it.
var BasenameFunc = function.New(&function.Spec{ var BasenameFunc = function.New(&function.Spec{
@ -316,6 +370,16 @@ func FileExists(baseDir string, path cty.Value) (cty.Value, error) {
return fn.Call([]cty.Value{path}) return fn.Call([]cty.Value{path})
} }
// FileSet enumerates a set of files given a glob pattern
//
// 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) {
fn := MakeFileSetFunc(baseDir)
return fn.Call([]cty.Value{pattern})
}
// FileBase64 reads the contents of the file at the given path. // FileBase64 reads the contents of the file at the given path.
// //
// The bytes from the file are encoded as base64 before returning. // The bytes from the file are encoded as base64 before returning.

View File

@ -224,6 +224,120 @@ func TestFileExists(t *testing.T) {
} }
} }
func TestFileSet(t *testing.T) {
tests := []struct {
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("testdata*"),
cty.SetValEmpty(cty.String),
false,
},
{
cty.StringVal("testdata/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/hello.???"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("testdata/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("*/hello.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("*/*.txt"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("*/hello*"),
cty.SetVal([]cty.Value{
cty.StringVal("testdata/hello.tmpl"),
cty.StringVal("testdata/hello.txt"),
}),
false,
},
{
cty.StringVal("["),
cty.SetValEmpty(cty.String),
true,
},
{
cty.StringVal("\\"),
cty.SetValEmpty(cty.String),
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileSet(\".\", %#v)", test.Pattern), func(t *testing.T) {
got, err := FileSet(".", test.Pattern)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
func TestFileBase64(t *testing.T) { func TestFileBase64(t *testing.T) {
tests := []struct { tests := []struct {
Path cty.Value Path cty.Value

View File

@ -56,6 +56,7 @@ func (s *Scope) Functions() map[string]function.Function {
"chunklist": funcs.ChunklistFunc, "chunklist": funcs.ChunklistFunc,
"file": funcs.MakeFileFunc(s.BaseDir, false), "file": funcs.MakeFileFunc(s.BaseDir, false),
"fileexists": funcs.MakeFileExistsFunc(s.BaseDir), "fileexists": funcs.MakeFileExistsFunc(s.BaseDir),
"fileset": funcs.MakeFileSetFunc(s.BaseDir),
"filebase64": funcs.MakeFileFunc(s.BaseDir, true), "filebase64": funcs.MakeFileFunc(s.BaseDir, true),
"filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir), "filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir),
"filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir), "filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir),

View File

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

View File

@ -0,0 +1,48 @@
---
layout: "functions"
page_title: "fileset - Functions - Configuration Language"
sidebar_current: "docs-funcs-file-file-set"
description: |-
The fileset function enumerates a set of regular file names given a pattern.
---
# `fileset` Function
-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and
earlier, see
[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html).
`fileset` enumerates a set of regular file names given a pattern.
```hcl
fileset(pattern)
```
Supported pattern matches:
- `*` - matches any sequence of non-separator characters
- `?` - matches any single non-separator character
- `[RANGE]` - matches a range of characters
- `[^RANGE]` - matches outside the range of characters
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
before Terraform takes any actions.
## Examples
```
> fileset("${path.module}/*.txt")
[
"path/to/module/hello.txt",
"path/to/module/world.txt",
]
```
```hcl
resource "example_thing" "example" {
for_each = fileset("${path.module}/files/*")
# other configuration using each.value
}
```

View File

@ -304,6 +304,10 @@
<a href="/docs/configuration/functions/fileexists.html">fileexists</a> <a href="/docs/configuration/functions/fileexists.html">fileexists</a>
</li> </li>
<li>
<a href="/docs/configuration/functions/fileset.html">fileset</a>
</li>
<li> <li>
<a href="/docs/configuration/functions/filebase64.html">filebase64</a> <a href="/docs/configuration/functions/filebase64.html">filebase64</a>
</li> </li>