Compare commits

..

11 Commits

18 changed files with 532 additions and 54 deletions

View File

@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
## [v1.0.10](https://github.com/TLINDEN/tablizer/tree/v1.0.10) - 2022-10-15
### Added
- Added various sort modes: sort by time, by duration, numerical (-a -t -i)
- Added possibility to modify sort order to descending (-D)
- Added support to specify a regexp in column selector -c, which can
also be mixed with numerical column spec
- More unit tests
### Fixed
- Column specification allowed to specify duplicate columns like `-c
1,2,1,2` unchecked. Now this list will be deduplicated before use.
## [v1.0.9](https://github.com/TLINDEN/tablizer/tree/v1.0.9) - 2022-10-14
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.8...v1.0.9)

View File

@@ -1,7 +1,9 @@
# No Code of Conduct
This project does **NOT** have a so called Code of Conduct, nor will
it ever get one.
*TL;DR:* This project does **NOT** have a so called Code of Conduct,
nor will it ever have one.
## The Rant
The reasons are somewhat complicated and I'll try my best to document
them here.
@@ -42,28 +44,22 @@ might be sarcastic connotations I don't understand or references to
historical figures, events or traditions I don't know and never have
heard of.
Juding over other peoples online behavior looks like a titanic task to
me. It is just not my job to judge others. I am not legitimized or
authorized to do so.
Judging over other peoples online behavior looks like a titanic task
to me. It is just not my job to judge others, I am not legitimized or
authorized to do so and I am not interested in this kind of business.
Another huge problem with ethical rules is that you need to outline
and enforce sanctions on thos who violate the rules. But since I am
not an elected authority how would I be able to do this? I don't
know. And what happens if someone complains about myself? Shall I
and enforce sanctions on those who violate the rules. But since I am
not an elected authority how would I be able to do this? I don't
know. And what happens if someone complains about myself? Shall I
remove myself from my own project? Come on!
Last but not least there's the law. I am a german citizen and am
living in relatively freedom. Unlike many other people living in
democracies these days, I myself fought for this very freedom on the
streets of Leipzig in 1989. I saw the tanks, the Stasi officers, I
felt the fear. But the laws under I live today and which I have to
adhere to, are only limited to the small speck on earth I am living on.
So, let's say someone in india says something insulting to some other
developer in an issue. Of course german law does not apply to indian
people. More, the insult might actually not be an insult in india. In
the end, nothing would happen. Under normal circumstances, maintainers
would delete the posting, ban the user or remove push privileges etc.
Last but not least there's the law. So, let's say someone in india
says something insulting to some other developer in an issue. Of
course german law does not apply to indian people. More, the insult
might actually not be an insult in india. In the end, nothing would
happen. Under normal circumstances, maintainers would delete the
posting, ban the user or remove push privileges etc.
But then, is there a way for the offending user to defend himself? Of
course not, since neither indian or german law alone applies. I cannot
@@ -71,13 +67,18 @@ go to a german court and sue the guy and he cannot do the same in
india. Or - we possibly could but the judges on both countries would
just laugh and close the case.
And let's not even start talking about there undemocratic "comitees"
many projects are forming to circumvent this problem.
That being said, I don't have the power nor the tools, nor the
authority to enforce serious sanctions of any meaningful kind against
others. Therefore I cannot outline any rules whatsoever.
And let's not even start talking about there undemocratic "comitees"
many projects are forming to circumvent this problem. Some projects
even include external entities like a lawer or some bureaucrat
somewhere just to have the ability to complain against a comitee
member. What a mess!
## So, which are the ethical rules within this project then?
Well, there are none.
@@ -86,7 +87,8 @@ This project is about code, not society. It doesn't matter where you
come from, how you look, how you think, what you believe, who your
friends are, whay you said or did sometime in the past. I don't even
care if you are a human being. You are an alien so bored that you need
to submit code on github? Fine with me.
to submit code on github? Fine with me. You're a convicted criminal? I
don't give a shit!
**The only thing I am interested here is Code and only Code.**

1
TODO
View File

@@ -1 +0,0 @@

11
TODO.md Normal file
View File

@@ -0,0 +1,11 @@
## Fixes to be implemented
## Features to be implemented
- add output modes yaml and csv
- add --no-headers option
- add input parsing support for CSV including unquoting of stuff
like: `"xxx","1919 b"` etc, maybe an extra option for unquoting

View File

@@ -60,15 +60,12 @@ var rootCmd = &cobra.Command{
return nil
}
err := lib.PrepareColumns()
err := lib.PrepareModeFlags()
if err != nil {
return err
}
err = lib.PrepareModeFlags()
if err != nil {
return err
}
lib.PrepareSortFlags()
return lib.ProcessFiles(args)
},
@@ -90,7 +87,13 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", lib.DefaultSeparator, "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
// sort options
rootCmd.PersistentFlags().IntVarP(&lib.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)")
rootCmd.PersistentFlags().BoolVarP(&lib.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)")
rootCmd.PersistentFlags().BoolVarP(&lib.SortNumeric, "sort-numeric", "i", false, "sort according to string numerical value")
rootCmd.PersistentFlags().BoolVarP(&lib.SortTime, "sort-time", "t", false, "sort according to time string")
rootCmd.PersistentFlags().BoolVarP(&lib.SortAge, "sort-age", "a", false, "sort according to age (duration) string")
// output flags, only 1 allowed, hidden, since just short cuts
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output")

View File

@@ -21,7 +21,11 @@ SYNOPSIS
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator
-a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-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
-v, --version Print program version
DESCRIPTION
@@ -64,7 +68,7 @@ DESCRIPTION
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which
columns you want to have in your output:
columns you want to have in your output (see COLUMNS:
kubectl get pods | tablizer -c1,3
@@ -78,7 +82,18 @@ DESCRIPTION
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.
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:
-a --sort-age
Sorts duration strings like "1d4h32m51s".
-i --sort-numeric
Sorts numeric fields.
-t --sort-time
Sorts timestamps.
Finally the -d option enables debugging output which is mostly useful
for the developer.
@@ -107,6 +122,32 @@ DESCRIPTION
kubectl get pods -A | tablizer "(?i)account"
COLUMNS
The parameter -c can be used to specify, which columns to display. By
default tablizer numerizes the header names and these numbers can be
used to specify which header to display, see example above.
However, beside numbers, you can also use regular expressions with -c,
also separated by comma. And you can mix column numbers with regexps.
Lets take this table:
PID TTY TIME CMD
14001 pts/0 00:00:00 bash
42871 pts/0 00:00:00 ps
42872 pts/0 00:00:00 sed
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
tablizer
sed
where "C" is our regexp which matches CMD.
OUTPUT MODES
There might be cases when the tabular output of a program is way too
large for your current terminal but you still need to see every column.

7
go.mod
View File

@@ -4,15 +4,18 @@ go 1.18
require (
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/gookit/color v1.5.2
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.5.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
github.com/xhit/go-str2duration/v2 v2.0.0
)
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
)

11
go.sum
View File

@@ -1,5 +1,7 @@
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -8,22 +10,29 @@ github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xhit/go-str2duration/v2 v2.0.0 h1:uFtk6FWB375bP7ewQl+/1wBcn840GPhnySOdcz/okPE=
github.com/xhit/go-str2duration/v2 v2.0.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=

View File

@@ -68,7 +68,7 @@ var (
validOutputmodes = "(orgtbl|markdown|extended|ascii)"
// main program version
Version = "v1.0.9"
Version = "v1.0.10"
// generated version string, used by -v contains lib.Version on
// main branch, and lib.Version-$branch-$lastcommit-$date on
@@ -76,7 +76,13 @@ var (
VERSION string
// sorting
SortByColumn int
SortByColumn int
SortDescending bool
SortNumeric bool
SortTime bool
SortAge bool
SortMode string
)
// contains a whole parsed table

View File

@@ -23,6 +23,7 @@ import (
"github.com/gookit/color"
"os"
"regexp"
"sort"
"strconv"
"strings"
)
@@ -36,22 +37,59 @@ func contains(s []int, e int) bool {
return false
}
func PrepareColumns() error {
// parse columns list given with -c
func PrepareColumns(data *Tabdata) error {
UseColumns = nil
if len(Columns) > 0 {
for _, use := range strings.Split(Columns, ",") {
usenum, err := strconv.Atoi(use)
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", Columns, err)
if len(use) == 0 {
msg := fmt.Sprintf("Could not parse columns list %s: empty column", Columns)
return errors.New(msg)
}
UseColumns = append(UseColumns, usenum)
usenum, err := strconv.Atoi(use)
if err != nil {
// might be a regexp
colPattern, err := regexp.Compile(use)
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", Columns, err)
return errors.New(msg)
}
// find matching header fields
for i, head := range data.headers {
if colPattern.MatchString(head) {
UseColumns = append(UseColumns, i+1)
}
}
} else {
// we digress from go best practises here, because if
// 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.
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(UseColumns))
for _, i := range UseColumns {
imap[i] = 0
}
UseColumns = nil
for k := range imap {
UseColumns = append(UseColumns, k)
}
sort.Ints(UseColumns)
}
return nil
}
// prepare headers: add numbers to headers
func numberizeHeaders(data *Tabdata) {
// prepare headers: add numbers to headers
numberedHeaders := []string{}
maxwidth := 0 // start from scratch, so we only look at displayed column widths
@@ -83,8 +121,8 @@ func numberizeHeaders(data *Tabdata) {
}
}
// exclude columns, if any
func reduceColumns(data *Tabdata) {
// exclude columns, if any
if len(Columns) > 0 {
reducedEntries := [][]string{}
var reducedEntry []string
@@ -136,6 +174,19 @@ func PrepareModeFlags() error {
return nil
}
func PrepareSortFlags() {
switch {
case SortNumeric:
SortMode = "numeric"
case SortAge:
SortMode = "duration"
case SortTime:
SortMode = "time"
default:
SortMode = "string"
}
}
func trimRow(row []string) []string {
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()!
var fixedrow []string

View File

@@ -45,6 +45,23 @@ func Testcontains(t *testing.T) {
}
func TestPrepareColumns(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"2", "3", "4",
},
},
}
var tests = []struct {
input string
exp []int
@@ -52,14 +69,15 @@ func TestPrepareColumns(t *testing.T) {
}{
{"1,2,3", []int{1, 2, 3}, false},
{"1,2,", []int{}, true},
{"a,b", []int{}, true},
{"T", []int{2, 3}, false},
{"T,2,3", []int{2, 3}, false},
}
for _, tt := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror)
t.Run(testname, func(t *testing.T) {
Columns = tt.input
err := PrepareColumns()
err := PrepareColumns(&data)
if err != nil {
if !tt.wanterror {
t.Errorf("got error: %v", err)

View File

@@ -44,6 +44,12 @@ func ProcessFiles(args []string) error {
if err != nil {
return err
}
err = PrepareColumns(&data)
if err != nil {
return err
}
printData(&data)
}

View File

@@ -105,6 +105,12 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) {
idx++
}
// fill up missing fields, if any
for i := len(values); i < len(data.headers); i++ {
values = append(values, "")
}
data.entries = append(data.entries, values)
}
}

View File

@@ -92,7 +92,7 @@ asd igig cxxxncnc
19191 EDD 1 X`
for _, tt := range tests {
testname := fmt.Sprintf("parse-with-inverted-pattern-%t", tt.invert)
testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert)
t.Run(testname, func(t *testing.T) {
InvertMatch = tt.invert
@@ -110,3 +110,41 @@ asd igig cxxxncnc
})
}
}
func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 1,
},
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"asd", "igig", "",
},
{
"19191", "EDD 1", "X",
},
},
}
table := `ONE TWO THREE
asd igig
19191 EDD 1 X`
readFd := strings.NewReader(table)
gotdata, err := parseFile(readFd, "")
Separator = DefaultSeparator
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
}
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
Separator, data, gotdata)
}
}

View File

@@ -153,11 +153,13 @@ func TestSortPrinter(t *testing.T) {
var tests = []struct {
data Tabdata
sortby int
desc bool
expect string
}{
{
data: startdata,
sortby: 1,
desc: false,
expect: `ONE(1) TWO(2) THREE(3)
abc 345 b1
bcd 234 a2
@@ -167,6 +169,7 @@ cde 123 c3`,
{
data: startdata,
sortby: 2,
desc: false,
expect: `ONE(1) TWO(2) THREE(3)
cde 123 c3
bcd 234 a2
@@ -176,11 +179,21 @@ abc 345 b1`,
{
data: startdata,
sortby: 3,
desc: false,
expect: `ONE(1) TWO(2) THREE(3)
bcd 234 a2
abc 345 b1
cde 123 c3`,
},
{
data: startdata,
sortby: 1,
desc: true,
expect: `ONE(1) TWO(2) THREE(3)
cde 123 c3
bcd 234 a2
abc 345 b1`,
},
}
NoColor = true
@@ -188,9 +201,11 @@ cde 123 c3`,
origStdout, reader := stdout2pipe(t)
for _, tt := range tests {
testname := fmt.Sprintf("print-sorted-table-%d", tt.sortby)
testname := fmt.Sprintf("print-sorted-table-by-column-%d-desc-%t",
tt.sortby, tt.desc)
t.Run(testname, func(t *testing.T) {
SortByColumn = tt.sortby
SortDescending = tt.desc
printData(&tt.data)
@@ -212,3 +227,111 @@ cde 123 c3`,
// Restore
os.Stdout = origStdout
}
func TestSortByPrinter(t *testing.T) {
data := Tabdata{
maxwidthHeader: 8,
maxwidthPerCol: []int{
5,
9,
3,
26,
},
columns: 4,
headers: []string{
"NAME",
"DURATION",
"COUNT",
"WHEN",
},
entries: [][]string{
{
"beta",
"1d10h5m1s",
"33",
"3/1/2014",
},
{
"alpha",
"4h35m",
"170",
"2013-Feb-03",
},
{
"ceta",
"33d12h",
"9",
"06/Jan/2008 15:04:05 -0700",
},
},
}
var tests = []struct {
sortby string
column int
desc bool
expect string
}{
{
column: 3,
sortby: "numeric",
desc: false,
expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03`,
},
{
column: 2,
sortby: "duration",
desc: false,
expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4)
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
{
column: 4,
sortby: "time",
desc: false,
expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014`,
},
}
NoColor = true
OutputMode = "ascii"
origStdout, reader := stdout2pipe(t)
for _, tt := range tests {
testname := fmt.Sprintf("print-sorted-table-by-column-%d-desc-%t-sort-by-%s",
tt.column, tt.desc, tt.sortby)
t.Run(testname, func(t *testing.T) {
SortByColumn = tt.column
SortDescending = tt.desc
SortMode = tt.sortby
testdata := data
printData(&testdata)
buf := make([]byte, 1024)
n, err := reader.Read(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
output := strings.TrimSpace(string(buf))
if output != tt.expect {
t.Errorf("sort column: %d, sortby: %s, got:\n%s\nwant:\n%s",
tt.column, tt.sortby, output, tt.expect)
}
})
}
// Restore
os.Stdout = origStdout
}

View File

@@ -18,7 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
import (
"github.com/araddon/dateparse"
str2duration "github.com/xhit/go-str2duration/v2"
"sort"
"strconv"
)
func sortTable(data *Tabdata, col int) {
@@ -41,6 +44,45 @@ func sortTable(data *Tabdata, col int) {
// actual sorting
sort.SliceStable(data.entries, func(i, j int) bool {
return data.entries[i][col] < data.entries[j][col]
return compare(data.entries[i][col], data.entries[j][col])
})
}
func compare(a string, b string) bool {
var comp bool
switch SortMode {
case "numeric":
left, err := strconv.Atoi(a)
if err != nil {
left = 0
}
right, err := strconv.Atoi(b)
if err != nil {
right = 0
}
comp = left < right
case "duration":
left, err := str2duration.ParseDuration(a)
if err != nil {
left = 0
}
right, err := str2duration.ParseDuration(b)
if err != nil {
right = 0
}
comp = left.Seconds() < right.Seconds()
case "time":
left, _ := dateparse.ParseAny(a)
right, _ := dateparse.ParseAny(b)
comp = left.Unix() < right.Unix()
default:
comp = a < b
}
if SortDescending {
comp = !comp
}
return comp
}

View File

@@ -133,7 +133,7 @@
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
.TH TABLIZER 1 "2022-10-14" "1" "User Commands"
.TH TABLIZER 1 "2022-10-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
@@ -159,7 +159,11 @@ tablizer \- Manipulate tabular output of other programs
\& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-s, \-\-separator string Custom field separator
\& \-a, \-\-sort\-age sort according to age (duration) string
\& \-k, \-\-sort\-by int Sort by column (default: 1)
\& \-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
\& \-v, \-\-version Print program version
.Ve
.SH "DESCRIPTION"
@@ -209,7 +213,7 @@ have a numer associated with it, e.g.:
.Ve
.PP
These numbers denote the column and you can use them to specify which
columns you want to have in your output:
columns you want to have in your output (see \s-1COLUMNS\s0:
.PP
.Vb 1
\& kubectl get pods | tablizer \-c1,3
@@ -225,7 +229,19 @@ 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.
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:
.IP "\fB\-a \-\-sort\-age\fR" 4
.IX Item "-a --sort-age"
Sorts duration strings like \*(L"1d4h32m51s\*(R".
.IP "\fB\-i \-\-sort\-numeric\fR" 4
.IX Item "-i --sort-numeric"
Sorts numeric fields.
.IP "\fB\-t \-\-sort\-time\fR" 4
.IX Item "-t --sort-time"
Sorts timestamps.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly
useful for the developer.
@@ -261,6 +277,38 @@ Example for a case insensitive search:
.Vb 1
\& kubectl get pods \-A | tablizer "(?i)account"
.Ve
.SS "\s-1COLUMNS\s0"
.IX Subsection "COLUMNS"
The parameter \fB\-c\fR can be used to specify, which columns to
display. By default tablizer numerizes the header names and these
numbers can be used to specify which header to display, see example
above.
.PP
However, beside numbers, you can also use regular expressions with
\&\fB\-c\fR, also separated by comma. And you can mix column numbers with
regexps.
.PP
Lets take this table:
.PP
.Vb 4
\& PID TTY TIME CMD
\& 14001 pts/0 00:00:00 bash
\& 42871 pts/0 00:00:00 ps
\& 42872 pts/0 00:00:00 sed
.Ve
.PP
We want to see only the \s-1CMD\s0 column and use a regex for this:
.PP
.Vb 6
\& ps | tablizer \-s \*(Aq\es+\*(Aq \-c C
\& CMD(4)
\& bash
\& ps
\& tablizer
\& sed
.Ve
.PP
where \*(L"C\*(R" is our regexp which matches \s-1CMD.\s0
.SS "\s-1OUTPUT MODES\s0"
.IX Subsection "OUTPUT MODES"
There might be cases when the tabular output of a program is way too

View File

@@ -20,7 +20,11 @@ tablizer - Manipulate tabular output of other programs
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator
-a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-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
-v, --version Print program version
@@ -67,7 +71,7 @@ have a numer associated with it, e.g.:
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which
columns you want to have in your output:
columns you want to have in your output (see L<COLUMNS>:
kubectl get pods | tablizer -c1,3
@@ -81,7 +85,26 @@ 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.
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:
=over
=item B<-a --sort-age>
Sorts duration strings like "1d4h32m51s".
=item B<-i --sort-numeric>
Sorts numeric fields.
=item B<-t --sort-time>
Sorts timestamps.
=back
Finally the B<-d> option enables debugging output which is mostly
useful for the developer.
@@ -114,6 +137,35 @@ Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account"
=head2 COLUMNS
The parameter B<-c> can be used to specify, which columns to
display. By default tablizer numerizes the header names and these
numbers can be used to specify which header to display, see example
above.
However, beside numbers, you can also use regular expressions with
B<-c>, also separated by comma. And you can mix column numbers with
regexps.
Lets take this table:
PID TTY TIME CMD
14001 pts/0 00:00:00 bash
42871 pts/0 00:00:00 ps
42872 pts/0 00:00:00 sed
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
tablizer
sed
where "C" is our regexp which matches CMD.
=head2 OUTPUT MODES
There might be cases when the tabular output of a program is way too