Docs overhaul, path expansion, more tests, better prompts
Signed-off-by: jolheiser <john.olheiser@gmail.com>pull/7/head v0.0.5
parent
ff2f159802
commit
0b0de4be35
|
@ -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
|
146
DOCS.md
146
DOCS.md
|
@ -1,79 +1,107 @@
|
||||||
# NAME
|
# tmpl templates
|
||||||
|
|
||||||
tmpl - Template automation
|
This documentation aims to cover FAQs and setup.
|
||||||
|
|
||||||
# SYNOPSIS
|
## Setting up a template
|
||||||
|
|
||||||
tmpl
|
A "valid" tmpl template only requires two things
|
||||||
|
|
||||||
```
|
1. A `template.toml` file in the root directory.
|
||||||
[--registry|-r]=[value]
|
2. A `template` directory that serves as the "root" of the template.
|
||||||
[--source|-s]=[value]
|
|
||||||
|
## 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"
|
||||||
|
|
||||||
|
# Extended properties MUST be added after any simple key-value pairs (due to how TOML works)
|
||||||
|
|
||||||
|
# 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**:
|
## template directory
|
||||||
|
|
||||||
|
This directory contains any and all files that are part of the template.
|
||||||
|
|
||||||
|
Everything in this directory (including paths and file names!) will be executed as a [Go template](https://golang.org/pkg/text/template/).
|
||||||
|
|
||||||
|
See the [documentation](https://golang.org/pkg/text/template/) for every available possibility, but some basic examples are...
|
||||||
|
|
||||||
|
* 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}}`
|
||||||
|
|
||||||
|
### template helpers
|
||||||
|
|
||||||
|
For a full list, see [helper.go](registry/helper.go)
|
||||||
|
|
||||||
|
|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/)|
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Setting up a source
|
||||||
|
|
||||||
|
Let's set up a source for [Gitea](https://gitea.com)
|
||||||
|
|
||||||
```
|
```
|
||||||
tmpl [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...]
|
tmpl source add https://gitea.com gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
# GLOBAL OPTIONS
|
To use it, either pass it in with the `--source` flag
|
||||||
|
|
||||||
**--registry, -r**="": Registry directory of tmpl (default: ~/.tmpl)
|
```
|
||||||
|
tmpl --source gitea download jolheiser/tmpls tmpls
|
||||||
|
```
|
||||||
|
|
||||||
**--source, -s**="": Short-name source to use
|
Or set it as the env variable `TMPL_SOURCE`
|
||||||
|
|
||||||
|
## Using a different branch
|
||||||
|
|
||||||
# COMMANDS
|
By default, tmpl will want to use a branch called `main` in your repository.
|
||||||
|
|
||||||
## download
|
If you are using another branch as your default, you can set it as the env variable `TMPL_BRANCH`
|
||||||
|
|
||||||
Download a template
|
Alternatively, you can specify on the command-line with the `--branch` flag of the `download` command
|
||||||
|
|
||||||
**--branch, -b**="": Branch to clone (default: main)
|
```
|
||||||
|
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`
|
||||||
|
|
||||||
## init
|
## Putting it all together
|
||||||
|
|
||||||
Initialize a template
|
I realize that many users will be using GitHub, and most will likely still be using the `master` branch.
|
||||||
|
|
||||||
## list
|
1. Set up a source for GitHub
|
||||||
|
1. `tmpl source add https://github.com github`
|
||||||
List templates in the registry
|
2. Set the env variable `TMPL_SOURCE` to `github`
|
||||||
|
2. Set the env variable `TMPL_BRANCH` to `master`
|
||||||
## remove
|
3. Happy templating! `tmpl download user/repo repo`
|
||||||
|
|
||||||
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
|
|
|
@ -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.
|
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
|
## 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
|
## License
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -16,6 +15,7 @@ var Download = &cli.Command{
|
||||||
Name: "download",
|
Name: "download",
|
||||||
Usage: "Download a template",
|
Usage: "Download a template",
|
||||||
Description: "Download a template and save it to the local registry",
|
Description: "Download a template and save it to the local registry",
|
||||||
|
ArgsUsage: "[repository URL] [name]",
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "branch",
|
Name: "branch",
|
||||||
|
@ -30,7 +30,7 @@ var Download = &cli.Command{
|
||||||
|
|
||||||
func runDownload(ctx *cli.Context) error {
|
func runDownload(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 2 {
|
if ctx.NArg() < 2 {
|
||||||
return errors.New("<repo> <name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
|
|
@ -49,7 +49,8 @@ func runInit(_ *cli.Context) error {
|
||||||
var comments = `# template.toml
|
var comments = `# template.toml
|
||||||
# Write any template args here to prompt the user for, giving any defaults/options as applicable
|
# Write any template args here to prompt the user for, giving any defaults/options as applicable
|
||||||
|
|
||||||
name = "MyProject"
|
[name]
|
||||||
|
prompt = "Project Name"
|
||||||
lang = ["Go", "Rust", "Python"]
|
help = "The name to use in the project"
|
||||||
|
default = "tmpl"
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.jolheiser.com/tmpl/cmd/flags"
|
"go.jolheiser.com/tmpl/cmd/flags"
|
||||||
"go.jolheiser.com/tmpl/registry"
|
"go.jolheiser.com/tmpl/registry"
|
||||||
|
|
||||||
|
@ -14,12 +12,13 @@ var Remove = &cli.Command{
|
||||||
Name: "remove",
|
Name: "remove",
|
||||||
Usage: "Remove a template",
|
Usage: "Remove a template",
|
||||||
Description: "Remove a template from the registry",
|
Description: "Remove a template from the registry",
|
||||||
|
ArgsUsage: "[name]",
|
||||||
Action: runRemove,
|
Action: runRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRemove(ctx *cli.Context) error {
|
func runRemove(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 1 {
|
if ctx.NArg() < 1 {
|
||||||
return errors.New("<name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"go.jolheiser.com/tmpl/cmd/flags"
|
"go.jolheiser.com/tmpl/cmd/flags"
|
||||||
|
@ -15,12 +14,13 @@ var Save = &cli.Command{
|
||||||
Name: "save",
|
Name: "save",
|
||||||
Usage: "Save a local template",
|
Usage: "Save a local template",
|
||||||
Description: "Save a local template to the registry",
|
Description: "Save a local template to the registry",
|
||||||
|
ArgsUsage: "[path] [name]",
|
||||||
Action: runSave,
|
Action: runSave,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSave(ctx *cli.Context) error {
|
func runSave(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 2 {
|
if ctx.NArg() < 2 {
|
||||||
return errors.New("<path> <name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
@ -37,6 +36,7 @@ var (
|
||||||
Name: "add",
|
Name: "add",
|
||||||
Usage: "Add a source",
|
Usage: "Add a source",
|
||||||
Description: "Add a new source to the registry",
|
Description: "Add a new source to the registry",
|
||||||
|
ArgsUsage: "[base URL] [name]",
|
||||||
Action: runSourceAdd,
|
Action: runSourceAdd,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ var (
|
||||||
Name: "remove",
|
Name: "remove",
|
||||||
Usage: "Remove a source",
|
Usage: "Remove a source",
|
||||||
Description: "Remove a source from the registry",
|
Description: "Remove a source from the registry",
|
||||||
|
ArgsUsage: "[name]",
|
||||||
Action: runSourceRemove,
|
Action: runSourceRemove,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -68,7 +69,7 @@ func runSourceList(_ *cli.Context) error {
|
||||||
|
|
||||||
func runSourceAdd(ctx *cli.Context) error {
|
func runSourceAdd(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 2 {
|
if ctx.NArg() < 2 {
|
||||||
return errors.New("<repo> <name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
@ -87,7 +88,7 @@ func runSourceAdd(ctx *cli.Context) error {
|
||||||
|
|
||||||
func runSourceRemove(ctx *cli.Context) error {
|
func runSourceRemove(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 1 {
|
if ctx.NArg() < 1 {
|
||||||
return errors.New("<name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
|
15
cmd/test.go
15
cmd/test.go
|
@ -2,6 +2,7 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"go.jolheiser.com/beaver"
|
"go.jolheiser.com/beaver"
|
||||||
|
@ -10,17 +11,23 @@ import (
|
||||||
var Test = &cli.Command{
|
var Test = &cli.Command{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Usage: "Test if a directory is a valid template",
|
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,
|
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
|
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")
|
errs = append(errs, "could not find template.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Lstat("template")
|
fi, err := os.Lstat(filepath.Join(testPath, "template"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, "no template directory found")
|
errs = append(errs, "no template directory found")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.jolheiser.com/tmpl/cmd/flags"
|
"go.jolheiser.com/tmpl/cmd/flags"
|
||||||
"go.jolheiser.com/tmpl/registry"
|
"go.jolheiser.com/tmpl/registry"
|
||||||
|
|
||||||
|
@ -14,12 +12,13 @@ var Update = &cli.Command{
|
||||||
Name: "update",
|
Name: "update",
|
||||||
Usage: "Update a template",
|
Usage: "Update a template",
|
||||||
Description: "Update a template in the registry from the original source",
|
Description: "Update a template in the registry from the original source",
|
||||||
|
ArgsUsage: "[name]",
|
||||||
Action: runUpdate,
|
Action: runUpdate,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runUpdate(ctx *cli.Context) error {
|
func runUpdate(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 1 {
|
if ctx.NArg() < 1 {
|
||||||
return errors.New("<name>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
|
18
cmd/use.go
18
cmd/use.go
|
@ -1,8 +1,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
|
|
||||||
"go.jolheiser.com/tmpl/cmd/flags"
|
"go.jolheiser.com/tmpl/cmd/flags"
|
||||||
"go.jolheiser.com/tmpl/registry"
|
"go.jolheiser.com/tmpl/registry"
|
||||||
|
|
||||||
|
@ -19,13 +17,23 @@ var Use = &cli.Command{
|
||||||
Name: "defaults",
|
Name: "defaults",
|
||||||
Usage: "Use template defaults",
|
Usage: "Use template defaults",
|
||||||
},
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Usage: "Overwrite existing files",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
ArgsUsage: "[name] [destination (default: \".\")]",
|
||||||
Action: runUse,
|
Action: runUse,
|
||||||
}
|
}
|
||||||
|
|
||||||
func runUse(ctx *cli.Context) error {
|
func runUse(ctx *cli.Context) error {
|
||||||
if ctx.NArg() < 2 {
|
if ctx.NArg() < 1 {
|
||||||
return errors.New("<name> <dest>")
|
return cli.ShowCommandHelp(ctx, ctx.Command.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := "."
|
||||||
|
if ctx.NArg() >= 2 {
|
||||||
|
dest = ctx.Args().Get(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := registry.Open(flags.Registry)
|
reg, err := registry.Open(flags.Registry)
|
||||||
|
@ -38,7 +46,7 @@ func runUse(ctx *cli.Context) error {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
docs.go
2
docs.go
|
@ -13,7 +13,7 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
app := cmd.NewApp()
|
app := cmd.NewApp()
|
||||||
|
|
||||||
fi, err := os.Create("DOCS.md")
|
fi, err := os.Create("CLI.md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
|
@ -13,11 +13,6 @@ var (
|
||||||
regDir string
|
regDir string
|
||||||
destDir string
|
destDir string
|
||||||
reg *Registry
|
reg *Registry
|
||||||
|
|
||||||
tmplContents = `{{title name}} {{.year}}`
|
|
||||||
tmplTemplate = `name = "john olheiser"
|
|
||||||
year = 2020`
|
|
||||||
tmplGold = "John Olheiser 2020"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
@ -78,30 +73,6 @@ func testGetFail(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
func setupTemplate() {
|
||||||
var err error
|
var err error
|
||||||
tmplDir, err = ioutil.TempDir(os.TempDir(), "tmpl")
|
tmplDir, err = ioutil.TempDir(os.TempDir(), "tmpl")
|
||||||
|
@ -122,10 +93,20 @@ func setupTemplate() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Template file
|
// Template directories
|
||||||
if err := os.Mkdir(filepath.Join(tmplDir, "template"), os.ModePerm); err != nil {
|
pkgPath := filepath.Join(tmplDir, "template", "{{upper package}}")
|
||||||
|
if err := os.MkdirAll(pkgPath, os.ModePerm); err != nil {
|
||||||
panic(err)
|
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"))
|
fi, err = os.Create(filepath.Join(tmplDir, "template", "TEST"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"github.com/pelletier/go-toml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template is a tmpl project
|
// Template is a tmpl project
|
||||||
|
@ -36,7 +34,7 @@ func (t *Template) ArchivePath() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute runs the Template and copies to dest
|
// 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")
|
tmp, err := ioutil.TempDir(os.TempDir(), "tmpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -47,11 +45,13 @@ func (t *Template) Execute(dest string, defaults bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
vars, err := prompt(tmp, defaults)
|
prompts, err := prompt(tmp, defaults)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
funcs := mergeMaps(funcMap, prompts.ToFuncMap())
|
||||||
|
|
||||||
base := filepath.Join(tmp, "template")
|
base := filepath.Join(tmp, "template")
|
||||||
return filepath.Walk(base, func(walkPath string, walkInfo os.FileInfo, walkErr error) error {
|
return filepath.Walk(base, func(walkPath string, walkInfo os.FileInfo, walkErr error) error {
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
|
@ -67,13 +67,19 @@ func (t *Template) Execute(dest string, defaults bool) error {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newDest := strings.TrimPrefix(walkPath, base+"/")
|
var buf bytes.Buffer
|
||||||
newDest = filepath.Join(dest, newDest)
|
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 {
|
if err := os.MkdirAll(filepath.Dir(newDest), os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -83,12 +89,22 @@ func (t *Template) Execute(dest string, defaults bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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())
|
newFi, err := os.OpenFile(newDest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, oldFi.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,66 +112,6 @@ func (t *Template) Execute(dest string, defaults bool) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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{} {
|
func mergeMaps(maps ...map[string]interface{}) map[string]interface{} {
|
||||||
m := make(map[string]interface{})
|
m := make(map[string]interface{})
|
||||||
for _, mm := range maps {
|
for _, mm := range maps {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue