diff options
| author | Dave Henderson <dhenderson@gmail.com> | 2019-04-09 16:31:05 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-04-09 16:31:05 -0700 |
| commit | 67e97968fdb190cbdbbe511c8d6f816ff65eb124 (patch) | |
| tree | de763083c0eeebe58ab679f595d4e95214534c84 | |
| parent | 32235c3c65d9c5fdc7171077f603b203197088a2 (diff) | |
| parent | ac0012f4f6d2cde52490294e1fb262770f95ba9f (diff) | |
Merge pull request #529 from hairyhenderson/output-file-template-288
Adding --output-map argument for templating output paths
| -rw-r--r-- | cmd/gomplate/main.go | 11 | ||||
| -rw-r--r-- | cmd/gomplate/main_test.go | 70 | ||||
| -rw-r--r-- | config.go | 3 | ||||
| -rw-r--r-- | config_test.go | 26 | ||||
| -rw-r--r-- | docs/content/usage.md | 50 | ||||
| -rw-r--r-- | gomplate.go | 52 | ||||
| -rw-r--r-- | gomplate_test.go | 26 | ||||
| -rw-r--r-- | template.go | 18 | ||||
| -rw-r--r-- | template_test.go | 12 | ||||
| -rw-r--r-- | template_unix_test.go | 4 | ||||
| -rw-r--r-- | template_windows_test.go | 4 | ||||
| -rw-r--r-- | tests/integration/basic_test.go | 6 | ||||
| -rw-r--r-- | tests/integration/inputdir_test.go | 85 |
13 files changed, 340 insertions, 27 deletions
diff --git a/cmd/gomplate/main.go b/cmd/gomplate/main.go index 20b1e5a2..3c125c7d 100644 --- a/cmd/gomplate/main.go +++ b/cmd/gomplate/main.go @@ -23,6 +23,7 @@ var ( opts gomplate.Config ) +// nolint: gocyclo func validateOpts(cmd *cobra.Command, args []string) error { if cmd.Flag("in").Changed && cmd.Flag("file").Changed { return errors.New("--in and --file may not be used together") @@ -44,6 +45,15 @@ func validateOpts(cmd *cobra.Command, args []string) error { return errors.New("--input-dir must be set when --output-dir is set") } } + + if cmd.Flag("output-map").Changed { + if cmd.Flag("out").Changed || cmd.Flag("output-dir").Changed { + return errors.New("--output-map can not be used together with --out or --output-dir") + } + if !cmd.Flag("input-dir").Changed { + return errors.New("--input-dir must be set when --output-map is set") + } + } return nil } @@ -140,6 +150,7 @@ func initFlags(command *cobra.Command) { command.Flags().StringArrayVarP(&opts.OutputFiles, "out", "o", []string{"-"}, "output `file` name. Omit to use standard output.") command.Flags().StringArrayVarP(&opts.Templates, "template", "t", []string{}, "Additional template file(s)") command.Flags().StringVar(&opts.OutputDir, "output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir") + command.Flags().StringVar(&opts.OutputMap, "output-map", "", "Template `string` to map the input file to an output path") command.Flags().StringVar(&opts.OutMode, "chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{") diff --git a/cmd/gomplate/main_test.go b/cmd/gomplate/main_test.go new file mode 100644 index 00000000..c8670853 --- /dev/null +++ b/cmd/gomplate/main_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestValidateOpts(t *testing.T) { + err := validateOpts(parseFlags(), nil) + assert.NoError(t, err) + + cmd := parseFlags("-i=foo", "-f", "bar") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("-i=foo", "-o=bar", "-o=baz") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("-i=foo", "--input-dir=baz") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("--input-dir=foo", "-f=bar") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("--output-dir=foo", "-o=bar") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("--output-dir=foo") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("--output-map", "bar") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags("-o", "foo", "--output-map", "bar") + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags( + "--input-dir", "in", + "--output-dir", "foo", + "--output-map", "bar", + ) + err = validateOpts(cmd, nil) + assert.Error(t, err) + + cmd = parseFlags( + "--input-dir", "in", + "--output-map", "bar", + ) + err = validateOpts(cmd, nil) + assert.NoError(t, err) +} + +func parseFlags(flags ...string) *cobra.Command { + cmd := &cobra.Command{} + initFlags(cmd) + err := cmd.ParseFlags(flags) + if err != nil { + panic(err) + } + return cmd +} @@ -15,6 +15,7 @@ type Config struct { ExcludeGlob []string OutputFiles []string OutputDir string + OutputMap string OutMode string DataSources []string @@ -81,6 +82,8 @@ func (o *Config) String() string { c += "\noutput: " if o.InputDir != "" && o.OutputDir != "." { c += o.OutputDir + } else if o.OutputMap != "" { + c += o.OutputMap } else { c += strings.Join(o.OutputFiles, ", ") } diff --git a/config_test.go b/config_test.go index d916e7e9..91c2b1a9 100644 --- a/config_test.go +++ b/config_test.go @@ -15,17 +15,37 @@ output: -` assert.Equal(t, expected, c.String()) c = &Config{ - LDelim: "{{", - RDelim: "}}", - Input: "{{ foo }}", + LDelim: "L", + RDelim: "R", + Input: "foo", OutputFiles: []string{"-"}, Templates: []string{"foo=foo.t", "bar=bar.t"}, } expected = `input: <arg> output: - +left_delim: L +right_delim: R templates: foo=foo.t, bar=bar.t` assert.Equal(t, expected, c.String()) + + c = &Config{ + InputDir: "in/", + OutputDir: "out/", + } + expected = `input: in/ +output: out/` + + assert.Equal(t, expected, c.String()) + + c = &Config{ + InputDir: "in/", + OutputMap: "{{ .in }}", + } + expected = `input: in/ +output: {{ .in }}` + + assert.Equal(t, expected, c.String()) } func TestGetMode(t *testing.T) { diff --git a/docs/content/usage.md b/docs/content/usage.md index 82870b72..46f133b0 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -33,18 +33,56 @@ You can specify multiple `--file` and `--out` arguments. The same number of each ### `--input-dir` and `--output-dir` -For processing multiple templates in a directory you can use `--input-dir` and `--output-dir` together. In this case all files in input directory will be processed as templates and the resulting files stored in `--output-dir`. The output directory will be created if it does not exist and the directory structure of the input directory will be preserved. +For processing multiple templates in a directory you can use `--input-dir` and `--output-dir` together. In this case all files in input directory will be processed as templates and the resulting files stored in `--output-dir`. The output directory will be created if it does not exist and the directory structure of the input directory will be preserved. You can use `.gomplateignore` to ignore some files in the input directory, with similar syntax and behaviour to [.gitignore](https://git-scm.com/docs/gitignore) files. Example: ```bash - # Process all files in directory "templates" with the datasource given - # and store the files with the same directory structure in "config" +# Process all files in directory "templates" with the datasource given +# and store the files with the same directory structure in "config" gomplate --input-dir=templates --output-dir=config --datasource config=config.yaml ``` +### `--output-map` + +Sometimes a 1-to-1 mapping betwen input filenames and output filenames is not desirable. For these cases, you can supply a template string as the argument to `--output-map`. The template string is interpreted as a regular gomplate template, and all datasources and external nested templates are available to the output map template. + +A new [context][] is provided, with the input filename is available at `.in`, and the original context is available at `.ctx`. For convenience, any context keys not conflicting with `in` or `ctx` are also copied. + +All whitespace on the left or right sides of the output is trimmed. + +For example, given an input directory `in/` containing files with the extension `.yaml.tmpl`, if we want to rename those to `.yaml`: + +```console +$ gomplate --input-dir=in/ --output-map='out/{{ .in | strings.ReplaceAll ".yaml.tmpl" ".yaml" }}' +``` + +#### Referencing complex output map template files + +It may be useful to store more complex output map templates in a file. This can be done with [external templates][]. + +Consider a template `out.t`: + +``` +{{- /* .in may contain a directory name - we want to preserve that */ -}} +{{ $f := filepath.Base .in -}} +out/{{ .in | strings.ReplaceAll $f (index .filemap $f) }}.out +``` + +And a datasource `filemap.json`: + +```json +{ "eins.txt": "uno", "deux.txt": "dos" } +``` + +We can blend these two together: + +```console +$ gomplate -t out=out.t -c filemap.json --input-dir=in --output-map='{{ template "out" }}' +``` + ### Ignorefile You can use ignore file `.gomplateignore` to ignore some files, have the similar behavior to the [.gitignore](https://git-scm.com/docs/gitignore) file. @@ -104,7 +142,7 @@ Use `--left-delim`/`--right-delim` or set `$GOMPLATE_LEFT_DELIM`/`$GOMPLATE_RIGH ### `--template`/`-t` -Add a nested template that can be referenced by the main input template(s) with the [`template`](https://golang.org/pkg/text/template/#hdr-Actions) built-in. Specify multiple times to add multiple template references. +Add a nested template that can be referenced by the main input template(s) with the [`template`](https://golang.org/pkg/text/template/#hdr-Actions) built-in or the functions in the [`tmpl`](../functions/tmpl/) namespace. Specify multiple times to add multiple template references. A few different forms are valid: @@ -164,3 +202,7 @@ $ gomplate -i '{{ print " \n" }}' -o out $ cat out cat: out: No such file or directory ``` + +[default context]: ../syntax/#the-context +[context]: ../syntax/#the-context +[external templates]: ../syntax/#external-templates diff --git a/gomplate.go b/gomplate.go index 0e08f0ce..4f7ef146 100644 --- a/gomplate.go +++ b/gomplate.go @@ -1,14 +1,17 @@ package gomplate import ( + "bytes" "io" "os" "path" + "path/filepath" "strings" "text/template" "time" "github.com/hairyhenderson/gomplate/data" + "github.com/pkg/errors" "github.com/spf13/afero" ) @@ -127,7 +130,7 @@ func RunTemplates(o *Config) error { func (g *gomplate) runTemplates(o *Config) error { start := time.Now() - tmpl, err := gatherTemplates(o) + tmpl, err := gatherTemplates(o, chooseNamer(o, g)) Metrics.GatherDuration = time.Since(start) if err != nil { Metrics.Errors++ @@ -148,3 +151,50 @@ func (g *gomplate) runTemplates(o *Config) error { } return nil } + +func chooseNamer(o *Config, g *gomplate) func(string) (string, error) { + if o.OutputMap == "" { + return simpleNamer(o.OutputDir) + } + return mappingNamer(o.OutputMap, g) +} + +func simpleNamer(outDir string) func(inPath string) (string, error) { + return func(inPath string) (string, error) { + outPath := filepath.Join(outDir, inPath) + return filepath.Clean(outPath), nil + } +} + +func mappingNamer(outMap string, g *gomplate) func(string) (string, error) { + return func(inPath string) (string, error) { + out := &bytes.Buffer{} + t := &tplate{ + name: "<OutputMap>", + contents: outMap, + target: out, + } + tpl, err := t.toGoTemplate(g) + if err != nil { + return "", err + } + ctx := &context{} + switch c := g.context.(type) { + case *context: + for k, v := range *c { + if k != "in" && k != "ctx" { + (*ctx)[k] = v + } + } + } + (*ctx)["ctx"] = g.context + (*ctx)["in"] = inPath + + err = tpl.Execute(t.target, ctx) + if err != nil { + return "", errors.Wrapf(err, "failed to render outputMap with ctx %+v and inPath %s", ctx, inPath) + } + + return filepath.Clean(strings.TrimSpace(out.String())), nil + } +} diff --git a/gomplate_test.go b/gomplate_test.go index 5bb7c69d..83e47adc 100644 --- a/gomplate_test.go +++ b/gomplate_test.go @@ -5,6 +5,7 @@ import ( "io" "net/http/httptest" "os" + "path/filepath" "testing" "github.com/spf13/afero" @@ -238,3 +239,28 @@ func TestParseTemplateArgs(t *testing.T) { _, err = parseTemplateArgs([]string{"bogus.t"}) assert.Error(t, err) } + +func TestSimpleNamer(t *testing.T) { + n := simpleNamer("out/") + out, err := n("file") + assert.NoError(t, err) + expected := filepath.FromSlash("out/file") + assert.Equal(t, expected, out) +} + +func TestMappingNamer(t *testing.T) { + g := &gomplate{funcMap: map[string]interface{}{ + "foo": func() string { return "foo" }, + }} + n := mappingNamer("out/{{ .in }}", g) + out, err := n("file") + assert.NoError(t, err) + expected := filepath.FromSlash("out/file") + assert.Equal(t, expected, out) + + n = mappingNamer("out/{{ foo }}{{ .in }}", g) + out, err = n("file") + assert.NoError(t, err) + expected = filepath.FromSlash("out/foofile") + assert.Equal(t, expected, out) +} diff --git a/template.go b/template.go index 7697cef2..78736b52 100644 --- a/template.go +++ b/template.go @@ -96,7 +96,7 @@ func (t *tplate) addTarget() (err error) { // gatherTemplates - gather and prepare input template(s) and output file(s) for rendering // nolint: gocyclo -func gatherTemplates(o *Config) (templates []*tplate, err error) { +func gatherTemplates(o *Config, outFileNamer func(string) (string, error)) (templates []*tplate, err error) { o.defaults() mode, modeOverride, err := o.getMode() if err != nil { @@ -112,11 +112,9 @@ func gatherTemplates(o *Config) (templates []*tplate, err error) { modeOverride: modeOverride, targetPath: o.OutputFiles[0], }} - } - - // input dirs presume output dirs are set too - if o.InputDir != "" { - templates, err = walkDir(o.InputDir, o.OutputDir, o.ExcludeGlob, mode, modeOverride) + } else if o.InputDir != "" { + // input dirs presume output dirs are set too + templates, err = walkDir(o.InputDir, outFileNamer, o.ExcludeGlob, mode, modeOverride) if err != nil { return nil, err } @@ -150,9 +148,8 @@ func processTemplates(templates []*tplate) ([]*tplate, error) { // walkDir - given an input dir `dir` and an output dir `outDir`, and a list // of .gomplateignore and exclude globs (if any), walk the input directory and create a list of // tplate objects, and an error, if any. -func walkDir(dir, outDir string, excludeGlob []string, mode os.FileMode, modeOverride bool) ([]*tplate, error) { +func walkDir(dir string, outFileNamer func(string) (string, error), excludeGlob []string, mode os.FileMode, modeOverride bool) ([]*tplate, error) { dir = filepath.Clean(dir) - outDir = filepath.Clean(outDir) dirStat, err := fs.Stat(dir) if err != nil { @@ -175,7 +172,10 @@ func walkDir(dir, outDir string, excludeGlob []string, mode os.FileMode, modeOve files := matches.UnmatchedFiles for _, file := range files { nextInPath := filepath.Join(dir, file) - nextOutPath := filepath.Join(outDir, file) + nextOutPath, err := outFileNamer(file) + if err != nil { + return nil, err + } if mode == 0 { stat, perr := fs.Stat(nextInPath) diff --git a/template_test.go b/template_test.go index 123eb58a..14fe6ed3 100644 --- a/template_test.go +++ b/template_test.go @@ -92,13 +92,13 @@ func TestGatherTemplates(t *testing.T) { afero.WriteFile(fs, "in/2", []byte("bar"), 0644) afero.WriteFile(fs, "in/3", []byte("baz"), 0644) - templates, err := gatherTemplates(&Config{}) + templates, err := gatherTemplates(&Config{}, nil) assert.NoError(t, err) assert.Len(t, templates, 1) templates, err = gatherTemplates(&Config{ Input: "foo", - }) + }, nil) assert.NoError(t, err) assert.Len(t, templates, 1) assert.Equal(t, "foo", templates[0].contents) @@ -107,7 +107,7 @@ func TestGatherTemplates(t *testing.T) { templates, err = gatherTemplates(&Config{ Input: "foo", OutputFiles: []string{"out"}, - }) + }, nil) assert.NoError(t, err) assert.Len(t, templates, 1) assert.Equal(t, "out", templates[0].targetPath) @@ -120,7 +120,7 @@ func TestGatherTemplates(t *testing.T) { templates, err = gatherTemplates(&Config{ InputFiles: []string{"foo"}, OutputFiles: []string{"out"}, - }) + }, nil) assert.NoError(t, err) assert.Len(t, templates, 1) assert.Equal(t, "bar", templates[0].contents) @@ -135,7 +135,7 @@ func TestGatherTemplates(t *testing.T) { InputFiles: []string{"foo"}, OutputFiles: []string{"out"}, OutMode: "755", - }) + }, nil) assert.NoError(t, err) assert.Len(t, templates, 1) assert.Equal(t, "bar", templates[0].contents) @@ -149,7 +149,7 @@ func TestGatherTemplates(t *testing.T) { templates, err = gatherTemplates(&Config{ InputDir: "in", OutputDir: "out", - }) + }, simpleNamer("out")) assert.NoError(t, err) assert.Len(t, templates, 3) assert.Equal(t, "foo", templates[0].contents) diff --git a/template_unix_test.go b/template_unix_test.go index 2e7ddc70..930b2835 100644 --- a/template_unix_test.go +++ b/template_unix_test.go @@ -15,7 +15,7 @@ func TestWalkDir(t *testing.T) { defer func() { fs = origfs }() fs = afero.NewMemMapFs() - _, err := walkDir("/indir", "/outdir", nil, 0, false) + _, err := walkDir("/indir", simpleNamer("/outdir"), nil, 0, false) assert.Error(t, err) _ = fs.MkdirAll("/indir/one", 0777) @@ -24,7 +24,7 @@ func TestWalkDir(t *testing.T) { afero.WriteFile(fs, "/indir/one/bar", []byte("bar"), 0644) afero.WriteFile(fs, "/indir/two/baz", []byte("baz"), 0644) - templates, err := walkDir("/indir", "/outdir", []string{"*/two"}, 0, false) + templates, err := walkDir("/indir", simpleNamer("/outdir"), []string{"*/two"}, 0, false) assert.NoError(t, err) expected := []*tplate{ diff --git a/template_windows_test.go b/template_windows_test.go index 52357f8a..824019b8 100644 --- a/template_windows_test.go +++ b/template_windows_test.go @@ -15,7 +15,7 @@ func TestWalkDir(t *testing.T) { defer func() { fs = origfs }() fs = afero.NewMemMapFs() - _, err := walkDir(`C:\indir`, `C:\outdir`, nil, 0, false) + _, err := walkDir(`C:\indir`, simpleNamer(`C:\outdir`), nil, 0, false) assert.Error(t, err) _ = fs.MkdirAll(`C:\indir\one`, 0777) @@ -24,7 +24,7 @@ func TestWalkDir(t *testing.T) { afero.WriteFile(fs, `C:\indir\one\bar`, []byte("bar"), 0644) afero.WriteFile(fs, `C:\indir\two\baz`, []byte("baz"), 0644) - templates, err := walkDir(`C:\indir`, `C:\outdir`, []string{`*\two`}, 0, false) + templates, err := walkDir(`C:\indir`, simpleNamer(`C:\outdir`), []string{`*\two`}, 0, false) assert.NoError(t, err) expected := []*tplate{ diff --git a/tests/integration/basic_test.go b/tests/integration/basic_test.go index 9789716a..31018806 100644 --- a/tests/integration/basic_test.go +++ b/tests/integration/basic_test.go @@ -140,6 +140,12 @@ func (s *BasicSuite) TestFlagRules(c *C) { ExitCode: 1, Err: "--output-dir can not be used together with --out", }) + + result = icmd.RunCommand(GomplateBin, "--output-map", ".", "--out", "param") + result.Assert(c, icmd.Expected{ + ExitCode: 1, + Err: "--output-map can not be used together with --out or --output-dir", + }) } func (s *BasicSuite) TestDelimsChangedThroughOpts(c *C) { diff --git a/tests/integration/inputdir_test.go b/tests/integration/inputdir_test.go index a9df9823..341aab57 100644 --- a/tests/integration/inputdir_test.go +++ b/tests/integration/inputdir_test.go @@ -24,6 +24,11 @@ var _ = Suite(&InputDirSuite{}) func (s *InputDirSuite) SetUpTest(c *C) { s.tmpDir = fs.NewDir(c, "gomplate-inttests", fs.WithFile("config.yml", "one: eins\ntwo: deux\n"), + fs.WithFile("filemap.json", `{"eins.txt":"uno","deux.txt":"dos"}`), + fs.WithFile("out.t", `{{- /* .in may contain a directory name - we want to preserve that */ -}} +{{ $f := filepath.Base .in -}} +out/{{ .in | strings.ReplaceAll $f (index .filemap $f) }}.out +`), fs.WithDir("in", fs.WithFile("eins.txt", `{{ (ds "config").one }}`, fs.WithMode(0644)), fs.WithDir("inner", @@ -116,6 +121,86 @@ func (s *InputDirSuite) TestInputDirWithModeOverride(c *C) { } } +func (s *InputDirSuite) TestOutputMapInline(c *C) { + result := icmd.RunCmd(icmd.Command(GomplateBin, + "--input-dir", s.tmpDir.Join("in"), + "--output-map", `OUT/{{ strings.ToUpper .in }}`, + "-d", "config.yml", + ), func(c *icmd.Cmd) { + c.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Success) + + files, err := ioutil.ReadDir(s.tmpDir.Join("OUT")) + assert.NilError(c, err) + tassert.Len(c, files, 2) + + files, err = ioutil.ReadDir(s.tmpDir.Join("OUT", "INNER")) + assert.NilError(c, err) + tassert.Len(c, files, 1) + + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {s.tmpDir.Join("OUT", "EINS.TXT"), 0644, "eins"}, + {s.tmpDir.Join("OUT", "INNER", "DEUX.TXT"), 0644, "deux"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + // chmod support on Windows is pretty weak for now + if runtime.GOOS != "windows" { + assert.Equal(c, v.mode, info.Mode()) + } + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } +} + +func (s *InputDirSuite) TestOutputMapExternal(c *C) { + result := icmd.RunCmd(icmd.Command(GomplateBin, + "--input-dir", s.tmpDir.Join("in"), + "--output-map", `{{ template "out" . }}`, + "-t", "out=out.t", + "-c", "filemap.json", + "-d", "config.yml", + ), func(c *icmd.Cmd) { + c.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Success) + + files, err := ioutil.ReadDir(s.tmpDir.Join("out")) + assert.NilError(c, err) + tassert.Len(c, files, 2) + + files, err = ioutil.ReadDir(s.tmpDir.Join("out", "inner")) + assert.NilError(c, err) + tassert.Len(c, files, 1) + + testdata := []struct { + path string + mode os.FileMode + content string + }{ + {s.tmpDir.Join("out", "uno.out"), 0644, "eins"}, + {s.tmpDir.Join("out", "inner", "dos.out"), 0644, "deux"}, + } + for _, v := range testdata { + info, err := os.Stat(v.path) + assert.NilError(c, err) + // chmod support on Windows is pretty weak for now + if runtime.GOOS != "windows" { + assert.Equal(c, v.mode, info.Mode()) + } + content, err := ioutil.ReadFile(v.path) + assert.NilError(c, err) + assert.Equal(c, v.content, string(content)) + } +} + func (s *InputDirSuite) TestDefaultOutputDir(c *C) { result := icmd.RunCmd(icmd.Command(GomplateBin, "--input-dir", s.tmpDir.Join("in"), |
