github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/internal/cmd/main.go (about) 1 package cmd 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "log/slog" 8 "os" 9 "os/exec" 10 "os/signal" 11 12 "github.com/hairyhenderson/gomplate/v4" 13 "github.com/hairyhenderson/gomplate/v4/env" 14 "github.com/hairyhenderson/gomplate/v4/internal/datafs" 15 "github.com/hairyhenderson/gomplate/v4/version" 16 17 "github.com/spf13/cobra" 18 ) 19 20 // postRunExec - if templating succeeds, the command following a '--' will be executed 21 func postRunExec(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error { 22 if len(args) > 0 { 23 slog.DebugContext(ctx, "running post-exec command", "args", args) 24 25 //nolint:govet 26 ctx, cancel := context.WithCancel(ctx) 27 defer cancel() 28 29 name := args[0] 30 args = args[1:] 31 32 c := exec.CommandContext(ctx, name, args...) 33 c.Stdin = stdin 34 c.Stderr = stderr 35 c.Stdout = stdout 36 37 // make sure all signals are propagated 38 sigs := make(chan os.Signal, 1) 39 signal.Notify(sigs) 40 41 err := c.Start() 42 if err != nil { 43 return err 44 } 45 46 go func() { 47 select { 48 case sig := <-sigs: 49 // Pass signals to the sub-process 50 if c.Process != nil { 51 _ = c.Process.Signal(sig) 52 } 53 case <-ctx.Done(): 54 } 55 }() 56 57 return c.Wait() 58 } 59 return nil 60 } 61 62 // optionalExecArgs - implements cobra.PositionalArgs. Allows extra args following 63 // a '--', but not otherwise. 64 func optionalExecArgs(cmd *cobra.Command, args []string) error { 65 if cmd.ArgsLenAtDash() == 0 { 66 return nil 67 } 68 return cobra.NoArgs(cmd, args) 69 } 70 71 // NewGomplateCmd - 72 func NewGomplateCmd(stderr io.Writer) *cobra.Command { 73 rootCmd := &cobra.Command{ 74 Use: "gomplate", 75 Short: "Process text files with Go templates", 76 Version: version.Version, 77 RunE: func(cmd *cobra.Command, args []string) error { 78 level := slog.LevelWarn 79 if v, _ := cmd.Flags().GetBool("verbose"); v { 80 level = slog.LevelDebug 81 } 82 initLogger(stderr, level) 83 84 ctx := cmd.Context() 85 86 cfg, err := loadConfig(ctx, cmd, args) 87 if err != nil { 88 return err 89 } 90 91 if cfg.Experimental { 92 slog.SetDefault(slog.With("experimental", true)) 93 slog.InfoContext(ctx, "experimental functions and features enabled!") 94 95 ctx = gomplate.SetExperimental(ctx) 96 } 97 98 slog.DebugContext(ctx, fmt.Sprintf("starting %s", cmd.Name())) 99 slog.DebugContext(ctx, fmt.Sprintf("config is:\n%v", cfg), 100 slog.String("version", version.Version), 101 slog.String("build", version.GitCommit), 102 ) 103 104 err = gomplate.Run(ctx, cfg) 105 cmd.SilenceErrors = true 106 cmd.SilenceUsage = true 107 108 slog.DebugContext(ctx, "completed rendering", 109 slog.Int("templatesRendered", gomplate.Metrics.TemplatesProcessed), 110 slog.Int("errors", gomplate.Metrics.Errors), 111 slog.Duration("duration", gomplate.Metrics.TotalRenderDuration)) 112 113 if err != nil { 114 return err 115 } 116 return postRunExec(ctx, cfg.PostExec, cfg.PostExecInput, cmd.OutOrStdout(), cmd.ErrOrStderr()) 117 }, 118 Args: optionalExecArgs, 119 } 120 return rootCmd 121 } 122 123 // InitFlags - initialize the various flags and help strings on the command. 124 // Note that the defaults set here are ignored, and instead defaults from 125 // *config.Config's ApplyDefaults method are used instead. Changes here must be 126 // reflected there as well. 127 func InitFlags(command *cobra.Command) { 128 command.Flags().SortFlags = false 129 130 command.Flags().StringSliceP("datasource", "d", nil, "`datasource` in alias=URL form. Specify multiple times to add multiple sources.") 131 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.") 132 133 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.") 134 135 command.Flags().StringSlice("plugin", nil, "plug in an external command as a function in name=path form. Can be specified multiple times") 136 137 command.Flags().StringSliceP("file", "f", []string{"-"}, "Template `file` to process. Omit to use standard input, or use --in or --input-dir") 138 command.Flags().StringP("in", "i", "", "Template `string` to process (alternative to --file and --input-dir)") 139 command.Flags().String("input-dir", "", "`directory` which is examined recursively for templates (alternative to --file and --in)") 140 141 command.Flags().StringSlice("exclude", []string{}, "glob of files to not parse") 142 command.Flags().StringSlice("exclude-processing", []string{}, "glob of files to be copied without parsing") 143 command.Flags().StringSlice("include", []string{}, "glob of files to parse") 144 145 command.Flags().StringSliceP("out", "o", []string{"-"}, "output `file` name. Omit to use standard output.") 146 command.Flags().StringSliceP("template", "t", []string{}, "Additional template file(s)") 147 command.Flags().String("output-dir", ".", "`directory` to store the processed templates. Only used for --input-dir") 148 command.Flags().String("output-map", "", "Template `string` to map the input file to an output path") 149 command.Flags().String("chmod", "", "set the mode for output file(s). Omit to inherit from input file(s)") 150 151 command.Flags().Bool("exec-pipe", false, "pipe the output to the post-run exec command") 152 153 // these are only set for the help output - these defaults aren't actually used 154 ldDefault := env.Getenv("GOMPLATE_LEFT_DELIM", "{{") 155 rdDefault := env.Getenv("GOMPLATE_RIGHT_DELIM", "}}") 156 command.Flags().String("left-delim", ldDefault, "override the default left-`delimiter` [$GOMPLATE_LEFT_DELIM]") 157 command.Flags().String("right-delim", rdDefault, "override the default right-`delimiter` [$GOMPLATE_RIGHT_DELIM]") 158 159 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>") 160 161 command.Flags().Bool("experimental", false, "enable experimental features [$GOMPLATE_EXPERIMENTAL]") 162 163 command.Flags().BoolP("verbose", "V", false, "output extra information about what gomplate is doing") 164 165 command.Flags().String("config", defaultConfigFile, "config file (overridden by commandline flags)") 166 } 167 168 // Main - 169 func Main(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error { 170 // inject default filesystem provider if it hasn't already been provided in 171 // the context 172 if datafs.FSProviderFromContext(ctx) == nil { 173 ctx = datafs.ContextWithFSProvider(ctx, gomplate.DefaultFSProvider) 174 } 175 176 command := NewGomplateCmd(stderr) 177 InitFlags(command) 178 command.SetArgs(args) 179 command.SetIn(stdin) 180 command.SetOut(stdout) 181 command.SetErr(stderr) 182 183 err := command.ExecuteContext(ctx) 184 if err != nil { 185 slog.Error("", slog.Any("err", err)) 186 } 187 return err 188 }