diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9592b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +.idea/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..68f594d --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,76 @@ +version: "2" +run: + timeout: 5m +linters: + enable: + # region General + + # Prevent improper directives in go.mod. + - gomoddirectives + # Prevent improper nolint directives. + - nolintlint + + # endregion + + + # region Code Quality and Comments + + # Inspect source code for potential security problems. This check has a fairly high false positive rate, + # comment with // nolint:gosec where not relevant. + - gosec + # Complain about deeply nested if cases. + - nestif + # Prevent naked returns in long functions. + - nakedret + # Make Go code more readable. + - gocritic + # Check if comments end in a period. This helps prevent incomplete comment lines, such as half-written sentences. + - godot + # Complain about comments as these indicate incomplete code. + - godox + # Keep the cyclomatic complexity of functions to a reasonable level. + - gocyclo + # Complain about cognitive complexity of functions. + - gocognit + # Find repeated strings that could be converted into constants. + - goconst + # Complain about unnecessary type conversions. + - unconvert + # Complain about unused parameters. These should be replaced with underscores. + - unparam + # Check for non-ASCII identifiers. + - asciicheck + # Check for HTTP response body being closed. Sometimes, you may need to disable this using // nolint:bodyclose. + - bodyclose + # Check for duplicate code. You may want to disable this with // nolint:dupl if the source code is the same, but + # legitimately exists for different reasons. + - dupl + # Prevent dogsledding (mass-ignoring return values). This typically indicates missing error handling. + - dogsled + # Enforce consistent import aliases across all files. + - importas + # Prevent faulty error checks. + - nilerr + # Prevent direct error checks that won't work with wrapped errors. + - errorlint + # Find slice usage that could potentially be preallocated. + - prealloc + # Check for improper duration handling. + - durationcheck + # Enforce tests being in the _test package. + - testpackage + # Enforce line length limits. + - lll + + # endregion + settings: + govet: + enable-all: true + disable: + # We don't care about variable shadowing. + - shadow + - fieldalignment +formatters: + enable: + # Make code properly formatted. + - gofmt diff --git a/exex.go b/exex.go index 2cd3682..355af2b 100644 --- a/exex.go +++ b/exex.go @@ -1,6 +1,6 @@ -// exex provides a custom Cmd type that wraps exec.Cmd in a way that -// it will always capture standard error stream if execution fails -// with an exec.ExitError. +// Package exex provides a custom Cmd type that wraps exec.Cmd in a +// way that it will always capture standard error stream if execution +// fails with an exec.ExitError. // // The standard library exec package contains a very useful API to // execute commands, however, the exec.Cmd.Run and exec.Cmd.Output @@ -26,6 +26,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "os/exec" ) @@ -45,7 +46,7 @@ type Cmd exec.Cmd // // Refer to the exec.Command documentation for additional information. func Command(name string, args ...string) *Cmd { - return (*Cmd)(exec.Command(name, args...)) + return (*Cmd)(exec.Command(name, args...)) //nolint:gosec // Variable args intentional. } // CommandContext is like Command but the Cmd is associated with a @@ -53,7 +54,7 @@ func Command(name string, args ...string) *Cmd { // // Refer to the exec.Command documentation for additional information. func CommandContext(ctx context.Context, name string, args ...string) *Cmd { - return (*Cmd)(exec.CommandContext(ctx, name, args...)) + return (*Cmd)(exec.CommandContext(ctx, name, args...)) //nolint:gosec // Variable args intentional. } // Run starts the command and waits for it to end. @@ -132,7 +133,7 @@ func (c *Cmd) StdinPipe() (io.WriteCloser, error) { return (*exec.Cmd)(c).StdinP // standard output when the command starts. func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { return (*exec.Cmd)(c).StdoutPipe() } -// String returns a human-readable description of c +// String returns a human-readable description of c. func (c *Cmd) String() string { return (*exec.Cmd)(c).String() } // RunCommand wraps an *exec.Cmd into a Cmd and returns the result of @@ -152,10 +153,26 @@ func RunContext(ctx context.Context, cmd string, args ...string) error { return CommandContext(ctx, cmd, args...).Run() } -// Error is a type alias for exec.Error +// CommandError returns the error with the stderr log appended, +// if the error is a command exit error, and the stderr log exists. +func CommandError(err error, errMsg string) error { + var exErr *ExitError + if err != nil { + if !errors.As(err, &exErr) { + return fmt.Errorf("error converting error to exex.ExitError") + } + if exErr.Stderr == nil { + return fmt.Errorf("%s (%w)", errMsg, err) + } + return fmt.Errorf("%s (%w)\n%s", errMsg, err, exErr.Stderr) + } + return nil +} + +// Error is a type alias for exec.Error. type Error = exec.Error -// ExitError is a type alias for exec.ExitError +// ExitError is a type alias for exec.ExitError. type ExitError = exec.ExitError // ErrNotFound is an alias for exec.ErrNotFound, the error resulting diff --git a/go.mod b/go.mod index 9b0351c..eb091f7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/inkel/exex -go 1.17 +go 1.25