// Copyright 2024 Matthew Rich . All rights reserved. package command import ( _ "context" "fmt" "errors" "io" "log/slog" _ "net/url" "os/exec" "strings" "text/template" "decl/internal/codec" "syscall" ) // A resource that implements the ExecProvider interface can be used as an exec target. type CommandProvider interface { Start() error Wait() error SetCmdEnv([]string) SetStdin(io.Reader) StdinPipe() (io.WriteCloser, error) StdoutPipe() (io.ReadCloser, error) StderrPipe() (io.ReadCloser, error) } var ErrUnknownCommand error = errors.New("Unable to find command in path") type CommandExecutor func(value any) ([]byte, error) type CommandExtractAttributes func(output []byte, target any) error type CommandExists func() error type CommandInput string type Command struct { Path string `json:"path" yaml:"path"` Args []CommandArg `json:"args" yaml:"args"` Env []string `json:"env" yaml:"env"` Split bool `json:"split" yaml:"split"` FailOnError bool `json:"failonerror" yaml:"failonerror"` StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"` ExitCode int `json:"exitcode,omitempty" yaml:"exitcode,omitempty"` Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"` Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"` Executor CommandExecutor `json:"-" yaml:"-"` Extractor CommandExtractAttributes `json:"-" yaml:"-"` CommandExists CommandExists `json:"-" yaml:"-"` Input CommandInput `json:"-" yaml:"-"` stdin io.Reader `json:"-" yaml:"-"` TargetRef CommandTargetRef `json:"targetref,omitempty" yaml:"targetref,omitempty"` execHandle CommandProvider `json:"-" yaml:"-"` } func NewCommand() *Command { c := &Command{ Split: true, FailOnError: true } c.Defaults() return c } func (c *Command) ClearOutput() { c.Stdout = "" c.Stderr = "" c.ExitCode = 0 } func (c *Command) Defaults() { c.ClearOutput() c.Split = true c.FailOnError = true c.CommandExists = func() error { if _, err := exec.LookPath(c.Path); err != nil { return fmt.Errorf("%w - %w", ErrUnknownCommand, err) } return nil } c.Executor = func(value any) ([]byte, error) { c.ClearOutput() c.execHandle = c.TargetRef.Provider(c, value) if inputErr := c.SetInput(value); inputErr != nil { return nil, inputErr } c.SetCmdEnv() cmd := c.execHandle if c.stdin != nil { cmd.SetStdin(c.stdin) } output, stdoutPipeErr := cmd.StdoutPipe() if stdoutPipeErr != nil { return nil, stdoutPipeErr } stderr, pipeErr := cmd.StderrPipe() if pipeErr != nil { return nil, pipeErr } if startErr := cmd.Start(); startErr != nil { return nil, startErr } slog.Info("execute() - start", "cmd", cmd) stdOutOutput, _ := io.ReadAll(output) stdErrOutput, _ := io.ReadAll(stderr) if len(stdOutOutput) > 100 { slog.Info("execute() - io", "stdout", string(stdOutOutput[:100]), "stderr", string(stdErrOutput)) } else { slog.Info("execute() - io", "stdout", string(stdOutOutput), "stderr", string(stdErrOutput)) } waitErr := cmd.Wait() c.Stdout = string(stdOutOutput) c.Stderr = string(stdErrOutput) c.ExitCode = c.GetExitCodeFromError(waitErr) /* if len(stdOutOutput) > 100 { slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput)) } else { slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput)) } */ if len(stdErrOutput) > 0 && c.FailOnError { return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput)) } return stdOutOutput, waitErr } } func (c *Command) Load(r io.Reader) error { return codec.NewYAMLDecoder(r).Decode(c) } func (c *Command) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c) } func (c *Command) SetCmdEnv() { c.execHandle.SetCmdEnv(c.Env) } func (c *Command) SetStdinReader(r io.Reader) { if c.StdinAvailable { c.stdin = r } } func (c *Command) Exists() bool { return c.CommandExists() == nil } func (c *Command) GetExitCodeFromError(err error) (ec int) { if exitErr, ok := err.(*exec.ExitError); ok { if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { return status.ExitStatus() } } return } func (c *Command) Template(value any) ([]string, error) { var args []string = make([]string, 0, len(c.Args) * 2) for i, arg := range c.Args { var commandLineArg strings.Builder err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value) if err != nil { return nil, err } if commandLineArg.Len() > 0 { var splitArg []string if c.Split { splitArg = strings.Split(commandLineArg.String(), " ") } else { splitArg = []string{commandLineArg.String()} } slog.Info("Template()", "split", splitArg, "len", len(splitArg)) args = append(args, splitArg...) } } slog.Info("Template()", "Args", c.Args, "lencargs", len(c.Args), "args", args, "lenargs", len(args), "value", value) return args, nil } func (c *Command) Execute(value any) ([]byte, error) { return c.Executor(value) } func (c *Command) SetInput(value any) error { if len(c.Input) > 0 { if r, err := c.Input.Template(value); err != nil { slog.Info("Command.SetInput", "input", r.String(), "error", err) return err } else { slog.Info("Command.SetInput", "input", r.String()) c.SetStdinReader(strings.NewReader(r.String())) } } return nil } func (c *CommandInput) Template(value any) (result strings.Builder, err error) { err = template.Must(template.New("commandInput").Parse(string(*c))).Execute(&result, value) return }