Repository: abs-lang/abs
Pull request #205 (2019-03-03 18:15:34)

Added ability to kill background command
https://github.com/abs-lang/abs/issues/198
Now we can use ```cmd.kill()``` to kill a background command
Needs review, and also I'm not sure how to handle if there's error during kill

odino: @mingwho can you also update the documentation?

Patch:
diff --git a/docs/syntax/system-commands.md b/docs/syntax/system-commands.md
index d1572d8..808032e 100644
--- a/docs/syntax/system-commands.md
+++ b/docs/syntax/system-commands.md
@@ -73,6 +73,16 @@ cmd.wait()
 echo("This will be printed after 10s")
 ```
 
+If you ever want to terminate a running command, you can
+use the `kill` method.
+
+``` bash
+cmd = `sleep 10 &`
+cmd.done # false
+cmd.kill()
+cmd.done # true
+```
+
 ## Interpolation
 
 You can also replace parts of the command with variables
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index cef4d0b..af645f7 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -1152,7 +1152,14 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment)
 		// let's set it as running. With this, others can
 		// wait for it by calling s.Wait().
 		s.SetRunning()
-		go evalCommandInBackground(c, s)
+
+		err := c.Start()
+		if err != nil {
+			s.SetCmdResult(FALSE)
+			return FALSE
+		}
+
+		go evalCommandInBackground(s)
 	} else {
 		err = c.Run()
 	}
@@ -1172,16 +1179,10 @@ func evalCommandExpression(tok token.Token, cmd string, env *object.Environment)
 // 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) {
+func evalCommandInBackground(s *object.String) {
 	defer s.SetDone()
-	err := c.Start()
-
-	if err != nil {
-		s.SetCmdResult(FALSE)
-		return
-	}
 
-	err = c.Wait()
+	err := s.Cmd.Wait()
 
 	if err != nil {
 		s.SetCmdResult(FALSE)
diff --git a/evaluator/functions.go b/evaluator/functions.go
index e5fced9..dc12837 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -237,6 +237,10 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.STRING_OBJ},
 			Fn:    waitFn,
 		},
+		"kill": &object.Builtin{
+			Types: []string{object.STRING_OBJ},
+			Fn:    killFn,
+		},
 		// trim("abc")
 		"trim": &object.Builtin{
 			Types: []string{object.STRING_OBJ},
@@ -1161,6 +1165,27 @@ func waitFn(tok token.Token, args ...object.Object) object.Object {
 	return cmd
 }
 
+// kill(`sleep 10 &`)
+func killFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "kill", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	cmd := args[0].(*object.String)
+
+	if cmd.Cmd == nil {
+		return cmd
+	}
+
+	errCmdKill := cmd.Kill()
+
+	if errCmdKill != nil {
+		return newError(tok, "Error killing command %s with error %s", cmd.Inspect(), errCmdKill.Error())
+	}
+	return cmd
+}
+
 // trim("abc")
 func trimFn(tok token.Token, args ...object.Object) object.Object {
 	err := validateArgs(tok, "trim", args, 1, [][]string{{object.STRING_OBJ}})
diff --git a/object/object.go b/object/object.go
index 6f85fe7..82d90f2 100644
--- a/object/object.go
+++ b/object/object.go
@@ -216,6 +216,25 @@ func (s *String) Wait() {
 	s.mux.Unlock()
 }
 
+// To be called when we want to
+// kill the background command
+func (s *String) Kill() error {
+	err := s.Cmd.Process.Kill()
+
+	// 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)
+
+	if err != nil {
+		return err
+	}
+
+	s.Done = TRUE
+	return nil
+}
+
 // Sets the result of the underlying command
 // on the string.
 // 3 things are set:


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index db23103..15cb3ac 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -337,13 +337,13 @@ func TestForElseExpressions(t *testing.T) {
 		input    string
 		expected interface{}
 	}{
-		{ "a = 0; b = 1; for v in [] { x = a } else { x = b }; x", 1},
-		{ "a = 100; x = 0; for i in  1..-1 { x = i } else { x = a }; x", 100},
-		{ "v = 100; for k, v in [] { v = 0 } else {}; v", 100},
-		{ "for k, v in [] {} else { x = v }; x", "identifier not found: v"},
-		{ "a = 0; for k, v in [] { x = a } else { x = b }; x", "identifier not found: b"},
-		{ "for k, v in [] { x = 0 } else { x = 100 }; z", "identifier not found: z"},
-		{ "for i in 1..3 { x = i } else { x = 0 }; x", 3},
+		{"a = 0; b = 1; for v in [] { x = a } else { x = b }; x", 1},
+		{"a = 100; x = 0; for i in  1..-1 { x = i } else { x = a }; x", 100},
+		{"v = 100; for k, v in [] { v = 0 } else {}; v", 100},
+		{"for k, v in [] {} else { x = v }; x", "identifier not found: v"},
+		{"a = 0; for k, v in [] { x = a } else { x = b }; x", "identifier not found: b"},
+		{"for k, v in [] { x = 0 } else { x = 100 }; z", "identifier not found: z"},
+		{"for i in 1..3 { x = i } else { x = 0 }; x", 3},
 	}
 
 	for _, tt := range tests {
@@ -1172,7 +1172,14 @@ func TestCommand(t *testing.T) {
 			{"`sleep 0.01 &`", ""},
 			{"`sleep 0.01 &`.done", false},
 			{"`sleep 0.01 &`.ok", false},
-			{"`sleep 0.01 &`.wait().ok", false},
+			{"`sleep 0.01 &`.wait().ok", true},
+			{"`sleep 0.01 && echo 123 &`.wait()", "123"},
+			{"`sleep 0.01 && echo 123 &`.kill()", ""},
+			{"`sleep 0.01 && echo 123 &`.kill().done", true},
+			{"`sleep 0.01 && echo 123 &`.kill().ok", false},
+			{"`echo 123; sleep 10 &`.ok", false},
+			{"`echo 123; sleep 10 &`.kill().done", true},
+			{"`echo 123; sleep 10 &`.kill().ok", false},
 		}
 	}
 	for _, tt := range tests {
@@ -1190,6 +1197,15 @@ func TestCommand(t *testing.T) {
 			if stringObj.Value != expected {
 				t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, stringObj.Value, expected)
 			}
+		case bool:
+			booleanObj, ok := evaluated.(*object.Boolean)
+			if !ok {
+				t.Errorf("object is not Boolean. got=%T (%+v)", evaluated, evaluated)
+				continue
+			}
+			if booleanObj.Value != expected {
+				t.Errorf("result is not the right boolean for '%s'. got='%t', want='%t'", tt.input, booleanObj.Value, expected)
+			}
 		}
 	}
 }