Skip to content

Commit

Permalink
Merge 9982f08 into 92de353
Browse files Browse the repository at this point in the history
  • Loading branch information
odino committed Feb 18, 2019
2 parents 92de353 + 9982f08 commit 72a2b51
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 21 deletions.
41 changes: 37 additions & 4 deletions docs/syntax/system-commands.md
Expand Up @@ -40,6 +40,41 @@ if `ls -la`.ok {
}
```
## Executing commands in background
Sometimes you might want to execute a command in
background, so that the script keeps executing
while the command is running. In order to do so,
you can simply add an `&` at the end of your script:
``` bash
`sleep 10 &`
echo("This will be printed right away!")
```
You might also want to check whether a command
is "done", by checking the boolean `.done` property:
``` bash
cmd = `sleep 10 &`
cmd.done # false
`sleep 11`
cmd.done # true
```
If, at some point, you want to wait for the command
to finish before running additional code, you can
use the `wait` method:
``` bash
cmd = `sleep 10 &`
echo("This will be printed right away!")
cmd.wait()
echo("This will be printed after 10s")
```
## Interpolation
You can also replace parts of the command with variables
declared within your program using the `$` symbol:
Expand All @@ -57,6 +92,8 @@ $(echo $PWD) # "" since the ABS variable PWD doesn't exist
$(echo \$PWD) # "/go/src/github.com/abs-lang/abs"
```
## Limitations
Currently, commands that use the `$()` syntax need to be
on their own line, meaning that you will not
be able to have additional code on the same line.
Expand All @@ -69,10 +106,6 @@ $(sleep 10); echo("hello world")
Note that this is currently a limitation that will likely
be removed in the future (see [#41](https://github.com/abs-lang/abs/issues/41)).
Commands are blocking and cannot be run in parallel, although
we're planning to support background execution in the future
(see [#70](https://github.com/abs-lang/abs/issues/70)).
Also note that, currently, the implementation of system commands
requires the `bash` executable to [be available on the system](https://github.com/abs-lang/abs/blob/5b5b0abf3115a5dd4dfe8485501f8765985ad0db/evaluator/evaluator.go#L696-L722).
On Windows, commands are executed through [cmd.exe](https://github.com/abs-lang/abs/blob/ee793641be09ad8572c3e913fef8468f69b0c0a2/evaluator/evaluator.go#L1101-L1103).
Expand Down
87 changes: 75 additions & 12 deletions evaluator/evaluator.go
Expand Up @@ -19,10 +19,10 @@ import (
)

var (
NULL = &object.Null{}
EOF = &object.Error{Message: "EOF"}
TRUE = &object.Boolean{Value: true}
FALSE = &object.Boolean{Value: false}
NULL = object.NULL
EOF = object.EOF
TRUE = object.TRUE
FALSE = object.FALSE
Fns map[string]*object.Builtin
)

Expand Down Expand Up @@ -51,7 +51,6 @@ func BeginEval(program ast.Node, env *object.Environment, lexer *lexer.Lexer) ob
}

func Eval(node ast.Node, env *object.Environment) object.Object {

switch node := node.(type) {
// Statements
case *ast.Program:
Expand Down Expand Up @@ -916,6 +915,14 @@ func evalPropertyExpression(pe *ast.PropertyExpression, env *object.Environment)
return obj.Ok
}

return FALSE
}
// Special .done property of commands
if pe.Property.String() == "done" {
if obj.Done != nil {
return obj.Done
}

return FALSE
}
case *object.Hash:
Expand Down Expand Up @@ -1068,6 +1075,7 @@ func evalHashIndexExpression(tok token.Token, hash, index object.Object) object.
}

func evalCommandExpression(tok token.Token, cmd string, env *object.Environment) object.Object {
cmd = strings.Trim(cmd, " ")
// Match all strings preceded by
// a $ or a \$
re := regexp.MustCompile("(\\\\)?\\$([a-zA-Z_]{1,})")
Expand Down Expand Up @@ -1095,6 +1103,18 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment)
return v.Inspect()
})

// A background command ends with a '&'
background := len(cmd) > 1 && cmd[len(cmd)-1] == '&'
// If this is a background command
// we'll remove the trailing '&' and
// execute it in background ourselves
if background {
cmd = cmd[:len(cmd)-2]
}

// The string holding the command
s := &object.String{}

// thanks to @haifenghuang
var commands []string
var executor string
Expand All @@ -1105,18 +1125,61 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment)
commands = []string{"-c", cmd}
executor = "bash"
}

c := exec.Command(executor, commands...)
c.Env = os.Environ()
var out bytes.Buffer
var stderr bytes.Buffer
c.Stdin = os.Stdin
c.Stdout = &out
var stdout bytes.Buffer
var stderr bytes.Buffer
c.Stdout = &stdout
c.Stderr = &stderr
err := c.Run()

s.Stdout = &stdout
s.Stderr = &stderr
s.Cmd = c
s.Token = tok

var err error
if background {
// If we want to run the command in background,
// let's set it as running. With this, others can
// wait for it by calling s.Wait().
s.SetRunning()
go evalCommandInBackground(c, s)
} else {
err = c.Run()
}

if !background {
if err != nil {
s.SetCmdResult(FALSE)
} else {
s.SetCmdResult(TRUE)
}
}

return s
}

// Runs a background command.
// We will start it, set its result
// and then mark it as done, so that
// callers stuck on s.Wait() can resume.
func evalCommandInBackground(c *exec.Cmd, s *object.String) {
defer s.SetDone()
err := c.Start()

if err != nil {
s.SetCmdResult(FALSE)
return
}

err = c.Wait()

if err != nil {
return &object.String{Token: tok, Value: stderr.String(), Ok: FALSE}
s.SetCmdResult(FALSE)
return
}
// trim space at both ends of out.String(); works in both linux and windows
return &object.String{Token: tok, Value: strings.TrimSpace(out.String()), Ok: TRUE}

s.SetCmdResult(TRUE)
}
19 changes: 19 additions & 0 deletions evaluator/evaluator_test.go
Expand Up @@ -800,6 +800,8 @@ c")`, []string{"a", "b", "c"}},
{`1.66.ceil()`, 2},
{`"1.23".ceil()`, 2},
{`"1.66".ceil()`, 2},
{`sleep(0.01)`, nil},
{`$()`, ""},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
Expand Down Expand Up @@ -1131,6 +1133,23 @@ func TestCommand(t *testing.T) {
{"`echo -n hello world`", "hello world"},
{"`echo hello world | xargs echo -n`", "hello world"},
{"`echo \\$CONTEXT`", "abs"},
{`a = "A"; b = "B"; eee = "-e"; $(echo $eee -n $a$a$b$b$c$c)`, "AABB"},
{`$(echo -n "123")`, "123"},
{`$(echo -n hello world)`, "hello world"},
{`$(echo hello world | xargs echo -n)`, "hello world"},
{`$(echo \$CONTEXT)`, "abs"},
{"a = 'A'; b = 'B'; eee = '-e'; `echo $eee -n $a$a$b$b$c$c`", "AABB"},
{"`echo -n '123'`", "123"},
{"`echo -n hello world`", "hello world"},
{"`echo hello world | xargs echo -n`", "hello world"},
{"`echo \\$CONTEXT`", "abs"},
{"`sleep 0.01`", ""},
{"`sleep 0.01`.done", true},
{"`sleep 0.01`.ok", true},
{"`sleep 0.01 &`", ""},
{"`sleep 0.01 &`.done", false},
{"`sleep 0.01 &`.ok", false},
{"`sleep 0.01 &`.wait().ok", false},
}
}
for _, tt := range tests {
Expand Down
22 changes: 22 additions & 0 deletions evaluator/functions.go
Expand Up @@ -231,6 +231,11 @@ func getFns() map[string]*object.Builtin {
Types: []string{object.STRING_OBJ},
Fn: upperFn,
},
// wait(`sleep 1 &`)
"wait": &object.Builtin{
Types: []string{object.STRING_OBJ},
Fn: waitFn,
},
// trim("abc")
"trim": &object.Builtin{
Types: []string{object.STRING_OBJ},
Expand Down Expand Up @@ -1126,6 +1131,23 @@ func upperFn(tok token.Token, args ...object.Object) object.Object {
return &object.String{Token: tok, Value: strings.ToUpper(args[0].(*object.String).Value)}
}

// wait(`sleep 10 &`)
func waitFn(tok token.Token, args ...object.Object) object.Object {
err := validateArgs(tok, "wait", args, 1, [][]string{{object.STRING_OBJ}})
if err != nil {
return err
}

cmd := args[0].(*object.String)

if cmd.Cmd == nil {
return cmd
}

cmd.Wait()
return cmd
}

// trim("abc")
func trimFn(tok token.Token, args ...object.Object) object.Object {
err := validateArgs(tok, "trim", args, 1, [][]string{{object.STRING_OBJ}})
Expand Down
1 change: 1 addition & 0 deletions examples/background-command-exit.abs
@@ -0,0 +1 @@
cmd = `sleep 10 &`
33 changes: 33 additions & 0 deletions examples/background-command.abs
@@ -0,0 +1,33 @@
# Simple background command
cmd = `sleep 1; ls -la &`
echo("Started")
sleep(1100)
echo("finished")
echo(cmd)
echo(cmd.ok)

# Simple background command, we get the output by sleeping
cmd = `sleep 1; ls -la &`
echo("Started")
sleep(1100)
echo("finished")
echo(cmd)
echo(cmd.ok)

# Background command wait
cmd = `sleep 1; ls -la &`
echo("Started")
cmd.wait()
echo("finished")
echo(cmd)
echo(cmd.ok)

# Background command wait with error
cmd = `sleep 1; ls la &`
echo("Started")
cmd.wait()
echo("finished")
echo(cmd)
echo(cmd.ok)

# TODO make cmd.wait() work

0 comments on commit 72a2b51

Please sign in to comment.