oss.indeed.com/go/go-opine@v1.3.0/internal/run/run.go (about)

     1  // Package run provides utilities for executing external programs.
     2  package run
     3  
     4  import (
     5  	"bytes"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"strings"
    10  
    11  	"oss.indeed.com/go/go-opine/internal/printing"
    12  )
    13  
    14  const (
    15  	outPrefix = "  > "
    16  	errPrefix = "  ! "
    17  )
    18  
    19  type cmdinfo struct {
    20  	cmd       *exec.Cmd
    21  	log       *printing.LogWriter
    22  	logStdout bool
    23  	logStderr bool
    24  }
    25  
    26  // Cmd runs the provided command (with the provided args) and returns the
    27  // stdout and stderr. A non-nil error will be returned when the command
    28  // exit code is non-zero.
    29  //
    30  // By default Cmd will write the following to os.Stdout:
    31  //   * A message before running the command that indicates the command that
    32  //     will be run, including the args.
    33  //   * The stdout of the command. Each line will be prefixed with "  > ". Note
    34  //     that the returned stdout will not have this prefix.
    35  //   * The stderr of the command. Each line will be prefixed with "  ! ". Note
    36  //     that the returned stderr will not have this prefix.
    37  //   * A message when the command completes indicating whether it completed
    38  //     successfully or not, and what exit code it returned in the case of a
    39  //     failure.
    40  //
    41  // The above output can be configured as follows:
    42  //   * Use Log to change the output destination (from os.Stdout) to the
    43  //     provided io.Writer. Use io.Discard if you do not want anything
    44  //     printed.
    45  //   * Use SuppressStdout to prevent the stdout from being written. It will
    46  //     still be returned.
    47  //   * Use SuppressStderr to prevent the stderr from being written. It will
    48  //     still be returned.
    49  func Cmd(command string, args []string, opts ...Option) (string, string, error) {
    50  	sp := cmdinfo{
    51  		cmd:       exec.Command(command, args...),
    52  		log:       printing.NewLogWriter(os.Stdout),
    53  		logStdout: true,
    54  		logStderr: true,
    55  	}
    56  	for _, opt := range opts {
    57  		opt(&sp)
    58  	}
    59  
    60  	var stdout, stderr bytes.Buffer
    61  
    62  	stdouts := append(make([]io.Writer, 0, 3), &stdout)
    63  	if sp.cmd.Stdout != nil {
    64  		stdouts = append(stdouts, sp.cmd.Stdout)
    65  	}
    66  	if sp.logStdout {
    67  		stdouts = append(stdouts, printing.NewLinePrefixWriter(sp.log, outPrefix))
    68  	}
    69  	sp.cmd.Stdout = io.MultiWriter(stdouts...)
    70  
    71  	stderrs := append(make([]io.Writer, 0, 2), &stderr)
    72  	if sp.logStderr {
    73  		stderrs = append(stderrs, printing.NewLinePrefixWriter(sp.log, errPrefix))
    74  	}
    75  	sp.cmd.Stderr = io.MultiWriter(stderrs...)
    76  
    77  	sp.log.Logf("Running %q with args %q...", sp.cmd.Path, sp.cmd.Args[1:])
    78  	err := sp.cmd.Run()
    79  	if err != nil {
    80  		sp.log.Logf("Command failed: %v", err)
    81  	} else {
    82  		sp.log.Logf("Command completed successfully")
    83  	}
    84  
    85  	return stdout.String(), stderr.String(), err
    86  }
    87  
    88  // Args returns the provided variadic args as a slice. This allows
    89  // you to use Cmd like this:
    90  //
    91  //     run.Cmd("echo", run.Args("hello", "world"))
    92  //
    93  // The above is arguably slightly more readable than using a []string
    94  // directly:
    95  //
    96  //     run.Cmd("echo", []string{"hello", "world"})
    97  func Args(args ...string) []string {
    98  	return args
    99  }
   100  
   101  // Option alters the way Cmd runs the provided command.
   102  type Option func(*cmdinfo)
   103  
   104  // Env causes Cmd to set *additional* environment variables for the command.
   105  func Env(env ...string) Option {
   106  	return func(s *cmdinfo) {
   107  		if len(s.cmd.Env) == 0 {
   108  			s.cmd.Env = append(os.Environ(), env...)
   109  		} else {
   110  			s.cmd.Env = append(s.cmd.Env, env...)
   111  		}
   112  	}
   113  }
   114  
   115  // Stdin causes Cmd to send the provided string to the command as stdin.
   116  func Stdin(in string) Option {
   117  	return func(s *cmdinfo) {
   118  		s.cmd.Stdin = strings.NewReader(in)
   119  	}
   120  }
   121  
   122  // Stdout causes Cmd to tee the Stdout of the process to the provided io.Writer.
   123  func Stdout(out io.Writer) Option {
   124  	return func(s *cmdinfo) {
   125  		s.cmd.Stdout = out
   126  	}
   127  }
   128  
   129  // Log changes where Cmd writes log-like information about running the command.
   130  func Log(to io.Writer) Option {
   131  	return func(s *cmdinfo) {
   132  		s.log = printing.NewLogWriter(to)
   133  	}
   134  }
   135  
   136  // SuppressStdout prevents Cmd from copying the command stdout (with each
   137  // line prefixed with "  > ") to the writer configured with Log (os.Stdout
   138  // by default).
   139  func SuppressStdout() Option {
   140  	return func(s *cmdinfo) {
   141  		s.logStdout = false
   142  	}
   143  }
   144  
   145  // SuppressStdout prevents Cmd from copying the command stderr (with each
   146  // line prefixed with "  ! ") to the writer configured with Log (os.Stdout
   147  // by default).
   148  func SuppressStderr() Option {
   149  	return func(s *cmdinfo) {
   150  		s.logStderr = false
   151  	}
   152  }