Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added exec template function. #1931

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions commands/hugo.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ func LoadDefaultSettings() {
viper.SetDefault("SectionPagesMenu", "")
viper.SetDefault("DisablePathToLower", false)
viper.SetDefault("HasCJKLanguage", false)
viper.SetDefault("ExecWhitelist", []string{})
}

// InitializeConfig initializes a config file with sensible default configuration flags.
Expand Down
2 changes: 2 additions & 0 deletions docs/content/overview/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ Following is a list of Hugo-defined variables that you can configure and their c
verboseLog: false
# watch filesystem for changes and recreate as needed
watch: true
# commands that can be executed via the exec command
execWhitelist: []
---

## Ignore files on build
Expand Down
12 changes: 12 additions & 0 deletions docs/content/templates/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -822,3 +822,15 @@ responses of APIs.
{{ $resp.content | base64Decode | markdownify }}

The response of the GitHub API contains the base64-encoded version of the [README.md](https://github.com/spf13/hugo/blob/master/README.md) in the Hugo repository. Now we can decode it and parse the Markdown. The final output will look similar to the rendered version on GitHub.

### exec

Executes an external command and include its standard output as content. The
actual commands that can be executed need to be given using the `execWhitelist`
configuration settings.

{{ echo 42 }}
<!-- will output 42 -->

This assumes that `echo` is listed within the `execWhitelist`, which is not the
case by default.
32 changes: 32 additions & 0 deletions tpl/template_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"html/template"
"math/rand"
"os"
_exec "os/exec"
"reflect"
"sort"
"strconv"
Expand All @@ -40,6 +41,7 @@ import (
"github.com/spf13/cast"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper"
)

var funcMap template.FuncMap
Expand Down Expand Up @@ -1137,6 +1139,35 @@ func highlight(in interface{}, lang, opts string) (template.HTML, error) {
return template.HTML(helpers.Highlight(html.UnescapeString(str), lang, opts)), nil
}

// exec executes a command and returns its output as string.
func exec(in interface{}, args ...string) (string, error) {
str, err := cast.ToStringE(in)

if err != nil {
return "", err
}

// Check if command is accepted (in white list)
commandAccepted := false
for _, validCommand := range viper.GetStringSlice("ExecWhiteList") {
if str == validCommand {
commandAccepted = true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cosmetic, but a break would be nice here, as it is no need to look further.

break
}
}

if !commandAccepted {
return "", fmt.Errorf("Executing %s is not allowed. Check execWhiteList settings.", str)
}

out, err := _exec.Command(strings.TrimSpace(str), args...).Output()
if err != nil {
return "", err
}

return string(out[:]), nil
}

var markdownTrimPrefix = []byte("<p>")
var markdownTrimSuffix = []byte("</p>\n")

Expand Down Expand Up @@ -1684,6 +1715,7 @@ func init() {
"div": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '/') },
"echoParam": returnWhenSet,
"eq": eq,
"exec": exec,
"first": first,
"ge": ge,
"getCSV": getCSV,
Expand Down
50 changes: 50 additions & 0 deletions tpl/template_funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,56 @@ func tstIsLt(tp tstCompareType) bool {
return tp == tstLt || tp == tstLe
}

func TestExecFuncInTemplate(t *testing.T) {

viper.Reset()
defer viper.Reset()

viper.Set("ExecWhitelist", []string{"echo"})

in := "{{exec \"echo\" \"test\"}}"
expected := "test\n"

templ, err := New().New("test").Parse(in)
if err != nil {
t.Fatal("Got error on parse", err)
}

var b bytes.Buffer
err = templ.Execute(&b, nil)

if err != nil {
t.Fatal("Got error on execute", err)
}

if b.String() != expected {
t.Errorf("Got\n%q\nExpected\n>%q<", b.String(), expected)
}
}

func TestExecFuncInTemplateCmdNotInWhitelist(t *testing.T) {

viper.Reset()
defer viper.Reset()

viper.Set("ExecWhitelist", []string{})

in := "{{exec \"echo\" \"test\"}}"
expectedErrSuffix := "Executing echo is not allowed. Check execWhiteList settings."

templ, err := New().New("test").Parse(in)
if err != nil {
t.Fatal("Got error on parse", err)
}

var b bytes.Buffer
err = templ.Execute(&b, nil)

if !strings.HasSuffix(err.Error(), expectedErrSuffix) {
t.Errorf("Expected suffix %q in %q", expectedErrSuffix, err.Error())
}
}

func TestFuncsInTemplate(t *testing.T) {

viper.Reset()
Expand Down