added transpose function (-T + -R)

This commit is contained in:
2025-01-12 19:28:52 +01:00
committed by T.v.Dein
parent 8792c5a40f
commit 4d894a728b
9 changed files with 271 additions and 134 deletions

View File

@@ -49,6 +49,11 @@ type Settings struct {
HighlightHdrBG string `hcl:"HighlightHdrBG"` HighlightHdrBG string `hcl:"HighlightHdrBG"`
} }
type Transposer struct {
Search regexp.Regexp
Replace string
}
// internal config // internal config
type Config struct { type Config struct {
Debug bool Debug bool
@@ -68,6 +73,11 @@ type Config struct {
SortDescending bool SortDescending bool
SortByColumn int SortByColumn int
TransposeColumns string // 1,2
UseTransposeColumns []int // []int{1,2}
Transposers []string // []string{"/ /-/", "/foo/bar/"}
UseTransposers []Transposer // {Search: re, Replace: string}
/* /*
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.
@@ -283,6 +293,29 @@ func (conf *Config) PrepareFilters() error {
return nil return nil
} }
// check if transposers match transposer columns and prepare transposer structs
func (conf *Config) PrepareTransposers() error {
if len(conf.Transposers) != len(conf.UseTransposeColumns) {
return fmt.Errorf("the number of transposers needs to correspond to the number of transpose columns: %d != %d",
len(conf.Transposers), len(strings.Split(conf.TransposeColumns, ",")))
}
for _, transposer := range conf.Transposers {
parts := strings.Split(transposer, "/")
if len(parts) != 4 {
return fmt.Errorf("transposer function must have the format /regexp/replace-string/")
}
conf.UseTransposers = append(conf.UseTransposers,
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

@@ -150,6 +150,8 @@ func Execute() {
"Custom field separator") "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "",
"Only show the speficied columns (separated by ,)") "Only show the speficied columns (separated by ,)")
rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "",
"Transpose the speficied columns (separated by ,)")
// sort options // sort options
rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0,
@@ -195,6 +197,7 @@ func Execute() {
// filters // filters
rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters, "filter", "F", nil, "Filter by field (field=regexp)") rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters, "filter", "F", nil, "Filter by field (field=regexp)")
rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers, "regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T")
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")

View File

@@ -18,6 +18,8 @@ SYNOPSIS
-k, --sort-by int Sort by column (default: 1) -k, --sort-by int Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-X, --extended Enable extended output -X, --extended Enable extended output
@@ -410,6 +412,8 @@ Operational Flags:
-k, --sort-by int Sort by column (default: 1) -k, --sort-by int Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-X, --extended Enable extended output -X, --extended Enable extended output

View File

@@ -44,10 +44,10 @@ func matchPattern(conf cfg.Config, line string) bool {
* more filters match on a row, it will be kept, otherwise it will be * more filters match on a row, it will be kept, otherwise it will be
* excluded. * excluded.
*/ */
func FilterByFields(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { func FilterByFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
if len(conf.Filters) == 0 { if len(conf.Filters) == 0 {
// no filters, no checking // no filters, no checking
return Tabdata{}, false, nil return nil, false, nil
} }
newdata := data.CloneEmpty() newdata := data.CloneEmpty()
@@ -75,7 +75,44 @@ func FilterByFields(conf cfg.Config, data Tabdata) (Tabdata, bool, error) {
} }
} }
return newdata, true, nil 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) */ /* generic map.Exists(key) */

View File

@@ -153,8 +153,8 @@ func TestFilterByFields(t *testing.T) {
t.Errorf("PrepareFilters returned error: %s", err) t.Errorf("PrepareFilters returned error: %s", err)
} }
data, _, _ := FilterByFields(conf, data) data, _, _ := FilterByFields(conf, &data)
if !reflect.DeepEqual(data, inputdata.expect) { if !reflect.DeepEqual(*data, inputdata.expect) {
t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect) t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect)
} }
}) })

View File

@@ -40,6 +40,16 @@ func contains(s []int, e int) bool {
return false return false
} }
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 // validate the consitency of parsed data
func ValidateConsistency(data *Tabdata) error { func ValidateConsistency(data *Tabdata) error {
expectedfields := len(data.headers) expectedfields := len(data.headers)
@@ -57,13 +67,44 @@ func ValidateConsistency(data *Tabdata) error {
// parse columns list given with -c, modifies config.UseColumns based // parse columns list given with -c, modifies config.UseColumns based
// on eventually given regex // on eventually given regex
func PrepareColumns(conf *cfg.Config, data *Tabdata) error { func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
if conf.Columns == "" { // -c columns
usecolumns, err := PrepareColumnVars(conf.Columns, data)
if err != nil {
return err
}
conf.UseColumns = usecolumns
return nil return nil
} }
for _, use := range strings.Split(conf.Columns, ",") { 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
}
return nil
}
func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) {
if columns == "" {
return nil, nil
}
usecolumns := []int{}
for _, use := range strings.Split(columns, ",") {
if len(use) == 0 { if len(use) == 0 {
return fmt.Errorf("could not parse columns list %s: empty column", conf.Columns) return nil, fmt.Errorf("could not parse columns list %s: empty column", columns)
} }
usenum, err := strconv.Atoi(use) usenum, err := strconv.Atoi(use)
@@ -71,15 +112,15 @@ func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
// might be a regexp // might be a regexp
colPattern, err := regexp.Compile(use) colPattern, err := regexp.Compile(use)
if err != nil { if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", conf.Columns, err) msg := fmt.Sprintf("Could not parse columns list %s: %v", columns, err)
return errors.New(msg) return nil, errors.New(msg)
} }
// find matching header fields // find matching header fields
for i, head := range data.headers { for i, head := range data.headers {
if colPattern.MatchString(head) { if colPattern.MatchString(head) {
conf.UseColumns = append(conf.UseColumns, i+1) usecolumns = append(usecolumns, i+1)
} }
} }
} else { } else {
@@ -87,27 +128,28 @@ func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
// a colum spec is not a number, we process them above // a colum spec is not a number, we process them above
// inside the err handler for atoi(). so only add the // inside the err handler for atoi(). so only add the
// number, if it's really just a number. // number, if it's really just a number.
conf.UseColumns = append(conf.UseColumns, usenum) usecolumns = append(usecolumns, usenum)
} }
} }
// deduplicate: put all values into a map (value gets map key) // deduplicate: put all values into a map (value gets map key)
// thereby removing duplicates, extract keys into new slice // thereby removing duplicates, extract keys into new slice
// and sort it // and sort it
imap := make(map[int]int, len(conf.UseColumns)) imap := make(map[int]int, len(usecolumns))
for _, i := range conf.UseColumns { for _, i := range usecolumns {
imap[i] = 0 imap[i] = 0
} }
conf.UseColumns = nil // fill with deduplicated columns
usecolumns = nil
for k := range imap { for k := range imap {
conf.UseColumns = append(conf.UseColumns, k) usecolumns = append(usecolumns, k)
} }
sort.Ints(conf.UseColumns) sort.Ints(usecolumns)
return nil return usecolumns, nil
} }
// prepare headers: add numbers to headers // prepare headers: add numbers to headers

View File

@@ -175,13 +175,27 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
} }
// filter by field filters, if any // filter by field filters, if any
filtereddata, changed, err := FilterByFields(conf, data) filtereddata, changed, err := FilterByFields(conf, &data)
if err != nil { if err != nil {
return data, fmt.Errorf("failed to filter fields: %w", err) return data, fmt.Errorf("failed to filter fields: %w", err)
} }
if changed { if changed {
data = filtereddata data = *filtereddata
}
// transpose if demanded
if err := PrepareTransposerColumns(&conf, &data); err != nil {
return data, err
}
modifieddata, changed, err := TransposeFields(conf, &data)
if err != nil {
return data, fmt.Errorf("failed to transpose fields: %w", err)
}
if changed {
data = *modifieddata
} }
// apply user defined lisp process hooks, if any // apply user defined lisp process hooks, if any

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "TABLIZER 1" .IX Title "TABLIZER 1"
.TH TABLIZER 1 "2025-01-10" "1" "User Commands" .TH TABLIZER 1 "2025-01-12" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .if n .ad l
@@ -156,6 +156,8 @@ tablizer \- Manipulate tabular output of other programs
\& \-k, \-\-sort\-by int Sort by column (default: 1) \& \-k, \-\-sort\-by int Sort by column (default: 1)
\& \-z, \-\-fuzzy Use fuzzy search [experimental] \& \-z, \-\-fuzzy Use fuzzy search [experimental]
\& \-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 ,)
\& \-R, \-\-regex\-transposer /from/to/ Apply /search/replace/ regexp to fields given in \-T
\& \&
\& Output Flags (mutually exclusive): \& Output Flags (mutually exclusive):
\& \-X, \-\-extended Enable extended output \& \-X, \-\-extended Enable extended output

View File

@@ -17,6 +17,8 @@ tablizer - Manipulate tabular output of other programs
-k, --sort-by int Sort by column (default: 1) -k, --sort-by int Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-X, --extended Enable extended output -X, --extended Enable extended output