/* Copyright © 2022-2025 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 ( "bufio" "io" "strings" "github.com/lithammer/fuzzysearch/fuzzy" "codeberg.org/scip/tablizer/cfg" ) /* * [!]Match a line, use fuzzy search for normal pattern strings and * regexp otherwise. 'foo bar' foo, /bar/! => false => line contains foo and not (not bar) 'foo nix' foo, /bar/! => ture => line contains foo and (not bar) 'foo bar' foo, /bar/ => true => line contains both foo and bar 'foo nix' foo, /bar/ => false => line does not contain bar 'foo bar' foo, /nix/ => false => line does not contain nix */ func matchPattern(conf cfg.Config, line string) bool { if len(conf.Patterns) == 0 { // any line always matches "" return true } if conf.UseFuzzySearch { // fuzzy search only considers the 1st pattern return fuzzy.MatchFold(conf.Patterns[0].Pattern, line) } var match int //fmt.Printf("<%s>\n", line) for _, re := range conf.Patterns { patmatch := re.PatternRe.MatchString(line) if re.Negate { // toggle the meaning of match patmatch = !patmatch } if patmatch { match++ } //fmt.Printf("patmatch: %t, match: %d, pattern: %s, negate: %t\n", patmatch, match, re.Pattern, re.Negate) } // fmt.Printf("result: %t\n", match == len(conf.Patterns)) //fmt.Println() return match == len(conf.Patterns) } /* * Filter parsed data by fields. The filter is positive, so if one or * more filters match on a row, it will be kept, otherwise it will be * excluded. */ func FilterByFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) { if len(conf.Filters) == 0 { // no filters, no checking return nil, false, nil } newdata := data.CloneEmpty() for _, row := range data.entries { keep := true for idx, header := range data.headers { lcheader := strings.ToLower(header) if !Exists(conf.Filters, lcheader) { // do not filter by unspecified field continue } match := conf.Filters[lcheader].Regex.MatchString(row[idx]) if conf.Filters[lcheader].Negate { match = !match } if !match { keep = false break } } if keep == !conf.InvertMatch { // also apply -v newdata.entries = append(newdata.entries, row) } } return &newdata, true, nil } /* * Transpose fields using search/replace regexp. */ func TransposeFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) { if len(conf.UseTransposers) == 0 { // nothing to be done return nil, false, nil } newdata := data.CloneEmpty() transposed := false for _, row := range data.entries { transposedrow := false for idx := range data.headers { transposeidx, hasone := findindex(conf.UseTransposeColumns, idx+1) if hasone { row[idx] = conf.UseTransposers[transposeidx].Search.ReplaceAllString( row[idx], conf.UseTransposers[transposeidx].Replace, ) transposedrow = true } } if transposedrow { // also apply -v newdata.entries = append(newdata.entries, row) transposed = true } } return &newdata, transposed, nil } /* generic map.Exists(key) */ func Exists[K comparable, V any](m map[K]V, v K) bool { if _, ok := m[v]; ok { return true } return false } /* * Filters the whole input lines, returns filtered lines */ func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) { if len(conf.Patterns) == 0 { return input, nil } scanner := bufio.NewScanner(input) lines := []string{} hadFirst := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if hadFirst { // don't match 1st line, it's the header if matchPattern(conf, line) == conf.InvertMatch { // by default -v is false, so if a line does NOT // match the pattern, we will ignore it. However, // if the user specified -v, the matching is inverted, // so we ignore all lines, which DO match. continue } } lines = append(lines, line) hadFirst = true } return strings.NewReader(strings.Join(lines, "\n")), nil }