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  }