add output colorization support using regexes

This commit is contained in:
2026-01-19 13:38:37 +01:00
parent 46fde289f5
commit cb3921458a
8 changed files with 86 additions and 23 deletions

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -104,9 +104,13 @@ type Config struct {
TransposeColumns string // 1,2 TransposeColumns string // 1,2
UseTransposeColumns []int // []int{1,2} UseTransposeColumns []int // []int{1,2}
Transposers []string // []string{"/ /-/", "/foo/bar/"} Transposers []string // []string{"/ /-/", "/foo/bar/"}
UseTransposers []Transposer // {Search: re, Replace: string} UseTransposers []Transposer // {Search: re, Replace: string}
Colorizers []string // []string{"/ /-/", "/foo/fg[:bg]/"}
UseColorizers []Transposer // {Search: re, Replace: color}
/* /*
FIXME: make configurable somehow, config file or ENV FIXME: make configurable somehow, config file or ENV
see https://github.com/gookit/color. see https://github.com/gookit/color.
@@ -357,6 +361,23 @@ func (conf *Config) PrepareTransposers() error {
return nil return nil
} }
func (conf *Config) PrepareColorizers() error {
for _, colorizer := range conf.Colorizers {
parts := strings.Split(colorizer, string(colorizer[0]))
if len(parts) != 4 {
return fmt.Errorf("colorizer function must have the format /regexp/foreground-color[:background-color]/")
}
conf.UseColorizers = append(conf.UseColorizers,
Transposer{
Search: *regexp.MustCompile(parts[1]),
Replace: parts[2]},
)
}
return nil
}
func (conf *Config) CheckEnv() { func (conf *Config) CheckEnv() {
// check for environment vars, command line flags have precedence, // check for environment vars, command line flags have precedence,
// NO_COLOR is being checked by the color module itself. // NO_COLOR is being checked by the color module itself.

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -95,6 +95,7 @@ func Execute() {
conf.PrepareCustomHeaders(headers) conf.PrepareCustomHeaders(headers)
wrapE(conf.PrepareFilters()) wrapE(conf.PrepareFilters())
wrapE(conf.PrepareColorizers())
conf.DetermineColormode() conf.DetermineColormode()
conf.ApplyDefaults() conf.ApplyDefaults()
@@ -191,6 +192,8 @@ func Execute() {
"filter", "F", nil, "Filter by field (field=regexp || field!=regexp)") "filter", "F", nil, "Filter by field (field=regexp || field!=regexp)")
rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers, rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers,
"regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T") "regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T")
rootCmd.PersistentFlags().StringArrayVarP(&conf.Colorizers,
"regex-colorizer", "K", nil, "apply /search/color[:background]/ to the whole output")
// input // input
rootCmd.PersistentFlags().StringVarP(&conf.InputFile, "read-file", "r", "", rootCmd.PersistentFlags().StringVarP(&conf.InputFile, "read-file", "r", "",

View File

@@ -13,6 +13,7 @@ const shortusage = `tablizer [regex,...] [-r file] [flags]
-x col,... use custom headers -d debug -x col,... use custom headers -d debug
-o char use char as output separator -g auto generate headers -o char use char as output separator -g auto generate headers
-K /pattern/foreground[:background]/ colorize pattern of output
-O org -C CSV -M md -X ext -S shell -Y yaml -J json -P template -O org -C CSV -M md -X ext -S shell -Y yaml -J json -P template
-a sort by age -i sort numerically -t sort by time -D sort descending order -a sort by age -i sort numerically -t sort by time -D sort descending order
-m show manual -v show version --help show detailed help` -m show manual -v show version --help show detailed help`

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -22,8 +22,8 @@ import (
"io" "io"
"strings" "strings"
"github.com/lithammer/fuzzysearch/fuzzy"
"codeberg.org/scip/tablizer/cfg" "codeberg.org/scip/tablizer/cfg"
"github.com/lithammer/fuzzysearch/fuzzy"
) )
/* /*

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022 Thomas von Dein Copyright © 2022-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -26,8 +26,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/gookit/color"
"codeberg.org/scip/tablizer/cfg" "codeberg.org/scip/tablizer/cfg"
"github.com/gookit/color"
) )
func findindex(s []int, e int) (int, bool) { func findindex(s []int, e int) (int, bool) {

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -23,6 +23,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
@@ -75,8 +76,8 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
} }
} }
func output(writer io.Writer, str string) { func output(writer io.Writer, conf cfg.Config, str string) {
_, err := fmt.Fprint(writer, str) _, err := fmt.Fprint(writer, colorizeOutput(conf, str))
if err != nil { if err != nil {
log.Fatalf("failed to print output: %s", err) log.Fatalf("failed to print output: %s", err)
} }
@@ -138,7 +139,7 @@ func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
log.Fatalf("Failed to render table: %s", err) log.Fatalf("Failed to render table: %s", err)
} }
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) output(writer, conf, color.Sprint(colorizeData(conf, tableString.String())))
} }
/* /*
@@ -197,7 +198,7 @@ func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
log.Fatalf("Failed to render table: %s", err) log.Fatalf("Failed to render table: %s", err)
} }
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) output(writer, conf, color.Sprint(colorizeData(conf, tableString.String())))
} }
/* /*
@@ -254,7 +255,7 @@ func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
log.Fatalf("Failed to render table: %s", err) log.Fatalf("Failed to render table: %s", err)
} }
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) output(writer, conf, color.Sprint(colorizeData(conf, tableString.String())))
} }
/* /*
@@ -275,7 +276,7 @@ func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) {
} }
} }
output(writer, colorizeData(conf, out)) output(writer, conf, colorizeData(conf, out))
} }
/* /*
@@ -298,7 +299,7 @@ func printShellData(writer io.Writer, data *Tabdata) {
} }
// no colorization here // no colorization here
output(writer, out) output(writer, cfg.Config{}, out)
} }
func printJsonData(writer io.Writer, data *Tabdata) { func printJsonData(writer io.Writer, data *Tabdata) {
@@ -327,7 +328,7 @@ func printJsonData(writer io.Writer, data *Tabdata) {
log.Fatal(err) log.Fatal(err)
} }
output(writer, string(jsonstr)) output(writer, cfg.Config{}, string(jsonstr))
} }
func printYamlData(writer io.Writer, data *Tabdata) { func printYamlData(writer io.Writer, data *Tabdata) {
@@ -364,7 +365,7 @@ func printYamlData(writer io.Writer, data *Tabdata) {
log.Fatal(err) log.Fatal(err)
} }
output(writer, string(yamlstr)) output(writer, cfg.Config{}, string(yamlstr))
} }
func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) { func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) {
@@ -414,3 +415,23 @@ func printTemplateData(writer io.Writer, conf cfg.Config, data *Tabdata) {
log.Fatalf("failed to print output: %s", err) log.Fatalf("failed to print output: %s", err)
} }
} }
func colorizeOutput(conf cfg.Config, input string) string {
if len(conf.UseColorizers) > 0 && !conf.NoColor && color.IsConsole(os.Stdout) {
for _, colorizer := range conf.UseColorizers {
// colorize matching parts of the whole output with given color, if the terminal supports it
// color may contain fg:bg or just fg. Color definitions see https://github.com/gookit/color
input = colorizer.Search.ReplaceAllStringFunc(input, func(in string) string {
col := colorizer.Replace
if strings.Contains(col, ":") {
parts := strings.Split(col, ":")
return color.Sprintf(fmt.Sprintf("<fg=%s;bg=%s>%s</>", parts[0], parts[1], in))
}
return color.Sprintf(fmt.Sprintf("<fg=%s>%s</>", col, in))
})
}
}
return input
}

5
t/testtable6.csv Normal file
View File

@@ -0,0 +1,5 @@
Date,Account Number,Subject,Amount
20250101,968723487,Dogs Medication Invoice 9919292,-450.00
20250103,172747812,Tax return tax id HHD813D/12564H,+912.14
20250105,987122711,Car repair order 020123,-299.45
20250108,731217273,Rent - 12234 Sunset Blvd,-2960.00
1 Date Account Number Subject Amount
2 20250101 968723487 Dogs Medication Invoice 9919292 -450.00
3 20250103 172747812 Tax return tax id HHD813D/12564H +912.14
4 20250105 987122711 Car repair order 020123 -299.45
5 20250108 731217273 Rent - 12234 Sunset Blvd -2960.00

View File

@@ -19,6 +19,7 @@ tablizer - Manipulate tabular output of other programs
-F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times -F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,) -T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T -R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T
-K --regex-colorizer /from/color/ colorize pattern of output (color: fg[:bg])
-j, --json Read JSON input (must be array of hashes) -j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows -I, --interactive Interactively filter and select rows
-g, --auto-headers Generate headers if there are none present in input -g, --auto-headers Generate headers if there are none present in input
@@ -514,9 +515,20 @@ black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
magenta, red, white, yellow magenta, red, white, yellow
The Variables B<FG> and B<BG> are being used to highlight matches. The but you may also use HTML color codes without the hash sign.
other *FG and *BG variables are for colored table output (enabled with
the C<-L> parameter). The Variables B<FG> and B<BG> are being used to highlight matching
rows. The other *FG and *BG variables are for colored table output
(enabled with the C<-L> parameter).
You can also use the option C<-K> to colorize particular patterns, not
whole lines. The option can be given multiple times and expects the
following parameter:
-K '/regex/foreground[:background]/
that is, background color is optional. This colorization will applied
on top of any previous colorizations, if any.
Colorization can be turned off completely either by setting the Colorization can be turned off completely either by setting the
parameter C<-N> or the environment variable B<NO_COLOR> to a true value. parameter C<-N> or the environment variable B<NO_COLOR> to a true value.