diff --git a/TODO.md b/TODO.md index 69ed0ab..cec4393 100644 --- a/TODO.md +++ b/TODO.md @@ -6,4 +6,13 @@ - add --no-headers option +### Lisp Plugin Infrastructure using zygo +Hooks: + +| Filter | Purpose | Args | Return | +|-----------|-------------------------------------------------------------|---------------------|--------| +| filter | include or exclude lines | row as hash | bool | +| process | do calculations with data, store results in global lisp env | whole dataset | nil | +| transpose | modify a cell | headername and cell | cell | +| append | add one or more rows to the dataset (use this to add stats) | nil | rows | diff --git a/cfg/config.go b/cfg/config.go index 512180c..8dcfddb 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -22,12 +22,15 @@ import ( "os" "regexp" + "github.com/glycerine/zygomys/zygo" "github.com/gookit/color" ) const DefaultSeparator string = `(\s\s+|\t)` const Version string = "v1.0.17" +var DefaultLoadPath string = os.Getenv("HOME") + "/.config/tablizer/lisp" + var VERSION string // maintained by -x type Config struct { @@ -54,6 +57,13 @@ type Config struct { ColorStyle color.Style NoColor bool + + // special case: we use the config struct to transport the lisp + // env trough the program + Lisp *zygo.Zlisp + + // a path containing lisp scripts to be loaded on startup + LispLoadPath string } // maps outputmode short flags to output mode, ie. -O => -o orgtbl @@ -85,6 +95,9 @@ type Sortmode struct { Age bool } +// valid lisp hooks +var ValidHooks []string + // default color schemes func Colors() map[color.Level]map[string]color.Color { return map[color.Level]map[string]color.Color{ @@ -187,6 +200,8 @@ func (c *Config) ApplyDefaults() { if c.OutputMode == Yaml || c.OutputMode == CSV { c.NoNumbering = true } + + ValidHooks = []string{"filter", "process", "transpose", "append"} } func (c *Config) PreparePattern(pattern string) error { diff --git a/cmd/root.go b/cmd/root.go index bdc39a4..d3bb98b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -98,6 +98,12 @@ func Execute() { conf.DetermineColormode() conf.ApplyDefaults() + // setup lisp env, load plugins etc + err := lib.SetupLisp(&conf) + if err != nil { + return err + } + // actual execution starts here return lib.ProcessFiles(&conf, args) }, @@ -136,6 +142,9 @@ func Execute() { rootCmd.PersistentFlags().BoolVarP(&modeflag.A, "ascii", "A", false, "Enable ASCII output (default)") rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv") + // lisp options + rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, "Load path for lisp plugins (expects *.zy files)") + rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") err := rootCmd.Execute() diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 61f12d8..078cd74 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -326,6 +326,7 @@ Sort Mode Flags (mutually exclusive): Other Flags: --completion Generate the autocompletion script for + -l --load-path Path to search for lisp plugins (*.zy files) -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page diff --git a/go.mod b/go.mod index 5a53370..c59f01c 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,16 @@ require ( ) require ( + github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be // indirect + github.com/glycerine/greenpack v5.1.1+incompatible // indirect + github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 // indirect + github.com/glycerine/zygomys v5.1.2+incompatible // indirect github.com/lithammer/fuzzysearch v1.1.7 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect + github.com/shurcooL/go-goon v1.0.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/text v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 2d1477a..9437625 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be h1:XBJdPGgA3qqhW+p9CANCAVdF7ZIXdu3pZAkypMkKAjE= +github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be/go.mod h1:OSCrScrFAjcBObrulk6BEQlytA462OkG1UGB5NYj9kE= +github.com/glycerine/greenpack v5.1.1+incompatible h1:fDr9i6MkSGZmAy4VXPfJhW+SyK2/LNnzIp5nHyDiaIM= +github.com/glycerine/greenpack v5.1.1+incompatible/go.mod h1:us0jVISAESGjsEuLlAfCd5nkZm6W6WQF18HPuOecIg4= +github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 h1:4ZegphJXBTc4uFQ08UVoWYmQXorGa+ipXetUj83sMBc= +github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0/go.mod h1:AqJLs6UeoC65dnHxyCQ6MO31P5STpjcmgaANAU+No8Q= +github.com/glycerine/zygomys v5.1.2+incompatible h1:jmcdmA3XPxgfOunAXFpipE9LQoUL6eX6d2mhYyjV4GE= +github.com/glycerine/zygomys v5.1.2+incompatible/go.mod h1:i3SPKZpmy9dwF/3iWrXJ/ZLyzZucegwypwOmqRkUUaQ= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= @@ -18,6 +26,8 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -25,6 +35,10 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E= +github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w= 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= @@ -35,6 +49,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= @@ -43,10 +61,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -59,20 +79,24 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/lib/lisp.go b/lib/lisp.go new file mode 100644 index 0000000..b6f79e4 --- /dev/null +++ b/lib/lisp.go @@ -0,0 +1,146 @@ +/* +Copyright © 2022 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package lib + +import ( + "errors" + "fmt" + "log" + "os" + "strings" + + "github.com/glycerine/zygomys/zygo" + "github.com/tlinden/tablizer/cfg" +) + +/* +needs to be global because we can't feed an cfg object to AddHook() +which is being called from user lisp code +*/ +var Hooks map[string][]*zygo.SexpSymbol + +/* +AddHook() (called addhook from lisp code) can be used by the user to +add a function to one of the available hooks provided by tablizer. +*/ +func AddHook(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) { + var hookname string + + if len(args) < 2 { + return zygo.SexpNull, errors.New("argument of %add-hook should be: %hook-name %your-function") + } + + switch t := args[0].(type) { + case *zygo.SexpSymbol: + if !HookExists(t.Name()) { + return zygo.SexpNull, errors.New("Unknown hook " + t.Name()) + } + hookname = t.Name() + default: + return zygo.SexpNull, errors.New("hook name must be a symbol!") + } + + switch t := args[1].(type) { + case *zygo.SexpSymbol: + _, exists := Hooks[hookname] + if !exists { + Hooks[hookname] = []*zygo.SexpSymbol{t} + } else { + Hooks[hookname] = append(Hooks[hookname], t) + } + default: + return zygo.SexpNull, errors.New("hook function must be a symbol!") + } + + return zygo.SexpNull, nil +} + +/* +Check if a hook exists +*/ +func HookExists(key string) bool { + for _, hook := range cfg.ValidHooks { + if hook == key { + return true + } + } + + return false +} + +/* + * Setup lisp interpreter environment + */ +func SetupLisp(c *cfg.Config) error { + Hooks = make(map[string][]*zygo.SexpSymbol) + + env := zygo.NewZlispSandbox() + env.AddFunction("addhook", AddHook) + + // iterate over load-path and evaluate all *.ty files there, if any + if _, err := os.Stat(c.LispLoadPath); !os.IsNotExist(err) { + dir, err := os.ReadDir(c.LispLoadPath) + if err != nil { + return err + } + + for _, entry := range dir { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), `.zy`) { + code, err := os.ReadFile(c.LispLoadPath + "/" + entry.Name()) + if err != nil { + return err + } + + // FIXME: check what res (_ here) could be and mean + _, err = env.EvalString(string(code)) + if err != nil { + log.Fatalf(env.GetStackTrace(err)) + } + } + } + } + + c.Lisp = env + return nil +} + +func RunFilterHooks(c cfg.Config, line string) (bool, error) { + for _, hook := range Hooks["filter"] { + var result bool + c.Lisp.Clear() + res, err := c.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line)) + if err != nil { + return false, err + } + + switch t := res.(type) { + case *zygo.SexpBool: + result = t.Val + default: + return false, errors.New("filter hook shall return BOOL!") + } + + if !result { + // the first hook which returns false leads to complete false + return result, nil + } + } + + // if no hook returned false, we succeed and accept the given line + return true, nil +} diff --git a/lib/parser.go b/lib/parser.go index 9b44c3c..417c50d 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -167,6 +167,17 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { continue } + accept, err := RunFilterHooks(c, line) + if err != nil { + return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err)) + } + + if !accept { + // IF there are filter hook[s] and IF one of them + // returns false on the current line, reject it + continue + } + idx := 0 // we cannot use the header index, because we could exclude columns values := []string{} for _, part := range parts { diff --git a/t/plugintest.zy b/t/plugintest.zy new file mode 100644 index 0000000..b20bc4e --- /dev/null +++ b/t/plugintest.zy @@ -0,0 +1,2 @@ +(defn ignoreall [line] false) +(addhook %filter %ignoreall)