Skip to content

Commit 6901558

Browse files
committed
improve expression-based search with functions
1 parent ddd8fc4 commit 6901558

5 files changed

Lines changed: 168 additions & 13 deletions

File tree

find/find.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"strings"
1111

1212
"github.com/araddon/dateparse"
13-
"github.com/maja42/goval"
1413

1514
"github.com/ayoisaiah/f2/v2/internal/config"
15+
"github.com/ayoisaiah/f2/v2/internal/eval"
1616
"github.com/ayoisaiah/f2/v2/internal/file"
1717
"github.com/ayoisaiah/f2/v2/internal/osutil"
1818
"github.com/ayoisaiah/f2/v2/internal/pathutil"
@@ -190,9 +190,7 @@ func evaluateSearchCondition(
190190
return match, false, err
191191
}
192192

193-
eval := goval.NewEvaluator()
194-
195-
result, err := eval.Evaluate(match.Target, nil, nil)
193+
result, err := eval.Evaluate(match.Target)
196194
if err != nil {
197195
if conf.Verbose {
198196
report.SearchEvalFailed(currentPath, match.Target, err)
@@ -201,8 +199,7 @@ func evaluateSearchCondition(
201199
return match, false, nil
202200
}
203201

204-
r, _ := result.(bool)
205-
if !r {
202+
if !result {
206203
return match, false, nil
207204
}
208205

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require (
4242
github.com/rogpeppe/go-internal v1.14.1 // indirect
4343
github.com/sergi/go-diff v1.3.1 // indirect
4444
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
45+
go.withmatt.com/size v0.0.0-20250220224316-11aee5773e67 // indirect
4546
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
4647
golang.org/x/term v0.32.0 // indirect
4748
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z
110110
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
111111
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
112112
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
113+
go.withmatt.com/size v0.0.0-20250220224316-11aee5773e67 h1:nHPqmzoLZv+9OqER7S2oXnIBDAbIaIKKrVtNtDvJzjc=
114+
go.withmatt.com/size v0.0.0-20250220224316-11aee5773e67/go.mod h1:WFmSrzphcXaSV4LG3YfIw42bPHDK8A6WsAYZCxyys/c=
113115
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
114116
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
115117
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=

internal/eval/eval.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package eval
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
"time"
7+
8+
"github.com/maja42/goval"
9+
"go.withmatt.com/size"
10+
11+
"github.com/ayoisaiah/f2/v2/internal/apperr"
12+
)
13+
14+
var functions = make(map[string]goval.ExpressionFunction)
15+
16+
var errInvalidArgs = &apperr.Error{
17+
Message: "invalid arguments",
18+
}
19+
20+
// ParseDuration parses a duration string.
21+
// examples: "10d", "-1.5w" or "3Y4M5d".
22+
// Add time units are "d"="D", "w"="W", "M", "y"="Y".
23+
// Adapted from: https://gist.github.com/xhit/79c9e137e1cfe332076cdda9f5e24699?permalink_comment_id=5170854#gistcomment-5170854
24+
func parseDuration(s string) (time.Duration, error) {
25+
neg := false
26+
if s != "" && s[0] == '-' {
27+
neg = true
28+
s = s[1:]
29+
}
30+
31+
re := regexp.MustCompile(`(\d*\.\d+|\d+)\D*`)
32+
unitMap := map[string]time.Duration{
33+
"d": 24,
34+
"w": 7 * 24,
35+
"M": 30 * 24,
36+
"y": 365 * 24,
37+
}
38+
39+
strs := re.FindAllString(s, -1)
40+
41+
var sumDur time.Duration
42+
43+
for _, str := range strs {
44+
var _hours time.Duration = 1
45+
46+
for unit, hours := range unitMap {
47+
if strings.Contains(str, unit) {
48+
str = strings.ReplaceAll(str, unit, "h")
49+
_hours = hours
50+
51+
break
52+
}
53+
}
54+
55+
dur, err := time.ParseDuration(str)
56+
if err != nil {
57+
return 0, err
58+
}
59+
60+
sumDur += dur * _hours
61+
}
62+
63+
if neg {
64+
sumDur = -sumDur
65+
}
66+
67+
return sumDur, nil
68+
}
69+
70+
func init() {
71+
functions["strlen"] = func(args ...any) (any, error) {
72+
if len(args) == 0 {
73+
return nil, errInvalidArgs
74+
}
75+
76+
str, _ := args[0].(string)
77+
78+
return len(str), nil
79+
}
80+
81+
functions["dur"] = func(args ...any) (any, error) {
82+
if len(args) == 0 {
83+
return nil, errInvalidArgs
84+
}
85+
86+
str, _ := args[0].(string)
87+
88+
dur, err := parseDuration(str)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
return dur.Seconds(), nil
94+
}
95+
96+
functions["contains"] = func(args ...any) (any, error) {
97+
if len(args) <= 1 {
98+
return nil, errInvalidArgs
99+
}
100+
101+
str, _ := args[0].(string)
102+
substr, _ := args[1].(string)
103+
104+
return strings.Contains(str, substr), nil
105+
}
106+
107+
functions["size"] = func(args ...any) (any, error) {
108+
if len(args) == 0 {
109+
return nil, errInvalidArgs
110+
}
111+
112+
str, _ := args[0].(string)
113+
114+
// Handle Exiftool format: "26 kB" -> "26K", "1.2 MB" -> "1.2M"
115+
// Remove spaces between number and unit for compatibility with size.ParseCapacity
116+
str = strings.ReplaceAll(str, " kB", "K")
117+
str = strings.ReplaceAll(str, " MB", "M")
118+
str = strings.ReplaceAll(str, " GB", "G")
119+
str = strings.ReplaceAll(str, " TB", "T")
120+
str = strings.ReplaceAll(str, " bytes", "")
121+
122+
s, err := size.ParseCapacity(str)
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
//nolint:gosec // risk of overflow is acceptable
128+
return int(s.Bytes()), nil
129+
}
130+
131+
functions["matches"] = func(args ...any) (any, error) {
132+
if len(args) <= 1 {
133+
return nil, errInvalidArgs
134+
}
135+
136+
str, _ := args[0].(string)
137+
exp, _ := args[1].(string)
138+
139+
reg := regexp.MustCompile(exp)
140+
141+
return reg.MatchString(str), nil
142+
}
143+
}
144+
145+
func Evaluate(expression string) (bool, error) {
146+
eval := goval.NewEvaluator()
147+
148+
result, err := eval.Evaluate(expression, nil, functions)
149+
if err != nil {
150+
return false, err
151+
}
152+
153+
r, _ := result.(bool)
154+
if !r {
155+
return false, nil
156+
}
157+
158+
return true, nil
159+
}

replace/replace.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import (
77
"path/filepath"
88
"strings"
99

10-
"github.com/maja42/goval"
11-
1210
"github.com/ayoisaiah/f2/v2/internal/config"
11+
"github.com/ayoisaiah/f2/v2/internal/eval"
1312
"github.com/ayoisaiah/f2/v2/internal/file"
1413
"github.com/ayoisaiah/f2/v2/internal/pathutil"
1514
"github.com/ayoisaiah/f2/v2/internal/sortfiles"
@@ -182,9 +181,7 @@ func prepNextChain(
182181
return err
183182
}
184183

185-
eval := goval.NewEvaluator()
186-
187-
result, err := eval.Evaluate(change.Target, nil, nil)
184+
result, err := eval.Evaluate(change.Target)
188185
if err != nil {
189186
if conf.Verbose {
190187
report.SearchEvalFailed(change.SourcePath, change.Target, err)
@@ -193,8 +190,7 @@ func prepNextChain(
193190
matches[j].MatchesFindCond = false
194191
}
195192

196-
r, _ := result.(bool)
197-
if !r {
193+
if !result {
198194
matches[j].MatchesFindCond = false
199195
}
200196

0 commit comments

Comments
 (0)