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