diff --git a/internal/command/command.go b/internal/command/command.go index d6d0ed5..6b69f71 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -16,6 +16,7 @@ import ( "strings" "text/template" "decl/internal/codec" + "syscall" ) var ErrUnknownCommand error = errors.New("Unable to find command in path") @@ -26,17 +27,23 @@ 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"` - Executor CommandExecutor `json:"-" yaml:"-"` - Extractor CommandExtractAttributes `json:"-" yaml:"-"` - CommandExists CommandExists `json:"-" yaml:"-"` - stdin io.Reader `json:"-" yaml:"-"` + 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 { @@ -45,7 +52,14 @@ func NewCommand() *Command { 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 { @@ -55,10 +69,14 @@ func (c *Command) Defaults() { 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) @@ -91,6 +109,10 @@ func (c *Command) Defaults() { } 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 { @@ -126,6 +148,15 @@ 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 { @@ -154,6 +185,22 @@ 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 diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 73b7925..fd08de1 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -81,3 +81,22 @@ stdinavailable: true assert.Nil(t, err) assert.Equal(t, expected, string(out)) } + +func TestCommandExitCode(t *testing.T) { + c := NewCommand() + assert.NotNil(t, c) + decl := ` +path: ls +args: +- "amissingfile" +` + + assert.Nil(t, c.LoadDecl(decl)) + assert.Equal(t, "ls", c.Path) + + out, err := c.Execute(nil) + assert.NotNil(t, err) + assert.Greater(t, c.ExitCode, 0) + assert.Equal(t, string(out), c.Stdout) + assert.Equal(t, string("ls: amissingfile: No such file or directory\n"), c.Stderr) +}