github.com/haraldrudell/parl@v0.4.176/mains/stdin-reader.go (about) 1 /* 2 © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package mains 7 8 import ( 9 "fmt" 10 "io" 11 "os" 12 "sync/atomic" 13 14 "github.com/haraldrudell/parl" 15 "github.com/haraldrudell/parl/perrors" 16 ) 17 18 // StdinReader is a reader wrapping the unclosable os.Stdin.Read 19 // - on error, the error is sent to addError and EOF is returned 20 type StdinReader struct { 21 // optionl error submitting function 22 addError parl.AddError 23 // whether error has occured in [StdinReader.Read] 24 isClosed atomic.Bool 25 // optional value set to true on error 26 isError *atomic.Bool 27 } 28 29 var _ io.Reader = &StdinReader{} 30 31 // NewStdinReader returns a reader that closes on error 32 // - addError is an optional function receiving errors occurring in [os.Stdin.Read]. 33 // if missing, errors are printed to stderr 34 // - isError is an optional atomic set to true on first error 35 func NewStdinReader(addError parl.AddError, isError *atomic.Bool) (reader *StdinReader) { 36 return &StdinReader{ 37 addError: addError, 38 isError: isError, 39 } 40 } 41 42 // Read reads from standard input 43 // - on error, the reader closes 44 // - errors are submitted separately or printed to stderr and not returned 45 // - the only error returned is [io.EOF] 46 // - [os.Stdin] cannot be closed so a blocking read cannot be canceled 47 // - if another process closes stdin, on the next keypress an error will result 48 // - on process exit, Read may hang until enter is pressed 49 func (r *StdinReader) Read(p []byte) (n int, err error) { 50 51 // already closed case 52 if r.isClosed.Load() { 53 err = io.EOF 54 return 55 } 56 57 var isPanic bool 58 59 n, isPanic, err = r.read(p) 60 61 // no error case 62 if err == nil { 63 return 64 } 65 66 // store error condition in object 67 r.isClosed.Store(true) 68 // if isError present, note error has occurred 69 if r.isError != nil { 70 r.isError.Store(true) 71 } 72 73 // do not submit or print EOF error 74 // - indication is r.isError true 75 if err == io.EOF { 76 return 77 } 78 79 // if another process closes stdin: 80 // os.StdinRead error: 81 // “read /dev/stdin: input/output error [*fs.PathError] 82 // input/output error [syscall.Errno]” 83 // isPanic: false 84 85 // if addError present, submit error to it 86 if r.addError != nil { 87 err = perrors.ErrorfPF("os.Stdin.Read error: “%w” isPanic: %t", 88 err, isPanic, 89 ) 90 r.addError(err) 91 err = io.EOF 92 return 93 } 94 95 fmt.Fprintf(os.Stderr, "os.Stdin.Read error: “%s” isPanic: %t", 96 perrors.Long(err), 97 isPanic, 98 ) 99 err = io.EOF 100 101 return 102 } 103 104 // read invokes [os.Stdin.Read] capturing panic 105 func (r *StdinReader) read(p []byte) (n int, isPanic bool, err error) { 106 defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err, &isPanic) 107 108 n, err = os.Stdin.Read(p) 109 110 return 111 }