summaryrefslogtreecommitdiff
path: root/internal/cmd/main.go
blob: 7a3dea98fb5f38f9e714b6015e13acbe51725958 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package cmd

import (
	"context"
	"io"
	"os"
	"os/exec"
	"os/signal"

	"github.com/hairyhenderson/gomplate/v4"
	"github.com/hairyhenderson/gomplate/v4/env"
	"github.com/hairyhenderson/gomplate/v4/internal/datafs"
	"github.com/hairyhenderson/gomplate/v4/version"

	"github.com/rs/zerolog"
	"github.com/spf13/cobra"
)

// postRunExec - if templating succeeds, the command following a '--' will be executed
func postRunExec(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
	if len(args) > 0 {
		log := zerolog.Ctx(ctx)
		log.Debug().Strs("args", args).Msg("running post-exec command")

		//nolint:govet
		ctx, cancel := context.WithCancel(ctx)
		defer cancel()

		name := args[0]
		args = args[1:]

		c := exec.CommandContext(ctx, name, args...)
		c.Stdin = stdin
		c.Stderr = stderr
		c.Stdout = stdout

		// make sure all signals are propagated
		sigs := make(chan os.Signal, 1)
		signal.Notify(sigs)

		err := c.Start()
		if err != nil {
			return err
		}

		go func() {
			select {
			case sig := <-sigs:
				// Pass signals to the sub-process
				if c.Process != nil {
					_ = c.Process.Signal(sig)
				}
			case <-ctx.Done():
			}
		}()

		return c.Wait()
	}
	return nil
}

// optionalExecArgs - implements cobra.PositionalArgs. Allows extra args following
// a '--', but not otherwise.
func optionalExecArgs(cmd *cobra.Command, args []string) error {
	if cmd.ArgsLenAtDash() == 0 {
		return nil
	}
	return cobra.NoArgs(cmd, args)
}

// NewGomplateCmd -
func NewGomplateCmd() *cobra.Command {
	rootCmd := &cobra.Command{
		Use:     "gomplate",
		Short:   "Process text files with Go templates",
		Version: version.Version,
		RunE: func(cmd *cobra.Command, args []string) error {
			if v, _ := cmd.Flags().GetBool("verbose"); v {
				zerolog.SetGlobalLevel(zerolog.DebugLevel)
			}
			ctx := cmd.Context()
			log := zerolog.Ctx(ctx)

			cfg, err := loadConfig(ctx, cmd, args)
			if err != nil {
				return err
			}

			if cfg.Experimental {
				log.UpdateContext(func(c zerolog.Context) zerolog.Context {
					return c.Bool("experimental", true)
				})
				log.Info().Msg("experimental functions and features enabled!")

				ctx = gomplate.SetExperimental(ctx)
			}

			log.Debug().Msgf("starting %s", cmd.Name())
			log.Debug().
				Str("version", version.Version).
				Str("build", version.GitCommit).
				Msgf("config is:\n%v", cfg)

			err = gomplate.Run(ctx, cfg)
			cmd.SilenceErrors = true
			cmd.SilenceUsage = true

			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.PostExec, cfg.PostExecInput, cmd.OutOrStdout(), cmd.ErrOrStderr())
		},
		Args: optionalExecArgs,
	}
	return rootCmd
}

// InitFlags - initialize the various flags and help strings on the command.
// Note that the defaults set here are ignored, and instead defaults from
// *config.Config's ApplyDefaults method are used instead. Changes here must be
// reflected there as well.
func InitFlags(command *cobra.Command) {
	command.Flags().SortFlags = false

	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().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().StringSlice("plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times")

	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().StringSlice("exclude", []string{}, "glob of files to not parse")
	command.Flags().StringSlice("include", []string{}, "glob of files to parse")

	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().Bool("exec-pipe", false, "pipe the output to the post-run exec command")

	// these are only set for the help output - these defaults aren't actually used
	ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{")
	rdDefault := env.Getenv("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().String("missing-key", "error", "Control the behavior during execution if a map is indexed with a key that is not present in the map. error (default) - return an error, zero - fallback to zero value, default/invalid - print <no value>")

	command.Flags().Bool("experimental", false, "enable experimental features [$GOMPLATE_EXPERIMENTAL]")

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

	command.Flags().String("config", defaultConfigFile, "config file (overridden by commandline flags)")
}

// Main -
func Main(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
	ctx = initLogger(ctx, stderr)

	// inject default filesystem provider if it hasn't already been provided in
	// the context
	if datafs.FSProviderFromContext(ctx) == nil {
		ctx = datafs.ContextWithFSProvider(ctx, gomplate.DefaultFSProvider)
	}

	command := NewGomplateCmd()
	InitFlags(command)
	command.SetArgs(args)
	command.SetIn(stdin)
	command.SetOut(stdout)
	command.SetErr(stderr)

	err := command.ExecuteContext(ctx)
	if err != nil {
		log := zerolog.Ctx(ctx)
		log.Error().Err(err).Send()
	}
	return err
}