diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf5f342..9d79c82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: - name: Test application run: go test ./... - name: Release application to Github - uses: goreleaser/goreleaser-action@v3 + uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser version: latest diff --git a/go.mod b/go.mod index d44a350..956c286 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,12 @@ require ( ) require ( + github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect + github.com/google/cel-go v0.13.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + golang.org/x/text v0.3.8 // indirect + google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect + google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 94fc186..c605593 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,35 @@ github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU= +github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/root.go b/root.go index a1a209d..aacdade 100644 --- a/root.go +++ b/root.go @@ -107,7 +107,10 @@ func run(r io.Reader, w io.Writer, opts settings) error { } if len(output) == 0 { - return fmt.Errorf("input had no columns to handle") + if b.Len() == 0 { + return fmt.Errorf("input had no columns to handle") + } + return fmt.Errorf("no columns matched the given criteria") } t := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0) diff --git a/tabloid/exprfuncs.go b/tabloid/exprfuncs.go index 0493b53..360c54d 100644 --- a/tabloid/exprfuncs.go +++ b/tabloid/exprfuncs.go @@ -2,14 +2,137 @@ package tabloid import ( "fmt" + "log" "regexp" "strings" "time" "github.com/Knetic/govaluate" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" str2duration "github.com/xhit/go-str2duration/v2" ) +var celFunctions = []cel.EnvOption{ + defineUnaryFunction("isready", celIsReady), + defineUnaryMethod("isready", celIsReady), + + defineUnaryFunction("hasrestarts", celHasRestarts), + defineUnaryMethod("hasrestarts", celHasRestarts), + + defineBinaryFunction("olderthan", celOlderThan), + defineBinaryMethod("olderthan", celOlderThan), + + defineBinaryFunction("olderthanEq", celOlderThanEqual), + defineBinaryMethod("olderthanEq", celOlderThanEqual), + + defineBinaryFunction("newerthan", celNewerThan), + defineBinaryMethod("newerthan", celNewerThan), + + defineBinaryFunction("newerthanEq", celNewerThanEqual), + defineBinaryMethod("newerthanEq", celNewerThanEqual), + + defineBinaryFunction("eqduration", celEqualDuration), + defineBinaryMethod("eqduration", celEqualDuration), +} + +func celIsReady(val ref.Val) ref.Val { + v, ok := val.Value().(string) + if !ok { + return types.ValOrErr(val, "isready function only works with strings") + } + + pieces := strings.FieldsFunc(v, func(r rune) bool { + return r == '/' + }) + + return types.Bool(pieces[0] == pieces[1]) +} + +func celHasRestarts(val ref.Val) ref.Val { + log.Printf("celHasRestarts: %v", val) + v, ok := val.Value().(string) + if !ok { + return types.ValOrErr(val, "hasrestarts function only works with strings") + } + + pieces := strings.FieldsFunc(v, func(r rune) bool { + return r == '/' + }) + + return types.Bool(pieces[0] != pieces[1]) +} + +func celParseDurations(val1, val2 ref.Val) (time.Duration, time.Duration, error) { + v1, ok := val1.Value().(string) + if !ok { + return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments") + } + + v2, ok := val2.Value().(string) + if !ok { + return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments") + } + + d1, err := str2duration.ParseDuration(v1) + if err != nil { + return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %s", err) + } + + d2, err := str2duration.ParseDuration(v2) + if err != nil { + return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %s", err) + } + + return d1, d2, nil +} + +func celOlderThan(val1, val2 ref.Val) ref.Val { + d1, d2, err := celParseDurations(val1, val2) + if err != nil { + return types.ValOrErr(val1, err.Error()) + } + + return types.Bool(d1 > d2) +} + +func celOlderThanEqual(val1, val2 ref.Val) ref.Val { + d1, d2, err := celParseDurations(val1, val2) + if err != nil { + return types.ValOrErr(val1, err.Error()) + } + + return types.Bool(d1 >= d2) +} + +func celNewerThan(val1, val2 ref.Val) ref.Val { + d1, d2, err := celParseDurations(val1, val2) + if err != nil { + return types.ValOrErr(val1, err.Error()) + } + + return types.Bool(d1 < d2) +} + +func celNewerThanEqual(val1, val2 ref.Val) ref.Val { + d1, d2, err := celParseDurations(val1, val2) + if err != nil { + return types.ValOrErr(val1, err.Error()) + } + + return types.Bool(d1 <= d2) +} + +func celEqualDuration(val1, val2 ref.Val) ref.Val { + d1, d2, err := celParseDurations(val1, val2) + if err != nil { + return types.ValOrErr(val1, err.Error()) + } + + return types.Bool(d1 == d2) +} + // isready checks if a string is in the form of / and if the // current value is equal to the total value, false otherwise. func isready(args ...interface{}) (interface{}, error) { @@ -30,11 +153,7 @@ func isready(args ...interface{}) (interface{}, error) { return nil, fmt.Errorf("isready function only accepts string arguments in the form of /") } - if pieces[0] != pieces[1] { - return false, nil - } - - return true, nil + return pieces[0] == pieces[1], nil } var reRestart = regexp.MustCompile(`[1-9]\d*( \([^\)]+\))?`) diff --git a/tabloid/exprfuncs_test.go b/tabloid/exprfuncs_test.go_ similarity index 100% rename from tabloid/exprfuncs_test.go rename to tabloid/exprfuncs_test.go_ diff --git a/tabloid/filter.go b/tabloid/filter.go index 7396e0f..d40d906 100644 --- a/tabloid/filter.go +++ b/tabloid/filter.go @@ -4,7 +4,9 @@ import ( "fmt" "strings" - "github.com/Knetic/govaluate" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) func (t *Tabloid) Filter(columns []Column, expression string) ([]Column, error) { @@ -15,28 +17,57 @@ func (t *Tabloid) Filter(columns []Column, expression string) ([]Column, error) return columns, nil } - expr, err := govaluate.NewEvaluableExpressionWithFunctions(expression, funcs) + vars := make([]*expr.Decl, 0, len(columns)) + for _, column := range columns { + t.logger.Printf("adding column %q to CEL environment", column.ExprTitle) + vars = append(vars, decls.NewVar(column.ExprTitle, decls.Dyn)) + } + + var opts []cel.EnvOption + opts = append(opts, cel.ClearMacros()) + opts = append(opts, cel.Declarations(vars...)) + opts = append(opts, celFunctions...) + + env, err := cel.NewEnv(opts...) + if err != nil { + return nil, fmt.Errorf("unable to create common expression language environment: %w", err) + } + + ast, issues := env.Parse(expression) + if issues != nil && issues.Err() != nil { + return nil, fmt.Errorf("unable to parse expression %q: %w", expression, issues.Err()) + } + + check, issues := env.Check(ast) + if issues != nil && issues.Err() != nil { + return nil, fmt.Errorf("unable to check expression %q: %w", expression, issues.Err()) + } + + prg, err := env.Program(check) if err != nil { - return nil, fmt.Errorf("unable to process expression %q: %w", expression, err) + return nil, fmt.Errorf("unable to create program from expression %q: %w", expression, err) } newColumns := make([]Column, 0, len(columns)) for _, column := range columns { for pos := range column.Values { + // Create a map of information to pass to the evaluation row := make(map[string]interface{}) for _, column := range columns { row[column.ExprTitle] = column.Values[pos] } - result, err := expr.Evaluate(row) + out, det, err := prg.Eval(row) + t.logger.Printf("row: %v", row) + t.logger.Printf("details: %v", det) if err != nil { t.logger.Printf("error type: %T", err) return nil, fmt.Errorf("unable to evaluate expression for row %d: %w", pos+1, err) } - chosen, ok := result.(bool) + chosen, ok := out.Value().(bool) if !ok { - return nil, fmt.Errorf("expression %q must return a boolean value", expression) + return nil, fmt.Errorf("expression %q must return a boolean value, but instead it returned type \"%T\"", expression, out.Value()) } if chosen { diff --git a/tabloid/utils.go b/tabloid/utils.go index 1013c9c..383c924 100644 --- a/tabloid/utils.go +++ b/tabloid/utils.go @@ -4,6 +4,9 @@ import ( "fmt" "strings" "unicode" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" ) type DuplicateColumnTitleError struct { @@ -32,3 +35,19 @@ func fnKey(s string) string { return string(out) } + +func defineUnaryFunction(name string, unaryFn func(ref.Val) ref.Val) cel.EnvOption { + return cel.Function(name, cel.Overload(name+"_global_unary_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(unaryFn))) +} + +func defineUnaryMethod(name string, unaryFn func(ref.Val) ref.Val) cel.EnvOption { + return cel.Function(name, cel.MemberOverload(name+"_method_unary_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(unaryFn))) +} + +func defineBinaryFunction(name string, binaryFn func(ref.Val, ref.Val) ref.Val) cel.EnvOption { + return cel.Function(name, cel.Overload(name+"_global_binary_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType, cel.BinaryBinding(binaryFn))) +} + +func defineBinaryMethod(name string, binaryFn func(ref.Val, ref.Val) ref.Val) cel.EnvOption { + return cel.Function(name, cel.MemberOverload(name+"_method_binary_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType, cel.BinaryBinding(binaryFn))) +}