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  }