From bf3307ae80754aae560bc863c317f2b16a8e63fd Mon Sep 17 00:00:00 2001 From: "Luis Gustavo S. Barreto" Date: Mon, 23 May 2022 16:55:51 -0300 Subject: [PATCH] Ability to read command output line by line in a loop NOTE: the command need to be executed in the background. ``` for line in `tail -f /tmp/log &` { echo(line) } ``` Also combines the stdout and stderr of executed command in the same ouput, like native Go implementation of `cmd.CombinedOutput`. --- evaluator/evaluator.go | 31 +++++++++++++++++------ object/object.go | 57 +++++++++++++++++++++++++++++------------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 0a64785c..34792bf6 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -1,8 +1,10 @@ package evaluator import ( + "bufio" "bytes" "fmt" + "io" "io/ioutil" "math" "os" @@ -1526,13 +1528,9 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment) c := exec.Command(parts[0], append(parts[1:], cmd)...) c.Env = os.Environ() c.Stdin = os.Stdin - var stdout bytes.Buffer - var stderr bytes.Buffer - c.Stdout = &stdout - c.Stderr = &stderr + var stdoutStderr bytes.Buffer - s.Stdout = &stdout - s.Stderr = &stderr + s.StdoutStderr = &stdoutStderr s.Cmd = c s.Token = tok @@ -1543,14 +1541,33 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment) // wait for it by calling s.Wait(). s.SetRunning() - err := c.Start() + stdoutPipe, err := s.Cmd.StdoutPipe() if err != nil { s.SetCmdResult(FALSE) return FALSE } + stderrPipe, err := s.Cmd.StderrPipe() + if err != nil { + s.SetCmdResult(FALSE) + return FALSE + } + + combinedReader := io.MultiReader(stdoutPipe, stderrPipe) + + s.Scanner = bufio.NewScanner(combinedReader) + s.Scanner.Split(bufio.ScanLines) + + if err := s.Cmd.Start(); err != nil { + s.SetCmdResult(FALSE) + return FALSE + } + go evalCommandInBackground(s) } else { + c.Stdout = &stdoutStderr + c.Stderr = &stdoutStderr + err = c.Run() } diff --git a/object/object.go b/object/object.go index 8a2f2d57..5a8827ea 100644 --- a/object/object.go +++ b/object/object.go @@ -1,6 +1,7 @@ package object import ( + "bufio" "bytes" "fmt" "os/exec" @@ -211,14 +212,15 @@ func (f *Function) Json() string { return f.Inspect() } // cmd.wait() // ... // cmd.done // TRUE type String struct { - Token token.Token - Value string - Ok *Boolean // A special property to check whether a command exited correctly - Cmd *exec.Cmd // A special property to access the underlying command - Stdout *bytes.Buffer - Stderr *bytes.Buffer - Done *Boolean - mux *sync.Mutex + Token token.Token + Value string + Ok *Boolean // A special property to check whether a command exited correctly + Cmd *exec.Cmd // A special property to access the underlying command + StdoutStderr *bytes.Buffer + Scanner *bufio.Scanner + Done *Boolean + lineno int64 + mux *sync.Mutex } func (s *String) Type() ObjectType { return STRING_OBJ } @@ -259,6 +261,15 @@ func (s *String) SetRunning() { // wait on the background command // to be done. func (s *String) Wait() { + // Read all 'unread bytes' from stdout/stderr but just in case it hasn't been read yet + if s.Scanner != nil { + for s.Scanner.Scan() { + s.StdoutStderr.Write(s.Scanner.Bytes()) + } + + s.Value = strings.TrimSpace(s.StdoutStderr.String()) + } + s.mustHaveMutex() s.mux.Lock() s.mux.Unlock() @@ -271,9 +282,8 @@ func (s *String) Kill() error { // The command value includes output and possible error // We might want to change this - output := s.Stdout.String() - outputErr := s.Stderr.String() - s.Value = strings.TrimSpace(output) + strings.TrimSpace(outputErr) + output := s.StdoutStderr.String() + s.Value = strings.TrimSpace(output) if err != nil { return err @@ -291,19 +301,32 @@ func (s *String) Kill() error { // - str.done func (s *String) SetCmdResult(Ok *Boolean) { s.Ok = Ok - var output string - if Ok.Value { - output = s.Stdout.String() - } else { - output = s.Stderr.String() - } + output := s.StdoutStderr.String() // trim space at both ends of out.String(); works in both linux and windows s.Value = strings.TrimSpace(output) s.Done = TRUE } +func (s *String) Next() (Object, Object) { + if s.Scanner == nil { + return nil, nil + } + + for s.Scanner.Scan() { + line := s.Scanner.Text() + s.lineno += 1 + return &Number{Value: float64(s.lineno - 1)}, &String{Value: line, Scanner: s.Scanner, lineno: s.lineno} + } + + return nil, nil +} + +func (s *String) Reset() { + s.lineno = 0 +} + type Builtin struct { Token token.Token Fn BuiltinFunction