Skip to content

Commit

Permalink
Ability to read command output line by line in a loop
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
gustavosbarreto committed May 24, 2022
1 parent 5de8d01 commit bf3307a
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 24 deletions.
31 changes: 24 additions & 7 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package evaluator

import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"math"
"os"
Expand Down Expand Up @@ -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

Expand All @@ -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()
}

Expand Down
57 changes: 40 additions & 17 deletions object/object.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package object

import (
"bufio"
"bytes"
"fmt"
"os/exec"
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit bf3307a

Please sign in to comment.