Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Range expressions #84

Merged
merged 14 commits into from
Oct 14, 2022
32 changes: 30 additions & 2 deletions cmd/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"rare/pkg/expressions/exprofiler"
"rare/pkg/expressions/stdlib"
"rare/pkg/humanize"
"rare/pkg/minijson"
"strconv"
"strings"
"time"

Expand All @@ -19,7 +21,7 @@ func expressionFunction(c *cli.Context) error {
expString = c.Args().First()
noOptimize = c.Bool("no-optimize")
data = c.StringSlice("data")
keys = c.StringSlice("key")
keyPairs = c.StringSlice("key")
benchmark = c.Bool("benchmark")
stats = c.Bool("stats")
)
Expand All @@ -36,13 +38,26 @@ func expressionFunction(c *cli.Context) error {
compiled, err := builder.Compile(expString)
expCtx := expressions.KeyBuilderContextArray{
Elements: data,
Keys: parseKeyValuesIntoMap(keys...),
Keys: parseKeyValuesIntoMap(keyPairs...),
}

if err != nil {
return err
}

// Emulate special keys
{
keys := parseKeyValuesIntoMap(keyPairs...)
expCtx.Keys["src"] = "<args>"
expCtx.Keys["line"] = "0"
expCtx.Keys["."] = buildSpecialKeyJson(nil, keys)
expCtx.Keys["#"] = buildSpecialKeyJson(data, nil)
expCtx.Keys[".#"] = buildSpecialKeyJson(data, keys)
expCtx.Keys["#."] = expCtx.Keys[".#"]
expCtx.Keys["$"] = expressions.MakeArray(data...)
}

// Output results
fmt.Printf("Expression: %s\n", color.Wrap(color.BrightWhite, expString))
result := compiled.BuildKey(&expCtx)
fmt.Printf("Result: %s\n", color.Wrap(color.BrightYellow, result))
Expand Down Expand Up @@ -88,6 +103,19 @@ func parseKeyValue(s string) (string, string) {
return s[:idx], s[idx+1:]
}

func buildSpecialKeyJson(matches []string, values map[string]string) string {
var json minijson.JsonObjectBuilder
json.Open()
for i, val := range matches {
json.WriteString(strconv.Itoa(i), val)
}
for k, v := range values {
json.WriteString(k, v)
}
json.Close()
return json.String()
}

func expressionCommand() *cli.Command {
return &cli.Command{
Name: "expression",
Expand Down
67 changes: 65 additions & 2 deletions docs/usage/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The following are special Keys:
* `{.}` Returns all matched values with match names as JSON
* `{#}` Returns all matched numbered values as JSON
* `{.#}` Returned numbered and named matches as JSON
* `{$}` All extracted matches in array form

### Testing

Expand Down Expand Up @@ -136,9 +137,9 @@ filter with `--ignore`

### Logic

#### If
#### If, Unless

Syntax: `{if val ifTrue ifFalse}` or `{if val ifTrue}`
Syntax: `{if val ifTrue ifFalse}`, `{if val ifTrue}`, `{unless val ifFalse}`

If `val` is truthy, then return `ifTrue` else optionally return `ifFalse`

Expand Down Expand Up @@ -242,6 +243,67 @@ to form arrays that have meaning for a given aggregator.

Specifying multiple expressions is equivalent, eg. `{$ a b}` is the same as `-e a -e b`

### Ranges (Arrays)

Range functions provide the ability to work with arrays in expressions. You
can create an array either manually with the `{$ ...}` helper (above) or
by `{$split ...}` a string into an array.

#### $split

Syntax: `{$split <arr> ["delim"]}`

Splits a string into an array with the separating `delim`. If `delim` isn't
specified, `" "` will be used.
#### $join

Syntax: `{$join <arr> ["delim"]}`

Re-joins an array back into a string. If `delim` is empty, it will be `" "`

#### $map

Syntax: `{$map <arr> <mapfunc>}`

Evaluates `mapfunc` against each element in the array. In `mapfunc`, `{0}`
is the current element. The function must be surrounded by quotes.

For example, given the array `[1,2,3]`, and the function
`{$map {array} "{multi {0} 2}"}` will output [2,4,6].

#### $reduce

Syntax: `{$reduce <arr> <reducefunc>}`

Evaluates `reducefunc` against each element and a memo. `{0}` is the memo, and
`{1}` is the current value.

For example, given the array `[1,2,3]`, and the function
`{$reduce {array} "{sumi {0} {1}}"}`, it will return `6`.

#### $filter

Syntax: `{$filter <arr> <filterfunc>}`

Evaluates `filterfunc` for each element. If *truthy*, item will be in resulting
array. If false, it will be omitted. `{0}` will be the value examined.

For example, given the array `[1,abc,23,efg]`, and the function
`{$filter {array} "{isnum {0}}"}` will return `[1,23]`.

#### $slice

Syntax: `{$slice <arr> "begin" ["length"]}`

Gets a slice of an array. If `begin` is a negative number, will start from the end.

Examples: (Array `[1,2,3,4]`)

- `{$slice {array} 1}` - [2,3,4]
- `{$slice {array} 1 1}` - [2]
- `{$slice {array} -2}` - [3,4]
- `{$slice {array} -2 1}` - [3]


### Drawing

Expand Down Expand Up @@ -351,5 +413,6 @@ const (
ErrorConst = "<CONST>" // Expected constant value
ErrorEnum = "<ENUM>" // A given value is not contained within a set
ErrorArgName = "<NAME>" // A variable accessed by a given name does not exist
ErrorEmpty = "<EMPTY>" // A value was expected, but was empty
)
```
13 changes: 13 additions & 0 deletions pkg/expressions/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package expressions
import (
"fmt"
"strconv"
"strings"
)

const (
Expand Down Expand Up @@ -40,3 +41,15 @@ func stageError(msg string) KeyBuilderStage {
return errMessage
})
}

// make a delim-separated array
func MakeArray(args ...string) string {
var sb strings.Builder
for i := 0; i < len(args); i++ {
if i > 0 {
sb.WriteRune(ArraySeparator)
}
sb.WriteString(args[i])
}
return sb.String()
}
12 changes: 11 additions & 1 deletion pkg/expressions/stageAnalysis.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package expressions

import "strconv"

// monitorContext allows monitoring of context use
// largely for static analysis of an expression
type monitorContext struct {
Expand All @@ -22,7 +24,6 @@ func EvalStaticStage(stage KeyBuilderStage) (ret string, ok bool) {
ok = (monitor.keyLookups == 0)
return
}

func EvalStageOrDefault(stage KeyBuilderStage, dflt string) string {
if val, ok := EvalStaticStage(stage); ok {
return val
Expand All @@ -36,3 +37,12 @@ func EvalStageIndexOrDefault(stages []KeyBuilderStage, idx int, dflt string) str
}
return dflt
}

func EvalStageInt(stage KeyBuilderStage, dflt int) int {
if s, ok := EvalStaticStage(stage); ok {
if v, err := strconv.Atoi(s); err == nil {
return v
}
}
return dflt
}
12 changes: 12 additions & 0 deletions pkg/expressions/stage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package expressions

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestMakeArray(t *testing.T) {
assert.Equal(t, "abc", MakeArray("abc"))
assert.Equal(t, "abc\x00def", MakeArray("abc", "def"))
}
1 change: 1 addition & 0 deletions pkg/expressions/stdlib/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ const (
ErrorConst = "<CONST>" // Expected constant value
ErrorEnum = "<ENUM>" // A given value is not contained within a set
ErrorArgName = "<NAME>" // A variable accessed by a given name does not exist
ErrorEmpty = "<EMPTY>" // A value was expected, but was empty
)
11 changes: 10 additions & 1 deletion pkg/expressions/stdlib/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"divf": arithmaticHelperf(func(a, b float64) float64 { return a / b }),

// Comparisons
"if": KeyBuilderFunction(kfIf),
"if": KeyBuilderFunction(kfIf),
"unless": KeyBuilderFunction(kfUnless),
"eq": stringComparator(func(a, b string) string {
if a == b {
return a
Expand Down Expand Up @@ -61,6 +62,14 @@ var StandardFunctions = map[string]KeyBuilderFunction{
"tab": kfJoin('\t'),
"$": kfJoin(ArraySeparator),

// Ranges
"$map": kfArrayMap,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add $select func

"$split": kfArraySplit,
"$join": kfArrayJoin,
zix99 marked this conversation as resolved.
Show resolved Hide resolved
"$reduce": kfArrayReduce,
"$filter": kfArrayFilter,
"$slice": kfArraySlice,

// Pathing
"basename": kfPathBase,
"dirname": kfPathDir,
Expand Down
13 changes: 13 additions & 0 deletions pkg/expressions/stdlib/funcsComparators.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,16 @@ func kfIf(args []KeyBuilderStage) KeyBuilderStage {
return FalsyVal
})
}

func kfUnless(args []KeyBuilderStage) KeyBuilderStage {
if len(args) != 2 {
return stageLiteral(ErrorArgCount)
}
return func(context KeyBuilderContext) string {
ifVal := args[0](context)
if !Truthy(ifVal) {
return args[1](context)
}
return ""
}
}
4 changes: 4 additions & 0 deletions pkg/expressions/stdlib/funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ func TestIfStatement(t *testing.T) {
testExpression(t, mockContext("abc efg"), `{if {eq {0} "abc efg"} beq}`, "beq")
}

func TestUnlessStatement(t *testing.T) {
testExpression(t, mockContext("abc"), `{unless {1} {0}} {unless abc efg} {unless "" bob} {unless joe}`, "abc bob <ARGN>")
}

func TestComparisonEquality(t *testing.T) {
testExpression(t, mockContext("123", "1234"),
"{eq {0} 123} {eq {0} 1234} {not {eq {0} abc}} {neq 1 2} {neq 1 1}",
Expand Down
Loading