Skip to content

Commit

Permalink
Support a config file to use instead of commandline arguments
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Henderson <[email protected]>
  • Loading branch information
hairyhenderson committed Feb 9, 2020
1 parent 79506e1 commit 59da894
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 48 deletions.
127 changes: 93 additions & 34 deletions cmd/gomplate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ import (
"github.com/hairyhenderson/gomplate/v3/env"
"github.com/hairyhenderson/gomplate/v3/version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
printVer bool
verbose bool
execPipe bool
opts gomplate.Config
includes []string

cfgFile string
postRunInput *bytes.Buffer
)

const (
defaultConfigName = ".gomplate"
defaultConfigFile = ".gomplate.yaml"
)

func printVersion(name string) {
fmt.Printf("%s version %s\n", name, version.Version)
}
Expand All @@ -38,7 +39,7 @@ func postRunExec(cmd *cobra.Command, args []string) error {
args = args[1:]
// nolint: gosec
c := exec.Command(name, args...)
if execPipe {
if viper.GetBool("exec-pipe") {
c.Stdin = postRunInput
} else {
c.Stdin = os.Stdin
Expand Down Expand Up @@ -91,29 +92,33 @@ func processIncludes(includes, excludes []string) []string {

func newGomplateCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "gomplate",
Short: "Process text files with Go templates",
Use: "gomplate",
Short: "Process text files with Go templates",
// TODO: cmd.SetVersionTemplate(s string)
// Version: version.Version,
PreRunE: validateOpts,
RunE: func(cmd *cobra.Command, args []string) error {
if printVer {
if viper.GetBool("version") {
printVersion(cmd.Name())
return nil
}
verbose := viper.GetBool("verbose")
if verbose {
// nolint: errcheck
fmt.Fprintf(os.Stderr, "%s version %s, build %s\nconfig is:\n%s\n\n",
cmd.Name(), version.Version, version.GitCommit,
&opts)
viper.AllSettings())
}

conf := buildConfig(viper.GetViper())
// support --include
opts.ExcludeGlob = processIncludes(includes, opts.ExcludeGlob)
conf.ExcludeGlob = processIncludes(viper.GetStringSlice("include"), viper.GetStringSlice("exclude"))

if execPipe {
if viper.GetBool("exec-pipe") {
postRunInput = &bytes.Buffer{}
opts.Out = postRunInput
conf.Out = postRunInput
}
err := gomplate.RunTemplates(&opts)
err := gomplate.RunTemplates(conf)
cmd.SilenceErrors = true
cmd.SilenceUsage = true
if verbose {
Expand All @@ -129,39 +134,93 @@ func newGomplateCmd() *cobra.Command {
return rootCmd
}

func buildConfig(v *viper.Viper) *gomplate.Config {
g := &gomplate.Config{
Input: v.GetString("in"),
InputFiles: v.GetStringSlice("file"),
InputDir: v.GetString("input-dir"),
ExcludeGlob: v.GetStringSlice("exclude"),
OutputFiles: v.GetStringSlice("out"),
OutputDir: v.GetString("output-dir"),
OutputMap: v.GetString("output-map"),
OutMode: v.GetString("chmod"),

DataSources: v.GetStringSlice("datasource"),
DataSourceHeaders: v.GetStringSlice("datasource-header"),
Contexts: v.GetStringSlice("context"),

Plugins: v.GetStringSlice("plugin"),

LDelim: v.GetString("left-delim"),
RDelim: v.GetString("right-delim"),

Templates: v.GetStringSlice("template"),
}

return g
}

func initFlags(command *cobra.Command) {
command.Flags().SortFlags = false

command.Flags().StringArrayVarP(&opts.DataSources, "datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.")
command.Flags().StringArrayVarP(&opts.DataSourceHeaders, "datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.")
command.Flags().StringSliceP("datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.")
command.Flags().StringSliceP("datasource-header", "H", nil, "HTTP `header` field in 'alias=Name: value' form to be provided on HTTP-based data sources. Multiples can be set.")

command.Flags().StringArrayVarP(&opts.Contexts, "context", "c", nil, "pre-load a `datasource` into the context, in alias=URL form. Use the special alias `.` to set the root context.")
command.Flags().StringSliceP("context", "c", nil, "pre-load a `datasource` into the context, in alias=URL form. Use the special alias `.` to set the root context.")

command.Flags().StringArrayVar(&opts.Plugins, "plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times")
command.Flags().StringSlice("plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times")

command.Flags().StringArrayVarP(&opts.InputFiles, "file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir")
command.Flags().StringVarP(&opts.Input, "in", "i", "", "Template `string` to process (alternative to --file and --input-dir)")
command.Flags().StringVar(&opts.InputDir, "input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)")
command.Flags().StringSliceP("file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir")
command.Flags().StringP("in", "i", "", "Template `string` to process (alternative to --file and --input-dir)")
command.Flags().String("input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)")

command.Flags().StringArrayVar(&opts.ExcludeGlob, "exclude", []string{}, "glob of files to not parse")
command.Flags().StringArrayVar(&includes, "include", []string{}, "glob of files to parse")
command.Flags().StringSlice("exclude", []string{}, "glob of files to not parse")
command.Flags().StringSlice("include", []string{}, "glob of files to parse")

command.Flags().StringArrayVarP(&opts.OutputFiles, "out", "o", []string{"-"}, "output `file` name. Omit to use standard output.")
command.Flags().StringArrayVarP(&opts.Templates, "template", "t", []string{}, "Additional template file(s)")
command.Flags().StringVar(&opts.OutputDir, "output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir")
command.Flags().StringVar(&opts.OutputMap, "output-map", "", "Template `string` to map the input file to an output path")
command.Flags().StringVar(&opts.OutMode, "chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)")
command.Flags().StringSliceP("out", "o", []string{"-"}, "output `file` name. Omit to use standard output.")
command.Flags().StringSliceP("template", "t", []string{}, "Additional template file(s)")
command.Flags().String("output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir")
command.Flags().String("output-map", "", "Template `string` to map the input file to an output path")
command.Flags().String("chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)")

command.Flags().BoolVar(&execPipe, "exec-pipe", false, "pipe the output to the post-run exec command")
command.Flags().Bool("exec-pipe", false, "pipe the output to the post-run exec command")

ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{")
rdDefault := env.Getenv("GOMPLATE_RIGHT_DELIM", "}}")
command.Flags().StringVar(&opts.LDelim, "left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]")
command.Flags().StringVar(&opts.RDelim, "right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]")
command.Flags().String("left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]")
command.Flags().String("right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]")

command.Flags().BoolVarP(&verbose, "verbose", "V", false, "output extra information about what gomplate is doing")
command.Flags().BoolP("verbose", "V", false, "output extra information about what gomplate is doing")

command.Flags().BoolVarP(&printVer, "version", "v", false, "print the version")
command.Flags().BoolP("version", "v", false, "print the version")

command.Flags().StringVar(&cfgFile, "config", defaultConfigFile, "config file (overridden by commandline flags)")

viper.BindPFlags(command.Flags())

cobra.OnInitialize(initConfig(command))
}

func initConfig(cmd *cobra.Command) func() {
return func() {
configRequired := false
if cmd.Flags().Changed("config") {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
configRequired = true
} else {
viper.AddConfigPath(".")
viper.SetConfigName(defaultConfigName)
}

err := viper.ReadInConfig()
if err != nil && configRequired {
panic(err)
}
if err == nil && viper.GetBool("verbose") {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
}

func main() {
Expand Down
42 changes: 42 additions & 0 deletions cmd/gomplate/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package main
import (
"testing"

"github.com/hairyhenderson/gomplate/v3"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)

Expand All @@ -21,3 +25,41 @@ func TestProcessIncludes(t *testing.T) {
assert.EqualValues(t, d.expected, processIncludes(d.inc, d.exc))
}
}

func TestBuildConfig(t *testing.T) {
v := viper.New()
c := buildConfig(v)
expected := &gomplate.Config{}
assert.Equal(t, expected, c)
}

func TestInitConfig(t *testing.T) {
fs := afero.NewMemMapFs()
viper.SetFs(fs)
defer viper.Reset()
cmd := &cobra.Command{}

assert.NotPanics(t, func() {
initConfig(cmd)()
})

cmd.Flags().String("config", "", "foo")

assert.NotPanics(t, func() {
initConfig(cmd)()
})

cmd.ParseFlags([]string{"--config", "config.file"})

assert.Panics(t, func() {
initConfig(cmd)()
})

f, err := fs.Create(defaultConfigName + ".yaml")
assert.NoError(t, err)
f.WriteString("")

assert.NotPanics(t, func() {
initConfig(cmd)()
})
}
22 changes: 13 additions & 9 deletions cmd/gomplate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import (
"strings"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func isSet(cmd *cobra.Command, flag string) bool {
return cmd.Flags().Changed(flag) || viper.InConfig(flag)
}

func notTogether(cmd *cobra.Command, flags ...string) error {
found := ""
for _, flag := range flags {
f := cmd.Flag(flag)
if f != nil && f.Changed {
if isSet(cmd, flag) {
if found != "" {
a := make([]string, len(flags))
for i := range a {
Expand All @@ -26,10 +30,8 @@ func notTogether(cmd *cobra.Command, flags ...string) error {
}

func mustTogether(cmd *cobra.Command, left, right string) error {
l := cmd.Flag(left)
if l != nil && l.Changed {
r := cmd.Flag(right)
if r != nil && !r.Changed {
if isSet(cmd, left) {
if !isSet(cmd, right) {
return fmt.Errorf("--%s must be set when --%s is set", right, left)
}
}
Expand All @@ -43,11 +45,13 @@ func validateOpts(cmd *cobra.Command, args []string) (err error) {
err = notTogether(cmd, "out", "output-dir", "output-map", "exec-pipe")
}

if err == nil && len(opts.InputFiles) != len(opts.OutputFiles) {
err = fmt.Errorf("must provide same number of --out (%d) as --file (%d) options", len(opts.OutputFiles), len(opts.InputFiles))
f := viper.GetStringSlice("file")
o := viper.GetStringSlice("out")
if err == nil && len(f) != len(o) {
err = fmt.Errorf("must provide same number of --out (%d) as --file (%d) options", len(o), len(f))
}

if err == nil && cmd.Flag("exec-pipe").Changed && len(args) == 0 {
if err == nil && isSet(cmd, "exec-pipe") && len(args) == 0 {
err = fmt.Errorf("--exec-pipe may only be used with a post-exec command after --")
}

Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,25 @@ require (
github.com/hashicorp/vault/api v1.0.4
github.com/johannesboyne/gofakes3 v0.0.0-20191228161223-9aee1c78a252
github.com/joho/godotenv v1.3.0
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/pkg/errors v0.9.1
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/spf13/afero v1.2.2
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.2
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
github.com/zealic/xignore v0.3.3
gocloud.dev v0.18.0
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15
gopkg.in/ini.v1 v1.52.0 // indirect
gopkg.in/src-d/go-billy.v4 v4.3.2
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
gotest.tools/v3 v3.0.0
k8s.io/client-go v11.0.0+incompatible
Expand Down
Loading

0 comments on commit 59da894

Please sign in to comment.