Skip to content
Snippets Groups Projects
Commit 9e3ec5cd authored by Tamer Tas's avatar Tamer Tas
Browse files

Extend user prompts with booleans and lists

parent 94c9a0c8
No related branches found
No related tags found
No related merge requests found
......@@ -4,75 +4,173 @@ import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/tmrts/tmplt/pkg/util/tlog"
)
func Ask(msg string) (value string) {
fmt.Print(msg)
const (
// TODO align brackets used in the prompt message
PromptFormatMessage = "[?] Please choose a value for %#q [default: %#v]: "
PromptChoiceFormatMessage = "[?] Please select an option for %#q\n%v Select from %v..%v [default: %#v]: "
)
_, err := fmt.Scanf("%s", &value)
func scanLine() (string, error) {
input := bufio.NewReader(os.Stdin)
line, err := input.ReadString('\n')
if err != nil {
tlog.Warn(err.Error())
return ""
return line, err
}
return
return strings.TrimSuffix(line, "\n"), nil
}
// TODO add GetLine method using a channel
// TODO use interfaces to eliminate code duplication
func newString(name string, defval interface{}) func() interface{} {
var cache interface{}
return func() interface{} {
if cache == nil {
cache = func() interface{} {
// TODO use colored prompts
fmt.Printf(PromptFormatMessage, name, defval)
line, err := scanLine()
if err != nil {
tlog.Warn(err.Error())
return line
}
if line == "" {
return defval
}
return line
}()
}
return cache
}
}
var (
booleanValues = map[string]bool{
"y": true,
"yes": true,
"yup": true,
"n": false,
"no": false,
"nope": false,
"y": true,
"yes": true,
"yup": true,
"true": true,
"n": false,
"no": false,
"nope": false,
"false": false,
}
)
func Confirm(msg string) bool {
fmt.Print(msg)
func newBool(name string, defval bool) func() interface{} {
var cache interface{}
return func() interface{} {
if cache == nil {
cache = func() interface{} {
fmt.Printf(PromptFormatMessage, name, defval)
var choice string
_, err := fmt.Scanf("%s", &choice)
if err != nil {
tlog.Warn(err.Error())
return false
choice, err := scanLine()
if err != nil {
tlog.Warn(err.Error())
return choice
}
if choice == "" {
return defval
}
val, ok := booleanValues[strings.ToLower(choice)]
if !ok {
tlog.Warn(fmt.Sprintf("Unrecognized choice %q, using the default", choice))
return defval
}
return val
}()
}
return cache
}
}
val, ok := booleanValues[choice]
if !ok {
tlog.Warn(fmt.Sprintf("unrecognized choice %#q", choice))
return false
type Choice struct {
Default int
Choices []string
}
func formattedChoices(cs []string) (s string) {
for i, c := range cs {
s += fmt.Sprintf(" %v - %q\n", i+1, c)
}
return val
return
}
const (
PromptFormatMessage = "? Please choose a value for %#q [default: %#q]: "
)
func newSlice(name string, choices []string) func() interface{} {
var cache interface{}
return func() interface{} {
if cache == nil {
defindex := 0
defval := choices[defindex]
cache = func() interface{} {
s := formattedChoices(choices)
fmt.Printf(PromptChoiceFormatMessage, name, s, 1, len(choices), defindex+1)
choice, err := scanLine()
if err != nil {
tlog.Warn(err.Error())
return choice
}
if choice == "" {
return defval
}
index, err := strconv.Atoi(choice)
if err != nil {
return err
}
if index > len(choices)+1 || index < 1 {
tlog.Warn(fmt.Sprintf("Unrecognized choice %v, using the default", index))
return defval
}
return choices[index-1]
}()
}
return cache
}
}
// TODO accept boolean, integer values in addition to string
func New(msg, defval string) func() string {
return func() string {
// TODO use colored prompts
fmt.Printf(PromptFormatMessage, msg, defval)
input := bufio.NewReader(os.Stdin)
line, err := input.ReadString('\n')
if err != nil {
tlog.Warn(err.Error())
return line
// New returns a prompt closure when executed asks for user input and returns result.
func New(name string, defval interface{}) func() interface{} {
// TODO use reflect package
switch defval := defval.(type) {
case bool:
return newBool(name, defval)
case []interface{}:
if len(defval) == 0 {
tlog.Warn(fmt.Sprintf("empty list for %q choices", name))
return nil
}
if line == "\n" {
return defval
var s []string
for _, v := range defval {
s = append(s, v.(string))
}
return strings.TrimSuffix(line, "\n")
return newSlice(name, s)
}
return newString(name, defval)
}
......@@ -2,28 +2,40 @@ package template
import (
"os"
"strings"
"text/template"
"time"
"github.com/tmrts/tmplt/pkg/prompt"
)
var (
FuncMap = template.FuncMap{
"env": os.Getenv,
"now": CurrentTime,
"ask": prompt.Ask,
"confirm": prompt.Confirm,
// TODO confirmation prompt
// TODO value prompt
// TODO encoding utilities (e.g. toBinary)
// TODO GET, POST utilities
// TODO Hostname(Also accesible through $HOSTNAME), IP addr, etc.
// TODO add validate for custom regex and expose validate package
"env": os.Getenv,
"time": CurrentTimeInFmt,
"hostname": func() string { return os.Getenv("HOSTNAME") },
// String utilities
"toLower": strings.ToLower,
"toUpper": strings.ToUpper,
"toTitle": strings.ToTitle,
"trimSpace": strings.TrimSpace,
"repeat": strings.Repeat,
}
Options = []string{
"missingkey=default",
// TODO ignore a field if no value is found instead of writing <no value>
"missingkey=invalid",
}
//BaseTemplate = template.New("base").Option(Options...).Funcs(FuncMap)
)
func CurrentTime(fmt string) string {
func CurrentTimeInFmt(fmt string) string {
t := time.Now()
return t.Format(fmt)
......
......@@ -4,11 +4,12 @@ import (
"encoding/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"text/template"
"github.com/tmrts/tmplt/pkg/prompt"
"github.com/tmrts/tmplt/pkg/tmplt"
"github.com/tmrts/tmplt/pkg/util/exec"
"github.com/tmrts/tmplt/pkg/util/stringutil"
)
......@@ -22,8 +23,8 @@ func Get(path string) (Interface, error) {
return nil, err
}
// TODO make metadata optional
md, err := func(fname string) (map[string]string, error) {
// TODO make context optional
ctxt, err := func(fname string) (map[string]interface{}, error) {
f, err := os.Open(fname)
if err != nil {
if os.IsNotExist(err) {
......@@ -40,61 +41,43 @@ func Get(path string) (Interface, error) {
return nil, err
}
var metadata map[string]string
var metadata map[string]interface{}
if err := json.Unmarshal(buf, &metadata); err != nil {
return nil, err
}
return metadata, nil
}(filepath.Join(filepath.Join(absPath, "template"), "metadata.json"))
}(filepath.Join(absPath, tmplt.ContextFileName))
return &dirTemplate{Path: absPath, Metadata: md, FuncMap: FuncMap}, err
return &dirTemplate{
Context: ctxt,
FuncMap: FuncMap,
Path: filepath.Join(absPath, tmplt.TemplateDirName),
}, err
}
type dirTemplate struct {
Path string
Metadata map[string]string
FuncMap template.FuncMap
promptMap map[string]promptFunc
}
type promptFunc func() string
func (f promptFunc) String() string {
return f()
}
func (t *dirTemplate) AddPromptFunctions() {
t.promptMap = make(map[string]promptFunc)
for s, v := range t.Metadata {
t.promptMap[s] = prompt.New(s, v)
}
// TODO allow nested maps
t.FuncMap["project"] = func() map[string]promptFunc {
//return t.promptMap
// TODO temporary stub
return map[string]promptFunc{
"Author": prompt.New("author", "Johann Sebastian"),
}
}
Path string
Context map[string]interface{}
FuncMap template.FuncMap
}
// Execute fills the template with the project metadata.
func (d *dirTemplate) Execute(dirPrefix string) error {
d.AddPromptFunctions()
func (t *dirTemplate) Execute(dirPrefix string) error {
for s, v := range t.Context {
t.FuncMap[s] = prompt.New(s, v)
}
// TODO(tmrts): create io.ReadWriter from string
// TODO(tmrts): refactor command execution
// TODO(tmrts): refactor name manipulation
return filepath.Walk(d.Path, func(filename string, info os.FileInfo, err error) error {
// TODO create io.ReadWriter from string
// TODO refactor command execution
// TODO refactor name manipulation
return filepath.Walk(t.Path, func(filename string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Path relative to the root of the template directory
oldName, err := filepath.Rel(filepath.Dir(d.Path), filename)
oldName, err := filepath.Rel(t.Path, filename)
if err != nil {
return err
}
......@@ -102,7 +85,7 @@ func (d *dirTemplate) Execute(dirPrefix string) error {
buf := stringutil.NewString("")
fnameTmpl := template.Must(template.
New("filename").
New("file name template").
Option(Options...).
Funcs(FuncMap).
Parse(oldName))
......@@ -116,7 +99,8 @@ func (d *dirTemplate) Execute(dirPrefix string) error {
target := filepath.Join(dirPrefix, newName)
if info.IsDir() {
if _, err := exec.Command("/bin/mkdir", target).Output(); err != nil {
// TODO create a new pkg for dir operations
if _, err := exec.Cmd("/bin/mkdir", "-p", target); err != nil {
return err
}
} else {
......@@ -125,6 +109,11 @@ func (d *dirTemplate) Execute(dirPrefix string) error {
return err
}
// Delete target file if it exists
if err := os.Remove(target); err != nil && !os.IsNotExist(err) {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, fi.Mode())
if err != nil {
return err
......@@ -133,7 +122,7 @@ func (d *dirTemplate) Execute(dirPrefix string) error {
}
contentsTmpl := template.Must(template.
New("filecontents").
New("file contents template").
Option(Options...).
Funcs(FuncMap).
ParseFiles(filename))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment