From 7ff174a86a935191a684f0c63f9e2a48058fabfb Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 11 Nov 2019 16:03:16 -0500 Subject: Support a config file to use instead of commandline arguments Signed-off-by: Dave Henderson --- cmd/gomplate/config.go | 253 +++++++++++++++++++++++++++++++++++++++++ cmd/gomplate/config_test.go | 258 ++++++++++++++++++++++++++++++++++++++++++ cmd/gomplate/logger.go | 3 +- cmd/gomplate/main.go | 115 ++++++++----------- cmd/gomplate/main_test.go | 22 ---- cmd/gomplate/validate.go | 63 ----------- cmd/gomplate/validate_test.go | 95 ---------------- 7 files changed, 560 insertions(+), 249 deletions(-) create mode 100644 cmd/gomplate/config.go create mode 100644 cmd/gomplate/config_test.go delete mode 100644 cmd/gomplate/validate.go delete mode 100644 cmd/gomplate/validate_test.go (limited to 'cmd') diff --git a/cmd/gomplate/config.go b/cmd/gomplate/config.go new file mode 100644 index 00000000..02278618 --- /dev/null +++ b/cmd/gomplate/config.go @@ -0,0 +1,253 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/hairyhenderson/gomplate/v3/conv" + "github.com/hairyhenderson/gomplate/v3/env" + "github.com/hairyhenderson/gomplate/v3/internal/config" + + "github.com/rs/zerolog" + + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +const ( + defaultConfigFile = ".gomplate.yaml" +) + +var fs = afero.NewOsFs() + +// loadConfig is intended to be called before command execution. It: +// - creates a config.Config from the cobra flags +// - creates a config.Config from the config file (if present) +// - merges the two (flags take precedence) +// - validates the final config +// - converts the config to a *gomplate.Config for further use (TODO: eliminate this part) +func loadConfig(cmd *cobra.Command, args []string) (*config.Config, error) { + ctx := cmd.Context() + flagConfig, err := cobraConfig(cmd, args) + if err != nil { + return nil, err + } + + cfg, err := readConfigFile(cmd) + if err != nil { + return nil, err + } + if cfg == nil { + cfg = flagConfig + } else { + cfg = cfg.MergeFrom(flagConfig) + } + + cfg, err = applyEnvVars(ctx, cfg) + if err != nil { + return nil, err + } + + // reset defaults before validation + cfg.ApplyDefaults() + + err = cfg.Validate() + if err != nil { + return nil, fmt.Errorf("failed to validate merged config: %w\n%+v", err, cfg) + } + return cfg, nil +} + +func pickConfigFile(cmd *cobra.Command) (cfgFile string, required bool) { + cfgFile = defaultConfigFile + if c := env.Getenv("GOMPLATE_CONFIG"); c != "" { + cfgFile = c + required = true + } + if cmd.Flags().Changed("config") && cmd.Flag("config").Value.String() != "" { + // Use config file from the flag if specified + cfgFile = cmd.Flag("config").Value.String() + required = true + } + return cfgFile, required +} + +func readConfigFile(cmd *cobra.Command) (cfg *config.Config, err error) { + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + log := zerolog.Ctx(ctx) + + cfgFile, configRequired := pickConfigFile(cmd) + + f, err := fs.Open(cfgFile) + if err != nil { + if configRequired { + return cfg, fmt.Errorf("config file requested, but couldn't be opened: %w", err) + } + return nil, nil + } + + cfg, err = config.Parse(f) + if err != nil && configRequired { + return cfg, fmt.Errorf("config file requested, but couldn't be parsed: %w", err) + } + + log.Debug().Str("cfgFile", cfgFile).Msg("using config file") + + return cfg, err +} + +// cobraConfig - initialize a config from the commandline options +func cobraConfig(cmd *cobra.Command, args []string) (cfg *config.Config, err error) { + cfg = &config.Config{} + cfg.InputFiles, err = getStringSlice(cmd, "file") + if err != nil { + return nil, err + } + cfg.Input, err = getString(cmd, "in") + if err != nil { + return nil, err + } + cfg.InputDir, err = getString(cmd, "input-dir") + if err != nil { + return nil, err + } + + cfg.ExcludeGlob, err = getStringSlice(cmd, "exclude") + if err != nil { + return nil, err + } + includesFlag, err := getStringSlice(cmd, "include") + if err != nil { + return nil, err + } + // support --include + cfg.ExcludeGlob = processIncludes(includesFlag, cfg.ExcludeGlob) + + cfg.OutputFiles, err = getStringSlice(cmd, "out") + if err != nil { + return nil, err + } + cfg.Templates, err = getStringSlice(cmd, "template") + if err != nil { + return nil, err + } + cfg.OutputDir, err = getString(cmd, "output-dir") + if err != nil { + return nil, err + } + cfg.OutputMap, err = getString(cmd, "output-map") + if err != nil { + return nil, err + } + cfg.OutMode, err = getString(cmd, "chmod") + if err != nil { + return nil, err + } + + if len(args) > 0 { + cfg.PostExec = args + } + + cfg.ExecPipe, err = getBool(cmd, "exec-pipe") + if err != nil { + return nil, err + } + + cfg.LDelim, err = getString(cmd, "left-delim") + if err != nil { + return nil, err + } + cfg.RDelim, err = getString(cmd, "right-delim") + if err != nil { + return nil, err + } + + ds, err := getStringSlice(cmd, "datasource") + if err != nil { + return nil, err + } + cx, err := getStringSlice(cmd, "context") + if err != nil { + return nil, err + } + hdr, err := getStringSlice(cmd, "datasource-header") + if err != nil { + return nil, err + } + err = cfg.ParseDataSourceFlags(ds, cx, hdr) + if err != nil { + return nil, err + } + + pl, err := getStringSlice(cmd, "plugin") + if err != nil { + return nil, err + } + err = cfg.ParsePluginFlags(pl) + if err != nil { + return nil, err + } + return cfg, nil +} + +func getStringSlice(cmd *cobra.Command, flag string) (s []string, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + s, err = cmd.Flags().GetStringSlice(flag) + } + return s, err +} + +func getString(cmd *cobra.Command, flag string) (s string, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + s, err = cmd.Flags().GetString(flag) + } + return s, err +} + +func getBool(cmd *cobra.Command, flag string) (b bool, err error) { + if cmd.Flag(flag) != nil && cmd.Flag(flag).Changed { + b, err = cmd.Flags().GetBool(flag) + } + return b, err +} + +// process --include flags - these are analogous to specifying --exclude '*', +// then the inverse of the --include options. +func processIncludes(includes, excludes []string) []string { + if len(includes) == 0 && len(excludes) == 0 { + return nil + } + + out := []string{} + // if any --includes are set, we start by excluding everything + if len(includes) > 0 { + out = make([]string, 1+len(includes)) + out[0] = "*" + } + for i, include := range includes { + // includes are just the opposite of an exclude + out[i+1] = "!" + include + } + out = append(out, excludes...) + return out +} + +func applyEnvVars(ctx context.Context, cfg *config.Config) (*config.Config, error) { + if to := env.Getenv("GOMPLATE_PLUGIN_TIMEOUT"); cfg.PluginTimeout == 0 && to != "" { + t, err := time.ParseDuration(to) + if err != nil { + return nil, fmt.Errorf("GOMPLATE_PLUGIN_TIMEOUT set to invalid value %q: %w", to, err) + } + cfg.PluginTimeout = t + } + + if !cfg.SuppressEmpty && conv.ToBool(env.Getenv("GOMPLATE_SUPPRESS_EMPTY", "false")) { + cfg.SuppressEmpty = true + } + + return cfg, nil +} diff --git a/cmd/gomplate/config_test.go b/cmd/gomplate/config_test.go new file mode 100644 index 00000000..dc05dfbb --- /dev/null +++ b/cmd/gomplate/config_test.go @@ -0,0 +1,258 @@ +package main + +import ( + "context" + "os" + "testing" + "time" + + "github.com/hairyhenderson/gomplate/v3/internal/config" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestReadConfigFile(t *testing.T) { + fs = afero.NewMemMapFs() + defer func() { fs = afero.NewOsFs() }() + cmd := &cobra.Command{} + + _, err := readConfigFile(cmd) + assert.NoError(t, err) + + cmd.Flags().String("config", defaultConfigFile, "foo") + + _, err = readConfigFile(cmd) + assert.NoError(t, err) + + cmd.ParseFlags([]string{"--config", "config.file"}) + + _, err = readConfigFile(cmd) + assert.Error(t, err) + + cmd = &cobra.Command{} + cmd.Flags().String("config", defaultConfigFile, "foo") + + f, err := fs.Create(defaultConfigFile) + assert.NoError(t, err) + f.WriteString("") + + cfg, err := readConfigFile(cmd) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{}, cfg) + + cmd.ParseFlags([]string{"--config", "config.yaml"}) + + f, err = fs.Create("config.yaml") + assert.NoError(t, err) + f.WriteString("in: hello world\n") + + cfg, err = readConfigFile(cmd) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{Input: "hello world"}, cfg) + + f.WriteString("in: ") + + _, err = readConfigFile(cmd) + assert.Error(t, err) +} + +func TestLoadConfig(t *testing.T) { + fs = afero.NewMemMapFs() + defer func() { fs = afero.NewOsFs() }() + + cmd := &cobra.Command{} + cmd.Args = optionalExecArgs + cmd.Flags().StringSlice("file", []string{"-"}, "...") + cmd.Flags().StringSlice("out", []string{"-"}, "...") + cmd.Flags().String("in", ".", "...") + cmd.Flags().String("output-dir", ".", "...") + cmd.Flags().String("left-delim", "{{", "...") + cmd.Flags().String("right-delim", "}}", "...") + cmd.Flags().Bool("exec-pipe", false, "...") + cmd.ParseFlags(nil) + + out, err := loadConfig(cmd, cmd.Flags().Args()) + expected := &config.Config{ + InputFiles: []string{"-"}, + OutputFiles: []string{"-"}, + LDelim: "{{", + RDelim: "}}", + PostExecInput: os.Stdin, + OutWriter: os.Stdout, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) + + cmd.ParseFlags([]string{"--in", "foo"}) + out, err = loadConfig(cmd, cmd.Flags().Args()) + expected = &config.Config{ + Input: "foo", + OutputFiles: []string{"-"}, + LDelim: "{{", + RDelim: "}}", + PostExecInput: os.Stdin, + OutWriter: os.Stdout, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) + + cmd.ParseFlags([]string{"--in", "foo", "--exec-pipe", "--", "tr", "[a-z]", "[A-Z]"}) + out, err = loadConfig(cmd, cmd.Flags().Args()) + expected = &config.Config{ + Input: "foo", + LDelim: "{{", + RDelim: "}}", + ExecPipe: true, + PostExec: []string{"tr", "[a-z]", "[A-Z]"}, + PostExecInput: out.PostExecInput, + OutWriter: out.PostExecInput, + OutputFiles: []string{"-"}, + PluginTimeout: 5 * time.Second, + } + assert.NoError(t, err) + assert.EqualValues(t, expected, out) +} + +func TestCobraConfig(t *testing.T) { + t.Parallel() + cmd := &cobra.Command{} + cmd.Flags().StringSlice("file", []string{"-"}, "...") + cmd.Flags().StringSlice("out", []string{"-"}, "...") + cmd.Flags().String("output-dir", ".", "...") + cmd.Flags().String("left-delim", "{{", "...") + cmd.Flags().String("right-delim", "}}", "...") + cmd.ParseFlags(nil) + + cfg, err := cobraConfig(cmd, cmd.Flags().Args()) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{}, cfg) + + cmd.ParseFlags([]string{"--file", "in", "--", "echo", "foo"}) + + cfg, err = cobraConfig(cmd, cmd.Flags().Args()) + assert.NoError(t, err) + assert.EqualValues(t, &config.Config{ + InputFiles: []string{"in"}, + PostExec: []string{"echo", "foo"}, + }, cfg) +} + +func TestProcessIncludes(t *testing.T) { + t.Parallel() + data := []struct { + inc, exc, expected []string + }{ + {nil, nil, nil}, + {[]string{}, []string{}, nil}, + {nil, []string{"*.foo"}, []string{"*.foo"}}, + {[]string{"*.bar"}, []string{"a*.bar"}, []string{"*", "!*.bar", "a*.bar"}}, + {[]string{"*.bar"}, nil, []string{"*", "!*.bar"}}, + } + + for _, d := range data { + assert.EqualValues(t, d.expected, processIncludes(d.inc, d.exc)) + } +} + +func TestPickConfigFile(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().String("config", defaultConfigFile, "foo") + + cf, req := pickConfigFile(cmd) + assert.False(t, req) + assert.Equal(t, defaultConfigFile, cf) + + os.Setenv("GOMPLATE_CONFIG", "foo.yaml") + defer os.Unsetenv("GOMPLATE_CONFIG") + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "foo.yaml", cf) + + cmd.ParseFlags([]string{"--config", "config.file"}) + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "config.file", cf) + + os.Setenv("GOMPLATE_CONFIG", "ignored.yaml") + cf, req = pickConfigFile(cmd) + assert.True(t, req) + assert.Equal(t, "config.file", cf) +} + +func TestApplyEnvVars_PluginTimeout(t *testing.T) { + os.Setenv("GOMPLATE_PLUGIN_TIMEOUT", "bogus") + + ctx := context.TODO() + cfg := &config.Config{} + _, err := applyEnvVars(ctx, cfg) + assert.Error(t, err) + + cfg = &config.Config{ + PluginTimeout: 2 * time.Second, + } + expected := &config.Config{ + PluginTimeout: 2 * time.Second, + } + actual, err := applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_PLUGIN_TIMEOUT", "2s") + defer os.Unsetenv("GOMPLATE_PLUGIN_TIMEOUT") + + cfg = &config.Config{} + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + cfg = &config.Config{ + PluginTimeout: 100 * time.Millisecond, + } + expected = &config.Config{ + PluginTimeout: 100 * time.Millisecond, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + +} + +func TestApplyEnvVars_SuppressEmpty(t *testing.T) { + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "bogus") + defer os.Unsetenv("GOMPLATE_SUPPRESS_EMPTY") + + ctx := context.TODO() + cfg := &config.Config{} + expected := &config.Config{ + SuppressEmpty: false, + } + actual, err := applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "true") + + cfg = &config.Config{} + expected = &config.Config{ + SuppressEmpty: true, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + + os.Setenv("GOMPLATE_SUPPRESS_EMPTY", "false") + + cfg = &config.Config{ + SuppressEmpty: true, + } + expected = &config.Config{ + SuppressEmpty: true, + } + actual, err = applyEnvVars(ctx, cfg) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) +} diff --git a/cmd/gomplate/logger.go b/cmd/gomplate/logger.go index d40281a5..c58dc31d 100644 --- a/cmd/gomplate/logger.go +++ b/cmd/gomplate/logger.go @@ -12,7 +12,8 @@ import ( ) func initLogger(ctx context.Context) context.Context { - zerolog.SetGlobalLevel(zerolog.InfoLevel) + // default to warn level + zerolog.SetGlobalLevel(zerolog.WarnLevel) zerolog.DurationFieldUnit = time.Second stdlogger := log.With().Bool("stdlog", true).Logger() diff --git a/cmd/gomplate/main.go b/cmd/gomplate/main.go index 62c85fdb..110a0503 100644 --- a/cmd/gomplate/main.go +++ b/cmd/gomplate/main.go @@ -5,33 +5,26 @@ The gomplate command package main import ( - "bytes" "context" + "fmt" "os" "os/exec" "os/signal" "github.com/hairyhenderson/gomplate/v3" "github.com/hairyhenderson/gomplate/v3/env" + "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/hairyhenderson/gomplate/v3/version" "github.com/rs/zerolog" - "github.com/spf13/cobra" -) -var ( - verbose bool - execPipe bool - opts gomplate.Config - includes []string - - postRunInput *bytes.Buffer + "github.com/spf13/cobra" ) // postRunExec - if templating succeeds, the command following a '--' will be executed -func postRunExec(cmd *cobra.Command, args []string) error { +func postRunExec(ctx context.Context, cfg *config.Config) error { + args := cfg.PostExec if len(args) > 0 { - ctx := cmd.Context() log := zerolog.Ctx(ctx) log.Debug().Strs("args", args).Msg("running post-exec command") @@ -39,11 +32,7 @@ func postRunExec(cmd *cobra.Command, args []string) error { args = args[1:] // nolint: gosec c := exec.CommandContext(ctx, name, args...) - if execPipe { - c.Stdin = postRunInput - } else { - c.Stdin = os.Stdin - } + c.Stdin = cfg.PostExecInput c.Stderr = os.Stderr c.Stdout = os.Stdout @@ -73,28 +62,10 @@ func optionalExecArgs(cmd *cobra.Command, args []string) error { return cobra.NoArgs(cmd, args) } -// process --include flags - these are analogous to specifying --exclude '*', -// then the inverse of the --include options. -func processIncludes(includes, excludes []string) []string { - out := []string{} - // if any --includes are set, we start by excluding everything - if len(includes) > 0 { - out = make([]string, 1+len(includes)) - out[0] = "*" - } - for i, include := range includes { - // includes are just the opposite of an exclude - out[i+1] = "!" + include - } - out = append(out, excludes...) - return out -} - func newGomplateCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "gomplate", Short: "Process text files with Go templates", - PreRunE: validateOpts, Version: version.Version, RunE: func(cmd *cobra.Command, args []string) error { if v, _ := cmd.Flags().GetBool("verbose"); v { @@ -103,27 +74,33 @@ func newGomplateCmd() *cobra.Command { ctx := cmd.Context() log := zerolog.Ctx(ctx) - log.Debug().Msgf("%s version %s, build %s\nconfig is:\n%s", - cmd.Name(), version.Version, version.GitCommit, - &opts) + cfg, err := loadConfig(cmd, args) + if err != nil { + return err + } - // support --include - opts.ExcludeGlob = processIncludes(includes, opts.ExcludeGlob) + log.Debug().Msgf("starting %s", cmd.Name()) + log.Debug(). + Str("version", version.Version). + Str("build", version.GitCommit). + Msgf("config is:\n%v", cfg) - if execPipe { - postRunInput = &bytes.Buffer{} - opts.Out = postRunInput - } - err := gomplate.RunTemplates(&opts) + err = gomplate.RunTemplatesWithContext(ctx, cfg) cmd.SilenceErrors = true cmd.SilenceUsage = true - log.Debug().Msgf("rendered %d template(s) with %d error(s) in %v", - gomplate.Metrics.TemplatesProcessed, gomplate.Metrics.Errors, gomplate.Metrics.TotalRenderDuration) - return err + fmt.Fprintf(os.Stderr, "\n") + log.Debug().Int("templatesRendered", gomplate.Metrics.TemplatesProcessed). + Int("errors", gomplate.Metrics.Errors). + Dur("duration", gomplate.Metrics.TotalRenderDuration). + Msg("completed rendering") + + if err != nil { + return err + } + return postRunExec(ctx, cfg) }, - PostRunE: postRunExec, - Args: optionalExecArgs, + Args: optionalExecArgs, } return rootCmd } @@ -131,34 +108,36 @@ func newGomplateCmd() *cobra.Command { 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().BoolP("verbose", "V", false, "output extra information about what gomplate is doing") - command.Flags().BoolVarP(&verbose, "verbose", "V", false, "output extra information about what gomplate is doing") + command.Flags().String("config", defaultConfigFile, "config file (overridden by commandline flags)") } func main() { diff --git a/cmd/gomplate/main_test.go b/cmd/gomplate/main_test.go index e0d74e9d..06ab7d0f 100644 --- a/cmd/gomplate/main_test.go +++ b/cmd/gomplate/main_test.go @@ -1,23 +1 @@ package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestProcessIncludes(t *testing.T) { - data := []struct { - inc, exc, expected []string - }{ - {nil, nil, []string{}}, - {[]string{}, []string{}, []string{}}, - {nil, []string{"*.foo"}, []string{"*.foo"}}, - {[]string{"*.bar"}, []string{"a*.bar"}, []string{"*", "!*.bar", "a*.bar"}}, - {[]string{"*.bar"}, nil, []string{"*", "!*.bar"}}, - } - - for _, d := range data { - assert.EqualValues(t, d.expected, processIncludes(d.inc, d.exc)) - } -} diff --git a/cmd/gomplate/validate.go b/cmd/gomplate/validate.go deleted file mode 100644 index f4c73133..00000000 --- a/cmd/gomplate/validate.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/spf13/cobra" -) - -func notTogether(cmd *cobra.Command, flags ...string) error { - found := "" - for _, flag := range flags { - f := cmd.Flag(flag) - if f != nil && f.Changed { - if found != "" { - a := make([]string, len(flags)) - for i := range a { - a[i] = "--" + flags[i] - } - return fmt.Errorf("only one of these flags is supported at a time: %s", strings.Join(a, ", ")) - } - found = flag - } - } - return nil -} - -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 { - return fmt.Errorf("--%s must be set when --%s is set", right, left) - } - } - - return nil -} - -func validateOpts(cmd *cobra.Command, args []string) (err error) { - err = notTogether(cmd, "in", "file", "input-dir") - if err == nil { - 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)) - } - - if err == nil && cmd.Flag("exec-pipe").Changed && len(args) == 0 { - err = fmt.Errorf("--exec-pipe may only be used with a post-exec command after --") - } - - if err == nil { - err = mustTogether(cmd, "output-dir", "input-dir") - } - - if err == nil { - err = mustTogether(cmd, "output-map", "input-dir") - } - - return err -} diff --git a/cmd/gomplate/validate_test.go b/cmd/gomplate/validate_test.go deleted file mode 100644 index 54f4ba01..00000000 --- a/cmd/gomplate/validate_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" -) - -func TestValidateOpts(t *testing.T) { - err := validateOpts(parseFlags()) - assert.NoError(t, err) - - err = validateOpts(parseFlags("-i=foo", "-f", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-i=foo", "-o=bar", "-o=baz")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-i=foo", "--input-dir=baz")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--input-dir=foo", "-f=bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-dir=foo", "-o=bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-dir=foo")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--output-map", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags("-o", "foo", "--output-map", "bar")) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--output-dir", "foo", - "--output-map", "bar", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags("--exec-pipe")) - assert.Error(t, err) - - err = validateOpts(parseFlags("--exec-pipe", "--")) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--exec-pipe", - "--", "echo", "foo", - )) - assert.NoError(t, err) - - err = validateOpts(parseFlags( - "--exec-pipe", - "--out", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--exec-pipe", - "--output-dir", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--exec-pipe", - "--output-map", "foo", - "--", "echo", - )) - assert.Error(t, err) - - err = validateOpts(parseFlags( - "--input-dir", "in", - "--output-map", "bar", - )) - assert.NoError(t, err) -} - -func parseFlags(flags ...string) (cmd *cobra.Command, args []string) { - cmd = &cobra.Command{} - initFlags(cmd) - err := cmd.ParseFlags(flags) - if err != nil { - panic(err) - } - return cmd, cmd.Flags().Args() -} -- cgit v1.2.3