diff --git a/cfg/config.go b/cfg/config.go index 1d8e846..371e1e1 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -49,6 +49,11 @@ type Settings struct { HighlightHdrBG string `hcl:"HighlightHdrBG"` } +type Transposer struct { + Search regexp.Regexp + Replace string +} + // internal config type Config struct { Debug bool @@ -68,6 +73,11 @@ type Config struct { SortDescending bool 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 see https://github.com/gookit/color. @@ -283,6 +293,29 @@ func (conf *Config) PrepareFilters() error { 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() { // check for environment vars, command line flags have precedence, // NO_COLOR is being checked by the color module itself. diff --git a/cmd/root.go b/cmd/root.go index c493a4d..169750d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -150,6 +150,8 @@ func Execute() { "Custom field separator") rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") + rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "", + "Transpose the speficied columns (separated by ,)") // sort options rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, @@ -195,6 +197,7 @@ func Execute() { // filters 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") diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 017feb8..450f721 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -9,40 +9,42 @@ SYNOPSIS tablizer [regex] [file, ...] [flags] Operational Flags: - -c, --columns string Only show the speficied columns (separated by ,) - -v, --invert-match select non-matching rows - -n, --no-numbering Disable header numbering - -N, --no-color Disable pattern highlighting - -H, --no-headers Disable headers display - -s, --separator string Custom field separator - -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy search [experimental] - -F, --filter field=reg Filter given field with regex, can be used multiple times + -c, --columns string Only show the speficied columns (separated by ,) + -v, --invert-match select non-matching rows + -n, --no-numbering Disable header numbering + -N, --no-color Disable pattern highlighting + -H, --no-headers Disable headers display + -s, --separator string Custom field separator + -k, --sort-by int Sort by column (default: 1) + -z, --fuzzy Use fuzzy search [experimental] + -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): - -X, --extended Enable extended output - -M, --markdown Enable markdown table output - -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable output - -Y, --yaml Enable yaml output - -C, --csv Enable CSV output - -A, --ascii Default output mode, ascii tabular - -L, --hightlight-lines Use alternating background colors for tables + -X, --extended Enable extended output + -M, --markdown Enable markdown table output + -O, --orgtbl Enable org-mode table output + -S, --shell Enable shell evaluable output + -Y, --yaml Enable yaml output + -C, --csv Enable CSV output + -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): - -a, --sort-age sort according to age (duration) string - -D, --sort-desc Sort in descending order (default: ascending) - -i, --sort-numeric sort according to string numerical value - -t, --sort-time sort according to time string + -a, --sort-age sort according to age (duration) string + -D, --sort-desc Sort in descending order (default: ascending) + -i, --sort-numeric sort according to string numerical value + -t, --sort-time sort according to time string Other Flags: - --completion Generate the autocompletion script for - -f, --config Configuration file (default: ~/.config/tablizer/config) - -l, --load-path Load path for lisp plugins (expects *.zy files) - -d, --debug Enable debugging - -h, --help help for tablizer - -m, --man Display manual page - -V, --version Print program version + --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) + -l, --load-path Load path for lisp plugins (expects *.zy files) + -d, --debug Enable debugging + -h, --help help for tablizer + -m, --man Display manual page + -V, --version Print program version DESCRIPTION Many programs generate tabular output. But sometimes you need to @@ -401,40 +403,42 @@ Usage: tablizer [regex] [file, ...] [flags] Operational Flags: - -c, --columns string Only show the speficied columns (separated by ,) - -v, --invert-match select non-matching rows - -n, --no-numbering Disable header numbering - -N, --no-color Disable pattern highlighting - -H, --no-headers Disable headers display - -s, --separator string Custom field separator - -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy search [experimental] - -F, --filter field=reg Filter given field with regex, can be used multiple times + -c, --columns string Only show the speficied columns (separated by ,) + -v, --invert-match select non-matching rows + -n, --no-numbering Disable header numbering + -N, --no-color Disable pattern highlighting + -H, --no-headers Disable headers display + -s, --separator string Custom field separator + -k, --sort-by int Sort by column (default: 1) + -z, --fuzzy Use fuzzy search [experimental] + -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): - -X, --extended Enable extended output - -M, --markdown Enable markdown table output - -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable output - -Y, --yaml Enable yaml output - -C, --csv Enable CSV output - -A, --ascii Default output mode, ascii tabular - -L, --hightlight-lines Use alternating background colors for tables + -X, --extended Enable extended output + -M, --markdown Enable markdown table output + -O, --orgtbl Enable org-mode table output + -S, --shell Enable shell evaluable output + -Y, --yaml Enable yaml output + -C, --csv Enable CSV output + -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): - -a, --sort-age sort according to age (duration) string - -D, --sort-desc Sort in descending order (default: ascending) - -i, --sort-numeric sort according to string numerical value - -t, --sort-time sort according to time string + -a, --sort-age sort according to age (duration) string + -D, --sort-desc Sort in descending order (default: ascending) + -i, --sort-numeric sort according to string numerical value + -t, --sort-time sort according to time string Other Flags: - --completion Generate the autocompletion script for - -f, --config Configuration file (default: ~/.config/tablizer/config) - -l, --load-path Load path for lisp plugins (expects *.zy files) - -d, --debug Enable debugging - -h, --help help for tablizer - -m, --man Display manual page - -V, --version Print program version + --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) + -l, --load-path Load path for lisp plugins (expects *.zy files) + -d, --debug Enable debugging + -h, --help help for tablizer + -m, --man Display manual page + -V, --version Print program version ` diff --git a/lib/filter.go b/lib/filter.go index 2d35113..103508f 100644 --- a/lib/filter.go +++ b/lib/filter.go @@ -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 * 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 { // no filters, no checking - return Tabdata{}, false, nil + return nil, false, nil } 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) */ diff --git a/lib/filter_test.go b/lib/filter_test.go index fc646cb..e779d31 100644 --- a/lib/filter_test.go +++ b/lib/filter_test.go @@ -153,8 +153,8 @@ func TestFilterByFields(t *testing.T) { t.Errorf("PrepareFilters returned error: %s", err) } - data, _, _ := FilterByFields(conf, data) - if !reflect.DeepEqual(data, inputdata.expect) { + data, _, _ := FilterByFields(conf, &data) + if !reflect.DeepEqual(*data, inputdata.expect) { t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect) } }) diff --git a/lib/helpers.go b/lib/helpers.go index 9ce52e8..7c28180 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -40,6 +40,16 @@ func contains(s []int, e int) bool { 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 func ValidateConsistency(data *Tabdata) error { expectedfields := len(data.headers) @@ -57,13 +67,44 @@ func ValidateConsistency(data *Tabdata) error { // parse columns list given with -c, modifies config.UseColumns based // on eventually given regex func PrepareColumns(conf *cfg.Config, data *Tabdata) error { - if conf.Columns == "" { - return nil + // -c columns + usecolumns, err := PrepareColumnVars(conf.Columns, data) + if err != nil { + return err } - for _, use := range strings.Split(conf.Columns, ",") { + conf.UseColumns = usecolumns + + return nil +} + +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 { - 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) @@ -71,15 +112,15 @@ func PrepareColumns(conf *cfg.Config, data *Tabdata) error { // might be a regexp colPattern, err := regexp.Compile(use) 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 for i, head := range data.headers { if colPattern.MatchString(head) { - conf.UseColumns = append(conf.UseColumns, i+1) + usecolumns = append(usecolumns, i+1) } } } else { @@ -87,27 +128,28 @@ func PrepareColumns(conf *cfg.Config, data *Tabdata) error { // 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. - conf.UseColumns = append(conf.UseColumns, usenum) + usecolumns = append(usecolumns, usenum) } } // deduplicate: put all values into a map (value gets map key) // thereby removing duplicates, extract keys into new slice // and sort it - imap := make(map[int]int, len(conf.UseColumns)) - for _, i := range conf.UseColumns { + imap := make(map[int]int, len(usecolumns)) + for _, i := range usecolumns { imap[i] = 0 } - conf.UseColumns = nil + // fill with deduplicated columns + usecolumns = nil 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 diff --git a/lib/parser.go b/lib/parser.go index dfaf62b..ec20a6e 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -175,13 +175,27 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { } // filter by field filters, if any - filtereddata, changed, err := FilterByFields(conf, data) + filtereddata, changed, err := FilterByFields(conf, &data) if err != nil { return data, fmt.Errorf("failed to filter fields: %w", err) } 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 diff --git a/tablizer.1 b/tablizer.1 index 5d98690..b0557c5 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .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 .\" way too many mistakes in technical documents. .if n .ad l @@ -147,40 +147,42 @@ tablizer \- Manipulate tabular output of other programs \& tablizer [regex] [file, ...] [flags] \& \& Operational Flags: -\& \-c, \-\-columns string Only show the speficied columns (separated by ,) -\& \-v, \-\-invert\-match select non\-matching rows -\& \-n, \-\-no\-numbering Disable header numbering -\& \-N, \-\-no\-color Disable pattern highlighting -\& \-H, \-\-no\-headers Disable headers display -\& \-s, \-\-separator string Custom field separator -\& \-k, \-\-sort\-by int Sort by column (default: 1) -\& \-z, \-\-fuzzy Use fuzzy search [experimental] -\& \-F, \-\-filter field=reg Filter given field with regex, can be used multiple times +\& \-c, \-\-columns string Only show the speficied columns (separated by ,) +\& \-v, \-\-invert\-match select non\-matching rows +\& \-n, \-\-no\-numbering Disable header numbering +\& \-N, \-\-no\-color Disable pattern highlighting +\& \-H, \-\-no\-headers Disable headers display +\& \-s, \-\-separator string Custom field separator +\& \-k, \-\-sort\-by int Sort by column (default: 1) +\& \-z, \-\-fuzzy Use fuzzy search [experimental] +\& \-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): -\& \-X, \-\-extended Enable extended output -\& \-M, \-\-markdown Enable markdown table output -\& \-O, \-\-orgtbl Enable org\-mode table output -\& \-S, \-\-shell Enable shell evaluable output -\& \-Y, \-\-yaml Enable yaml output -\& \-C, \-\-csv Enable CSV output -\& \-A, \-\-ascii Default output mode, ascii tabular -\& \-L, \-\-hightlight\-lines Use alternating background colors for tables +\& \-X, \-\-extended Enable extended output +\& \-M, \-\-markdown Enable markdown table output +\& \-O, \-\-orgtbl Enable org\-mode table output +\& \-S, \-\-shell Enable shell evaluable output +\& \-Y, \-\-yaml Enable yaml output +\& \-C, \-\-csv Enable CSV output +\& \-A, \-\-ascii Default output mode, ascii tabular +\& \-L, \-\-hightlight\-lines Use alternating background colors for tables \& \& Sort Mode Flags (mutually exclusive): -\& \-a, \-\-sort\-age sort according to age (duration) string -\& \-D, \-\-sort\-desc Sort in descending order (default: ascending) -\& \-i, \-\-sort\-numeric sort according to string numerical value -\& \-t, \-\-sort\-time sort according to time string +\& \-a, \-\-sort\-age sort according to age (duration) string +\& \-D, \-\-sort\-desc Sort in descending order (default: ascending) +\& \-i, \-\-sort\-numeric sort according to string numerical value +\& \-t, \-\-sort\-time sort according to time string \& \& Other Flags: -\& \-\-completion Generate the autocompletion script for -\& \-f, \-\-config Configuration file (default: ~/.config/tablizer/config) -\& \-l, \-\-load\-path Load path for lisp plugins (expects *.zy files) -\& \-d, \-\-debug Enable debugging -\& \-h, \-\-help help for tablizer -\& \-m, \-\-man Display manual page -\& \-V, \-\-version Print program version +\& \-\-completion Generate the autocompletion script for +\& \-f, \-\-config Configuration file (default: ~/.config/tablizer/config) +\& \-l, \-\-load\-path Load path for lisp plugins (expects *.zy files) +\& \-d, \-\-debug Enable debugging +\& \-h, \-\-help help for tablizer +\& \-m, \-\-man Display manual page +\& \-V, \-\-version Print program version .Ve .SH "DESCRIPTION" .IX Header "DESCRIPTION" diff --git a/tablizer.pod b/tablizer.pod index d90b2f1..f7ea3cd 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -8,40 +8,42 @@ tablizer - Manipulate tabular output of other programs tablizer [regex] [file, ...] [flags] Operational Flags: - -c, --columns string Only show the speficied columns (separated by ,) - -v, --invert-match select non-matching rows - -n, --no-numbering Disable header numbering - -N, --no-color Disable pattern highlighting - -H, --no-headers Disable headers display - -s, --separator string Custom field separator - -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy search [experimental] - -F, --filter field=reg Filter given field with regex, can be used multiple times + -c, --columns string Only show the speficied columns (separated by ,) + -v, --invert-match select non-matching rows + -n, --no-numbering Disable header numbering + -N, --no-color Disable pattern highlighting + -H, --no-headers Disable headers display + -s, --separator string Custom field separator + -k, --sort-by int Sort by column (default: 1) + -z, --fuzzy Use fuzzy search [experimental] + -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): - -X, --extended Enable extended output - -M, --markdown Enable markdown table output - -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable output - -Y, --yaml Enable yaml output - -C, --csv Enable CSV output - -A, --ascii Default output mode, ascii tabular - -L, --hightlight-lines Use alternating background colors for tables + -X, --extended Enable extended output + -M, --markdown Enable markdown table output + -O, --orgtbl Enable org-mode table output + -S, --shell Enable shell evaluable output + -Y, --yaml Enable yaml output + -C, --csv Enable CSV output + -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): - -a, --sort-age sort according to age (duration) string - -D, --sort-desc Sort in descending order (default: ascending) - -i, --sort-numeric sort according to string numerical value - -t, --sort-time sort according to time string + -a, --sort-age sort according to age (duration) string + -D, --sort-desc Sort in descending order (default: ascending) + -i, --sort-numeric sort according to string numerical value + -t, --sort-time sort according to time string Other Flags: - --completion Generate the autocompletion script for - -f, --config Configuration file (default: ~/.config/tablizer/config) - -l, --load-path Load path for lisp plugins (expects *.zy files) - -d, --debug Enable debugging - -h, --help help for tablizer - -m, --man Display manual page - -V, --version Print program version + --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) + -l, --load-path Load path for lisp plugins (expects *.zy files) + -d, --debug Enable debugging + -h, --help help for tablizer + -m, --man Display manual page + -V, --version Print program version =head1 DESCRIPTION