Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas von Dein committed Sep 28, 2022
1 parent 02a64a5 commit 10f4a81
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 2 deletions.
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# no need to modify anything below
tool = tablizer
version = $(shell egrep "^var version = " cmd/root.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows

all:
@echo "Type 'make install' to install $(tool)"

install:
install -m 755 -d $(bindir)
install -m 755 -d $(linkdir)
install -m 755 $(tool) $(bindir)/$(tool)-$(version)
ln -sf $(bindir)/$(tool)-$(version) $(linkdir)/$(tool)

release:
mkdir -p releases
$(foreach arch,$(archs), GOOS=$(arch) GOARCH=amd64 go build -x -o releases/$(tool)-$(arch)-amd64-$(version); sha256sum releases/$(tool)-$(arch)-amd64-$(version) | cut -d' ' -f1 > releases/$(tool)-$(arch)-amd64-$(version).sha256sum;)
95 changes: 93 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,93 @@
# tablizer
Manipulate tabular output of other programs
## tablizer - Manipulate tabular output of other programs

Tablizer can be used to re-format tabular output of other
programs. While you could do this using standard unix tools, in some
cases it's a hard job.

Let's take this output:
```
% kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE
repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-m48n8 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
```

But you're only interested in the NAME and STATUS columns. Here's how
to do this with tablizer:

```
% kubectl get pods | ./tablizer
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-m48n8 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
% kubectl get pods | ./tablizer -c 1,3
NAME(1) STATUS(3)
repldepl-7bcd8d5b64-7zq4l Running
repldepl-7bcd8d5b64-m48n8 Running
repldepl-7bcd8d5b64-q2bf4 Running
```

Another use case is when the tabular output is so wide that lines are
being broken and the whole output is completely distorted. In such a
case you can use the `-x` flag to get an output similar to `\x` in `psql`:

```
% kubectl get pods | ./tablizer -x
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
NAME: repldepl-7bcd8d5b64-m48n8
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
NAME: repldepl-7bcd8d5b64-q2bf4
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
```

Tablize can read one or more files or - if none specified - from STDIN.

You can also specify a regex pattern to reduce the output:

```
% kubectl get pods | ./tablizer q2bf4
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
```


## Installation

Download the latest release file for your architecture and put it into
a directory within your `$PATH`.

## Getting help

Although I'm happy to hear from udpxd users in private email,
that's the best way for me to forget to do something.

In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
https://github.com/TLINDEN/tablizer/issues.

## Copyright and license

This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.

## Authors

T.v.Dein <tom AT vondein DOT org>

## Project homepage

https://github.com/TLINDEN/tablizer
4 changes: 4 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add a mode like FreeBSD stat(1):

stat -s dead.letter
st_dev=170671546954750497 st_ino=159667 st_mode=0100644 st_nlink=1 st_uid=1001 st_gid=1001 st_rdev=18446744073709551615 st_size=573 st_atime=1661994007 st_mtime=1661961878 st_ctime=1661961878 st_birthtime=1658394900 st_blksize=4096 st_blocks=3 st_flags=2048
152 changes: 152 additions & 0 deletions cmd/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package cmd

import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)

// contains a whole parsed table
type Tabdata struct {
maxwidthHeader int // longest header
maxwidthPerCol []int // max width per column
columns int
headerIndices []map[string]int // [ {beg=>0, end=>17}, ... ]
headers []string // [ "ID", "NAME", ...]
entries [][]string
}

func die(v ...interface{}) {
fmt.Fprintln(os.Stderr, v...)
os.Exit(1)
}

/*
Parse tabular input. We split the header (first line) by 2 or more
spaces, remember the positions of the header fields. We then split
the data (everything after the first line) by those positions. That
way we can turn "tabular data" (with fields containing whitespaces)
into real tabular data. We re-tabulate our input if you will.
*/
func parseFile(input io.Reader, pattern string) Tabdata {
data := Tabdata{}

var scanner *bufio.Scanner
var spaces = `\s\s+|$`

if len(Separator) > 0 {
spaces = Separator
}

hadFirst := false
spacefinder := regexp.MustCompile(spaces)
beg := 0

scanner = bufio.NewScanner(input)

for scanner.Scan() {
line := scanner.Text()
values := []string{}

patternR, err := regexp.Compile(pattern)
if err != nil {
die(err)
}

if !hadFirst {
// header processing
parts := spacefinder.FindAllStringIndex(line, -1)
data.columns = len(parts)
// if Debug {
// fmt.Println(parts)
// }

// process all header fields
for _, part := range parts {
// if Debug {
// fmt.Printf("Part: <%s>\n", string(line[beg:part[0]]))
//}

// current field
head := string(line[beg:part[0]])

// register begin and end of field within line
indices := make(map[string]int)
indices["beg"] = beg
if part[0] == part[1] {
indices["end"] = 0
} else {
indices["end"] = part[1] - 1
}

// register widest header field
headerlen := len(head)
if headerlen > data.maxwidthHeader {
data.maxwidthHeader = headerlen
}

// register fields data
data.headerIndices = append(data.headerIndices, indices)
data.headers = append(data.headers, head)

// end of current field == begin of next one
beg = part[1]

// done
hadFirst = true
}
// if Debug {
// fmt.Println(data.headerIndices)
// }
} else {
// data processing
if len(pattern) > 0 {
//fmt.Println(patternR.MatchString(line))
if !patternR.MatchString(line) {
continue
}
}

idx := 0 // we cannot use the header index, because we could exclude columns

for _, index := range data.headerIndices {
value := ""
if index["end"] == 0 {
value = string(line[index["beg"]:])
} else {
value = string(line[index["beg"]:index["end"]])
}

width := len(strings.TrimSpace(value))

if len(data.maxwidthPerCol)-1 < idx {
data.maxwidthPerCol = append(data.maxwidthPerCol, width)
} else {
if width > data.maxwidthPerCol[idx] {
data.maxwidthPerCol[idx] = width
}
}

// if Debug {
// fmt.Printf("<%s> ", value)
// }
values = append(values, value)

idx++
}
if Debug {
fmt.Println()
}
data.entries = append(data.entries, values)
}
}

if scanner.Err() != nil {
die(scanner.Err())
}

return data
}
109 changes: 109 additions & 0 deletions cmd/printer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cmd

import (
"fmt"
"strings"
)

func printTable(data Tabdata) {
if XtendedOut {
printExtended(data)
return
}

// needed for data output
var formats []string

if len(data.entries) > 0 {
// headers
for i, head := range data.headers {
if len(Columns) > 0 {
if !contains(UseColumns, i+1) {
continue
}
}

// calculate column width
var width int
var iwidth int
var format string

// generate format string
if len(head) > data.maxwidthPerCol[i] {
width = len(head)
} else {
width = data.maxwidthPerCol[i]
}

if NoNumbering {
iwidth = 0
} else {
iwidth = len(fmt.Sprintf("%d", i)) // in case i > 9
}

format = fmt.Sprintf("%%-%ds", 3+iwidth+width)

if NoNumbering {
fmt.Printf(format, fmt.Sprintf("%s ", head))
} else {
fmt.Printf(format, fmt.Sprintf("%s(%d) ", head, i+1))
}

// register
formats = append(formats, format)
}
fmt.Println()

// entries
var idx int
for _, entry := range data.entries {
idx = 0
//fmt.Println(entry)
for i, value := range entry {
if len(Columns) > 0 {
if !contains(UseColumns, i+1) {
continue
}
}
fmt.Printf(formats[idx], strings.TrimSpace(value))
idx++
}
fmt.Println()
}
}
}

/*
We simulate the \x command of psql (the PostgreSQL client)
*/
func printExtended(data Tabdata) {
// needed for data output
format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) // FIXME: re-calculate if -c has been set

if len(data.entries) > 0 {
var idx int
for _, entry := range data.entries {
idx = 0
for i, value := range entry {
if len(Columns) > 0 {
if !contains(UseColumns, i+1) {
continue
}
}

fmt.Printf(format, data.headers[idx], value)
idx++
}
fmt.Println()
}
}
}

func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
Loading

0 comments on commit 10f4a81

Please sign in to comment.