Files
tablizer/lib/helpers.go

303 lines
7.0 KiB
Go
Raw Permalink Normal View History

/*
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 <http://www.gnu.org/licenses/>.
*/
package lib
import (
"errors"
"fmt"
2022-10-10 20:14:51 +02:00
"os"
"regexp"
"slices"
"strconv"
"strings"
"github.com/gookit/color"
2025-11-03 22:06:11 +01:00
"codeberg.org/scip/tablizer/cfg"
)
2025-01-12 19:28:52 +01:00
func findindex(s []int, e int) (int, bool) {
for i, a := range s {
if a == e {
return i, true
}
}
return 0, false
}
// validate the consitency of parsed data
func ValidateConsistency(data *Tabdata) error {
expectedfields := len(data.headers)
for idx, row := range data.entries {
if len(row) != expectedfields {
return fmt.Errorf("row %d does not contain expected %d elements, but %d",
idx, expectedfields, len(row))
}
}
return nil
}
// parse columns list given with -c, modifies config.UseColumns based
// on eventually given regex.
// This is an output filter, because -cN,N,... is being applied AFTER
// processing of the input data.
2024-05-07 15:19:54 +02:00
func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
2025-01-12 19:28:52 +01:00
// -c columns
usecolumns, err := PrepareColumnVars(conf.Columns, data)
if err != nil {
return err
}
conf.UseColumns = usecolumns
2025-01-20 19:28:19 +01:00
// -y columns
useyankcolumns, err := PrepareColumnVars(conf.YankColumns, data)
if err != nil {
return err
}
conf.UseYankColumns = useyankcolumns
2025-01-12 19:28:52 +01:00
return nil
}
// Same thing as above but for -T option, which is an input option,
// because transposers are being applied before output.
2025-01-12 19:28:52 +01:00
func PrepareTransposerColumns(conf *cfg.Config, data *Tabdata) error {
// -T columns
usetransposecolumns, err := PrepareColumnVars(conf.TransposeColumns, data)
if err != nil {
return err
}
conf.UseTransposeColumns = usetransposecolumns
// verify that columns and transposers match and prepare transposer structs
if err := conf.PrepareTransposers(); err != nil {
return err
2024-05-07 18:01:12 +02:00
}
2025-01-12 19:28:52 +01:00
return nil
}
// output option, prepare -k1,2 sort fields
func PrepareSortColumns(conf *cfg.Config, data *Tabdata) error {
// -c columns
usecolumns, err := PrepareColumnVars(conf.SortByColumn, data)
if err != nil {
return err
}
conf.UseSortByColumn = usecolumns
return nil
}
2025-01-12 19:28:52 +01:00
func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) {
if columns == "" {
return nil, nil
}
usecolumns := []int{}
isregex := regexp.MustCompile(`\W`)
for _, columnpattern := range strings.Split(columns, ",") {
if len(columnpattern) == 0 {
2025-01-12 19:28:52 +01:00
return nil, fmt.Errorf("could not parse columns list %s: empty column", columns)
2024-05-07 18:01:12 +02:00
}
usenum, err := strconv.Atoi(columnpattern)
2024-05-07 18:01:12 +02:00
if err != nil {
// not a number
if !isregex.MatchString(columnpattern) {
// is not a regexp (contains no non-word chars)
// lc() it so that word searches are case insensitive
columnpattern = strings.ToLower(columnpattern)
for i, head := range data.headers {
if columnpattern == strings.ToLower(head) {
usecolumns = append(usecolumns, i+1)
}
}
} else {
colPattern, err := regexp.Compile("(?i)" + columnpattern)
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", columns, err)
return nil, errors.New(msg)
}
// find matching header fields, ignoring case
for i, head := range data.headers {
if colPattern.MatchString(strings.ToLower(head)) {
usecolumns = append(usecolumns, i+1)
}
}
}
2024-05-07 18:01:12 +02:00
} else {
// we digress from go best practises here, because if
// a colum spec is not a number, we process them above
// inside the err handler for atoi(). so only add the
// number, if it's really just a number.
2025-01-12 19:28:52 +01:00
usecolumns = append(usecolumns, usenum)
}
2024-05-07 18:01:12 +02:00
}
2025-10-06 23:27:48 +02:00
// deduplicate columns, preserve order
deduped := []int{}
2025-01-12 19:28:52 +01:00
for _, i := range usecolumns {
if !slices.Contains(deduped, i) {
deduped = append(deduped, i)
}
2024-05-07 18:01:12 +02:00
}
return deduped, nil
}
// prepare headers: add numbers to headers
2024-05-07 15:19:54 +02:00
func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
numberedHeaders := make([]string, len(data.headers))
2022-10-11 09:11:46 +02:00
maxwidth := 0 // start from scratch, so we only look at displayed column widths
// add numbers to headers if needed, get widest cell width
2024-05-07 15:19:54 +02:00
for idx, head := range data.headers {
2024-05-07 18:01:12 +02:00
var headlen int
if conf.Numbering {
newhead := fmt.Sprintf("%s(%d)", head, idx+1)
numberedHeaders[idx] = newhead
headlen = len(newhead)
} else {
headlen = len(head)
2022-10-11 09:11:46 +02:00
}
if headlen > maxwidth {
maxwidth = headlen
}
}
2024-05-07 18:01:12 +02:00
if conf.Numbering {
data.headers = numberedHeaders
}
if len(conf.UseColumns) > 0 {
// re-align headers based on user requested column list
headers := make([]string, len(conf.UseColumns))
for i, col := range conf.UseColumns {
for idx := range data.headers {
if col-1 == idx {
headers[i] = data.headers[col-1]
}
}
}
data.headers = headers
}
2024-05-07 18:01:12 +02:00
2022-10-11 09:11:46 +02:00
if data.maxwidthHeader != maxwidth && maxwidth > 0 {
data.maxwidthHeader = maxwidth
}
}
// exclude columns, if any
2024-05-07 15:19:54 +02:00
func reduceColumns(conf cfg.Config, data *Tabdata) {
if len(conf.Columns) > 0 {
reducedEntries := [][]string{}
2024-05-07 18:01:12 +02:00
for _, entry := range data.entries {
var reducedEntry []string
2024-05-07 18:01:12 +02:00
for _, col := range conf.UseColumns {
col--
for idx, value := range entry {
if idx == col {
reducedEntry = append(reducedEntry, value)
}
}
}
2024-05-07 18:01:12 +02:00
reducedEntries = append(reducedEntries, reducedEntry)
}
2024-05-07 18:01:12 +02:00
data.entries = reducedEntries
}
}
2024-05-07 18:01:12 +02:00
// FIXME: refactor this beast!
2024-05-07 15:19:54 +02:00
func colorizeData(conf cfg.Config, output string) string {
switch {
case conf.UseHighlight && color.IsConsole(os.Stdout):
highlight := true
colorized := ""
first := true
for _, line := range strings.Split(output, "\n") {
2023-11-21 17:41:04 +01:00
if highlight {
if first {
// we need to add two spaces to the header line
// because tablewriter omits them for some reason
// in pprint mode. This doesn't matter as long as
// we don't use colorization. But with colors the
// missing spaces can be seen.
2024-05-07 15:19:54 +02:00
if conf.OutputMode == cfg.ASCII {
line += " "
}
2024-05-07 15:19:54 +02:00
line = conf.HighlightHdrStyle.Sprint(line)
first = false
} else {
2024-05-07 15:19:54 +02:00
line = conf.HighlightStyle.Sprint(line)
}
} else {
2024-05-07 15:19:54 +02:00
line = conf.NoHighlightStyle.Sprint(line)
}
2024-05-07 18:01:12 +02:00
2023-11-21 17:41:04 +01:00
highlight = !highlight
colorized += line + "\n"
}
return colorized
2024-05-07 18:01:12 +02:00
case len(conf.Patterns) > 0 && !conf.NoColor && color.IsConsole(os.Stdout):
out := output
2024-05-07 18:01:12 +02:00
for _, re := range conf.Patterns {
if !re.Negate {
r := regexp.MustCompile("(" + re.Pattern + ")")
out = r.ReplaceAllStringFunc(out, func(in string) string {
return conf.ColorStyle.Sprint(in)
})
}
}
return out
2024-05-07 18:01:12 +02:00
2024-05-07 15:19:54 +02:00
default:
2022-10-10 20:14:51 +02:00
return output
}
}