diff --git a/cfg/config.go b/cfg/config.go index 424f6ad..3d4840a 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -28,7 +28,7 @@ import ( ) const DefaultSeparator string = `(\s\s+|\t)` -const Version string = "v1.3.0" +const Version string = "v1.3.1" const MAXPARTS = 2 var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config" @@ -67,9 +67,10 @@ type Config struct { UseFuzzySearch bool UseHighlight bool - SortMode string - SortDescending bool - SortByColumn int + SortMode string + SortDescending bool + SortByColumn string // 1,2 + UseSortByColumn []int // []int{1,2} TransposeColumns string // 1,2 UseTransposeColumns []int // []int{1,2} diff --git a/cmd/root.go b/cmd/root.go index 057794b..afbf0df 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -151,7 +151,7 @@ func Execute() { "Transpose the speficied columns (separated by ,)") // sort options - rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, + rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "", "Sort by column (default: 1)") // sort mode, only 1 allowed diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 4139129..71f5b21 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -15,7 +15,7 @@ SYNOPSIS -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) + -k, --sort-by int|name 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 ,) @@ -103,10 +103,19 @@ DESCRIPTION highlighted. You can disable this behavior with the -N option. Use the -k option to specify by which column to sort the tabular data - (as in GNU sort(1)). The default sort column is the first one. To - disable sorting at all, supply 0 (Zero) to -k. The default sort order is - ascending. You can change this to descending order using the option -D. - The default sort order is by string, but there are other sort modes: + (as in GNU sort(1)). The default sort column is the first one. You can + specify column numbers or names. Column numbers start with 1, names are + case insensitive. You can specify multiple columns separated by comma to + sort, but the type must be the same. For example if you want to sort + numerically, all columns must be numbers. If you use column numbers, + then be aware, that these are the numbers before column extraction. For + example if you have a table with 4 columns and specify "-c4", then only + 1 column (the fourth) will be printed, however if you want to sort by + this column, you'll have to specify "-k4". + + The default sort order is ascending. You can change this to descending + order using the option -D. The default sort order is by alphanumeric + string, but there are other sort modes: -a --sort-age Sorts duration strings like "1d4h32m51s". @@ -392,7 +401,7 @@ Operational Flags: -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) + -k, --sort-by int|name 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 ,) diff --git a/lib/helpers.go b/lib/helpers.go index 176e64e..32b6705 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -99,6 +99,19 @@ func PrepareTransposerColumns(conf *cfg.Config, data *Tabdata) error { 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 +} + func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) { if columns == "" { return nil, nil diff --git a/lib/io.go b/lib/io.go index c0dd937..86954b8 100644 --- a/lib/io.go +++ b/lib/io.go @@ -48,6 +48,11 @@ func ProcessFiles(conf *cfg.Config, args []string) error { return err } + err = PrepareSortColumns(conf, &data) + if err != nil { + return err + } + err = PrepareColumns(conf, &data) if err != nil { return err diff --git a/lib/printer.go b/lib/printer.go index 7adcd00..3189060 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -33,15 +33,17 @@ import ( ) func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { - // add numbers to headers and remove this we're not interested in + // Sort the data first, before headers+entries are being + // reduced. That way the user can specify any valid column to sort + // by, independently if it's being used for display or not. + sortTable(conf, data) + + // add numbers to headers and remove those we're not interested in numberizeAndReduceHeaders(conf, data) // remove unwanted columns, if any reduceColumns(conf, data) - // sort the data - sortTable(conf, data) - switch conf.OutputMode { case cfg.Extended: printExtendedData(writer, conf, data) diff --git a/lib/printer_test.go b/lib/printer_test.go index 73ced90..0939028 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -63,7 +63,7 @@ var tests = []struct { name string // so we can identify which one fails, can be the same // for multiple tests, because flags will be appended to the name sortby string // empty == default - column int // sort by this column, 0 == default first or NO Sort + column int // sort by this column (numbers start by 1) desc bool // sort in descending order, default == ascending nonum bool // hide numbering mode int // shell, orgtbl, etc. empty == default: ascii @@ -162,7 +162,7 @@ DURATION(2): 33d12h //------------------------ SORT TESTS { - name: "sortbycolumn", + name: "sortbycolumn3", column: 3, sortby: "numeric", desc: false, @@ -173,7 +173,7 @@ beta 1d10h5m1s 33 3/1/2014 alpha 4h35m 170 2013-Feb-03`, }, { - name: "sortbycolumn", + name: "sortbycolumn4", column: 4, sortby: "time", desc: false, @@ -184,7 +184,7 @@ alpha 4h35m 170 2013-Feb-03 beta 1d10h5m1s 33 3/1/2014`, }, { - name: "sortbycolumn", + name: "sortbycolumn2", column: 2, sortby: "duration", desc: false, @@ -251,15 +251,14 @@ DURATION(2) WHEN(4) func TestPrinter(t *testing.T) { for _, testdata := range tests { - testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s", - testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr) + testname := fmt.Sprintf("print-%s-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s", + testdata.name, testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr) t.Run(testname, func(t *testing.T) { // replaces os.Stdout, but we ignore it var writer bytes.Buffer // cmd flags conf := cfg.Config{ - SortByColumn: testdata.column, SortDescending: testdata.desc, SortMode: testdata.sortby, OutputMode: testdata.mode, @@ -268,6 +267,10 @@ func TestPrinter(t *testing.T) { NoColor: true, } + if testdata.column > 0 { + conf.UseSortByColumn = []int{testdata.column} + } + conf.ApplyDefaults() // the test checks the len! diff --git a/lib/sort.go b/lib/sort.go index 2d41bf9..5d65e3f 100644 --- a/lib/sort.go +++ b/lib/sort.go @@ -18,6 +18,7 @@ along with this program. If not, see . package lib import ( + "cmp" "regexp" "sort" "strconv" @@ -27,34 +28,41 @@ import ( ) func sortTable(conf cfg.Config, data *Tabdata) { - if conf.SortByColumn <= 0 { + if len(conf.UseSortByColumn) == 0 { // no sorting wanted return } - // slightly modified here to match internal array indicies - col := conf.SortByColumn - - col-- // ui starts counting by 1, but use 0 internally - // sanity checks if len(data.entries) == 0 { return } - if col >= len(data.headers) { - // fall back to default column - col = 0 - } - // actual sorting sort.SliceStable(data.entries, func(i, j int) bool { - return compare(&conf, data.entries[i][col], data.entries[j][col]) + // holds the result of a sort of one column + comparators := []int{} + + // iterate over all columns to be sorted, conf.SortMode must be identical! + for _, column := range conf.UseSortByColumn { + comparators = append(comparators, compare(&conf, data.entries[i][column-1], data.entries[j][column-1])) + } + + // return the combined result + res := cmp.Or(comparators...) + + switch res { + case 0: + return true + default: + return false + } + }) } // config is not modified here, but it would be inefficient to copy it every loop -func compare(conf *cfg.Config, left string, right string) bool { +func compare(conf *cfg.Config, left string, right string) int { var comp bool switch conf.SortMode { @@ -88,7 +96,12 @@ func compare(conf *cfg.Config, left string, right string) bool { comp = !comp } - return comp + switch comp { + case true: + return 0 + default: + return 1 + } } /* diff --git a/lib/sort_test.go b/lib/sort_test.go index 4d19cfa..83df8b3 100644 --- a/lib/sort_test.go +++ b/lib/sort_test.go @@ -53,18 +53,18 @@ func TestCompare(t *testing.T) { mode string a string b string - want bool + want int desc bool }{ // ascending - {"numeric", "10", "20", true, false}, - {"duration", "2d4h5m", "45m", false, false}, - {"time", "12/24/2022", "1/1/1970", false, false}, + {"numeric", "10", "20", 0, false}, + {"duration", "2d4h5m", "45m", 1, false}, + {"time", "12/24/2022", "1/1/1970", 1, false}, // descending - {"numeric", "10", "20", false, true}, - {"duration", "2d4h5m", "45m", true, true}, - {"time", "12/24/2022", "1/1/1970", true, true}, + {"numeric", "10", "20", 1, true}, + {"duration", "2d4h5m", "45m", 0, true}, + {"time", "12/24/2022", "1/1/1970", 0, true}, } for _, testdata := range tests { @@ -75,7 +75,7 @@ func TestCompare(t *testing.T) { c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc} got := compare(&c, testdata.a, testdata.b) if got != testdata.want { - t.Errorf("got %t, want %t", got, testdata.want) + t.Errorf("got %d, want %d", got, testdata.want) } }) } diff --git a/t/testtable3 b/t/testtable3 new file mode 100644 index 0000000..4d8dac7 --- /dev/null +++ b/t/testtable3 @@ -0,0 +1,6 @@ +NAME READY STATUS STARTS AGE +alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 11d +kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 1h44m +grafana-fcc54cbc9-bk7s8 1/1 Running 17 1d +kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 45m +kube-prometheus-node-exporter-bfzpl 1/1 Running 17 54s diff --git a/t/testtable4 b/t/testtable4 new file mode 100644 index 0000000..cc06395 --- /dev/null +++ b/t/testtable4 @@ -0,0 +1,4 @@ +ONE TWO +1 4 +3 1 +5 2 diff --git a/tablizer.1 b/tablizer.1 index 49b640e..73dd6b4 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2025-01-14" "1" "User Commands" +.TH TABLIZER 1 "2025-01-15" "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 @@ -153,7 +153,7 @@ tablizer \- Manipulate tabular output of other programs \& \-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) +\& \-k, \-\-sort\-by int|name 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 ,) @@ -250,11 +250,20 @@ By default, if a \fBpattern\fR has been speficied, matches will be highlighted. You can disable this behavior with the \fB\-N\fR option. .PP Use the \fB\-k\fR option to specify by which column to sort the tabular -data (as in \s-1GNU\s0 \fBsort\fR\|(1)). The default sort column is the first one. To -disable sorting at all, supply 0 (Zero) to \-k. The default sort order -is ascending. You can change this to descending order using the option -\&\fB\-D\fR. The default sort order is by string, but there are other sort -modes: +data (as in \s-1GNU\s0 \fBsort\fR\|(1)). The default sort column is the first +one. You can specify column numbers or names. Column numbers start +with 1, names are case insensitive. You can specify multiple columns +separated by comma to sort, but the type must be the same. For example +if you want to sort numerically, all columns must be numbers. If you +use column numbers, then be aware, that these are the numbers before +column extraction. For example if you have a table with 4 columns and +specify \f(CW\*(C`\-c4\*(C'\fR, then only 1 column (the fourth) will be printed, +however if you want to sort by this column, you'll have to specify +\&\f(CW\*(C`\-k4\*(C'\fR. +.PP +The default sort order is ascending. You can change this to +descending order using the option \fB\-D\fR. The default sort order is by +alphanumeric string, but there are other sort modes: .IP "\fB\-a \-\-sort\-age\fR" 4 .IX Item "-a --sort-age" Sorts duration strings like \*(L"1d4h32m51s\*(R". diff --git a/tablizer.pod b/tablizer.pod index 90fd331..8f170ae 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -14,7 +14,7 @@ tablizer - Manipulate tabular output of other programs -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) + -k, --sort-by int|name 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 ,) @@ -106,11 +106,20 @@ By default, if a B has been speficied, matches will be highlighted. You can disable this behavior with the B<-N> option. Use the B<-k> option to specify by which column to sort the tabular -data (as in GNU sort(1)). The default sort column is the first one. To -disable sorting at all, supply 0 (Zero) to -k. The default sort order -is ascending. You can change this to descending order using the option -B<-D>. The default sort order is by string, but there are other sort -modes: +data (as in GNU sort(1)). The default sort column is the first +one. You can specify column numbers or names. Column numbers start +with 1, names are case insensitive. You can specify multiple columns +separated by comma to sort, but the type must be the same. For example +if you want to sort numerically, all columns must be numbers. If you +use column numbers, then be aware, that these are the numbers before +column extraction. For example if you have a table with 4 columns and +specify C<-c4>, then only 1 column (the fourth) will be printed, +however if you want to sort by this column, you'll have to specify +C<-k4>. + +The default sort order is ascending. You can change this to +descending order using the option B<-D>. The default sort order is by +alphanumeric string, but there are other sort modes: =over