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 }