Skip to content

Commit 4087270

Browse files
authored
Add buffered combinedoutput (#76)
* updates to add combined output 2>&1 into the buffered stdout updated options, logic, and tests to implement a redirection of all stderr into the stdout buffer that can be retrieve from the status object in the proper order. * Update cmd_test.go * Updated comment on BufferedCombined Option * renamed BufferedCombined option to CombinedOutput * add test for coverage and updated option name.
1 parent fa11d77 commit 4087270

3 files changed

Lines changed: 290 additions & 54 deletions

File tree

cmd.go

Lines changed: 76 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,33 @@
55
//
66
// A basic example that runs env and prints its output:
77
//
8-
// import (
9-
// "fmt"
10-
// "github.com/go-cmd/cmd"
11-
// )
8+
// import (
9+
// "fmt"
10+
// "github.com/go-cmd/cmd"
11+
// )
1212
//
13-
// func main() {
14-
// // Create Cmd, buffered output
15-
// envCmd := cmd.NewCmd("env")
13+
// func main() {
14+
// // Create Cmd, buffered output
15+
// envCmd := cmd.NewCmd("env")
1616
//
17-
// // Run and wait for Cmd to return Status
18-
// status := <-envCmd.Start()
17+
// // Run and wait for Cmd to return Status
18+
// status := <-envCmd.Start()
1919
//
20-
// // Print each line of STDOUT from Cmd
21-
// for _, line := range status.Stdout {
22-
// fmt.Println(line)
23-
// }
24-
// }
20+
// // Print each line of STDOUT from Cmd
21+
// for _, line := range status.Stdout {
22+
// fmt.Println(line)
23+
// }
24+
// }
2525
//
2626
// Commands can be ran synchronously (blocking) or asynchronously (non-blocking):
2727
//
28-
// envCmd := cmd.NewCmd("env") // create
28+
// envCmd := cmd.NewCmd("env") // create
2929
//
30-
// status := <-envCmd.Start() // run blocking
30+
// status := <-envCmd.Start() // run blocking
3131
//
32-
// statusChan := envCmd.Start() // run non-blocking
33-
// // Do other work while Cmd is running...
34-
// status <- statusChan // blocking
32+
// statusChan := envCmd.Start() // run non-blocking
33+
// // Do other work while Cmd is running...
34+
// status <- statusChan // blocking
3535
//
3636
// Start returns a channel to which the final Status is sent when the command
3737
// finishes for any reason. The first example blocks receiving on the channel.
@@ -111,9 +111,9 @@ var (
111111
// for any reason, this combination of values indicates success (presuming the
112112
// command only exits zero on success):
113113
//
114-
// Exit = 0
115-
// Error = nil
116-
// Complete = true
114+
// Exit = 0
115+
// Error = nil
116+
// Complete = true
117117
//
118118
// Error is a Go error from the underlying os/exec.Cmd.Start or os/exec.Cmd.Wait.
119119
// If not nil, the command either failed to start (it never ran) or it started
@@ -147,6 +147,12 @@ type Options struct {
147147
// See Cmd.Status for more info.
148148
Buffered bool
149149

150+
// If CombinedOutput is true, STDOUT and STDERR are written to Status.Stdout ONLY similar to 2>&1.
151+
// If CombinedOutput is used at the same time as Buffered, CombinedOutput will take preference.
152+
// Status.StdErr will be empty. The caller can call Cmd.Status to read output at intervals.
153+
// See Cmd.Status for more info.
154+
CombinedOutput bool
155+
150156
// If Streaming is true, Cmd.Stdout and Cmd.Stderr channels are created and
151157
// STDOUT and STDERR output lines are written them in real time. This is
152158
// faster and more efficient than polling Cmd.Status. The caller must read both
@@ -193,6 +199,11 @@ func NewCmdOptions(options Options, name string, args ...string) *Cmd {
193199
c.stderrBuf = NewOutputBuffer()
194200
}
195201

202+
if options.CombinedOutput {
203+
c.stdoutBuf = NewOutputBuffer()
204+
c.stderrBuf = nil
205+
}
206+
196207
if options.Streaming {
197208
c.Stdout = make(chan string, DEFAULT_STREAM_CHAN_SIZE)
198209
c.stdoutStream = NewOutputStream(c.Stdout)
@@ -223,8 +234,9 @@ func NewCmdOptions(options Options, name string, args ...string) *Cmd {
223234
func (c *Cmd) Clone() *Cmd {
224235
clone := NewCmdOptions(
225236
Options{
226-
Buffered: c.stdoutBuf != nil,
227-
Streaming: c.stdoutStream != nil,
237+
Buffered: c.stdoutBuf != nil,
238+
CombinedOutput: c.stdoutBuf != nil,
239+
Streaming: c.stdoutStream != nil,
228240
},
229241
c.Name,
230242
c.Args...,
@@ -246,13 +258,13 @@ func (c *Cmd) Clone() *Cmd {
246258
// can use to receive the final Status of the command when it ends. The caller
247259
// can start the command and wait like,
248260
//
249-
// status := <-myCmd.Start() // blocking
261+
// status := <-myCmd.Start() // blocking
250262
//
251263
// or start the command asynchronously and be notified later when it ends,
252264
//
253-
// statusChan := myCmd.Start() // non-blocking
254-
// // Do other work while Cmd is running...
255-
// status := <-statusChan // blocking
265+
// statusChan := myCmd.Start() // non-blocking
266+
// // Do other work while Cmd is running...
267+
// status := <-statusChan // blocking
256268
//
257269
// Exactly one Status is sent on the channel when the command ends. The channel
258270
// is not closed. Any Go error is set to Status.Error. Start is idempotent; it
@@ -320,9 +332,9 @@ func (c *Cmd) Stop() error {
320332
// as of the Status call time. For example, if the command counts to 3 and three
321333
// calls are made between counts, Status.Stdout contains:
322334
//
323-
// "1"
324-
// "1 2"
325-
// "1 2 3"
335+
// "1"
336+
// "1 2"
337+
// "1 2 3"
326338
//
327339
// The caller is responsible for tailing the buffered output if needed. Else,
328340
// consider using streaming output. When the command finishes, buffered output
@@ -344,9 +356,12 @@ func (c *Cmd) Status() Status {
344356
if !c.final {
345357
if c.stdoutBuf != nil {
346358
c.status.Stdout = c.stdoutBuf.Lines()
347-
c.status.Stderr = c.stderrBuf.Lines()
348359
c.stdoutBuf = nil // release buffers
349-
c.stderrBuf = nil
360+
361+
}
362+
if c.stderrBuf != nil {
363+
c.status.Stderr = c.stderrBuf.Lines()
364+
c.stderrBuf = nil // release buffers
350365
}
351366
c.final = true
352367
}
@@ -355,6 +370,9 @@ func (c *Cmd) Status() Status {
355370
c.status.Runtime = time.Now().Sub(c.startTime).Seconds()
356371
if c.stdoutBuf != nil {
357372
c.status.Stdout = c.stdoutBuf.Lines()
373+
374+
}
375+
if c.stderrBuf != nil {
358376
c.status.Stderr = c.stderrBuf.Lines()
359377
}
360378
}
@@ -391,12 +409,19 @@ func (c *Cmd) run(in io.Reader) {
391409
// Set exec.Cmd.Stdout and .Stderr to our concurrent-safe stdout/stderr
392410
// buffer, stream both, or neither
393411
switch {
394-
case c.stdoutBuf != nil && c.stdoutStream != nil: // buffer and stream
412+
413+
case c.stdoutBuf != nil && c.stderrBuf != nil && c.stdoutStream != nil: // buffer and stream
395414
cmd.Stdout = io.MultiWriter(c.stdoutStream, c.stdoutBuf)
396415
cmd.Stderr = io.MultiWriter(c.stderrStream, c.stderrBuf)
397-
case c.stdoutBuf != nil: // buffer only
416+
case c.stdoutBuf != nil && c.stderrBuf == nil && c.stdoutStream != nil: // combined buffer and stream
417+
cmd.Stdout = io.MultiWriter(c.stdoutStream, c.stdoutBuf)
418+
cmd.Stderr = io.MultiWriter(c.stderrStream, c.stdoutBuf)
419+
case c.stdoutBuf != nil && c.stderrBuf != nil: // buffer only
398420
cmd.Stdout = c.stdoutBuf
399421
cmd.Stderr = c.stderrBuf
422+
case c.stdoutBuf != nil && c.stderrBuf == nil: // buffer combining stderr into stdout
423+
cmd.Stdout = c.stdoutBuf
424+
cmd.Stderr = c.stdoutBuf
400425
case c.stdoutStream != nil: // stream only
401426
cmd.Stdout = c.stdoutStream
402427
cmd.Stderr = c.stderrStream
@@ -521,11 +546,11 @@ func (c *Cmd) run(in io.Reader) {
521546
// default when created by calling NewCmd. To use OutputBuffer directly with
522547
// a Go standard library os/exec.Command:
523548
//
524-
// import "os/exec"
525-
// import "github.com/go-cmd/cmd"
526-
// runnableCmd := exec.Command(...)
527-
// stdout := cmd.NewOutputBuffer()
528-
// runnableCmd.Stdout = stdout
549+
// import "os/exec"
550+
// import "github.com/go-cmd/cmd"
551+
// runnableCmd := exec.Command(...)
552+
// stdout := cmd.NewOutputBuffer()
553+
// runnableCmd.Stdout = stdout
529554
//
530555
// While runnableCmd is running, call stdout.Lines() to read all output
531556
// currently written.
@@ -613,20 +638,19 @@ func (e ErrLineBufferOverflow) Error() string {
613638
// created by calling NewCmdOptions and Options.Streaming is true. To use
614639
// OutputStream directly with a Go standard library os/exec.Command:
615640
//
616-
// import "os/exec"
617-
// import "github.com/go-cmd/cmd"
618-
//
619-
// stdoutChan := make(chan string, 100)
620-
// go func() {
621-
// for line := range stdoutChan {
622-
// // Do something with the line
623-
// }
624-
// }()
641+
// import "os/exec"
642+
// import "github.com/go-cmd/cmd"
625643
//
626-
// runnableCmd := exec.Command(...)
627-
// stdout := cmd.NewOutputStream(stdoutChan)
628-
// runnableCmd.Stdout = stdout
644+
// stdoutChan := make(chan string, 100)
645+
// go func() {
646+
// for line := range stdoutChan {
647+
// // Do something with the line
648+
// }
649+
// }()
629650
//
651+
// runnableCmd := exec.Command(...)
652+
// stdout := cmd.NewOutputStream(stdoutChan)
653+
// runnableCmd.Stdout = stdout
630654
//
631655
// While runnableCmd is running, lines are sent to the channel as soon as they
632656
// are written and newline-terminated by the command.

0 commit comments

Comments
 (0)