From f8947011d6544cb48c179b855cbeea47c8aeb628 Mon Sep 17 00:00:00 2001 From: IfanTsai Date: Wed, 10 Aug 2022 00:17:20 +0800 Subject: [PATCH] implement built-in function @ to run shell and upgrade golang to 1.19 --- .github/workflows/benchmark.yml | 22 +++++++-------- .github/workflows/test.yml | 4 +-- .golangci.yml | 1 + go.mod | 18 +++++------- go.sum | 33 +++++++++++----------- interpreter/builtin.go | 33 +++++++++++++++++++--- interpreter/interpreter.go | 4 ++- interpreter/interpreter_test.go | 12 ++++++++ lexer/lexer.go | 16 +++++++---- parser/node.go | 34 ++++++++++++++++++++++ parser/parser.go | 50 +++++++++++++++++++++++++-------- 11 files changed, 165 insertions(+), 62 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index f555e4f..62c30f5 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,25 +1,25 @@ name: Run benchmark tests - -on: + +on: schedule: - cron: '0 0 * * *' - push: + push: branches: [ master ] pull_request: branches: [ master ] - -jobs: + +jobs: benchmark: name: Benchmark Test runs-on: ubuntu-latest - - steps: + + steps: - uses: actions/checkout@v2 - + - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.18 - + go-version: 1.19 + - name: Benchmark - run: go test -run=none -v --cover ./... -benchmem --bench=. \ No newline at end of file + run: go test -run=none -v --cover ./... -benchmem --bench=. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a17aad..9fe48c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.45.2 + version: v1.48.0 unint-test: name: Unint Test @@ -29,7 +29,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.18 + go-version: 1.19 - name: Test run: go test -v -cover ./... diff --git a/.golangci.yml b/.golangci.yml index e8ff8a1..fdacc85 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -55,4 +55,5 @@ linters: - ireturn # Do not check the return object is interface - gocyclo - cyclop + - exhaustruct enable-all: true diff --git a/go.mod b/go.mod index ec927d1..c7c092a 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,21 @@ module github.com/IfanTsai/jirachi -go 1.18 +go 1.19 require ( - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e + github.com/chzyer/readline v1.5.1 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e ) -require ( - github.com/IfanTsai/go-lib v0.0.0-20220225122438-322a14038807 - github.com/chzyer/test v0.0.0-20210722231415-061457976a23 // indirect -) +require github.com/IfanTsai/go-lib v0.0.0-20220723175004-1e4694e968d7 require ( - github.com/chzyer/logex v1.1.10 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/exp v0.0.0-20220328175248-053ad81199eb // indirect - golang.org/x/sys v0.0.0-20220403020550-483a9cbc67c0 // indirect + golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/go.sum b/go.sum index 2cac504..f580fb8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -github.com/IfanTsai/go-lib v0.0.0-20220225122438-322a14038807 h1:TFB1keU9wjC1yCIQx07irBPQH0aQIHJUFTbgeKx25Ek= -github.com/IfanTsai/go-lib v0.0.0-20220225122438-322a14038807/go.mod h1:bseMhaFLXs6QUFvZniJLnDwuS4Ktc2pOpcIT0VKMqV4= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/IfanTsai/go-lib v0.0.0-20220723175004-1e4694e968d7 h1:aM/LmlsrslHScURXiAqekPCajoiycKFT3E6bzyFKvsw= +github.com/IfanTsai/go-lib v0.0.0-20220723175004-1e4694e968d7/go.mod h1:Jm1YEsJ/pF3oYAlnkLI+FbQw8ceqIDu69DnbbkZhUH0= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,15 +19,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/exp v0.0.0-20220328175248-053ad81199eb h1:pC9Okm6BVmxEw76PUu0XUbOTQ92JX11hfvqTjAV3qxM= -golang.org/x/exp v0.0.0-20220328175248-053ad81199eb/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/sys v0.0.0-20220403020550-483a9cbc67c0 h1:PgUUmg0gNMIPY2WafhL/oLyQGw+kdTNPlVWOjltpp3w= -golang.org/x/sys v0.0.0-20220403020550-483a9cbc67c0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs= +golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interpreter/builtin.go b/interpreter/builtin.go index a1e8bcf..674af86 100644 --- a/interpreter/builtin.go +++ b/interpreter/builtin.go @@ -2,9 +2,11 @@ package interpreter import ( "bufio" + "bytes" "fmt" "io" "os" + "os/exec" "strconv" "github.com/pkg/errors" @@ -28,6 +30,7 @@ var ( IsString = object.NewJBuiltInFunction("is_string", []string{"value"}, ExecuteIsString) IsList = object.NewJBuiltInFunction("is_list", []string{"value"}, ExecuteIsList) IsFunction = object.NewJBuiltInFunction("is_function", []string{"value"}, ExecuteIsFunction) + RunShell = object.NewJBuiltInFunction("run_shell", []string{"text"}, ExecuteRunShell) RunScript = object.NewJBuiltInFunction("run", []string{"filename"}, ExecuteRun) ) @@ -110,8 +113,10 @@ func ExecuteInputNumber(function *object.JBuiltInFunction, args []object.JValue) textBytes, _, _ := bufio.NewReader(os.Stdin).ReadLine() text := string(textBytes) - var number interface{} - var err error + var ( + number interface{} + err error + ) number, err = strconv.Atoi(text) if err != nil { @@ -168,8 +173,7 @@ func ExecuteRun(function *object.JBuiltInFunction, args []object.JValue) (object }, "failed to call run") } - _, err = Run(filename, string(bytes)) - if err != nil { + if _, err = Run(filename, string(bytes)); err != nil { return nil, errors.Wrap(&common.JRunTimeError{ JError: &common.JError{ StartPos: function.StartPos, @@ -182,3 +186,24 @@ func ExecuteRun(function *object.JBuiltInFunction, args []object.JValue) (object return nil, nil } + +func ExecuteRunShell(function *object.JBuiltInFunction, args []object.JValue) (object.JValue, error) { + shellStr := args[0].GetValue().(string) + cmd := exec.Command("/bin/sh", "-c", shellStr) + + var outBuf, errBuf bytes.Buffer + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + + if err := cmd.Run(); err != nil || errBuf.Len() > 0 { + return nil, errors.Wrap(&common.JRunTimeError{ + JError: &common.JError{ + StartPos: function.StartPos, + EndPos: function.EndPos, + }, + Context: function.GetContext(), + }, errBuf.String()) + } + + return object.NewJString(outBuf.String()), nil +} diff --git a/interpreter/interpreter.go b/interpreter/interpreter.go index 493b7ed..47df5a1 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -34,7 +34,9 @@ func init() { Set("is_string", IsString). Set("is_list", IsList). Set("is_function", IsFunction). - Set("run", RunScript) + Set("run", RunScript). + Set("run_shell", RunShell). + Set("@", RunShell) } func Run(filename, text string) (interface{}, error) { diff --git a/interpreter/interpreter_test.go b/interpreter/interpreter_test.go index 6d080c9..da56c58 100644 --- a/interpreter/interpreter_test.go +++ b/interpreter/interpreter_test.go @@ -137,6 +137,18 @@ func TestRun(t *testing.T) { }, }, + { + name: "shell", + source: ` + @"echo -n hello" + `, + checkResult: func(t *testing.T, resValue interface{}, err error) { + t.Helper() + require.NoError(t, err) + require.IsType(t, object.NewJString(nil), resValue) + require.Equal(t, "hello", resValue.(*object.JString).Value) + }, + }, } for i := range testCases { diff --git a/lexer/lexer.go b/lexer/lexer.go index 7573f5b..a4ec84d 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -30,10 +30,12 @@ func NewJLexer(filename, text string) *JLexer { } func (l *JLexer) MakeTokens() ([]*token.JToken, error) { - tokens := make([]*token.JToken, 0, len(l.Text)) + var ( + err error + tok *token.JToken + ) - var err error - var tok *token.JToken + tokens := make([]*token.JToken, 0, len(l.Text)) for advanceAble := l.advance(); advanceAble; { char := l.getCurrentChar() @@ -51,7 +53,7 @@ func (l *JLexer) MakeTokens() ([]*token.JToken, error) { return nil, err } tokens = append(tokens, tok) - case isLetters(char): + case isLetters(char), isAt(char): tok, advanceAble = l.makeIdentifierToken() tokens = append(tokens, tok) case char == '"' || char == '\'': @@ -187,7 +189,7 @@ func (l *JLexer) makeIdentifierToken() (*token.JToken, bool) { for { char := l.getCurrentChar() - if !isLetters(char) && !isDigit(char) { + if !isLetters(char) && !isDigit(char) && !isAt(char) { break } @@ -363,3 +365,7 @@ func isDigit(char byte) bool { func isLetters(char byte) bool { return ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z') || char == '_' } + +func isAt(char byte) bool { + return char == '@' +} diff --git a/parser/node.go b/parser/node.go index 07c89a8..b1b7cfb 100644 --- a/parser/node.go +++ b/parser/node.go @@ -242,6 +242,29 @@ func (n *JIfExprNode) Type() JNodeType { return IfExpr } +func (n *JIfExprNode) String() string { + strBuilder := strings.Builder{} + strBuilder.WriteString("if ") + for index, caseNode := range n.CaseNodes { + if index != 0 { + strBuilder.WriteString(" else if ") + } + + strBuilder.WriteString("(") + strBuilder.WriteString(caseNode[0].String()) + strBuilder.WriteString(") {") + strBuilder.WriteString(caseNode[1].String()) + strBuilder.WriteString("}") + } + + if n.ElseCaseNode != nil { + strBuilder.WriteString(" else ") + strBuilder.WriteString(n.ElseCaseNode.String()) + } + + return strBuilder.String() +} + // JForExprNode is for expression node structure of AST type JForExprNode struct { *JBaseNode // JBaseNode.Token is variable name token @@ -268,6 +291,17 @@ func (n *JWhileExprNode) Type() JNodeType { return WhileExpr } +func (n *JWhileExprNode) String() string { + strBuilder := strings.Builder{} + strBuilder.WriteString("while (") + strBuilder.WriteString(n.ConditionNode.String()) + strBuilder.WriteString(") {") + strBuilder.WriteString(n.BodyNode.String()) + strBuilder.WriteString("}") + + return strBuilder.String() +} + // JFuncDefNode is function definition node structure of AST type JFuncDefNode struct { *JBaseNode // JBaseNode.Token is function name token diff --git a/parser/parser.go b/parser/parser.go index 8cc013e..76f4ecf 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -427,8 +427,6 @@ func (p *JParser) parseIfExprCases(caseKeyword string) ([][2]JNode, JNode, error p.advance() - var cases [][2]JNode - condition, err := p.expr() if err != nil { return nil, nil, err @@ -440,8 +438,11 @@ func (p *JParser) parseIfExprCases(caseKeyword string) ([][2]JNode, JNode, error p.advance() - var elseCase JNode - var newCases [][2]JNode + var ( + cases [][2]JNode // { condition expression, body } + elseCase JNode + newCases [][2]JNode + ) if p.CurrentToken.Type == token.NEWLINE { p.advance() @@ -527,8 +528,10 @@ func (p *JParser) funcDef() (JNode, error) { p.advance() - var body JNode - var err error + var ( + body JNode + err error + ) if p.CurrentToken.Type == token.ARROW { p.advance() @@ -643,6 +646,32 @@ func (p *JParser) call() (JNode, error) { return nil, err } + if _, ok := atom.(*JVarAccessNode); ok { + if val, ok := atom.GetToken().Value.(string); ok && val == "@" { + if p.CurrentToken.Type == token.STRING { + currentToken := p.CurrentToken + p.advance() + + return &JCallExprNode{ + JBaseNode: &JBaseNode{ + StartPos: atom.GetStartPos(), + EndPos: p.CurrentToken.EndPos, + }, + CallNode: atom, + ArgNodes: []JNode{ + &JStringNode{ + JBaseNode: &JBaseNode{ + Token: currentToken, + StartPos: currentToken.StartPos, + EndPos: currentToken.EndPos, + }, + }, + }, + }, nil + } + } + } + if p.CurrentToken.Type == token.LPAREN { p.advance() @@ -854,15 +883,12 @@ func (p *JParser) statements(isBlock bool) (JNode, error) { p.advance() } - var err error - var statementNode JNode - var statementNodes []JNode - - statementNode, err = p.statement() + statementNode, err := p.statement() if err != nil { return nil, err } - statementNodes = append(statementNodes, statementNode) + + statementNodes := []JNode{statementNode} moreStatements := false for {