Skip to content

Commit

Permalink
Add if/elseif/else statements
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWright committed Oct 5, 2024
1 parent d92a503 commit 1f26dbd
Show file tree
Hide file tree
Showing 16 changed files with 309 additions and 19 deletions.
2 changes: 2 additions & 0 deletions execution/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) {
return objectExprExecutor(e)
case ast.MapExpr:
return mapExprExecutor(e)
case ast.ConditionalExpr:
return conditionalExprExecutor(e)
default:
return nil, fmt.Errorf("unhandled expression type: %T", e)
}
Expand Down
32 changes: 32 additions & 0 deletions execution/execute_conditional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package execution

import (
"fmt"

"github.com/tomwright/dasel/v3/model"
"github.com/tomwright/dasel/v3/selector/ast"
)

func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) {
return func(data *model.Value) (*model.Value, error) {
cond, err := ExecuteAST(e.Cond, data)
if err != nil {
return nil, fmt.Errorf("error evaluating condition: %w", err)
}

condBool, err := cond.BoolValue()
if err != nil {
return nil, fmt.Errorf("error converting condition to boolean: %w", err)
}

if condBool {
return ExecuteAST(e.Then, data)
}

if e.Else != nil {
return ExecuteAST(e.Else, data)
}

return model.NewNullValue(), nil
}, nil
}
31 changes: 31 additions & 0 deletions execution/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,35 @@ func TestExecuteSelector_HappyPath(t *testing.T) {
},
}))
})

t.Run("conditional", func(t *testing.T) {
t.Run("true", runTest(testCase{
s: `if (true) { "yes" } else { "no" }`,
out: model.NewStringValue("yes"),
}))
t.Run("false", runTest(testCase{
s: `if (false) { "yes" } else { "no" }`,
out: model.NewStringValue("no"),
}))
t.Run("nested", runTest(testCase{
s: `if (true) { if (true) { "yes" } else { "no" } } else { "no" }`,
out: model.NewStringValue("yes"),
}))
t.Run("nested false", runTest(testCase{
s: `if (true) { if (false) { "yes" } else { "no" } } else { "no" }`,
out: model.NewStringValue("no"),
}))
t.Run("else if", runTest(testCase{
s: `if (false) { "yes" } elseif (true) { "no" } else { "maybe" }`,
out: model.NewStringValue("no"),
}))
t.Run("else if else", runTest(testCase{
s: `if (false) { "yes" } elseif (false) { "no" } else { "maybe" }`,
out: model.NewStringValue("maybe"),
}))
t.Run("if elseif elseif else", runTest(testCase{
s: `if (false) { "yes" } elseif (false) { "no" } elseif (true) { "maybe" } else { "nope" }`,
out: model.NewStringValue("maybe"),
}))
})
}
4 changes: 2 additions & 2 deletions func_len_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ func TestLenFunc(t *testing.T) {
),
)
t.Run(
"False Bool",
"Else Bool",
selectTest(
"falseBool.len()",
data,
[]interface{}{0},
),
)
t.Run(
"True Bool",
"Then Bool",
selectTest(
"trueBool.len()",
data,
Expand Down
2 changes: 1 addition & 1 deletion internal/command/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func deleteFlags(cmd *cobra.Command) {
cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.")

_ = cmd.MarkFlagFilename("file")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/command/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func putFlags(cmd *cobra.Command) {
cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.")

_ = cmd.MarkFlagFilename("file")
}
Expand Down
2 changes: 1 addition & 1 deletion internal/command/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func selectFlags(cmd *cobra.Command) {
cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.")
cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.")
cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.")
cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.")
cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.")

_ = cmd.MarkFlagFilename("file")
}
Expand Down
6 changes: 3 additions & 3 deletions model/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (v *Value) Kind() reflect.Kind {
func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value {
res := v.Value
for {
if !slices.Contains(kinds, res.Kind()) {
if !slices.Contains(kinds, res.Kind()) || res.IsNil() {
return NewValue(res)
}
res = res.Elem()
Expand All @@ -70,7 +70,7 @@ func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) {
if res.Type() == t {
return NewValue(res), nil
}
if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr {
if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
res = res.Elem()
continue
}
Expand All @@ -84,7 +84,7 @@ func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) {
if res.Kind() == k {
return NewValue(res), nil
}
if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr {
if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() {
res = res.Elem()
continue
}
Expand Down
12 changes: 12 additions & 0 deletions model/value_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import (
"slices"
)

func NewNullValue() *Value {
return NewValue(reflect.New(reflect.TypeFor[any]()))
}

func (v *Value) IsNull() bool {
return v.UnpackKinds(reflect.Ptr, reflect.Interface).isNull()
}

func (v *Value) isNull() bool {
return v.Value.IsNil()
}

func NewStringValue(x string) *Value {
res := reflect.New(reflect.TypeFor[string]())
res.Elem().Set(reflect.ValueOf(x))
Expand Down
8 changes: 8 additions & 0 deletions selector/ast/expression_complex.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ type GroupExpr struct {
}

func (GroupExpr) expr() {}

type ConditionalExpr struct {
Cond Expr
Then Expr
Else Expr
}

func (ConditionalExpr) expr() {}
3 changes: 3 additions & 0 deletions selector/lexer/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const (
LessThanOrEqual
Exclamation
Null
If
Else
ElseIf
)

type Tokens []Token
Expand Down
35 changes: 29 additions & 6 deletions selector/lexer/tokenize.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package lexer
import (
"strings"
"unicode"

"github.com/tomwright/dasel/v3/internal/ptr"
)

type Tokenizer struct {
Expand Down Expand Up @@ -172,14 +174,35 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
default:
pos := p.i

if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "null") {
return NewToken(Null, p.src[pos:pos+4], p.i, 4), nil
matchStr := func(pos int, m string, caseInsensitive bool, kind TokenKind) *Token {
l := len(m)
if pos+(l-1) >= p.srcLen {
return nil
}
other := p.src[pos : pos+l]
if m == other || caseInsensitive && strings.EqualFold(m, other) {
return ptr.To(NewToken(kind, other, pos, l))
}
return nil
}

if t := matchStr(pos, "null", true, Null); t != nil {
return *t, nil
}
if t := matchStr(pos, "true", true, Bool); t != nil {
return *t, nil
}
if t := matchStr(pos, "false", true, Bool); t != nil {
return *t, nil
}
if t := matchStr(pos, "elseif", false, ElseIf); t != nil {
return *t, nil
}
if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "true") {
return NewToken(Bool, p.src[pos:pos+4], p.i, 4), nil
if t := matchStr(pos, "if", false, If); t != nil {
return *t, nil
}
if pos+4 < p.srcLen && strings.EqualFold(p.src[pos:pos+5], "false") {
return NewToken(Bool, p.src[pos:pos+5], p.i, 5), nil
if t := matchStr(pos, "else", false, Else); t != nil {
return *t, nil
}

if unicode.IsDigit(rune(p.src[pos])) {
Expand Down
9 changes: 9 additions & 0 deletions selector/lexer/tokenize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func TestTokenizer_Parse(t *testing.T) {
},
}))

t.Run("if", runTest(testCase{
in: `if elseif else`,
out: []TokenKind{
If,
ElseIf,
Else,
},
}))

t.Run("everything", runTest(testCase{
in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null",
out: []TokenKind{
Expand Down
104 changes: 104 additions & 0 deletions selector/parser/parse_if.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package parser

import (
"github.com/tomwright/dasel/v3/selector/ast"
"github.com/tomwright/dasel/v3/selector/lexer"
)

func parseIfBody(p *Parser) (ast.Expr, error) {
return p.parseExpressionsFromTo(lexer.OpenCurly, lexer.CloseCurly, []lexer.TokenKind{}, true, bpDefault)
}

func parseIfCondition(p *Parser) (ast.Expr, error) {
return p.parseExpressionsFromTo(lexer.OpenParen, lexer.CloseParen, []lexer.TokenKind{}, true, bpDefault)
}

func parseIf(p *Parser) (ast.Expr, error) {
p.pushScope(scopeIf)
defer p.popScope()

if err := p.expect(lexer.If); err != nil {
return nil, err
}
p.advance()

var exprs []*ast.ConditionalExpr

process := func(parseCond bool) error {
var err error
var cond ast.Expr
if parseCond {
cond, err = parseIfCondition(p)
if err != nil {
return err
}
}

body, err := parseIfBody(p)
if err != nil {
return err
}

exprs = append(exprs, &ast.ConditionalExpr{
Cond: cond,
Then: body,
})

return nil
}

if err := process(true); err != nil {
return nil, err
}

for p.current().IsKind(lexer.ElseIf) {
p.advance()

if err := process(true); err != nil {
return nil, err
}
}

if p.current().IsKind(lexer.Else) {
p.advance()

body, err := parseIfBody(p)
if err != nil {
return nil, err
}
exprs[len(exprs)-1].Else = body
}

for i := len(exprs) - 1; i >= 0; i-- {
if i > 0 {
exprs[i-1].Else = *exprs[i]
}
}

return *exprs[0], nil
}

func (p *Parser) parseExpressionsFromTo(
from lexer.TokenKind,
to lexer.TokenKind,
splitOn []lexer.TokenKind,
requireExpressions bool,
bp bindingPower,
) (ast.Expr, error) {
if err := p.expect(from); err != nil {
return nil, err
}
p.advance()

t, err := p.parseExpressions(
[]lexer.TokenKind{to},
splitOn,
requireExpressions,
bp,
)
if err != nil {
return nil, err
}

return t, nil
}
13 changes: 8 additions & 5 deletions selector/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
scopeObject scope = "object"
scopeMap scope = "map"
scopeGroup scope = "group"
scopeIf scope = "if"
)

type Parser struct {
Expand Down Expand Up @@ -144,11 +145,11 @@ func (p *Parser) Parse() (ast.Expr, error) {
}

func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) {
defer func() {
if err == nil {
err = p.expectEndOfExpression()
}
}()
//defer func() {
// if err == nil {
// err = p.expectEndOfExpression()
// }
//}()

switch p.current().Kind {
case lexer.String:
Expand All @@ -169,6 +170,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) {
left, err = parseVariable(p)
case lexer.OpenParen:
left, err = parseGroup(p)
case lexer.If:
left, err = parseIf(p)
default:
return nil, &UnexpectedTokenError{
Token: p.current(),
Expand Down
Loading

0 comments on commit 1f26dbd

Please sign in to comment.