mirror of
https://codeberg.org/scip/tablizer.git
synced 2025-12-17 04:30:56 +01:00
added transpose function (-T + -R)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ SYNOPSIS
|
||||
-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
|
||||
@@ -410,6 +412,8 @@ Operational Flags:
|
||||
-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
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -156,6 +156,8 @@ tablizer \- Manipulate tabular output of other programs
|
||||
\& \-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
|
||||
|
||||
@@ -17,6 +17,8 @@ tablizer - Manipulate tabular output of other programs
|
||||
-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
|
||||
|
||||
Reference in New Issue
Block a user