From 6be7f2b8988f8428c44dbd7ce8ac20a0856d0dc7 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Fri, 17 May 2024 09:39:33 +0200 Subject: [PATCH] refactor: rework plugin cmd (#82) BREAKING CHANGE: `types.Cmd` was moved to `exec.Cmd` and the `Private` field from the struct was removed. The filed `Trace` is now a bool field instead of a bool pointer, and the helper method `SetTrace` was also removed. BREAKING CHANGE: The method `trace.Cmd` was removed, please use the `Trace` field of `exec.Cmd`. --- exec/command.go | 58 ++++++++++++++++++++++++++ {types => exec}/command_test.go | 73 ++++++++------------------------- trace/http.go | 8 ---- types/command.go | 67 ------------------------------ 4 files changed, 75 insertions(+), 131 deletions(-) create mode 100644 exec/command.go rename {types => exec}/command_test.go (63%) delete mode 100644 types/command.go diff --git a/exec/command.go b/exec/command.go new file mode 100644 index 0000000..86dc649 --- /dev/null +++ b/exec/command.go @@ -0,0 +1,58 @@ +package exec + +import ( + "fmt" + "io" + "os" + "os/exec" + "strings" + + "golang.org/x/sys/execabs" +) + +// Cmd represents a command to be executed, with options to control its behavior. +// The Cmd struct embeds the standard library's exec.Cmd, adding additional fields +// to control the command's output and tracing. +type Cmd struct { + *exec.Cmd + Trace bool // Print composed command before execution. + TraceWriter io.Writer // Where to write the trace output. +} + +// Run runs the command and waits for it to complete. +// If there is an error starting the command, it is returned. +// Otherwise, the command is waited for and its exit status is returned. +func (c *Cmd) Run() error { + if c.Trace { + fmt.Fprintf(c.TraceWriter, "+ %s\n", strings.Join(c.Args, " ")) + } + + if err := c.Start(); err != nil { + return err + } + + return c.Wait() +} + +// Command creates a new Cmd struct with the given name and arguments. It looks up the +// absolute path of the executable using execabs.LookPath, and sets up the Cmd with +// the necessary environment and output streams. The Cmd is configured to trace +// the command execution by setting Trace to true and TraceWriter to os.Stdout. +func Command(name string, arg ...string) (*Cmd, error) { + abs, err := execabs.LookPath(name) + if err != nil { + return nil, fmt.Errorf("could not find executable %q: %w", name, err) + } + + cmd := &Cmd{ + Cmd: execabs.Command(abs, arg...), + Trace: true, + TraceWriter: os.Stdout, + } + + cmd.Env = os.Environ() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd, nil +} diff --git a/types/command_test.go b/exec/command_test.go similarity index 63% rename from types/command_test.go rename to exec/command_test.go index df07da9..d17697c 100644 --- a/types/command_test.go +++ b/exec/command_test.go @@ -1,4 +1,4 @@ -package types +package exec import ( "bytes" @@ -20,7 +20,7 @@ func TestCmdRun(t *testing.T) { { name: "trace enabled", cmd: &Cmd{ - Trace: boolPtr(true), + Trace: true, Cmd: &exec.Cmd{ Path: "/usr/bin/echo", Args: []string{"echo", "hello"}, @@ -30,19 +30,20 @@ func TestCmdRun(t *testing.T) { wantStdout: "hello\n", }, { - name: "private output", + name: "trace disabled", cmd: &Cmd{ - Private: true, + Trace: false, Cmd: &exec.Cmd{ Path: "/usr/bin/echo", Args: []string{"echo", "hello"}, }, }, - wantTrace: "+ echo hello\n", + wantStdout: "hello\n", }, { name: "custom env", cmd: &Cmd{ + Trace: true, Cmd: &exec.Cmd{ Path: "/bin/sh", Args: []string{"sh", "-c", "echo $TEST"}, @@ -52,21 +53,10 @@ func TestCmdRun(t *testing.T) { wantTrace: "+ sh -c echo $TEST\n", wantStdout: "1\n", }, - { - name: "custom stdout", - cmd: &Cmd{ - Cmd: &exec.Cmd{ - Path: "/bin/sh", - Args: []string{"sh", "-c", "echo hello"}, - Stdout: new(bytes.Buffer), - }, - }, - wantTrace: "+ sh -c echo hello\n", - wantStdout: "hello\n", - }, { name: "custom stderr", cmd: &Cmd{ + Trace: true, Cmd: &exec.Cmd{ Path: "/bin/sh", Args: []string{"sh", "-c", "echo error >&2"}, @@ -76,6 +66,16 @@ func TestCmdRun(t *testing.T) { wantTrace: "+ sh -c echo error >&2\n", wantStderr: "error\n", }, + { + name: "error", + cmd: &Cmd{ + Trace: true, + Cmd: &exec.Cmd{ + Path: "/invalid/path", + }, + }, + wantErr: true, + }, } for _, tt := range tests { @@ -101,42 +101,3 @@ func TestCmdRun(t *testing.T) { }) } } - -func TestCmdSetTrace(t *testing.T) { - tests := []struct { - name string - cmd *Cmd - trace bool - expected *bool - }{ - { - name: "set trace to true", - cmd: &Cmd{}, - trace: true, - expected: boolPtr(true), - }, - { - name: "set trace to false", - cmd: &Cmd{}, - trace: false, - expected: boolPtr(false), - }, - { - name: "overwrite existing trace value", - cmd: &Cmd{Trace: boolPtr(true)}, - trace: false, - expected: boolPtr(false), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.cmd.SetTrace(tt.trace) - assert.Equal(t, tt.expected, tt.cmd.Trace) - }) - } -} - -func boolPtr(b bool) *bool { - return &b -} diff --git a/trace/http.go b/trace/http.go index 0624922..2f2a065 100644 --- a/trace/http.go +++ b/trace/http.go @@ -12,9 +12,6 @@ import ( "fmt" "net/http/httptrace" "net/textproto" - "os" - "os/exec" - "strings" "github.com/rs/zerolog/log" ) @@ -120,8 +117,3 @@ func HTTP(ctx context.Context) context.Context { }, }) } - -// Cmd prints the executed command to stdout. -func Cmd(cmd *exec.Cmd) { - fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) -} diff --git a/types/command.go b/types/command.go deleted file mode 100644 index f955bbf..0000000 --- a/types/command.go +++ /dev/null @@ -1,67 +0,0 @@ -package types - -import ( - "fmt" - "io" - "os" - "strings" - - "golang.org/x/sys/execabs" -) - -// Cmd represents a command to be executed. It extends the execabs.Cmd struct -// and adds fields for controlling the command's execution, such as whether -// it should be executed in private mode (with output discarded) and whether -// its execution should be traced. -type Cmd struct { - *execabs.Cmd - Private bool - Trace *bool - TraceWriter io.Writer -} - -// Run executes the command and waits for it to complete. -// If the Trace field is nil, it is set to true. -// The Env, Stdout, and Stderr fields are set to their default values if they are nil. -// If the Private field is true, the Stdout field is set to io.Discard. -// If the Trace field is true, the command line is printed to Stdout. -func (c *Cmd) Run() error { - if c.Trace == nil { - c.SetTrace(true) - } - - if c.Env == nil { - c.Env = os.Environ() - } - - if c.Stdout == nil { - c.Stdout = os.Stdout - } - - if c.Stderr == nil { - c.Stderr = os.Stderr - } - - if c.Private { - c.Stdout = io.Discard - } - - if c.TraceWriter == nil { - c.TraceWriter = os.Stdout - } - - if *c.Trace { - fmt.Fprintf(c.TraceWriter, "+ %s\n", strings.Join(c.Args, " ")) - } - - if err := c.Start(); err != nil { - return err - } - - return c.Wait() -} - -// SetTrace sets the Trace field of the Cmd to the provided boolean value. -func (c *Cmd) SetTrace(trace bool) { - c.Trace = &trace -}