summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/content/config.md6
-rw-r--r--internal/iohelpers/filemode.go6
-rw-r--r--internal/iohelpers/filemode_test.go4
-rw-r--r--internal/tests/integration/basic_test.go45
-rw-r--r--template.go15
-rw-r--r--template_test.go6
6 files changed, 66 insertions, 16 deletions
diff --git a/docs/content/config.md b/docs/content/config.md
index c1179f63..22b6f239 100644
--- a/docs/content/config.md
+++ b/docs/content/config.md
@@ -243,6 +243,9 @@ See [`--output-dir`](../usage/#--input-dir-and---output-dir).
The directory to write rendered output files. Must be used with
[`inputDir`](#inputdir).
+If the directory is missing, it will be created with the same permissions as the
+`inputDir`.
+
```yaml
inputDir: templates/
outputDir: out/
@@ -258,6 +261,9 @@ An array of output file paths. The special value `-` means `Stdout`. Multiple
values can be set, but there must be a corresponding number of `inputFiles`
entries present.
+If any of the parent directories are missing, they will be created with the same
+permissions as the input directories.
+
```yaml
inputFiles:
- first.tmpl
diff --git a/internal/iohelpers/filemode.go b/internal/iohelpers/filemode.go
index 35a3c142..7ac0e3df 100644
--- a/internal/iohelpers/filemode.go
+++ b/internal/iohelpers/filemode.go
@@ -17,8 +17,10 @@ func NormalizeFileMode(mode os.FileMode) os.FileMode {
}
func windowsFileMode(mode os.FileMode) os.FileMode {
- // non-owner and execute bits are stripped
- mode &^= 0o177
+ // non-owner and execute bits are stripped on files
+ if !mode.IsDir() {
+ mode &^= 0o177
+ }
if mode&0o200 != 0 {
// writeable implies read/write on Windows
diff --git a/internal/iohelpers/filemode_test.go b/internal/iohelpers/filemode_test.go
index 2ebb0ed7..8a7c9a2e 100644
--- a/internal/iohelpers/filemode_test.go
+++ b/internal/iohelpers/filemode_test.go
@@ -2,6 +2,7 @@ package iohelpers
import (
"fmt"
+ "io/fs"
"os"
"testing"
@@ -34,4 +35,7 @@ func TestWindowsFileMode(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%o", d.expected), fmt.Sprintf("%o", actual))
assert.Equal(t, d.expected, actual)
}
+
+ // directories are always 0777
+ assert.Equal(t, 0o777|fs.ModeDir, windowsFileMode(0o755|fs.ModeDir))
}
diff --git a/internal/tests/integration/basic_test.go b/internal/tests/integration/basic_test.go
index 243bef4b..a1e2dc70 100644
--- a/internal/tests/integration/basic_test.go
+++ b/internal/tests/integration/basic_test.go
@@ -1,6 +1,7 @@
package integration
import (
+ "io/fs"
"io/ioutil"
"os"
"testing"
@@ -8,14 +9,19 @@ import (
"github.com/hairyhenderson/gomplate/v3/internal/iohelpers"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
- "gotest.tools/v3/fs"
+ testfs "gotest.tools/v3/fs"
)
-func setupBasicTest(t *testing.T) *fs.Dir {
- tmpDir := fs.NewDir(t, "gomplate-inttests",
- fs.WithFile("one", "hi\n", fs.WithMode(0640)),
- fs.WithFile("two", "hello\n"),
- fs.WithFile("broken", "", fs.WithMode(0000)))
+func setupBasicTest(t *testing.T) *testfs.Dir {
+ tmpDir := testfs.NewDir(t, "gomplate-inttests",
+ testfs.WithFile("one", "hi\n", testfs.WithMode(0640)),
+ testfs.WithFile("two", "hello\n"),
+ testfs.WithFile("broken", "", testfs.WithMode(0000)),
+ testfs.WithDir("subdir",
+ testfs.WithFile("f1", "first\n", testfs.WithMode(0640)),
+ testfs.WithFile("f2", "second\n"),
+ ),
+ )
t.Cleanup(tmpDir.Remove)
return tmpDir
}
@@ -256,3 +262,30 @@ func TestBasic_AppliesChmodBeforeWrite(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, "hi\n", string(content))
}
+
+func TestBasic_CreatesMissingDirectory(t *testing.T) {
+ tmpDir := setupBasicTest(t)
+ out := tmpDir.Join("foo/bar/baz")
+ o, e, err := cmd(t, "-f", tmpDir.Join("one"), "-o", out).run()
+ assertSuccess(t, o, e, err, "")
+
+ info, err := os.Stat(out)
+ assert.NilError(t, err)
+ assert.Equal(t, iohelpers.NormalizeFileMode(0640), info.Mode())
+ content, err := ioutil.ReadFile(out)
+ assert.NilError(t, err)
+ assert.Equal(t, "hi\n", string(content))
+
+ out = tmpDir.Join("outdir")
+ o, e, err = cmd(t,
+ "--input-dir", tmpDir.Join("subdir"),
+ "--output-dir", out,
+ ).run()
+ assertSuccess(t, o, e, err, "")
+
+ info, err = os.Stat(out)
+ assert.NilError(t, err)
+
+ assert.Equal(t, iohelpers.NormalizeFileMode(0o755|fs.ModeDir), info.Mode())
+ assert.Equal(t, true, info.IsDir())
+}
diff --git a/template.go b/template.go
index 6e5dfb5c..9d66dee0 100644
--- a/template.go
+++ b/template.go
@@ -145,7 +145,7 @@ func processTemplates(cfg *config.Config, templates []*tplate) ([]*tplate, error
}
if t.target == nil {
- out, err := openOutFile(cfg, t.targetPath, t.mode, t.modeOverride)
+ out, err := openOutFile(cfg, t.targetPath, 0755, t.mode, t.modeOverride)
if err != nil {
return nil, err
}
@@ -241,13 +241,13 @@ func fileToTemplates(inFile, outFile string, mode os.FileMode, modeOverride bool
return tmpl, nil
}
-func openOutFile(cfg *config.Config, filename string, mode os.FileMode, modeOverride bool) (out io.Writer, err error) {
+func openOutFile(cfg *config.Config, filename string, dirMode, mode os.FileMode, modeOverride bool) (out io.Writer, err error) {
if cfg.SuppressEmpty {
out = iohelpers.NewEmptySkipper(func() (io.Writer, error) {
if filename == "-" {
return cfg.Stdout, nil
}
- return createOutFile(filename, mode, modeOverride)
+ return createOutFile(filename, dirMode, mode, modeOverride)
})
return out, nil
}
@@ -255,10 +255,10 @@ func openOutFile(cfg *config.Config, filename string, mode os.FileMode, modeOver
if filename == "-" {
return cfg.Stdout, nil
}
- return createOutFile(filename, mode, modeOverride)
+ return createOutFile(filename, dirMode, mode, modeOverride)
}
-func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) {
+func createOutFile(filename string, dirMode, mode os.FileMode, modeOverride bool) (out io.WriteCloser, err error) {
mode = iohelpers.NormalizeFileMode(mode.Perm())
if modeOverride {
err = fs.Chmod(filename, mode)
@@ -268,6 +268,11 @@ func createOutFile(filename string, mode os.FileMode, modeOverride bool) (out io
}
open := func() (out io.WriteCloser, err error) {
+ // Ensure file parent dirs
+ if err = fs.MkdirAll(filepath.Dir(filename), dirMode); err != nil {
+ return nil, err
+ }
+
out, err = fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return out, fmt.Errorf("failed to open output file '%s' for writing: %w", filename, err)
diff --git a/template_test.go b/template_test.go
index 10b2f8b9..02a88ded 100644
--- a/template_test.go
+++ b/template_test.go
@@ -22,7 +22,7 @@ func TestOpenOutFile(t *testing.T) {
_ = fs.Mkdir("/tmp", 0777)
cfg := &config.Config{}
- f, err := openOutFile(cfg, "/tmp/foo", 0644, false)
+ f, err := openOutFile(cfg, "/tmp/foo", 0755, 0644, false)
assert.NoError(t, err)
wc, ok := f.(io.WriteCloser)
@@ -36,7 +36,7 @@ func TestOpenOutFile(t *testing.T) {
cfg.Stdout = &bytes.Buffer{}
- f, err = openOutFile(cfg, "-", 0644, false)
+ f, err = openOutFile(cfg, "-", 0755, 0644, false)
assert.NoError(t, err)
assert.Equal(t, cfg.Stdout, f)
}
@@ -261,7 +261,7 @@ func TestCreateOutFile(t *testing.T) {
fs = afero.NewMemMapFs()
_ = fs.Mkdir("in", 0755)
- _, err := createOutFile("in", 0644, false)
+ _, err := createOutFile("in", 0755, 0644, false)
assert.Error(t, err)
assert.IsType(t, &os.PathError{}, err)
}