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(-)
I CLI.md
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.md -> DOCS.md
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.md -> README.md
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.go -> cmd/download.go
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.go -> cmd/init.go
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.go -> cmd/remove.go
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.go -> cmd/save.go
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.go -> cmd/source.go
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.go -> cmd/test.go
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.go -> cmd/update.go
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.go -> cmd/use.go
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.go -> docs.go
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
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.go -> registry/registry_test.go
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.go -> registry/template.go
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
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()
+	}
+}