diff --git a/lang/funcs/filesystem.go b/lang/funcs/filesystem.go index c54964daa..142be4dff 100644 --- a/lang/funcs/filesystem.go +++ b/lang/funcs/filesystem.go @@ -77,6 +77,20 @@ var BasenameFunc = function.New(&function.Spec{ }, }) +// DirnameFunc constructs a function that takes a string containing a filesystem path and removes the last portion from it. +var DirnameFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(filepath.Dir(args[0].AsString())), nil + }, +}) + // File reads the contents of the file at the given path. // // The file must contain valid UTF-8 bytes, or this function will return an error. @@ -110,3 +124,13 @@ func FileBase64(baseDir string, path cty.Value) (cty.Value, error) { func Basename(path cty.Value) (cty.Value, error) { return BasenameFunc.Call([]cty.Value{path}) } + +// Dirname takes a string containing a filesystem path and removes the last portion from it. +// +// The underlying function implementation works only with the path string and does not access the filesystem itself. +// It is therefore unable to take into account filesystem features such as symlinks. +// +// If the path is empty then the result is ".", representing the current working directory. +func Dirname(path cty.Value) (cty.Value, error) { + return DirnameFunc.Call([]cty.Value{path}) +} diff --git a/lang/funcs/filesystem_test.go b/lang/funcs/filesystem_test.go index 213a7c4cb..43fb30b51 100644 --- a/lang/funcs/filesystem_test.go +++ b/lang/funcs/filesystem_test.go @@ -141,3 +141,53 @@ func TestBasename(t *testing.T) { }) } } + +func TestDirname(t *testing.T) { + tests := []struct { + Path cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("testdata/hello.txt"), + cty.StringVal("testdata"), + false, + }, + { + cty.StringVal("testdata/foo/hello.txt"), + cty.StringVal("testdata/foo"), + false, + }, + { + cty.StringVal("hello.txt"), + cty.StringVal("."), + false, + }, + { + cty.StringVal(""), + cty.StringVal("."), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) { + got, err := Dirname(test.Path) + + 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) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index cd05fef60..9aba8d8bf 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -47,7 +47,7 @@ func (s *Scope) Functions() map[string]function.Function { "concat": stdlib.ConcatFunc, "contains": unimplFunc, // TODO "csvdecode": stdlib.CSVDecodeFunc, - "dirname": unimplFunc, // TODO + "dirname": funcs.DirnameFunc, "distinct": unimplFunc, // TODO "element": funcs.ElementFunc, "chunklist": unimplFunc, // TODO