github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/internal/run/run.go (about)

     1  package run
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  )
    12  
    13  // Runnable is typically an exec.Cmd or its stub in tests
    14  type Runnable interface {
    15  	Output() ([]byte, error)
    16  	Run() error
    17  }
    18  
    19  // PrepareCmd extends exec.Cmd with extra error reporting features and provides a
    20  // hook to stub command execution in tests
    21  var PrepareCmd = func(cmd *exec.Cmd) Runnable {
    22  	return &cmdWithStderr{cmd}
    23  }
    24  
    25  // cmdWithStderr augments exec.Cmd by adding stderr to the error message
    26  type cmdWithStderr struct {
    27  	*exec.Cmd
    28  }
    29  
    30  func (c cmdWithStderr) Output() ([]byte, error) {
    31  	if os.Getenv("DEBUG") != "" {
    32  		_ = printArgs(os.Stderr, c.Cmd.Args)
    33  	}
    34  	if c.Cmd.Stderr != nil {
    35  		return c.Cmd.Output()
    36  	}
    37  	errStream := &bytes.Buffer{}
    38  	c.Cmd.Stderr = errStream
    39  	out, err := c.Cmd.Output()
    40  	if err != nil {
    41  		err = &CmdError{errStream, c.Cmd.Args, err}
    42  	}
    43  	return out, err
    44  }
    45  
    46  func (c cmdWithStderr) Run() error {
    47  	if os.Getenv("DEBUG") != "" {
    48  		_ = printArgs(os.Stderr, c.Cmd.Args)
    49  	}
    50  	if c.Cmd.Stderr != nil {
    51  		return c.Cmd.Run()
    52  	}
    53  	errStream := &bytes.Buffer{}
    54  	c.Cmd.Stderr = errStream
    55  	err := c.Cmd.Run()
    56  	if err != nil {
    57  		err = &CmdError{errStream, c.Cmd.Args, err}
    58  	}
    59  	return err
    60  }
    61  
    62  // CmdError provides more visibility into why an exec.Cmd had failed
    63  type CmdError struct {
    64  	Stderr *bytes.Buffer
    65  	Args   []string
    66  	Err    error
    67  }
    68  
    69  func (e CmdError) Error() string {
    70  	msg := e.Stderr.String()
    71  	if msg != "" && !strings.HasSuffix(msg, "\n") {
    72  		msg += "\n"
    73  	}
    74  	return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err)
    75  }
    76  
    77  func printArgs(w io.Writer, args []string) error {
    78  	if len(args) > 0 {
    79  		// print commands, but omit the full path to an executable
    80  		args = append([]string{filepath.Base(args[0])}, args[1:]...)
    81  	}
    82  	_, err := fmt.Fprintf(w, "%v\n", args)
    83  	return err
    84  }