Home

tmpl @main - refs - log -
-
https://git.jolheiser.com/tmpl.git
Template automation
tree log patch
Docs overhaul, path expansion, more tests, better prompts Signed-off-by: jolheiser <john.olheiser@gmail.com>
Signature
-----BEGIN PGP SIGNATURE----- iQIzBAABCgAdFiEEgqEQpE3xoo1QwJO/uFOtpdp7v3oFAl+59kQACgkQuFOtpdp7 v3rGbxAAhFBl6gk3BAvh2Cj7Y5sEFiSoSL9UEtLgL+VVe8xw3oCFM3RbfSXqk6SK 4jTUQqRyyKxi/bsa02HeElvSgEWHkPyvvF3frksS/ME1vCadzIRBtUtNzv/nUcxM 6ZUzumXZ/nk28e8UWQT0BMs5kab963cQH7xRpKLt3jJ8QO+zeZ2FHt340r/0Endk gjKHMVbIsLP/evmwYd9SLtMc1LiLquFcWv8zA807e2MrM4+ooCkI2HQ6rf4YhgJh dDilM9JEO5tYpUv40j9JvQuCWa/rAH84U/SUliMWxNjc4jEAK192qNfKHBrn6i4j ZF/MfD20pwmHL9MSBgwpO0tkwVjC3Mdywf/V74zXd6vswLIzF763RzuK7aGJQC71 R1wzuy3Oy63+amLd+kOXuTj23TOwSyLKO+yG2SwUQ1d/RbT0ZpG9D3aAaxNMFx2k J0nrGpIzCZr0XhR6kJcJnjV7oIGQr4xcx6tQ1ZdkGDNNfA50zlcbNsbCQB+wtbyo KEvSF3QnZZixv6emyHZIvxa/GMs8CEB4+yRsBFQEKaO3wnz+p2qqcpgwZXISL8fh dOkzkLUCYVuW29mtCCmDEYSHCWgY5z5tQCbih/iXtfwa+l/ocMNSy4OGR+BXViae 26HmDTuuhQEqEkT9+kW9sfNfrJpL3p4yW3+P5obV7PhJ9GF/fcE= =Ghoz -----END PGP SIGNATURE-----
jolheiser <john.olheiser@gmail.com>
4 years ago
16 changed files, 446 additions(+), 175 deletions(-)
CLI.mdDOCS.mdREADME.mdcmd/download.gocmd/init.gocmd/remove.gocmd/save.gocmd/source.gocmd/test.gocmd/update.gocmd/use.godocs.goregistry/prompt.goregistry/registry_test.goregistry/template.goregistry/template_test.go
I CLI.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
diff --git a/CLI.md b/CLI.md
new file mode 100644
index 0000000000000000000000000000000000000000..0850bb67c30dabc8434ef792b46ac65b0dde3e2a
--- /dev/null
+++ b/CLI.md
@@ -0,0 +1,81 @@
+# NAME
+
+tmpl - Template automation
+
+# SYNOPSIS
+
+tmpl
+
+```
+[--registry|-r]=[value]
+[--source|-s]=[value]
+```
+
+**Usage**:
+
+```
+tmpl [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
+```
+
+# GLOBAL OPTIONS
+
+**--registry, -r**="": Registry directory of tmpl (default: ~/.tmpl)
+
+**--source, -s**="": Short-name source to use
+
+
+# COMMANDS
+
+## download
+
+Download a template
+
+**--branch, -b**="": Branch to clone (default: main)
+
+## init
+
+Initialize a template
+
+## list
+
+List templates in the registry
+
+## remove
+
+Remove a template
+
+## save
+
+Save a local template
+
+## source
+
+Commands for working with sources
+
+### list
+
+List available sources
+
+### add
+
+Add a source
+
+### remove
+
+Remove a source
+
+## test
+
+Test if a directory is a valid template
+
+## update
+
+Update a template
+
+## use
+
+Use a template
+
+**--defaults**: Use template defaults
+
+**--force**: Overwrite existing files
M DOCS.mdDOCS.md
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
diff --git a/DOCS.md b/DOCS.md
index 189a2c4b3b4c76c234df85debf04edb3e20e3b3a..889b7f0c14d2300a38eda9b9e92d2970e668f0de 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -1,79 +1,107 @@
-# NAME
-
-tmpl - Template automation
-
-# SYNOPSIS
+# tmpl templates
 
-tmpl
-
-```
-[--registry|-r]=[value]
-[--source|-s]=[value]
-```
-
-**Usage**:
+This documentation aims to cover FAQs and setup.
 
-```
-tmpl [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
-```
+## Setting up a template
 
-# GLOBAL OPTIONS
+A "valid" tmpl template only requires two things
 
-**--registry, -r**="": Registry directory of tmpl (default: ~/.tmpl)
+1. A `template.toml` file in the root directory.
+2. A `template` directory that serves as the "root" of the template.
 
-**--source, -s**="": Short-name source to use
+## template.toml
 
+```toml
+# Key-value pairs can be simple
+# The user will receive a basic prompt asking them to fill out the variable
+project = "my-project"
 
-# COMMANDS
+# Extended properties MUST be added after any simple key-value pairs (due to how TOML works)
 
-## download
+# The "key" is enclosed in braces
+[author]
+# prompt is what will be shown to prompt the user
+prompt = "The name of the author of this project"
+# help would be extra information (generally seen by giving '?' to a prompt)
+help = "Who will be primarily writing this project"
+# default is the "value" part of the simple pair. This could be a suggested value
+default = "me"
+```
 
-Download a template
+## template directory
 
-**--branch, -b**="": Branch to clone (default: main)
+This directory contains any and all files that are part of the template.
 
-## init
+Everything in this directory (including paths and file names!) will be executed as a [Go template](https://golang.org/pkg/text/template/).
 
-Initialize a template
+See the [documentation](https://golang.org/pkg/text/template/) for every available possibility, but some basic examples are...
 
-## list
+* A variable defined in template.toml (tmpl allows for keys to be called as a func or variable, whichever you prefer!)
+   * `{{project}}` or `{{.project}}`
+   * `{{author}}` or `{{.author}}`
+* Conditionally including something
+   * `{{if eq project ""}} something... {{end}}`
 
-List templates in the registry
+### template helpers
 
-## remove
+For a full list, see [helper.go](registry/helper.go)
 
-Remove a template
+|Helper|Example|Output|
+|-----|-----|-----|
+|upper|`{{upper project}}`|`MY-PROJECT`|
+|lower|`{{lower project}}`|`my-project`|
+|title|`{{title project}}`|`My-Project`|
+|snake|`{{snake project}}`|`my_project`|
+|kebab|`{{kebab project}}`|`my-project`|
+|pascal|`{{pascal project}}`|`MyProject`|
+|camel|`{{camel project}}`|`myProject`|
+|env|`{{env "USER"}}`|The current user|
+|sep|`{{sep}}`|Filepath separator for current OS|
+|time}|`{{time "01/02/2006"}}`|`11/21/2020` - The time according to the given [format](https://flaviocopes.com/go-date-time-format/)|
 
-## save
+## Sources
 
-Save a local template
+tmpl was designed to work with any local or git-based template. Unfortunately, in contrast to boilr, this means 
+it cannot be used with `user/repo` notation out of the box. 
 
-## source
+However, you _can_ set up a source (and subsequent env variable) to make it easier to use your preferred source while
+still allowing for others.
 
-Commands for working with sources
+### Setting up a source
 
-### list
+Let's set up a source for [Gitea](https://gitea.com)
 
-List available sources
+```
+tmpl source add https://gitea.com gitea
+```
 
-### add
+To use it, either pass it in with the `--source` flag
 
-Add a source
+```
+tmpl --source gitea download jolheiser/tmpls tmpls
+```
 
-### remove
+Or set it as the env variable `TMPL_SOURCE`
 
-Remove a source
+## Using a different branch
 
-## test
+By default, tmpl will want to use a branch called `main` in your repository.
 
-Test if a directory is a valid template
+If you are using another branch as your default, you can set it as the env variable `TMPL_BRANCH`
 
-## update
+Alternatively, you can specify on the command-line with the `--branch` flag of the `download` command
 
-Update a template
+```
+tmpl --source gitea download --branch license jolheiser/tmpls license
+```
+The above command would download the [license](https://gitea.com/jolheiser/tmpls/src/branch/license) template from `jolheiser/tmpls`
 
-## use
+## Putting it all together
 
-Use a template
+I realize that many users will be using GitHub, and most will likely still be using the `master` branch.
 
-**--defaults**: Use template defaults
+1. Set up a source for GitHub
+   1. `tmpl source add https://github.com github`
+   2. Set the env variable `TMPL_SOURCE` to `github`
+2. Set the env variable `TMPL_BRANCH` to `master`
+3. Happy templating! `tmpl download user/repo repo`
\ No newline at end of file
M README.mdREADME.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/README.md b/README.md
index 3853e8279d04d75936a9a2966657649ce8322104..ea9fca4db42d71a35902008512fe1f393775e029 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,13 @@ Heavily inspired by [boilr](https://github.com/tmrts/boilr). 
 
 The two projects share many similarities, however other than general layout/structure the implementation is entirely my own.
 
-[CLI Docs](DOCS.md)
+[CLI Docs](CLI.md)
+
+[Project Docs/FAQs](DOCS.md)
 
 ## Examples 
 
-Checkout the [license](https://gitea.com/jolheiser/tmpls/src/branch/license) and [makefile](https://gitea.com/jolheiser/tmpls/src/branch/makefile) branch of my [template repository](https://gitea.com/jolheiser/tmpls). 
+Check out the [license](https://gitea.com/jolheiser/tmpls/src/branch/license) and [makefile](https://gitea.com/jolheiser/tmpls/src/branch/makefile) branch of my [template repository](https://gitea.com/jolheiser/tmpls). 
 
 ## License
 
M cmd/download.gocmd/download.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
diff --git a/cmd/download.go b/cmd/download.go
index f88fd7cde5342e9b67dccd104ad47344427699c3..76983a57df3814713e5929dc8d7841167405b70b 100644
--- a/cmd/download.go
+++ b/cmd/download.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"errors"
 	"fmt"
 	"strings"
 
@@ -16,6 +15,7 @@ var Download = &cli.Command{
 	Name:        "download",
 	Usage:       "Download a template",
 	Description: "Download a template and save it to the local registry",
+	ArgsUsage:   "[repository URL] [name]",
 	Flags: []cli.Flag{
 		&cli.StringFlag{
 			Name:    "branch",
@@ -30,7 +30,7 @@ }
 
 func runDownload(ctx *cli.Context) error {
 	if ctx.NArg() < 2 {
-		return errors.New("<repo> <name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
M cmd/init.gocmd/init.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
diff --git a/cmd/init.go b/cmd/init.go
index b5e4e0b83c9e5cec30386c68065c89697a7b0ae3..9f2f5cc21ecc3d9e3ce39a5d1bccc5934ac272e9 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -49,7 +49,8 @@
 var comments = `# template.toml
 # Write any template args here to prompt the user for, giving any defaults/options as applicable
 
-name = "MyProject"
-
-lang = ["Go", "Rust", "Python"]
+[name]
+prompt = "Project Name"
+help = "The name to use in the project"
+default = "tmpl"
 `
M cmd/remove.gocmd/remove.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
diff --git a/cmd/remove.go b/cmd/remove.go
index e753fb4d4106a21cbbde71a2f427c3417d8dc42c..3521a29b7be9000718f399653f8793af6004c105 100644
--- a/cmd/remove.go
+++ b/cmd/remove.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"errors"
-
 	"go.jolheiser.com/tmpl/cmd/flags"
 	"go.jolheiser.com/tmpl/registry"
 
@@ -14,12 +12,13 @@ var Remove = &cli.Command{
 	Name:        "remove",
 	Usage:       "Remove a template",
 	Description: "Remove a template from the registry",
+	ArgsUsage:   "[name]",
 	Action:      runRemove,
 }
 
 func runRemove(ctx *cli.Context) error {
 	if ctx.NArg() < 1 {
-		return errors.New("<name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
M cmd/save.gocmd/save.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
diff --git a/cmd/save.go b/cmd/save.go
index c7e50dbd99084298695434c6f7d67c9f8c33b53f..cd647ccc57793fd0a675cecb04d4b8e0baffd940 100644
--- a/cmd/save.go
+++ b/cmd/save.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"errors"
 	"path/filepath"
 
 	"go.jolheiser.com/tmpl/cmd/flags"
@@ -15,12 +14,13 @@ var Save = &cli.Command{
 	Name:        "save",
 	Usage:       "Save a local template",
 	Description: "Save a local template to the registry",
+	ArgsUsage:   "[path] [name]",
 	Action:      runSave,
 }
 
 func runSave(ctx *cli.Context) error {
 	if ctx.NArg() < 2 {
-		return errors.New("<path> <name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
M cmd/source.gocmd/source.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
diff --git a/cmd/source.go b/cmd/source.go
index 9514091ca3ff1cb5f9da40e5eed6d2372dace980..e14530e429abd2249e3dd14472bdfd6c00623ade 100644
--- a/cmd/source.go
+++ b/cmd/source.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"errors"
 	"fmt"
 	"os"
 	"text/tabwriter"
@@ -37,6 +36,7 @@ 	SourceAdd = &cli.Command{
 		Name:        "add",
 		Usage:       "Add a source",
 		Description: "Add a new source to the registry",
+		ArgsUsage:   "[base URL] [name]",
 		Action:      runSourceAdd,
 	}
 
@@ -44,6 +44,7 @@ 	SourceRemove = &cli.Command{
 		Name:        "remove",
 		Usage:       "Remove a source",
 		Description: "Remove a source from the registry",
+		ArgsUsage:   "[name]",
 		Action:      runSourceRemove,
 	}
 )
@@ -68,7 +69,7 @@ }
 
 func runSourceAdd(ctx *cli.Context) error {
 	if ctx.NArg() < 2 {
-		return errors.New("<repo> <name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
@@ -87,7 +88,7 @@ }
 
 func runSourceRemove(ctx *cli.Context) error {
 	if ctx.NArg() < 1 {
-		return errors.New("<name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
M cmd/test.gocmd/test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
diff --git a/cmd/test.go b/cmd/test.go
index be6b2585c6c6c53c90e19cd73a3b063b0f72760e..cecaf1193836ee2be8c237ef4972e1140b5ac9dc 100644
--- a/cmd/test.go
+++ b/cmd/test.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"os"
+	"path/filepath"
 
 	"github.com/urfave/cli/v2"
 	"go.jolheiser.com/beaver"
@@ -10,17 +11,23 @@
 var Test = &cli.Command{
 	Name:        "test",
 	Usage:       "Test if a directory is a valid template",
-	Description: "Test whether the current directory is valid for use with tmpl",
+	Description: "Test whether a directory is valid for use with tmpl",
+	ArgsUsage:   "[path (default: \".\")]",
 	Action:      runTest,
 }
 
-func runTest(_ *cli.Context) error {
+func runTest(ctx *cli.Context) error {
+	testPath := "."
+	if ctx.NArg() > 0 {
+		testPath = ctx.Args().First()
+	}
+
 	var errs []string
-	if _, err := os.Lstat("template.toml"); err != nil {
+	if _, err := os.Lstat(filepath.Join(testPath, "template.toml")); err != nil {
 		errs = append(errs, "could not find template.toml")
 	}
 
-	fi, err := os.Lstat("template")
+	fi, err := os.Lstat(filepath.Join(testPath, "template"))
 	if err != nil {
 		errs = append(errs, "no template directory found")
 	}
M cmd/update.gocmd/update.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
diff --git a/cmd/update.go b/cmd/update.go
index 00536464d7f774ff260a8de775ed8022e0dd0fa8..38cff987b281718bd378c264ff347f02ae925f6a 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"errors"
-
 	"go.jolheiser.com/tmpl/cmd/flags"
 	"go.jolheiser.com/tmpl/registry"
 
@@ -14,12 +12,13 @@ var Update = &cli.Command{
 	Name:        "update",
 	Usage:       "Update a template",
 	Description: "Update a template in the registry from the original source",
+	ArgsUsage:   "[name]",
 	Action:      runUpdate,
 }
 
 func runUpdate(ctx *cli.Context) error {
 	if ctx.NArg() < 1 {
-		return errors.New("<name>")
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
 	}
 
 	reg, err := registry.Open(flags.Registry)
M cmd/use.gocmd/use.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
diff --git a/cmd/use.go b/cmd/use.go
index 6654f4dcc7cee4dceda144e173fe834dbdec8c0a..351b22f9b330e1868c4f9e75390bf6d169d2c4ec 100644
--- a/cmd/use.go
+++ b/cmd/use.go
@@ -1,8 +1,6 @@
 package cmd
 
 import (
-	"errors"
-
 	"go.jolheiser.com/tmpl/cmd/flags"
 	"go.jolheiser.com/tmpl/registry"
 
@@ -19,13 +17,23 @@ 		&cli.BoolFlag{
 			Name:  "defaults",
 			Usage: "Use template defaults",
 		},
+		&cli.BoolFlag{
+			Name:  "force",
+			Usage: "Overwrite existing files",
+		},
 	},
-	Action: runUse,
+	ArgsUsage: "[name] [destination (default: \".\")]",
+	Action:    runUse,
 }
 
 func runUse(ctx *cli.Context) error {
-	if ctx.NArg() < 2 {
-		return errors.New("<name> <dest>")
+	if ctx.NArg() < 1 {
+		return cli.ShowCommandHelp(ctx, ctx.Command.Name)
+	}
+
+	dest := "."
+	if ctx.NArg() >= 2 {
+		dest = ctx.Args().Get(1)
 	}
 
 	reg, err := registry.Open(flags.Registry)
@@ -38,7 +46,7 @@ 	if err != nil {
 		return err
 	}
 
-	if err := tmpl.Execute(ctx.Args().Get(1), ctx.Bool("defaults")); err != nil {
+	if err := tmpl.Execute(dest, ctx.Bool("defaults"), ctx.Bool("force")); err != nil {
 		return err
 	}
 
M docs.godocs.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/docs.go b/docs.go
index 5f18aae7e9016ae49be44dbea722514c473e7ab9..3ca29f5fce836dde5d89ab9a045b34941d50cbcf 100644
--- a/docs.go
+++ b/docs.go
@@ -13,7 +13,7 @@
 func main() {
 	app := cmd.NewApp()
 
-	fi, err := os.Create("DOCS.md")
+	fi, err := os.Create("CLI.md")
 	if err != nil {
 		panic(err)
 	}
I registry/prompt.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
diff --git a/registry/prompt.go b/registry/prompt.go
new file mode 100644
index 0000000000000000000000000000000000000000..b683205df5a629705312d4ad1b5063a3af76ee8d
--- /dev/null
+++ b/registry/prompt.go
@@ -0,0 +1,130 @@
+package registry
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"text/template"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/pelletier/go-toml"
+)
+
+type templatePrompt struct {
+	Key     string      `toml:"-"`
+	Value   interface{} `toml:"-"`
+	Message string      `toml:"prompt"`
+	Help    string      `toml:"help"`
+	Default interface{} `toml:"default"`
+}
+
+func prompt(dir string, defaults bool) (templatePrompts, error) {
+	templatePath := filepath.Join(dir, "template.toml")
+	if _, err := os.Lstat(templatePath); err != nil {
+		return nil, err
+	}
+
+	tree, err := toml.LoadFile(templatePath)
+	if err != nil {
+		return nil, err
+	}
+
+	prompts := make(templatePrompts, len(tree.Keys()))
+	for idx, k := range tree.Keys() {
+		v := tree.Get(k)
+
+		obj, ok := v.(*toml.Tree)
+		if !ok {
+			prompts[idx] = templatePrompt{
+				Key:     k,
+				Message: k,
+				Default: v,
+			}
+			continue
+		}
+
+		var p templatePrompt
+		if err := obj.Unmarshal(&p); err != nil {
+			return nil, err
+		}
+		p.Key = k
+		if p.Message == "" {
+			p.Message = p.Key
+		}
+		if p.Default == nil {
+			p.Default = ""
+		}
+		prompts[idx] = p
+	}
+
+	// Return early if we only want defaults
+	if defaults {
+		return prompts, nil
+	}
+
+	// Sort the prompts so they are consistent
+	sort.Sort(prompts)
+
+	for idx, prompt := range prompts {
+		var p survey.Prompt
+		switch t := prompt.Default.(type) {
+		case []string:
+			p = &survey.Select{
+				Message: prompt.Message,
+				Options: t,
+				Help:    prompt.Help,
+			}
+		default:
+			p = &survey.Input{
+				Message: prompt.Message,
+				Default: fmt.Sprintf("%v", t),
+				Help:    prompt.Help,
+			}
+		}
+		var a string
+		if err := survey.AskOne(p, &a); err != nil {
+			return nil, err
+		}
+		prompts[idx].Value = a
+	}
+
+	return prompts, nil
+}
+
+type templatePrompts []templatePrompt
+
+func (t templatePrompts) ToMap() map[string]interface{} {
+	m := make(map[string]interface{})
+	for _, p := range t {
+		if p.Value != nil {
+			m[p.Key] = p.Value
+			continue
+		}
+		m[p.Key] = p.Default
+	}
+	return m
+}
+
+func (t templatePrompts) ToFuncMap() template.FuncMap {
+	m := make(map[string]interface{})
+	for k, v := range t.ToMap() {
+		vv := v // Enclosure
+		m[k] = func() string {
+			return fmt.Sprintf("%v", vv)
+		}
+	}
+	return m
+}
+
+func (t templatePrompts) Len() int {
+	return len(t)
+}
+
+func (t templatePrompts) Less(i, j int) bool {
+	return t[i].Key > t[j].Key
+}
+
+func (t templatePrompts) Swap(i, j int) {
+	t[i], t[j] = t[j], t[i]
+}
M registry/registry_test.goregistry/registry_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
diff --git a/registry/registry_test.go b/registry/registry_test.go
index 7c5caac00a2403526d768d4daf263a8933083db5..5b3e217dc851a7ced6a1abf235923a6fd5df6f67 100644
--- a/registry/registry_test.go
+++ b/registry/registry_test.go
@@ -13,11 +13,6 @@ 	tmplDir string
 	regDir  string
 	destDir string
 	reg     *Registry
-
-	tmplContents = `{{title name}} {{.year}}`
-	tmplTemplate = `name = "john olheiser"
-year = 2020`
-	tmplGold = "John Olheiser 2020"
 )
 
 func TestMain(m *testing.M) {
@@ -78,30 +73,6 @@ 		t.FailNow()
 	}
 }
 
-func testExecute(t *testing.T) {
-	tmpl, err := reg.GetTemplate("test")
-	if err != nil {
-		t.Logf("could not get template")
-		t.FailNow()
-	}
-
-	if err := tmpl.Execute(destDir, true); err != nil {
-		t.Logf("could not execute template: %v\n", err)
-		t.FailNow()
-	}
-
-	contents, err := ioutil.ReadFile(filepath.Join(destDir, "TEST"))
-	if err != nil {
-		t.Logf("could not read file: %v\n", err)
-		t.FailNow()
-	}
-
-	if string(contents) != tmplGold {
-		t.Logf("contents did not match:\n\tExpected: %s\n\tGot: %s", tmplGold, string(contents))
-		t.FailNow()
-	}
-}
-
 func setupTemplate() {
 	var err error
 	tmplDir, err = ioutil.TempDir(os.TempDir(), "tmpl")
@@ -122,10 +93,20 @@ 	if err := fi.Close(); err != nil {
 		panic(err)
 	}
 
-	// Template file
-	if err := os.Mkdir(filepath.Join(tmplDir, "template"), os.ModePerm); err != nil {
+	// Template directories
+	pkgPath := filepath.Join(tmplDir, "template", "{{upper package}}")
+	if err := os.MkdirAll(pkgPath, os.ModePerm); err != nil {
 		panic(err)
 	}
+	fi, err = os.Create(filepath.Join(pkgPath, ".keep"))
+	if err != nil {
+		panic(err)
+	}
+	if err := fi.Close(); err != nil {
+		panic(err)
+	}
+
+	// Template file
 	fi, err = os.Create(filepath.Join(tmplDir, "template", "TEST"))
 	if err != nil {
 		panic(err)
M registry/template.goregistry/template.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
diff --git a/registry/template.go b/registry/template.go
index e1651dddecde230eaaa8d3e75bab4a42263a5636..6703f14ed7c88637a97c23af162c82fc1bc940c2 100644
--- a/registry/template.go
+++ b/registry/template.go
@@ -1,18 +1,16 @@
 package registry
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"sort"
 	"strings"
 	"text/template"
 	"time"
 
-	"github.com/AlecAivazis/survey/v2"
 	"github.com/mholt/archiver/v3"
-	"github.com/pelletier/go-toml"
 )
 
 // Template is a tmpl project
@@ -36,7 +34,7 @@ 	return filepath.Join(t.reg.dir, t.ArchiveName())
 }
 
 // Execute runs the Template and copies to dest
-func (t *Template) Execute(dest string, defaults bool) error {
+func (t *Template) Execute(dest string, defaults, overwrite bool) error {
 	tmp, err := ioutil.TempDir(os.TempDir(), "tmpl")
 	if err != nil {
 		return err
@@ -47,10 +45,12 @@ 	if err := archiver.Unarchive(t.ArchivePath(), tmp); err != nil {
 		return err
 	}
 
-	vars, err := prompt(tmp, defaults)
+	prompts, err := prompt(tmp, defaults)
 	if err != nil {
 		return err
 	}
+
+	funcs := mergeMaps(funcMap, prompts.ToFuncMap())
 
 	base := filepath.Join(tmp, "template")
 	return filepath.Walk(base, func(walkPath string, walkInfo os.FileInfo, walkErr error) error {
@@ -67,13 +67,19 @@ 		if err != nil {
 			return err
 		}
 
-		tmpl, err := template.New("tmpl").Funcs(mergeMaps(funcMap, convertMap(vars))).Parse(string(contents))
+		newDest := strings.TrimPrefix(walkPath, base+"/")
+		newDest = filepath.Join(dest, newDest)
+
+		tmplDest, err := template.New("dest").Funcs(funcs).Parse(newDest)
 		if err != nil {
 			return err
 		}
 
-		newDest := strings.TrimPrefix(walkPath, base+"/")
-		newDest = filepath.Join(dest, newDest)
+		var buf bytes.Buffer
+		if err := tmplDest.Execute(&buf, prompts.ToMap()); err != nil {
+			return err
+		}
+		newDest = buf.String()
 
 		if err := os.MkdirAll(filepath.Dir(newDest), os.ModePerm); err != nil {
 			return err
@@ -83,77 +89,27 @@ 		oldFi, err := os.Lstat(walkPath)
 		if err != nil {
 			return err
 		}
+
+		// Check if new file exists. If it does, only skip if not overwriting
+		if _, err := os.Lstat(newDest); err == nil && !overwrite {
+			return nil
+		}
+
 		newFi, err := os.OpenFile(newDest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, oldFi.Mode())
 		if err != nil {
 			return err
 		}
 
-		if err := tmpl.Execute(newFi, vars); err != nil {
+		tmplContents, err := template.New("tmpl").Funcs(funcs).Parse(string(contents))
+		if err != nil {
+			return err
+		}
+		if err := tmplContents.Execute(newFi, prompts.ToMap()); err != nil {
 			return err
 		}
 
 		return newFi.Close()
 	})
-}
-
-func prompt(dir string, defaults bool) (map[string]interface{}, error) {
-	templatePath := filepath.Join(dir, "template.toml")
-	if _, err := os.Lstat(templatePath); err != nil {
-		return nil, err
-	}
-
-	tree, err := toml.LoadFile(templatePath)
-	if err != nil {
-		return nil, err
-	}
-	vars := tree.ToMap()
-
-	// Return early if we only want defaults
-	if defaults {
-		return vars, nil
-	}
-
-	// Sort the map keys so they are consistent
-	sorted := make([]string, 0, len(vars))
-	for k := range vars {
-		sorted = append(sorted, k)
-	}
-	sort.Strings(sorted)
-
-	for _, k := range sorted {
-		v := vars[k]
-		var p survey.Prompt
-		switch t := v.(type) {
-		case []string:
-			p = &survey.Select{
-				Message: k,
-				Options: t,
-			}
-		default:
-			p = &survey.Input{
-				Message: k,
-				Default: fmt.Sprintf("%v", t),
-			}
-		}
-		var a string
-		if err := survey.AskOne(p, &a); err != nil {
-			return nil, err
-		}
-		vars[k] = a
-	}
-
-	return vars, nil
-}
-
-func convertMap(m map[string]interface{}) template.FuncMap {
-	mm := make(template.FuncMap)
-	for k, v := range m {
-		vv := v // Enclosures in a loop
-		mm[k] = func() interface{} {
-			return fmt.Sprintf("%v", vv)
-		}
-	}
-	return mm
 }
 
 func mergeMaps(maps ...map[string]interface{}) map[string]interface{} {
I registry/template_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
diff --git a/registry/template_test.go b/registry/template_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..be32ced12b454cee374abb3fed0f5829aa1c1eb4
--- /dev/null
+++ b/registry/template_test.go
@@ -0,0 +1,78 @@
+package registry
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+var (
+	tmplContents = `{{title name}} {{.year}}`
+	tmplTemplate = `name = "john olheiser"
+
+[year]
+default = 2020
+
+[package]
+default = "pkg"`
+	tmplGold    = "John Olheiser 2020"
+	tmplNewGold = "DO NOT OVERWRITE!"
+)
+
+func testExecute(t *testing.T) {
+	// Get template
+	tmpl, err := reg.GetTemplate("test")
+	if err != nil {
+		t.Logf("could not get template")
+		t.FailNow()
+	}
+
+	// Execute template
+	if err := tmpl.Execute(destDir, true, true); err != nil {
+		t.Logf("could not execute template: %v\n", err)
+		t.FailNow()
+	}
+
+	// Check contents of file
+	testPath := filepath.Join(destDir, "TEST")
+	contents, err := ioutil.ReadFile(testPath)
+	if err != nil {
+		t.Logf("could not read file: %v\n", err)
+		t.FailNow()
+	}
+
+	if string(contents) != tmplGold {
+		t.Logf("contents did not match:\n\tExpected: %s\n\tGot: %s", tmplGold, string(contents))
+		t.FailNow()
+	}
+
+	// Check if directory was created
+	pkgPath := filepath.Join(destDir, "PKG")
+	if _, err := os.Lstat(pkgPath); err != nil {
+		t.Logf("expected a directory at %s: %v\n", pkgPath, err)
+		t.FailNow()
+	}
+
+	// Change file to test non-overwrite
+	if err := ioutil.WriteFile(testPath, []byte(tmplNewGold), os.ModePerm); err != nil {
+		t.Logf("could not write file: %v\n", err)
+		t.FailNow()
+	}
+
+	if err := tmpl.Execute(destDir, true, false); err != nil {
+		t.Logf("could not execute template: %v\n", err)
+		t.FailNow()
+	}
+
+	contents, err = ioutil.ReadFile(testPath)
+	if err != nil {
+		t.Logf("could not read file: %v\n", err)
+		t.FailNow()
+	}
+
+	if string(contents) != tmplNewGold {
+		t.Logf("contents did not match:\n\tExpected: %s\n\tGot: %s", tmplNewGold, string(contents))
+		t.FailNow()
+	}
+}