diff --git a/interp/builtin.go b/interp/builtin.go index 5217bca29..8918e9631 100644 --- a/interp/builtin.go +++ b/interp/builtin.go @@ -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 @@ -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) diff --git a/interp/interp_test.go b/interp/interp_test.go index 6e4acbb6b..25c19868e 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -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))", diff --git a/interp/test_classic.go b/interp/test_classic.go new file mode 100644 index 000000000..622bc364e --- /dev/null +++ b/interp/test_classic.go @@ -0,0 +1,189 @@ +// Copyright (c) 2017, Daniel Martí +// 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 + } +}