Repository: abs-lang/abs
Pull request #196 (2019-02-20 06:19:24)

Tracking PR for 1.3.x

ntwrick: @odino: I see that we have a large number of open topic branches, most of which I am assuming are already merged. Can we delete these branches leaving just the release tracking branches and the current WIP?
ntwrick: I will kill my branches, but maybe you should do the others so I don't accidently kill your WIP :-)
odino: I'm gonna use ABS :)

```
branches = `git branch -r`
for branch in branches.lines().map(trim) { echo("%s: %s", branch, `git checkout $branch; git --no-pager log -1 --pretty=format:'%an'`) }
```

result:

```
origin/1.0.x: odino
origin/1.1.x: odino
origin/1.2.x: odino
origin/1.3.x: odino
origin/HEAD -> origin/master: odino
origin/add-code-of-conduct-1: Alessandro Nadalin
origin/add-license-1: Alessandro Nadalin
origin/allowing-single-quotes: odino
origin/background-commands: odino
origin/backport-152: odino
origin/backtick-commands: odino
origin/badges: Alessandro Nadalin
origin/bats-testing: Rick Arnold
origin/better-illegal-tokens-when-parsing-numbers: odino
origin/bitwise-operators: odino
origin/cli-flags-parsing: odino
origin/contributing: Alessandro Nadalin
origin/destructuring-gotchas: odino
origin/destructuring-hashes: Alessandro Nadalin
origin/doc-command-executor: odino
origin/elseif: odino
origin/fix-ifelse-parser: odino
origin/issue-171-alex: odino
origin/json-null: odino
origin/json-on-all-types: odino
origin/key-in-hash: odino
origin/make-hashes-iterable: odino
origin/master: odino
origin/mount-abs-history-in-dev: odino
origin/number-math-rounding: Alessandro Nadalin
origin/number-output: odino
origin/os-builds: odino
origin/parse-exponential-notation: Alessandro Nadalin
origin/pwd: odino
origin/refactored-release-script: odino
origin/release-1.1.0: odino
origin/reset-of-iterables: odino
origin/similarity-numbers: odino
origin/sleep: Alessandro Nadalin
origin/string-in: odino
origin/travis: odino
```

With this I know which branches I can safely kill, you can do the same for yours :-P
odino: @mingwho BTW would you mind sending another PR to update the docs for `for else`?
odino: @ntwrick what do you think, shall we release 1.3? There were a few more things I would have liked to include in it but I think it's better to release often rather than waiting for big stuff to pile up (might take a while)
ntwrick: Agree. Let 'er rip.

On Sat, Apr 27, 2019, 7:30 AM Alessandro Nadalin <notifications@github.com>
wrote:

> @ntwrick <https://github.com/ntwrick> what do you think, shall we release
> 1.3? There were a few more things I would have liked to include in it but I
> think it's better to release often rather than waiting for big stuff to
> pile up (might take a while)
>
> —
> You are receiving this because you were mentioned.
>
>
> Reply to this email directly, view it on GitHub
> <https://github.com/abs-lang/abs/pull/196#issuecomment-487278235>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ACIDOGIIGMCNEPB7TKNHLH3PSQ2OHANCNFSM4GYR65LQ>
> .
>


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d929309..dee0875 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@ repository.
 
 Since we follow [semver](https://semver.org/),
 when a new feature is released we don't backport it but simply
-create a new version branch, such as `1.2.x`. Bugs, instead,
+create a new version branch, such as `1.3.x`. Bugs, instead,
 might be backported from `1.1.0` to, for example, `1.0.x` and we
 will have a new [release](https://github.com/abs-lang/abs/releases),
 say `1.0.1` for the `1.0.x` version branch.
diff --git a/Dockerfile b/Dockerfile
index a1267e0..71ab6ca 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.11
+FROM golang:1.12
 
 RUN apt-get update
 RUN apt-get install bash make git curl jq -y
@@ -14,11 +14,4 @@ COPY . /go/src/github.com/abs-lang/abs
 WORKDIR /go/src/github.com/abs-lang/abs
 RUN go get -d -v ./...
 
-# This is simply done because Go
-# will build faster. A docker build
-# is probably less frequent than an ABS
-# build, so...
-RUN make build_simple
-RUN ./builds/abs ./scripts/release.abs
-
 CMD bash
diff --git a/ast/ast.go b/ast/ast.go
index 99de537..b88ed97 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -317,6 +317,7 @@ type ForInExpression struct {
 	Iterable Expression      // An expression that should return an iterable ([1, 2, 3] or x in 1..10)
 	Key      string
 	Value    string
+	Alternative *BlockStatement
 }
 
 func (fie *ForInExpression) expressionNode()      {}
@@ -334,6 +335,11 @@ func (fie *ForInExpression) String() string {
 	out.WriteString(fie.Iterable.String())
 	out.WriteString(fie.Block.String())
 
+	if fie.Alternative != nil {
+		out.WriteString("else")
+		out.WriteString(fie.Alternative.String())
+	}
+
 	return out.String()
 }
 
diff --git a/docs/installer.sh b/docs/installer.sh
index 13dfb4a..5c1db36 100644
--- a/docs/installer.sh
+++ b/docs/installer.sh
@@ -24,7 +24,7 @@ MACHINE_TYPE=$(uname -m)
 if [ "${MACHINE_TYPE}" = 'x86_64' ]; then
   ARCH="amd64"
 fi
-VERSION=1.2.0
+VERSION=1.3.0
 
 echo "Trying to detect the details of your architecture."
 echo ""
diff --git a/docs/misc/technical-details.md b/docs/misc/technical-details.md
index 34bf06c..afedede 100644
--- a/docs/misc/technical-details.md
+++ b/docs/misc/technical-details.md
@@ -205,7 +205,7 @@ ERROR: type mismatch: NULL + NUMBER
 
 ## Roadmap
 
-We're currently working on [1.3](https://github.com/abs-lang/abs/milestone/10).
+We're currently working on [1.4](https://github.com/abs-lang/abs/milestone/11).
 
 ## Next
 
diff --git a/docs/syntax/for.md b/docs/syntax/for.md
index b26563e..0ef773c 100644
--- a/docs/syntax/for.md
+++ b/docs/syntax/for.md
@@ -97,6 +97,25 @@ echo(k) # "hello world"
 echo(v) # v is not defined
 ```
 
+## For ... else ...
+
+`For` loops can also have `else` clause which executes if 
+the list in the `for` condition is empty.
+
+For example, when we run a database query like the following,
+if the "users" list is empty, we will only execute the statements
+inside the `else` clause.
+
+``` bash
+users = db.query("SELECT students WHERE age > 20")
+
+for user in users {
+  print(user)
+} else {
+  print("We don't have students above the age of 20")
+}
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/docs/syntax/system-commands.md b/docs/syntax/system-commands.md
index d1572d8..89ee8b5 100644
--- a/docs/syntax/system-commands.md
+++ b/docs/syntax/system-commands.md
@@ -39,7 +39,48 @@ if `ls -la`.ok {
     echo("hello world")
 }
 ```
+It is also possible to execute a shell command without capturing its
+input or output using the `exec(command)` function. This allows long running
+or interactive programs to be run using the terminal's Standard IO
+(stdin, stdout, stderr). For example:
+```bash
+exec("sudo visudo")
+```
+would open the default text editor in super user mode on the /etc/sudoers file.
+
+Unlike the normal backtick command execution syntax above,
+the `exec(command)` function call does not return a result string unless it fails. 
+Therefore, the `exec(command)` may be the last command executed in a script
+file leaving the executed command in charge of the terminal IO until it
+terminates.  
+
+For example, an ABS script might be used to marshall the command line args
+for an interactive program such as the nano editor:
 
+``` bash
+$ cat abs/tests/test-exec.abs
+# marshall the args for the nano editor
+# if the filename is not given in the args, prompt for it
+# if the file is located outside the user's home dir, invoke sudo nano filename
+
+cmd = 'nano'
+filename = arg(2)
+homedir = env("HOME")
+
+while filename == '' {
+    echo("Please enter file name for %s: ", cmd)
+    filename = stdin()
+}
+
+if filename.prefix('~/') || filename.prefix(homedir) {
+    sudo = ''
+} else {
+    sudo = 'sudo'
+}
+
+# execute the command with live stdIO
+exec("$sudo $cmd $filename")
+```
 ## Executing commands in background
 
 Sometimes you might want to execute a command in
@@ -73,6 +114,26 @@ 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
+```
+Also note that when an `exec()` command string terminates with an `&`, 
+the `exec(command)` function will terminate immediately after launching
+the command which will run independently in the background.
+This means that the command must either terminate on its own or be killed
+using `pkill` or similar. This way an ABS script can launch a true daemon
+process that may operate on its own outside of ABS. For example you can
+reboot a remote computer via ssh without interacting with it:
+```bash
+exec("ssh user@host.local 'sudo reboot' &")
+```
+
 ## Interpolation
 
 You can also replace parts of the command with variables
@@ -83,6 +144,12 @@ file = "cpuinfo"
 x = $(cat /proc/$file)
 echo(x) # processor: 0\nvendor_id: GenuineIntel...
 ```
+or interpolation within an `exec(command`)
+```bash
+cmd = args(2)
+filename = args(3)
+exec("sudo $cmd $filename")
+```
 
 and if you need `$` literals in your command, you
 simply need to escape them with a `\`:
diff --git a/docs/types/builtin-function.md b/docs/types/builtin-function.md
index d6f6390..7418ed2 100644
--- a/docs/types/builtin-function.md
+++ b/docs/types/builtin-function.md
@@ -234,7 +234,7 @@ $ cat ~/.absrc
 source("~/abs/lib/library.abs")
 
 $ abs
-Hello user, welcome to the ABS (1.2.0) programming language!
+Hello user, welcome to the ABS (1.3.0) programming language!
 Type 'quit' when you are done, 'help' if you get lost!
 ⧐ adder(1, 2)
 3
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index 95e46b4..86a85df 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -6,7 +6,6 @@ import (
 	"math"
 	"os"
 	"os/exec"
-	"regexp"
 	"runtime"
 	"strconv"
 	"strings"
@@ -790,22 +789,25 @@ func evalForInExpression(
 			i.Reset()
 		}()
 
-		return loopIterable(i.Next, env, fie)
+		return loopIterable(i.Next, env, fie, 0)
 	case *object.Builtin:
 		if i.Next == nil {
 			return newError(fie.Token, "builtin function cannot be used in loop")
 		}
 
-		return loopIterable(i.Next, env, fie)
+		return loopIterable(i.Next, env, fie, 0)
 	default:
 		return newError(fie.Token, "'%s' is a %s, not an iterable, cannot be used in for loop", i.Inspect(), i.Type())
 	}
 }
 
-func loopIterable(next func() (object.Object, object.Object), env *object.Environment, fie *ast.ForInExpression) object.Object {
+func loopIterable(next func() (object.Object, object.Object), env *object.Environment, fie *ast.ForInExpression, index int64) object.Object {
 	k, v := next()
 
 	if k == nil || v == EOF {
+		if index == 0 && fie.Alternative != nil {
+			return Eval(fie.Alternative, env)
+		}
 		return NULL
 	}
 
@@ -820,7 +822,7 @@ func loopIterable(next func() (object.Object, object.Object), env *object.Enviro
 	}
 
 	if k != nil {
-		return loopIterable(next, env, fie)
+		return loopIterable(next, env, fie, index+1)
 	}
 
 	return NULL
@@ -1081,32 +1083,9 @@ 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,})")
-	cmd = re.ReplaceAllStringFunc(cmd, func(m string) string {
-		// If the string starts with a backslash,
-		// that's an escape, so we should replace
-		// it with the remaining portion of the match.
-		// \$VAR becomes $VAR
-		if string(m[0]) == "\\" {
-			return m[1:]
-		}
-
-		// If the string starts with $, then
-		// it's an interpolation. Let's
-		// replace $VAR with the variable
-		// named VAR in the ABS' environment.
-		// If the variable is not found, we
-		// just dump an empty string
-		v, ok := env.Get(m[1:])
 
-		if !ok {
-			return ""
-		}
-
-		return v.Inspect()
-	})
+	// interpolate any $vars in the cmd string
+	cmd = util.InterpolateStringVars(cmd, env)
 
 	// A background command ends with a '&'
 	background := len(cmd) > 1 && cmd[len(cmd)-1] == '&'
@@ -1149,7 +1128,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()
 	}
@@ -1169,16 +1155,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..5e07049 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -8,7 +8,9 @@ import (
 	"math"
 	"math/big"
 	"os"
+	"os/exec"
 	"os/user"
+	"runtime"
 	"sort"
 	"strconv"
 	"strings"
@@ -237,6 +239,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},
@@ -319,6 +325,11 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.STRING_OBJ},
 			Fn:    sourceFn,
 		},
+		// exec(command) -- execute command with interactive stdIO
+		"exec": &object.Builtin{
+			Types: []string{object.STRING_OBJ},
+			Fn:    execFn,
+		},
 	}
 }
 
@@ -1161,6 +1172,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}})
@@ -1521,3 +1553,43 @@ func sourceFn(tok token.Token, args ...object.Object) object.Object {
 
 	return evaluated
 }
+
+func execFn(tok token.Token, args ...object.Object) object.Object {
+	err := validateArgs(tok, "exec", args, 1, [][]string{{object.STRING_OBJ}})
+	if err != nil {
+		return err
+	}
+	cmd := args[0].Inspect()
+	cmd = strings.Trim(cmd, " ")
+
+	// interpolate any $vars in the cmd string
+	cmd = util.InterpolateStringVars(cmd, globalEnv)
+
+	var commands []string
+	var executor string
+	if runtime.GOOS == "windows" {
+		commands = []string{"/C", cmd}
+		executor = "cmd.exe"
+	} else { //assume it's linux, darwin, freebsd, openbsd, solaris, etc
+		// invoke bash commands with login option (-l) --
+		// this allows the use of commands in $PATH
+		commands = []string{"-lc", cmd}
+		executor = "bash"
+	}
+	// set up command to execute using our stdIO
+	c := exec.Command(executor, commands...)
+	c.Env = os.Environ()
+	c.Stdin = os.Stdin
+	c.Stdout = os.Stdout
+	c.Stderr = os.Stderr
+
+	// N.B. that a bash command may end with '&' --
+	// in this case bash will launch it as a daemon process and then exit c.Run() immediately
+	// this may require pkill to terminate the daemon process using the pid
+	runErr := c.Run()
+
+	if runErr != nil {
+		return &object.String{Value: runErr.Error()}
+	}
+	return NULL
+}
diff --git a/main.go b/main.go
index c8e35cf..0a71ba7 100644
--- a/main.go
+++ b/main.go
@@ -7,7 +7,7 @@ import (
 	"github.com/abs-lang/abs/repl"
 )
 
-var VERSION = "1.2.0"
+var VERSION = "1.3.0"
 
 // The ABS interpreter
 func main() {
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:
diff --git a/parser/parser.go b/parser/parser.go
index f1ad0fe..df57540 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -677,6 +677,21 @@ func (p *Parser) parseForInExpression(initialExpression *ast.ForExpression) ast.
 
 	expression.Block = p.parseBlockStatement()
 
+	// for x in [] {
+	// 	echo("shouldn't be here")
+	// } else {
+	//  echo("ok")
+	// }
+	if p.peekTokenIs(token.ELSE) {
+		p.nextToken()
+
+		if !p.expectPeek(token.LBRACE) {
+			return nil
+		}
+
+		expression.Alternative = p.parseBlockStatement()
+	}
+
 	return expression
 }
 
diff --git a/util/util.go b/util/util.go
index 511db3d..f713258 100644
--- a/util/util.go
+++ b/util/util.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"os/user"
 	"path/filepath"
+	"regexp"
 	"strconv"
 
 	"github.com/abs-lang/abs/object"
@@ -55,3 +56,33 @@ func GetEnvVar(env *object.Environment, varName, defaultVal string) string {
 	}
 	return value
 }
+
+// InterpolateStringVars (str, env)
+// return input string with $vars interpolated from environment
+func InterpolateStringVars(str string, env *object.Environment) string {
+	// Match all strings preceded by
+	// a $ or a \$
+	re := regexp.MustCompile("(\\\\)?\\$([a-zA-Z_]{1,})")
+	str = re.ReplaceAllStringFunc(str, func(m string) string {
+		// If the string starts with a backslash,
+		// that's an escape, so we should replace
+		// it with the remaining portion of the match.
+		// \$VAR becomes $VAR
+		if string(m[0]) == "\\" {
+			return m[1:]
+		}
+		// If the string starts with $, then
+		// it's an interpolation. Let's
+		// replace $VAR with the variable
+		// named VAR in the ABS' environment.
+		// If the variable is not found, we
+		// just dump an empty string
+		v, ok := env.Get(m[1:])
+
+		if !ok {
+			return ""
+		}
+		return v.Inspect()
+	})
+	return str
+}


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index d8c18c6..15cb3ac 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -332,6 +332,39 @@ func TestForInExpressions(t *testing.T) {
 	}
 }
 
+func TestForElseExpressions(t *testing.T) {
+	tests := []struct {
+		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},
+	}
+
+	for _, tt := range tests {
+		evaluated := testEval(tt.input)
+
+		switch ev := tt.expected.(type) {
+		case int:
+			testNumberObject(t, evaluated, float64(ev))
+		case bool:
+			testBooleanObject(t, evaluated, ev)
+		default:
+			errObj, ok := evaluated.(*object.Error)
+			if !ok {
+				t.Errorf("no error object returned. got=%T(%+v)", evaluated, evaluated)
+				continue
+			}
+			logErrorWithPosition(t, errObj.Message, ev)
+		}
+	}
+}
+
 func TestWhileExpressions(t *testing.T) {
 	tests := []struct {
 		input    string
@@ -1139,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 {
@@ -1157,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)
+			}
 		}
 	}
 }
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 5308765..92252af 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -912,6 +912,61 @@ func TestForInExpression(t *testing.T) {
 	}
 }
 
+func TestForElseExpression(t *testing.T) {
+	tests := []struct {
+		input string
+	}{
+		{`for x in [] { x } else { y }`},
+		{`for x in {} { x } else { y }`},
+	}
+
+	for _, tt := range tests {
+		l := lexer.New(tt.input)
+		p := New(l)
+		program := p.ParseProgram()
+		checkParserErrors(t, p)
+
+		if len(program.Statements) != 1 {
+			t.Fatalf("program.Statements does not contain %d statements. got=%d\n", 1, len(program.Statements))
+		}
+		stmt, ok := program.Statements[0].(*ast.ExpressionStatement)
+		if !ok {
+			t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", program.Statements[0])
+		}
+
+		exp, ok := stmt.Expression.(*ast.ForInExpression)
+		if !ok {
+			t.Fatalf("stmt.Expression is not ast.ForExpression. got=%T", stmt.Expression)
+		}
+
+		if len(exp.Block.Statements) != 1 {
+			t.Errorf("block is not 1 statements. got=%d\n", len(exp.Block.Statements))
+		}
+
+		block, ok := exp.Block.Statements[0].(*ast.ExpressionStatement)
+		if !ok {
+			t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", exp.Block.Statements[0])
+		}
+
+		if !testIdentifier(t, block.Expression, "x") {
+			return
+		}
+
+		if len(exp.Alternative.Statements) != 1 {
+			t.Errorf("Alternative is not 1 statements. got=%d\n", len(exp.Block.Statements))
+		}
+
+		alternative, ok := exp.Alternative.Statements[0].(*ast.ExpressionStatement)
+		if !ok {
+			t.Fatalf("Alternative statements[0] is not ast.ExpressionStatement. got=%T", exp.Block.Statements[0])
+		}
+
+		if !testIdentifier(t, alternative.Expression, "y") {
+			return
+		}
+	}
+}
+
 func TestFunctionLiteralParsing(t *testing.T) {
 	input := `f(x, y) { x + y; }`
 
diff --git a/tests/test-exec.abs b/tests/test-exec.abs
new file mode 100644
index 0000000..2718721
--- /dev/null
+++ b/tests/test-exec.abs
@@ -0,0 +1,21 @@
+# marshall the args for the nano editor
+# if the filename is not given in the args, prompt for it
+# if the file is located outside the user's home dir, invoke sudo nano filename
+
+cmd = 'nano'
+filename = arg(2)
+homedir = env("HOME")
+
+while filename == '' {
+    echo("Please enter file name for %s: ", cmd)
+    filename = stdin()
+}
+
+if filename.prefix('~/') || filename.prefix(homedir) {
+    sudo = ''
+} else {
+    sudo = 'sudo'
+}
+
+# execute the command with live stdIO
+exec("$sudo $cmd $filename")
diff --git a/tests/test-str-interpolate.abs b/tests/test-str-interpolate.abs
new file mode 100644
index 0000000..268b2b8
--- /dev/null
+++ b/tests/test-str-interpolate.abs
@@ -0,0 +1,44 @@
+#! /usr/local/bin/abs
+
+from_dir = '/var/log'
+to_dir = '/tmp/log'
+pat = '*.log'
+
+# currently: only command strings get $var interpolation
+# this means that bash $vars are not escaped and str.fmt() is the only way to interpolate raw strings (using %formats) 
+cmd = "mkdir -p %s && cd %s && for f in $(ls %s); do echo $f; cp $f %s/$f; done".fmt(to_dir, from_dir, pat, to_dir)
+echo("%q", cmd) # "mkdir -p /tmp/log && cd /var/log && for f in $(ls *.log); do echo $f; cp $f /tmp/log/$f; done"
+$($cmd)
+
+# proposal 1: all string literals get $var interpolation by default
+# this means that all bash $vars must be retrofitted as \$var in existing code which may break due to unknown $vars 
+# note that default $var string interpolation fails in this example because there is no string var f in the abs env
+# also str.fmt() will receive a pre-interpolated string as its format spec -- this may be hard to work around
+cmd = "mkdir -p $to_dir && cd $from_dir && for f in $(ls $pat); do echo \$f; cp \$f $to_dir/\$f; done"
+echo("%q", cmd) # "mkdir -p /tmp/log && cd /var/log && for f in $(ls *.log); do echo $f; cp $f /tmp/log/$f; done"
+$($cmd)
+
+# proposal 2: upgrade str.fmt() to include $var and ${var[:format]} interpolation in addition to %format
+# this means that string literals do not get interpolated by default because str.fmt() needs a raw format spec
+# furthermore, if a $var string object is not found in the abs env, then we would just leave the $var literal in place for bash
+# this would be a primitive, but useful, form of $var namespace separation between abs and bash
+# thus, only if there is a $var namespace collision would \$var be needed to force the $var into the bash namespace
+
+# a) no default interpolation of string literals means no changes needed to legacy abs string parsing code or scripts
+cmd = "mkdir -p $to_dir && cd $from_dir && for f in $(ls $pat); do echo \$f; cp $f $to_dir/$f; done"
+echo("%q", cmd) # "mkdir -p $to_dir && cd $from_dir && for f in $(ls $pat); do echo \$f; cp $f $to_dir/$f; done"
+
+# b) support for $var and ${var[:format]} interpolation via str.fmt() with no args; 
+# unknown $vars in the abs env such as $f are left untouched and are assumed to be in the bash $var namespace
+# escaped \$vars such as \$f are forced into the bash namespace and the escape backslashes are removed
+cmd = cmd.fmt()
+echo("%q", cmd) # "mkdir -p /tmp/log && cd /var/log && for f in $(ls *.log); do echo $f; cp $f /tmp/log/$f; done"
+$($cmd)
+
+# c) support for mixed $var, ${var[:format]} and %format interpolation via str.fmt(args...)
+# this also implies that legacy str.fmt(args...) %format-only strings still work
+cmd = "mkdir -p $to_dir && cd $from_dir && for f in $(ls %s); do echo $f; cp $f $to_dir/$f; done"
+echo("%q", cmd) # "mkdir -p $to_dir && cd $from_dir && for f in $(ls %s); do echo $f; cp $f $to_dir/$f; done"
+cmd = cmd.fmt(pat)
+echo("%q", cmd) # "mkdir -p /tmp/log && cd /var/log && for f in $(ls *.log); do echo $f; cp $f /tmp/log/$f; done"
+$($cmd)