Skip to content

Commit

Permalink
interp: support test and [ builtins
Browse files Browse the repository at this point in the history
Initial version, started off from a fairly modified copy of
syntax's code that parses [[ expressions. From that, we had to remove
handling of some tokens like (, &&, and =~. Also had to rework the logic
a bit to merge p.tok into p.val, since we only have strings here.

Need to add more tests for parse errors.

Updates #97.
  • Loading branch information
mvdan committed May 30, 2017
1 parent 217b4c1 commit dbe30ba
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 1 deletion.
18 changes: 17 additions & 1 deletion interp/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func isBuiltin(name string) bool {
"echo", "printf", "break", "continue", "pwd", "cd",
"wait", "builtin", "trap", "type", "source", "command",
"pushd", "popd", "umask", "alias", "unalias", "fg", "bg",
"getopts", "eval":
"getopts", "eval", "test", "[":
return true
}
return false
Expand Down Expand Up @@ -207,6 +207,22 @@ func (r *Runner) builtinCode(pos syntax.Pos, name string, args []string) int {
r2.File = file
r2.Run()
return r2.exit
case "[":
if args[len(args)-1] != "]" {
r.runErr(pos, "[: missing matching ]")
}
args = args[:len(args)-1]
fallthrough
case "test":
p := testParser{
rem: args,
err: func(format string, a ...interface{}) {
r.runErr(pos, format, a...)
},
}
p.next()
expr := p.classicTest("[", 0)
return oneIf(r.bashTest(expr) == "")
case "trap", "source", "command", "pushd", "popd",
"umask", "alias", "unalias", "fg", "bg", "getopts":
r.runErr(pos, "unhandled builtin: %s", name)
Expand Down
46 changes: 46 additions & 0 deletions interp/interp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,52 @@ var fileCases = []struct {
"y\n",
},

// classic test
{
"[ a",
"1:1: [: missing matching ] #JUSTERR",
},
{
"[ a ]",
"",
},
{
"[ a != b ]",
"",
},
{
"[ ! a != b ]",
"exit status 1",
},
{
"[ a -a '' ]",
"exit status 1",
},
{
"[ a -o '' ]",
"",
},
{
"[ 3 -gt 4 ]",
"exit status 1",
},
{
"[ 3 -lt 4 ]",
"",
},
{
"[ -e a ] && echo x; touch a; [ -e a ] && echo y; rm a",
"y\n",
},
{
"test 3 -gt 4",
"exit status 1",
},
{
"test 3 -lt 4",
"",
},

// arithm
{
"echo $((1 == +1))",
Expand Down
189 changes: 189 additions & 0 deletions interp/test_classic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package interp

import (
"github.com/mvdan/sh/syntax"
)

const illegalTok = 0

type testParser struct {
eof bool
val string
rem []string

err func(format string, a ...interface{})
}

func (p *testParser) next() {
if p.eof || len(p.rem) == 0 {
p.eof = true
return
}
p.val = p.rem[0]
p.rem = p.rem[1:]
}

func (p *testParser) followWord(fval string) *syntax.Word {
if p.eof {
p.err("%s must be followed by a word", fval)
}
p.next()
return &syntax.Word{Parts: []syntax.WordPart{
&syntax.Lit{Value: p.val},
}}
}

func (p *testParser) classicTest(fval string, level int) syntax.TestExpr {
var left syntax.TestExpr
if level > 1 {
left = p.testExprBase(fval)
} else {
left = p.classicTest(fval, level+1)
}
if left == nil || p.eof {
return left
}
op := testBinaryOp(p.val)
var newLevel int
switch op {
case illegalTok:
p.err("not a valid test operator: %s", p.val)
case syntax.AndTest, syntax.OrTest:
default:
newLevel = 1
}
if newLevel < level {
return left
}
b := &syntax.BinaryTest{
Op: op,
X: left,
}
switch b.Op {
case syntax.AndTest, syntax.OrTest:
p.next()
if b.Y = p.classicTest(b.Op.String(), newLevel); b.Y == nil {
//p.followErrExp(b.OpPos, b.Op.String())
}
default:
p.next()
b.Y = p.followWord(b.Op.String())
}
return b
}

func (p *testParser) testExprBase(fval string) syntax.TestExpr {
if p.eof {
return nil
}
op := testUnaryOp(p.val)
switch op {
case syntax.TsNot:
u := &syntax.UnaryTest{Op: op}
p.next()
u.X = p.classicTest(op.String(), 0)
return u
case illegalTok:
return p.followWord(fval)
default:
u := &syntax.UnaryTest{Op: op}
p.next()
u.X = p.followWord(fval)
return u
}
}

// testUnaryOp is an exact copy of syntax's.
func testUnaryOp(val string) syntax.UnTestOperator {
switch val {
case "!":
return syntax.TsNot
case "-e", "-a":
return syntax.TsExists
case "-f":
return syntax.TsRegFile
case "-d":
return syntax.TsDirect
case "-c":
return syntax.TsCharSp
case "-b":
return syntax.TsBlckSp
case "-p":
return syntax.TsNmPipe
case "-S":
return syntax.TsSocket
case "-L", "-h":
return syntax.TsSmbLink
case "-k":
return syntax.TsSticky
case "-g":
return syntax.TsGIDSet
case "-u":
return syntax.TsUIDSet
case "-G":
return syntax.TsGrpOwn
case "-O":
return syntax.TsUsrOwn
case "-N":
return syntax.TsModif
case "-r":
return syntax.TsRead
case "-w":
return syntax.TsWrite
case "-x":
return syntax.TsExec
case "-s":
return syntax.TsNoEmpty
case "-t":
return syntax.TsFdTerm
case "-z":
return syntax.TsEmpStr
case "-n":
return syntax.TsNempStr
case "-o":
return syntax.TsOptSet
case "-v":
return syntax.TsVarSet
case "-R":
return syntax.TsRefVar
default:
return illegalTok
}
}

// testBinaryOp is like syntax's, but with -a and -o, and without =~.
func testBinaryOp(val string) syntax.BinTestOperator {
switch val {
case "-a":
return syntax.AndTest
case "-o":
return syntax.OrTest
case "==", "=":
return syntax.TsMatch
case "!=":
return syntax.TsNoMatch
case "-nt":
return syntax.TsNewer
case "-ot":
return syntax.TsOlder
case "-ef":
return syntax.TsDevIno
case "-eq":
return syntax.TsEql
case "-ne":
return syntax.TsNeq
case "-le":
return syntax.TsLeq
case "-ge":
return syntax.TsGeq
case "-lt":
return syntax.TsLss
case "-gt":
return syntax.TsGtr
default:
return illegalTok
}
}

0 comments on commit dbe30ba

Please sign in to comment.