diff --git a/lang/funcs/filesystem.go b/lang/funcs/filesystem.go index 142be4dff6..b5a71777f1 100644 --- a/lang/funcs/filesystem.go +++ b/lang/funcs/filesystem.go @@ -91,6 +91,22 @@ var DirnameFunc = function.New(&function.Spec{ }, }) +// PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory. +var PathExpandFunc = 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) { + + homePath, err := homedir.Expand(args[0].AsString()) + return cty.StringVal(homePath), err + }, +}) + // 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. @@ -134,3 +150,14 @@ func Basename(path cty.Value) (cty.Value, error) { func Dirname(path cty.Value) (cty.Value, error) { return DirnameFunc.Call([]cty.Value{path}) } + +// Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with +// the current user's home directory path. +// +// 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 leading segment in the path is not `~` then the given path is returned unmodified. +func Pathexpand(path cty.Value) (cty.Value, error) { + return PathExpandFunc.Call([]cty.Value{path}) +} diff --git a/lang/funcs/filesystem_test.go b/lang/funcs/filesystem_test.go index 43fb30b518..970feb6963 100644 --- a/lang/funcs/filesystem_test.go +++ b/lang/funcs/filesystem_test.go @@ -2,8 +2,10 @@ package funcs import ( "fmt" + "path/filepath" "testing" + homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" ) @@ -191,3 +193,58 @@ func TestDirname(t *testing.T) { }) } } + +func TestPathExpand(t *testing.T) { + homePath, err := homedir.Dir() + if err != nil { + t.Fatalf("Error getting home directory: %v", err) + } + + tests := []struct { + Path cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("~/test-file"), + cty.StringVal(filepath.Join(homePath, "test-file")), + false, + }, + { + cty.StringVal("~/another/test/file"), + cty.StringVal(filepath.Join(homePath, "another/test/file")), + false, + }, + { + cty.StringVal("/root/file"), + cty.StringVal("/root/file"), + false, + }, + { + cty.StringVal("/"), + cty.StringVal("/"), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) { + got, err := Pathexpand(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 9aba8d8bf9..695c55ebab 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -73,7 +73,7 @@ func (s *Scope) Functions() map[string]function.Function { "md5": unimplFunc, // TODO "merge": unimplFunc, // TODO "min": stdlib.MinFunc, - "pathexpand": unimplFunc, // TODO + "pathexpand": funcs.PathExpandFunc, "pow": unimplFunc, // TODO "replace": unimplFunc, // TODO "rsadecrypt": unimplFunc, // TODO