diff --git a/adapter/handlebars/handlebars.go b/adapter/handlebars/handlebars.go index e416faa..1068f5b 100644 --- a/adapter/handlebars/handlebars.go +++ b/adapter/handlebars/handlebars.go @@ -14,6 +14,7 @@ import ( func Init(lang string, logger util.Logger, date date.Provider) { helpers.RegisterSlug(logger, lang) helpers.RegisterDate(logger, date) + helpers.RegisterShell(logger) } // HandlebarsRenderer holds parsed handlebars template and renders them. diff --git a/adapter/handlebars/handlebars_test.go b/adapter/handlebars/handlebars_test.go index 7c9a848..aedcd24 100644 --- a/adapter/handlebars/handlebars_test.go +++ b/adapter/handlebars/handlebars_test.go @@ -2,20 +2,18 @@ package handlebars import ( "fmt" - "log" - "os" "testing" "time" + "github.com/mickael-menu/zk/util" "github.com/mickael-menu/zk/util/assert" "github.com/mickael-menu/zk/util/date" "github.com/mickael-menu/zk/util/fixtures" ) func init() { - logger := log.New(os.Stderr, "zk: warning: ", 0) date := date.NewFrozen(time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)) - Init("en", logger, &date) + Init("en", &util.NullLogger, &date) } func TestRenderString(t *testing.T) { @@ -82,3 +80,15 @@ func TestDateHelper(t *testing.T) { test("timestamp-unix", "1258490098") test("cust: %Y-%m", "cust: 2009-11") } + +func TestShellHelper(t *testing.T) { + sut := NewRenderer() + // block is passed as piped input + res, err := sut.Render(`{{#sh "tr '[a-z]' '[A-Z]'"}}Hello, world!{{/sh}}`, nil) + assert.Nil(t, err) + assert.Equal(t, res, "HELLO, WORLD!") + // inline + res, err = sut.Render(`{{sh "echo 'Hello, world!'"}}`, nil) + assert.Nil(t, err) + assert.Equal(t, res, "Hello, world!\n") +} diff --git a/adapter/handlebars/helpers/date.go b/adapter/handlebars/helpers/date.go index 1d1eb56..c6e81a5 100644 --- a/adapter/handlebars/helpers/date.go +++ b/adapter/handlebars/helpers/date.go @@ -7,6 +7,14 @@ import ( "github.com/mickael-menu/zk/util/date" ) +// RegisterDate registers the {{date}} template helpers which format a given date. +// +// It supports various styles: short, medium, long, full, year, time, +// timestamp, timestamp-unix or a custom strftime format. +// +// {{date}} -> 2009-11-17 +// {{date "medium"}} -> Nov 17, 2009 +// {{date "%Y-%m"}} -> 2009-11 func RegisterDate(logger util.Logger, date date.Provider) { raymond.RegisterHelper("date", func(arg string) string { format := findFormat(arg) diff --git a/adapter/handlebars/helpers/shell.go b/adapter/handlebars/helpers/shell.go new file mode 100644 index 0000000..2bd0b69 --- /dev/null +++ b/adapter/handlebars/helpers/shell.go @@ -0,0 +1,40 @@ +package helpers + +import ( + "os/exec" + "strings" + + "github.com/aymerick/raymond" + "github.com/kballard/go-shellquote" + "github.com/mickael-menu/zk/util" +) + +// RegisterShell registers the {{sh}} template helper, which runs shell commands. +// +// {{#sh "tr '[a-z]' '[A-Z]'"}}Hello, world!{{/sh}} -> HELLO, WORLD! +// {{sh "echo 'Hello, world!'"}} -> Hello, world! +func RegisterShell(logger util.Logger) { + raymond.RegisterHelper("sh", func(arg string, options *raymond.Options) string { + args, err := shellquote.Split(arg) + if err != nil { + logger.Printf("{{sh}} failed to parse command: %v: %v", arg, err) + return "" + } + if len(args) == 0 { + logger.Printf("{{sh}} expects a valid shell command, received: %v", arg) + return "" + } + + cmd := exec.Command(args[0], args[1:]...) + // Feed any block content as piped input + cmd.Stdin = strings.NewReader(options.Fn()) + + output, err := cmd.Output() + if err != nil { + logger.Printf("{{sh}} command failed: %v", err) + return "" + } + + return string(output) + }) +} diff --git a/adapter/handlebars/helpers/slug.go b/adapter/handlebars/helpers/slug.go index 9a1ee89..4d68ece 100644 --- a/adapter/handlebars/helpers/slug.go +++ b/adapter/handlebars/helpers/slug.go @@ -6,6 +6,10 @@ import ( "github.com/mickael-menu/zk/util" ) +// RegisterSlug registers a {{slug}} template helper to slugify text. +// +// {{slug "This will be slugified!"}} -> this-will-be-slugified +// {{#slug}}This will be slugified!{{/slug}} -> this-will-be-slugified func RegisterSlug(logger util.Logger, lang string) { raymond.RegisterHelper("slug", func(opt interface{}) string { switch arg := opt.(type) { diff --git a/go.mod b/go.mod index aac88d7..bf50181 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/google/go-cmp v0.3.1 github.com/gosimple/slug v1.9.0 github.com/hashicorp/hcl/v2 v2.8.1 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/lestrrat-go/strftime v1.0.3 gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 78f23d8..a64d0f9 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= github.com/hashicorp/hcl/v2 v2.8.1 h1:FJ60CIYaMyJOKzPndhMyjiz353Fd+2jr6PodF5Xzb08= github.com/hashicorp/hcl/v2 v2.8.1/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/main.go b/main.go index ca297ce..54cde9f 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,13 @@ package main import ( + "log" + "os" + "github.com/alecthomas/kong" + "github.com/mickael-menu/zk/adapter/handlebars" "github.com/mickael-menu/zk/cmd" + "github.com/mickael-menu/zk/util/date" ) var cli struct { @@ -11,6 +16,13 @@ var cli struct { } func main() { + logger := log.New(os.Stderr, "zk: warning: ", 0) + // zk is short-lived, so we freeze the current date to use the same date + // for any rendering during the execution. + date := date.NewFrozenNow() + // FIXME take the language from the config + handlebars.Init("en", logger, &date) + ctx := kong.Parse(&cli, kong.Name("zk"), ) diff --git a/util/logger.go b/util/logger.go index d8f626d..3a6cdba 100644 --- a/util/logger.go +++ b/util/logger.go @@ -6,3 +6,12 @@ type Logger interface { Printf(format string, v ...interface{}) Println(v ...interface{}) } + +// NullLogger is a logger ignoring any input. +var NullLogger = nullLogger{} + +type nullLogger struct{} + +func (n *nullLogger) Printf(format string, v ...interface{}) {} + +func (n *nullLogger) Println(v ...interface{}) {}