mirror of
https://codeberg.org/scip/tablizer.git
synced 2025-12-18 04:51:05 +01:00
get rid of lisp interpreter, -R and -F are enough, fixes #30
This commit is contained in:
@@ -19,7 +19,6 @@ package lib
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
@@ -144,18 +143,6 @@ func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
|
||||
// so we ignore all lines, which DO match.
|
||||
continue
|
||||
}
|
||||
|
||||
// apply user defined lisp filters, if any
|
||||
accept, err := RunFilterHooks(conf, line)
|
||||
if err != nil {
|
||||
return input, 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
|
||||
}
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
|
||||
319
lib/lisp.go
319
lib/lisp.go
@@ -1,319 +0,0 @@
|
||||
/*
|
||||
Copyright © 2023 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 sexptype := args[0].(type) {
|
||||
case *zygo.SexpSymbol:
|
||||
if !HookExists(sexptype.Name()) {
|
||||
return zygo.SexpNull, errors.New("Unknown hook " + sexptype.Name())
|
||||
}
|
||||
|
||||
hookname = sexptype.Name()
|
||||
|
||||
default:
|
||||
return zygo.SexpNull, errors.New("hook name must be a symbol ")
|
||||
}
|
||||
|
||||
switch sexptype := args[1].(type) {
|
||||
case *zygo.SexpSymbol:
|
||||
_, exists := Hooks[hookname]
|
||||
if !exists {
|
||||
Hooks[hookname] = []*zygo.SexpSymbol{sexptype}
|
||||
} else {
|
||||
Hooks[hookname] = append(Hooks[hookname], sexptype)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
* Basic sanity checks and load lisp file
|
||||
*/
|
||||
func LoadAndEvalFile(env *zygo.Zlisp, path string) error {
|
||||
if strings.HasSuffix(path, `.zy`) {
|
||||
code, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read lisp file %s: %w", path, err)
|
||||
}
|
||||
|
||||
// FIXME: check what res (_ here) could be and mean
|
||||
_, err = env.EvalString(string(code))
|
||||
if err != nil {
|
||||
log.Fatal(env.GetStackTrace(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Setup lisp interpreter environment
|
||||
*/
|
||||
func SetupLisp(conf *cfg.Config) error {
|
||||
// iterate over load-path and evaluate all *.zy files there, if any
|
||||
// we ignore if load-path does not exist, which is the default anyway
|
||||
path, err := os.Stat(conf.LispLoadPath)
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore non-existent files
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to stat path: %w", err)
|
||||
}
|
||||
|
||||
// init global hooks
|
||||
Hooks = make(map[string][]*zygo.SexpSymbol)
|
||||
|
||||
// init sandbox
|
||||
env := zygo.NewZlispSandbox()
|
||||
env.AddFunction("addhook", AddHook)
|
||||
|
||||
if !path.IsDir() {
|
||||
// load single lisp file
|
||||
err = LoadAndEvalFile(env, conf.LispLoadPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// load all lisp file in load dir
|
||||
dir, err := os.ReadDir(conf.LispLoadPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read lisp dir %s: %w",
|
||||
conf.LispLoadPath, err)
|
||||
}
|
||||
|
||||
for _, entry := range dir {
|
||||
if !entry.IsDir() {
|
||||
err := LoadAndEvalFile(env, conf.LispLoadPath+"/"+entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RegisterLib(env)
|
||||
|
||||
conf.Lisp = env
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Execute every user lisp function registered as filter hook.
|
||||
|
||||
Each function is given the current line as argument and is expected to
|
||||
return a boolean. True indicates to keep the line, false to skip
|
||||
it.
|
||||
|
||||
If there are multiple such functions registered, then the first one
|
||||
returning false wins, that is if each function returns true the line
|
||||
will be kept, if at least one of them returns false, it will be
|
||||
skipped.
|
||||
*/
|
||||
func RunFilterHooks(conf cfg.Config, line string) (bool, error) {
|
||||
for _, hook := range Hooks["filter"] {
|
||||
var result bool
|
||||
|
||||
conf.Lisp.Clear()
|
||||
|
||||
res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to evaluate hook loader: %w", err)
|
||||
}
|
||||
|
||||
switch sexptype := res.(type) {
|
||||
case *zygo.SexpBool:
|
||||
result = sexptype.Val
|
||||
default:
|
||||
return false, fmt.Errorf("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
|
||||
}
|
||||
|
||||
/*
|
||||
These hooks get the data (Tabdata) readily processed by tablizer as
|
||||
argument. They are expected to return a SexpPair containing a boolean
|
||||
denoting if the data has been modified and the actual modified
|
||||
data. Columns must be the same, rows may differ. Cells may also have
|
||||
been modified.
|
||||
|
||||
Replaces the internal data structure Tabdata with the user supplied
|
||||
version.
|
||||
|
||||
Only one process hook function is supported.
|
||||
|
||||
The somewhat complicated code is being caused by the fact, that we
|
||||
need to convert our internal structure to a lisp variable and vice
|
||||
versa afterwards.
|
||||
*/
|
||||
func RunProcessHooks(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
|
||||
var userdata Tabdata
|
||||
|
||||
lisplist := []zygo.Sexp{}
|
||||
|
||||
if len(Hooks["process"]) == 0 {
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
if len(Hooks["process"]) > 1 {
|
||||
fmt.Println("Warning: only one process hook is allowed!")
|
||||
}
|
||||
|
||||
// there are hook[s] installed, convert the go data structure 'data to lisp
|
||||
for _, row := range data.entries {
|
||||
var entry zygo.SexpHash
|
||||
|
||||
for idx, cell := range row {
|
||||
err := entry.HashSet(&zygo.SexpStr{S: data.headers[idx]}, &zygo.SexpStr{S: cell})
|
||||
if err != nil {
|
||||
return data, false, fmt.Errorf("failed to convert to lisp data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
lisplist = append(lisplist, &entry)
|
||||
}
|
||||
|
||||
// we need to add it to the env so that the function can use the struct directly
|
||||
conf.Lisp.AddGlobal("data", &zygo.SexpArray{Val: lisplist, Env: conf.Lisp})
|
||||
|
||||
// execute the actual hook
|
||||
hook := Hooks["process"][0]
|
||||
|
||||
conf.Lisp.Clear()
|
||||
|
||||
var result bool
|
||||
|
||||
res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s data)", hook.Name()))
|
||||
if err != nil {
|
||||
return data, false, fmt.Errorf("failed to eval lisp loader: %w", err)
|
||||
}
|
||||
|
||||
// we expect (bool, array(hash)) as return from the function
|
||||
switch sexptype := res.(type) {
|
||||
case *zygo.SexpPair:
|
||||
switch th := sexptype.Head.(type) {
|
||||
case *zygo.SexpBool:
|
||||
result = th.Val
|
||||
default:
|
||||
return data, false, errors.New("expect (bool, array(hash)) as return value")
|
||||
}
|
||||
|
||||
switch sexptailtype := sexptype.Tail.(type) {
|
||||
case *zygo.SexpArray:
|
||||
lisplist = sexptailtype.Val
|
||||
default:
|
||||
return data, false, errors.New("expect (bool, array(hash)) as return value ")
|
||||
}
|
||||
default:
|
||||
return data, false, errors.New("process hook shall return array of hashes ")
|
||||
}
|
||||
|
||||
if !result {
|
||||
// no further processing required
|
||||
return data, result, nil
|
||||
}
|
||||
|
||||
// finally convert lispdata back to Tabdata
|
||||
for _, item := range lisplist {
|
||||
row := []string{}
|
||||
|
||||
switch hash := item.(type) {
|
||||
case *zygo.SexpHash:
|
||||
for _, header := range data.headers {
|
||||
entry, err := hash.HashGetDefault(
|
||||
conf.Lisp,
|
||||
&zygo.SexpStr{S: header},
|
||||
&zygo.SexpStr{S: ""})
|
||||
if err != nil {
|
||||
return data, false, fmt.Errorf("failed to get lisp hash entry: %w", err)
|
||||
}
|
||||
|
||||
switch sexptype := entry.(type) {
|
||||
case *zygo.SexpStr:
|
||||
row = append(row, sexptype.S)
|
||||
default:
|
||||
return data, false, errors.New("hash values should be string ")
|
||||
}
|
||||
}
|
||||
default:
|
||||
return data, false, errors.New("returned array should contain hashes ")
|
||||
}
|
||||
|
||||
userdata.entries = append(userdata.entries, row)
|
||||
}
|
||||
|
||||
userdata.headers = data.headers
|
||||
|
||||
return &userdata, result, nil
|
||||
}
|
||||
110
lib/lisplib.go
110
lib/lisplib.go
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
Copyright © 2023 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package lib
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/glycerine/zygomys/zygo"
|
||||
)
|
||||
|
||||
func Splice2SexpList(list []string) zygo.Sexp {
|
||||
slist := []zygo.Sexp{}
|
||||
|
||||
for _, item := range list {
|
||||
slist = append(slist, &zygo.SexpStr{S: item})
|
||||
}
|
||||
|
||||
return zygo.MakeList(slist)
|
||||
}
|
||||
|
||||
func StringReSplit(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
|
||||
if len(args) < 2 {
|
||||
return zygo.SexpNull, errors.New("expecting 2 arguments: <string>, <regex>")
|
||||
}
|
||||
|
||||
var separator, input string
|
||||
|
||||
switch t := args[0].(type) {
|
||||
case *zygo.SexpStr:
|
||||
input = t.S
|
||||
default:
|
||||
return zygo.SexpNull, errors.New("first argument must be a string")
|
||||
}
|
||||
|
||||
switch t := args[1].(type) {
|
||||
case *zygo.SexpStr:
|
||||
separator = t.S
|
||||
default:
|
||||
return zygo.SexpNull, errors.New("second argument must be a string")
|
||||
}
|
||||
|
||||
sep := regexp.MustCompile(separator)
|
||||
|
||||
return Splice2SexpList(sep.Split(input, -1)), nil
|
||||
}
|
||||
|
||||
func String2Int(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
|
||||
var number int
|
||||
|
||||
switch t := args[0].(type) {
|
||||
case *zygo.SexpStr:
|
||||
num, err := strconv.Atoi(t.S)
|
||||
|
||||
if err != nil {
|
||||
return zygo.SexpNull, fmt.Errorf("failed to convert string to number: %w", err)
|
||||
}
|
||||
|
||||
number = num
|
||||
|
||||
default:
|
||||
return zygo.SexpNull, errors.New("argument must be a string")
|
||||
}
|
||||
|
||||
return &zygo.SexpInt{Val: int64(number)}, nil
|
||||
}
|
||||
|
||||
func RegMatch(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
|
||||
if len(args) != 2 {
|
||||
return zygo.SexpNull, fmt.Errorf("argument must be <regexp>, <string>")
|
||||
}
|
||||
|
||||
arguments := []string{}
|
||||
|
||||
for _, arg := range args {
|
||||
switch t := arg.(type) {
|
||||
case *zygo.SexpStr:
|
||||
arguments = append(arguments, t.S)
|
||||
default:
|
||||
return zygo.SexpNull, errors.New("argument must be a string")
|
||||
}
|
||||
}
|
||||
|
||||
reg := regexp.MustCompile(arguments[0])
|
||||
|
||||
return &zygo.SexpBool{Val: reg.MatchString(arguments[1])}, nil
|
||||
}
|
||||
|
||||
func RegisterLib(env *zygo.Zlisp) {
|
||||
env.AddFunction("resplit", StringReSplit)
|
||||
env.AddFunction("atoi", String2Int)
|
||||
env.AddFunction("matchre", RegMatch)
|
||||
}
|
||||
@@ -145,18 +145,6 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// apply user defined lisp filters, if any
|
||||
accept, err := RunFilterHooks(conf, line)
|
||||
if err != nil {
|
||||
return data, 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 {
|
||||
@@ -214,17 +202,6 @@ func PostProcess(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
|
||||
modified = true
|
||||
}
|
||||
|
||||
// apply user defined lisp process hooks, if any
|
||||
userdata, changed, err := RunProcessHooks(conf, data)
|
||||
if err != nil {
|
||||
return data, false, fmt.Errorf("failed to apply filter hook: %w", err)
|
||||
}
|
||||
|
||||
if changed {
|
||||
data = userdata
|
||||
modified = true
|
||||
}
|
||||
|
||||
if conf.Debug {
|
||||
repr.Print(data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user