// Copyright 2024 Matthew Rich . All rights reserved. package command import ( _ "context" "encoding/json" "fmt" "errors" "gopkg.in/yaml.v3" "io" "log/slog" _ "net/url" "os" "os/exec" "strings" "text/template" "decl/internal/codec" "syscall" ) 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 CommandArg string 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:"-"` } 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() args, err := c.Template(value) if err != nil { return nil, err } if inputErr := c.SetInput(value); inputErr != nil { return nil, inputErr } cmd := exec.Command(c.Path, args...) c.SetCmdEnv(cmd) if c.stdin != nil { cmd.Stdin = c.stdin } slog.Info("execute() - cmd", "path", c.Path, "args", args) 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(cmd *exec.Cmd) { cmd.Env = append(os.Environ(), 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 { return err } else { 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 } func (c *CommandArg) UnmarshalValue(value string) error { *c = CommandArg(value) return nil } func (c *CommandArg) UnmarshalJSON(data []byte) error { var s string if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { return unmarshalRouteTypeErr } return c.UnmarshalValue(s) } func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error { var s string if err := value.Decode(&s); err != nil { return err } return c.UnmarshalValue(s) }