treek

Treek, like awk but for tree input. Take JSON or TOML or some other tree like input format and run code on bits of it.
git clone http://shtanton.xyz/git/repo/treek
Log | Files | Refs | README

commit f010bf4fc1cbf1129e19c4bff052a6d50277ad17
parent 3e39549231249ace1bb886aa8b70345c5febca1d
Author: Charlie Stanton <charlie@shtanton.xyz>
Date:   Sat, 20 Aug 2022 11:55:43 +0100

Add context variables to filter patterns and some boolean operators

Diffstat:
MREADME.md | 5+++++
Mmain/eval.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mmain/lex.go | 10++++++++++
Mmain/parser.go | 23++++++++++++++++++++++-
4 files changed, 98 insertions(+), 5 deletions(-)

diff --git a/README.md b/README.md @@ -22,3 +22,8 @@ treek 'people.* {println($0.first_name + " " + $0.last_name)}' ``` treek 'people.*.age {total += $0; count += 1} {println(total / count)}' ``` + +#### Print the first names of all the Johnsons +``` +treek 'people.($0.last_name=="Johnson").first_name' +``` diff --git a/main/eval.go b/main/eval.go @@ -40,6 +40,7 @@ type Value interface{ mul(Value) Value div(Value) Value index(Value) Value + equals(Value) ValueBool } func castToType(v Value, t ValueType) Value { @@ -124,6 +125,13 @@ func (v ValueNull) div(w Value) Value { func (v ValueNull) index(w Value) Value { return ValueNull {} } +func (v ValueNull) equals(w Value) ValueBool { + typ := w.typ() + if typ == TypeNull { + return true + } + return castToType(v, typ).equals(w) +} func (v ValueBool) withAssignment(path []Value, value Value) Value { if len(path) == 0 { @@ -193,6 +201,10 @@ func (v ValueBool) div(w Value) Value { func (v ValueBool) index(w Value) Value { return v } +func (v ValueBool) equals(w Value) ValueBool { + rhs := w.castToBool() + return v == rhs +} func (v ValueNumber) withAssignment(path []Value, value Value) Value { if len(path) == 0 { @@ -250,6 +262,10 @@ func (v ValueNumber) div(w Value) Value { func (v ValueNumber) index(w Value) Value { return v } +func (v ValueNumber) equals(w Value) ValueBool { + rhs := w.castToNumber() + return v == rhs +} func (v ValueString) withAssignment(path []Value, value Value) Value { if len(path) == 0 { @@ -348,6 +364,10 @@ func (v ValueString) index(w Value) Value { // TODO: use proper strings functions here return ValueString(v[index]) } +func (v ValueString) equals(w Value) ValueBool { + rhs := w.castToString() + return v == rhs +} func (v ValueArray) withAssignment(path []Value, value Value) Value { if len(path) == 0 { @@ -449,6 +469,18 @@ func (v ValueArray) index(w Value) Value { index := int(math.Round(float64(w.castToNumber()))) return v[index] } +func (v ValueArray) equals(w Value) ValueBool { + rhs := w.castToArray() + if len(v) != len(rhs) { + return false + } + for i, el := range v { + if !el.equals(rhs[i]) { + return false + } + } + return true +} func (v ValueMap) withAssignment(path []Value, value Value) Value { if len(path) == 0 { @@ -559,6 +591,22 @@ func (v ValueMap) index(w Value) Value { } return res } +func (v ValueMap) equals(w Value) ValueBool { + rhs := w.castToMap() + for key, lvalue := range v { + rvalue, rhsHasValue := rhs[key] + if !rhsHasValue || !bool(lvalue.equals(rvalue)) { + return false + } + } + for key := range rhs { + _, lhsHasValue := v[key] + if !lhsHasValue { + return false + } + } + return true +} type VariableReference string type IndexReference struct { @@ -679,7 +727,7 @@ func (state *EvalState) popAddress() Address { return state.pop().toAddress() } -func (index PatternSegmentIndex) matches(_ *EvalState, pathSegment TreePathSegment) bool { +func (index PatternSegmentIndex) matches(_ *EvalState, path []TreePathSegment, pathSegment TreePathSegment) bool { switch pathSegment.(type) { case string: return string(index) == pathSegment.(string) @@ -690,12 +738,14 @@ func (index PatternSegmentIndex) matches(_ *EvalState, pathSegment TreePathSegme } } -func (filter PatternSegmentFilter) matches(state *EvalState, pathSegment TreePathSegment) bool { +func (filter PatternSegmentFilter) matches(state *EvalState, path []TreePathSegment, pathSegment TreePathSegment) bool { + state.variables["path"] = pathToValueArray(path).clone() + state.variables["$0"] = state.data.getPath(path).clone() result := evalExpr(state, Expression(filter)) return bool(result.castToBool()) } -func (segment PatternSegmentBasic) matches(state *EvalState, pathSegment TreePathSegment) bool { +func (segment PatternSegmentBasic) matches(state *EvalState, path []TreePathSegment, pathSegment TreePathSegment) bool { switch segment { case PatternSegmentAll: return true @@ -710,7 +760,7 @@ func matchPattern(state *EvalState, pattern Pattern, walkItem TreeWalkItem) bool } for i, patternSegment := range pattern.segments { pathSegment := walkItem.path[i] - if !patternSegment.matches(state, pathSegment) { + if !patternSegment.matches(state, walkItem.path[0:i+1], pathSegment) { return false } } @@ -762,6 +812,13 @@ func (instruction InstructionBasic) eval(state *EvalState) { val := state.pop() state.push(val) state.push(val.toValue(state).clone()) + case InstructionEqual: + rhs := state.popValue() + lhs := state.popValue() + state.push(lhs.equals(rhs)) + case InstructionNot: + val := state.popValue().castToBool() + state.push(!val) default: panic("Error: Tried to execute invalid basic instruction") } diff --git a/main/lex.go b/main/lex.go @@ -128,6 +128,9 @@ const ( TokenSubAssign // -= TokenAstAssign // *= TokenDivAssign // /= + TokenEqual // == + TokenNotEqual // != + TokenNot // ! ) type Token struct { @@ -271,6 +274,12 @@ func lexAction(l *lexer) stateFunc { '/': { '=': TokenDivAssign, }, + '=': { + '=': TokenEqual, + }, + '!': { + '=': TokenNotEqual, + }, } charTokens := map[rune]TokenType{ '+': TokenAdd, @@ -281,6 +290,7 @@ func lexAction(l *lexer) stateFunc { ',': TokenComma, ';': TokenSemicolon, '=': TokenAssign, + '!': TokenNot, } r := l.next() charToken, isCharToken := charTokens[r] diff --git a/main/parser.go b/main/parser.go @@ -16,6 +16,8 @@ const ( InstructionAssign InstructionIndex InstructionDup + InstructionEqual + InstructionNot ) type InstructionPushNumber float64 @@ -56,6 +58,10 @@ func (i InstructionBasic) debug() { fmt.Println("Index") case InstructionDup: fmt.Println("Dup") + case InstructionEqual: + fmt.Println("Equal") + case InstructionNot: + fmt.Println("Not") default: fmt.Println("Unknown Basic Instruction") } @@ -91,7 +97,7 @@ const ( type PatternSegment interface { debug() - matches(*EvalState, TreePathSegment) bool + matches(*EvalState, []TreePathSegment, TreePathSegment) bool } type Pattern struct { @@ -242,6 +248,13 @@ func (p *parser) parseExpression(minPower int) (expr Expression, noExpression bo case TokenEOF: p.rewind() return nil, true + case TokenNot: + e, noExpression := p.parseExpression(14) + if noExpression { + panic("Missing expression after !") + } + expr = append(expr, e...) + expr = append(expr, InstructionNot) case TokenNumber: num, err := strconv.ParseFloat(token.val, 64) if err != nil { @@ -315,6 +328,7 @@ func (p *parser) parseExpression(minPower int) (expr Expression, noExpression bo TokenAst: {InstructionMul, 12, 13}, TokenDiv: {InstructionDiv, 12, 13}, TokenAssign: {InstructionAssign, 3, 2}, + TokenEqual: {InstructionEqual, 8, 9}, } binop, isBinop := binops[token.typ] assigns := map[TokenType]InstructionBasic { @@ -354,6 +368,13 @@ func (p *parser) parseExpression(minPower int) (expr Expression, noExpression bo panic("Expected identifier after .") } expr = append(expr, InstructionPushString(index), InstructionIndex) + case token.typ == TokenNotEqual && 8 >= minPower: + e, noExpression := p.parseExpression(9) + if noExpression { + panic("Missing expression after operator") + } + expr = append(expr, e...) + expr = append(expr, InstructionEqual, InstructionNot) default: p.rewind() break oploop