Change all builtin commands that write to byte output to surface write errors.

Also replace (*Frame).OutputFile with (*Frame).ByteOutput, which returns a
small interface for writing bytes and converts EPIPE to ReaderGone.
This commit is contained in:
Qi Xiao 2021-06-11 22:20:27 +01:00
parent 641f0ebf04
commit 067c809fc5
7 changed files with 114 additions and 49 deletions

View File

@ -70,7 +70,7 @@ func BenchmarkOutputCapture_Values(b *testing.B) {
func BenchmarkOutputCapture_Bytes(b *testing.B) {
bytesToWrite := []byte("test")
benchmarkOutputCapture(b.N, func(fm *Frame) {
fm.OutputFile().Write(bytesToWrite)
fm.ByteOutput().Write(bytesToWrite)
})
}
@ -78,7 +78,7 @@ func BenchmarkOutputCapture_Mixed(b *testing.B) {
bytesToWrite := []byte("test")
benchmarkOutputCapture(b.N, func(fm *Frame) {
fm.OutputChan() <- false
fm.OutputFile().Write(bytesToWrite)
fm.ByteOutput().Write(bytesToWrite)
})
}

View File

@ -84,14 +84,14 @@ func _gc() {
//
// This is only useful for debug purposes.
func _stack(fm *Frame) {
out := fm.OutputFile()
func _stack(fm *Frame) error {
// TODO(xiaq): Dup with main.go.
buf := make([]byte, 1024)
for runtime.Stack(buf, true) == cap(buf) {
buf = make([]byte, cap(buf)*2)
}
out.Write(buf)
_, err := fm.ByteOutput().Write(buf)
return err
}
//elvdoc:fn -log

View File

@ -187,14 +187,21 @@ type printOpts struct{ Sep string }
func (o *printOpts) SetDefaultOptions() { o.Sep = " " }
func print(fm *Frame, opts printOpts, args ...interface{}) {
out := fm.OutputFile()
func print(fm *Frame, opts printOpts, args ...interface{}) error {
out := fm.ByteOutput()
for i, arg := range args {
if i > 0 {
out.WriteString(opts.Sep)
_, err := out.WriteString(opts.Sep)
if err != nil {
return err
}
}
_, err := out.WriteString(vals.ToString(arg))
if err != nil {
return err
}
out.WriteString(vals.ToString(arg))
}
return nil
}
//elvdoc:fn printf
@ -272,13 +279,14 @@ func print(fm *Frame, opts printOpts, args ...interface{}) {
//
// @cf print echo pprint repr
func printf(fm *Frame, template string, args ...interface{}) {
func printf(fm *Frame, template string, args ...interface{}) error {
wrappedArgs := make([]interface{}, len(args))
for i, arg := range args {
wrappedArgs[i] = formatter{arg}
}
fmt.Fprintf(fm.OutputFile(), template, wrappedArgs...)
_, err := fmt.Fprintf(fm.ByteOutput(), template, wrappedArgs...)
return err
}
type formatter struct {
@ -372,9 +380,13 @@ func writeFmt(state fmt.State, v rune, val interface{}) {
//
// Etymology: Bourne sh.
func echo(fm *Frame, opts printOpts, args ...interface{}) {
print(fm, opts, args...)
fm.OutputFile().WriteString("\n")
func echo(fm *Frame, opts printOpts, args ...interface{}) error {
err := print(fm, opts, args...)
if err != nil {
return err
}
_, err = fm.ByteOutput().WriteString("\n")
return err
}
//elvdoc:fn pprint
@ -404,12 +416,19 @@ func echo(fm *Frame, opts printOpts, args ...interface{}) {
//
// @cf repr
func pprint(fm *Frame, args ...interface{}) {
out := fm.OutputFile()
func pprint(fm *Frame, args ...interface{}) error {
out := fm.ByteOutput()
for _, arg := range args {
out.WriteString(vals.Repr(arg, 0))
out.WriteString("\n")
_, err := out.WriteString(vals.Repr(arg, 0))
if err != nil {
return err
}
_, err = out.WriteString("\n")
if err != nil {
return err
}
}
return nil
}
//elvdoc:fn repr
@ -430,15 +449,22 @@ func pprint(fm *Frame, args ...interface{}) {
//
// Etymology: [Python](https://docs.python.org/3/library/functions.html#repr).
func repr(fm *Frame, args ...interface{}) {
out := fm.OutputFile()
func repr(fm *Frame, args ...interface{}) error {
out := fm.ByteOutput()
for i, arg := range args {
if i > 0 {
out.WriteString(" ")
_, err := out.WriteString(" ")
if err != nil {
return err
}
}
_, err := out.WriteString(vals.Repr(arg, vals.NoPretty))
if err != nil {
return err
}
out.WriteString(vals.Repr(arg, vals.NoPretty))
}
out.WriteString("\n")
_, err := out.WriteString("\n")
return err
}
//elvdoc:fn show
@ -462,9 +488,14 @@ func repr(fm *Frame, args ...interface{}) {
// [tty 3], line 1: e = ?(fail lorem-ipsum)
// ```
func show(fm *Frame, v diag.Shower) {
fm.OutputFile().WriteString(v.Show(""))
fm.OutputFile().WriteString("\n")
func show(fm *Frame, v diag.Shower) error {
out := fm.ByteOutput()
_, err := out.WriteString(v.Show(""))
if err != nil {
return err
}
_, err = out.WriteString("\n")
return err
}
const bytesReadBufferSize = 512
@ -495,22 +526,8 @@ func onlyBytes(fm *Frame) error {
// Make sure the goroutine has finished before returning.
defer func() { <-valuesDone }()
// Forward bytes.
buf := make([]byte, bytesReadBufferSize)
for {
nr, errRead := fm.InputFile().Read(buf[:])
if nr > 0 {
// Even when there are write errors, we will continue reading. So we
// ignore the error.
fm.OutputFile().Write(buf[:nr])
}
if errRead != nil {
if errRead == io.EOF {
return nil
}
return errRead
}
}
_, err := io.Copy(fm.ByteOutput(), fm.InputFile())
return err
}
//elvdoc:fn only-values
@ -711,9 +728,10 @@ func fromJSONInterface(v interface{}) (interface{}, error) {
// @cf from-lines
func toLines(fm *Frame, inputs Inputs) {
out := fm.OutputFile()
out := fm.ByteOutput()
inputs(func(v interface{}) {
// TODO: Don't ignore the error.
fmt.Fprintln(out, vals.ToString(v))
})
}
@ -738,7 +756,7 @@ func toLines(fm *Frame, inputs Inputs) {
// @cf from-json
func toJSON(fm *Frame, inputs Inputs) error {
encoder := json.NewEncoder(fm.OutputFile())
encoder := json.NewEncoder(fm.ByteOutput())
var errEncode error
inputs(func(v interface{}) {

View File

@ -473,7 +473,10 @@ func timeCmd(fm *Frame, opts timeOpt, f Callable) error {
err = errCb
}
} else {
fmt.Fprintln(fm.OutputFile(), dt)
_, errWrite := fmt.Fprintln(fm.ByteOutput(), dt)
if err == nil {
err = errWrite
}
}
return err

View File

@ -21,6 +21,14 @@ func TestPipeline_Unix(t *testing.T) {
"{ yes; reached = $true } | nop",
"put $reached",
).Puts(false),
// Internal commands that encounters EPIPE also raises ReaderGone, which
// is then suppressed.
That("while $true { echo y } | nop").DoesNothing(),
That(
"var reached = $false",
"{ while $true { echo y }; reached = $true } | nop",
"put $reached",
).Puts(false),
)
}

View File

@ -100,12 +100,12 @@ func (fm *Frame) OutputChan() chan<- interface{} {
return fm.ports[1].Chan
}
// OutputFile returns a file onto which output can be written.
func (fm *Frame) OutputFile() *os.File {
return fm.ports[1].File
// ByteOutput returns a handle for writing byte outputs.
func (fm *Frame) ByteOutput() ByteOutput {
return byteOutput{fm.ports[1].File}
}
// ErrorFile returns a file onto which error messages can be written.
// ByteErrorFile returns a file onto which error messages can be written.
func (fm *Frame) ErrorFile() *os.File {
return fm.ports[2].File
}

View File

@ -6,7 +6,9 @@ import (
"io"
"os"
"sync"
"syscall"
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/strutil"
)
@ -231,3 +233,37 @@ func PortsFromFiles(files [3]*os.File, prefix string) ([]*Port, func()) {
cleanup2()
}
}
// ByteOutput defines the interface through which builtin commands access the
// byte output.
//
// It is a thin wrapper around the underlying *os.File value, only exposing
// the necessary methods for writing bytes and strings, and converting any
// syscall.EPIPE errors to errs.ReaderGone.
type ByteOutput interface {
io.Writer
io.StringWriter
}
type byteOutput struct {
f *os.File
}
func (bo byteOutput) Write(p []byte) (int, error) {
n, err := bo.f.Write(p)
return n, convertReaderGone(err)
}
func (bo byteOutput) WriteString(s string) (int, error) {
n, err := bo.f.WriteString(s)
return n, convertReaderGone(err)
}
func convertReaderGone(err error) error {
if pathErr, ok := err.(*os.PathError); ok {
if pathErr.Err == syscall.EPIPE {
return errs.ReaderGone{}
}
}
return err
}