Skip to content

Commit 7acd35c

Browse files
bitfield"Masci, Richard (rx7322)"jesselangnchintfloydwch
committed
Add WithStderr (fixes #128, fixes #146)
Co-authored-by: "Masci, Richard (rx7322)" <rx7322@us.att.com> Co-authored-by: Jesse Lang <jdlang@spscommerce.com> Co-authored-by: Nithin Chintala <nithin.chintala@schrodinger.com> Co-authored-by: floydwch <floydwch@gmail.com>
1 parent bee1290 commit 7acd35c

File tree

3 files changed

+68
-11
lines changed

3 files changed

+68
-11
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
336336

337337
| Version | New |
338338
| ----------- | ------- |
339-
| v0.22.0 | [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee) |
339+
| v0.22.0 | [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee), [`WithStderr`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithStderr) |
340340
| v0.21.0 | HTTP support: [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do), [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get), [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
341341
| v0.20.0 | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
342342

script.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import (
2727
// Pipe represents a pipe object with an associated [ReadAutoCloser].
2828
type Pipe struct {
2929
// Reader is the underlying reader.
30-
Reader ReadAutoCloser
31-
stdout io.Writer
32-
httpClient *http.Client
30+
Reader ReadAutoCloser
31+
stdout, stderr io.Writer
32+
httpClient *http.Client
3333

3434
// because pipe stages are concurrent, protect 'err'
3535
mu *sync.Mutex
@@ -369,8 +369,9 @@ func (p *Pipe) Error() error {
369369
}
370370

371371
// Exec runs cmdLine as an external command, sending it the contents of the
372-
// pipe as input, and produces the command's combined output. The effect of
373-
// this is to filter the contents of the pipe through the external command.
372+
// pipe as input, and produces the command's standard output (see below for
373+
// error output). The effect of this is to filter the contents of the pipe
374+
// through the external command.
374375
//
375376
// # Error handling
376377
//
@@ -381,6 +382,10 @@ func (p *Pipe) Error() error {
381382
// because [Pipe.String] is a no-op if the pipe's error status is set, if you
382383
// want output you will need to reset the error status before calling
383384
// [Pipe.String].
385+
//
386+
// If the command writes to its standard error stream, this will also go to the
387+
// pipe, along with its standard output. However, the standard error text can
388+
// instead be redirected to a supplied writer, using [Pipe.WithStderr].
384389
func (p *Pipe) Exec(cmdLine string) *Pipe {
385390
return p.Filter(func(r io.Reader, w io.Writer) error {
386391
args, ok := shell.Split(cmdLine) // strings.Fields doesn't handle quotes
@@ -391,9 +396,12 @@ func (p *Pipe) Exec(cmdLine string) *Pipe {
391396
cmd.Stdin = r
392397
cmd.Stdout = w
393398
cmd.Stderr = w
399+
if p.stderr != nil {
400+
cmd.Stderr = p.stderr
401+
}
394402
err := cmd.Start()
395403
if err != nil {
396-
fmt.Fprintln(w, err)
404+
fmt.Fprintln(cmd.Stderr, err)
397405
return err
398406
}
399407
return cmd.Wait()
@@ -429,14 +437,17 @@ func (p *Pipe) ExecForEach(cmdLine string) *Pipe {
429437
cmd := exec.Command(args[0], args[1:]...)
430438
cmd.Stdout = w
431439
cmd.Stderr = w
440+
if p.stderr != nil {
441+
cmd.Stderr = p.stderr
442+
}
432443
err = cmd.Start()
433444
if err != nil {
434-
fmt.Fprintln(w, err)
445+
fmt.Fprintln(cmd.Stderr, err)
435446
continue
436447
}
437448
err = cmd.Wait()
438449
if err != nil {
439-
fmt.Fprintln(w, err)
450+
fmt.Fprintln(cmd.Stderr, err)
440451
continue
441452
}
442453
}
@@ -886,6 +897,14 @@ func (p *Pipe) WithReader(r io.Reader) *Pipe {
886897
return p
887898
}
888899

900+
// WithStderr redirects the standard error output for commands run via
901+
// [Pipe.Exec] or [Pipe.ExecForEach] to the writer w, instead of going to the
902+
// pipe as it normally would.
903+
func (p *Pipe) WithStderr(w io.Writer) *Pipe {
904+
p.stderr = w
905+
return p
906+
}
907+
889908
// WithStdout sets the pipe's standard output to the writer w, instead of the
890909
// default [os.Stdout].
891910
func (p *Pipe) WithStdout(w io.Writer) *Pipe {

script_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,36 @@ func TestExecForEach_ErrorsOnUnbalancedQuotes(t *testing.T) {
343343
}
344344
}
345345

346+
func TestExecForEach_SendsStderrOutputToPipeStderr(t *testing.T) {
347+
t.Parallel()
348+
buf := new(bytes.Buffer)
349+
out, err := script.Echo("go").WithStderr(buf).ExecForEach("{{.}}").String()
350+
if err != nil {
351+
t.Fatal(err)
352+
}
353+
if out != "" {
354+
t.Fatalf("unexpected output: %q", out)
355+
}
356+
if !strings.Contains(buf.String(), "Usage") {
357+
t.Errorf("want stderr output containing the word 'Usage', got %q", buf.String())
358+
}
359+
}
360+
361+
func TestExecSendsStderrOutputToPipeStderr(t *testing.T) {
362+
t.Parallel()
363+
buf := new(bytes.Buffer)
364+
out, err := script.NewPipe().WithStderr(buf).Exec("go").String()
365+
if err == nil {
366+
t.Fatal("want error when command returns a non-zero exit status")
367+
}
368+
if out != "" {
369+
t.Fatalf("unexpected output: %q", out)
370+
}
371+
if !strings.Contains(buf.String(), "Usage") {
372+
t.Errorf("want stderr output containing the word 'Usage', got %q", buf.String())
373+
}
374+
}
375+
346376
func TestFilterByCopyPassesInputThroughUnchanged(t *testing.T) {
347377
t.Parallel()
348378
p := script.Echo("hello").Filter(func(r io.Reader, w io.Writer) error {
@@ -1157,7 +1187,7 @@ func TestExecRunsGoWithNoArgsAndGetsUsageMessagePlusErrorExitStatus2(t *testing.
11571187
t.Error("want error when command returns a non-zero exit status")
11581188
}
11591189
if !strings.Contains(output, "Usage") {
1160-
t.Errorf("want output containing the word 'usage', got %q", output)
1190+
t.Errorf("want output containing the word 'Usage', got %q", output)
11611191
}
11621192
want := 2
11631193
got := p.ExitStatus()
@@ -1177,7 +1207,7 @@ func TestExecRunsGoHelpAndGetsUsageMessage(t *testing.T) {
11771207
t.Fatal(err)
11781208
}
11791209
if !strings.Contains(output, "Usage") {
1180-
t.Fatalf("want output containing the word 'usage', got %q", output)
1210+
t.Fatalf("want output containing the word 'Usage', got %q", output)
11811211
}
11821212
}
11831213

@@ -2245,6 +2275,14 @@ func ExamplePipe_Tee_writers() {
22452275
// hello
22462276
}
22472277

2278+
func ExamplePipe_WithStderr() {
2279+
buf := new(bytes.Buffer)
2280+
script.NewPipe().WithStderr(buf).Exec("go").Wait()
2281+
fmt.Println(strings.Contains(buf.String(), "Usage"))
2282+
// Output:
2283+
// true
2284+
}
2285+
22482286
func ExampleSlice() {
22492287
input := []string{"1", "2", "3"}
22502288
script.Slice(input).Stdout()

0 commit comments

Comments
 (0)