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 }