Compare commits

...

44 Commits

Author SHA1 Message Date
5be18e27c9 Merge branch 'development' 2022-10-05 09:20:02 +02:00
2c410e1cb3 added -v flag, replace 'help' subcommand wich -m, more tests 2022-10-05 09:12:46 +02:00
1b622284a1 Merge branch 'development' 2022-10-04 16:14:04 +02:00
404481c3dc fixed badge uri and workflow name 2022-10-04 16:12:12 +02:00
15f437314a Merge branch 'main' of github.com:TLINDEN/tablizer 2022-10-04 16:08:34 +02:00
3746c7f326 Merge branch 'development' 2022-10-04 16:06:59 +02:00
b7b638636d removed circleci, added badge 2022-10-04 16:05:02 +02:00
dd13300c8b add manpage to source (regen @ make) 2022-10-04 15:25:22 +02:00
a59a6cb7d8 fixed actions 2022-10-04 15:15:14 +02:00
d7ea0017b7 moved again 2022-10-04 15:14:21 +02:00
09dc1f3e60 mv 2022-10-04 15:12:55 +02:00
43dc4ff031 added dev version, changed go namespace, added inline manpage 2022-10-04 15:09:13 +02:00
4596d9d589 added gh workflow 2022-10-04 15:08:49 +02:00
T.v.Dein
f2acd2c1b1 Merge pull request #2 from TLINDEN/development
Merge latest dev
2022-10-03 13:35:25 +02:00
76f49a532f added shell mode output 2022-10-03 13:28:04 +02:00
3fd2e6ac2f use pointer of Tabdata, so there's only 1 copy around 2022-10-03 13:11:39 +02:00
65cbaddd5f don't use die() anymore, butt dtd go errros 2022-10-03 12:57:49 +02:00
9f5fc6924e fix release maker 2022-10-02 15:09:25 +02:00
07b65bcff5 fix release maker 2022-10-02 15:06:11 +02:00
T.v.Dein
2f46716a7a Merge pull request #1 from TLINDEN/development
Dev => main
2022-10-02 14:59:08 +02:00
e6723a6951 continued refactoring, added more tests, better error handling 2022-10-02 14:22:31 +02:00
66c4b68036 Merge branch 'development' of github.com:TLINDEN/tablizer into development 2022-10-01 14:15:38 +02:00
4ca3a56280 refactored output printer, now using tablewriter 2022-10-01 14:14:53 +02:00
T.v.Dein
487470818c No more x compile 2022-09-30 22:28:18 +02:00
1b1b63caa3 +badge 2022-09-30 19:52:16 +02:00
d38bae0dd1 ci fixes 2022-09-30 19:47:27 +02:00
282e87d8cc ci fixes 2022-09-30 19:45:39 +02:00
a9979714ba ci fixes 2022-09-30 19:44:47 +02:00
f4dc6c62e6 ci fixes 2022-09-30 19:42:29 +02:00
c8ebf7fde2 added tests 2022-09-30 19:38:54 +02:00
eda702c914 added circleci 2022-09-30 19:24:00 +02:00
19dabb7385 reorganized directory and package structure 2022-09-30 19:14:58 +02:00
e617e52127 added checks 2022-09-30 18:36:15 +02:00
a09f7b59c2 add 2022-09-30 14:25:26 +02:00
8fc831537e various changes:
- added copying in every file
- disasembled rootCmd into separate wrapper func
- print usage if no file and no stdin
2022-09-30 14:08:59 +02:00
61f6e05515 added binary tarballs to the release, using a script now. 2022-09-29 18:13:53 +02:00
f4e8e92a6e fix uri 2022-09-29 16:56:17 +02:00
54babec276 another one 2022-09-29 16:55:45 +02:00
f32ac18cdd typo 2022-09-29 16:55:15 +02:00
4fc3beec31 fixed pod uri 2022-09-29 16:53:21 +02:00
2d8127dd67 added clean command 2022-09-29 16:52:03 +02:00
b8059eb676 Various additions:
- added a man page
- added build and install code to the Makefile
- enhanced the README
2022-09-29 16:50:36 +02:00
Thomas von Dein
febb0b13d7 fix typo 2022-09-29 06:04:45 +02:00
T.v.Dein
cfc02f01a8 Update TODO 2022-09-28 19:33:57 +02:00
21 changed files with 1576 additions and 211 deletions

25
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: build-and-test-tablizer
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.17, 1.18, 1.19]
os: [ubuntu-latest, windows-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go 1.18
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.version }}
id: go
- name: checkout
uses: actions/checkout@v3
- name: build
run: make
- name: test
run: make test

View File

@@ -1,18 +1,59 @@
# Copyright © 2022 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# no need to modify anything below
tool = tablizer
version = $(shell egrep "^var version = " cmd/root.go | cut -d'=' -f2 | cut -d'"' -f 2)
version = $(shell egrep "= .v" lib/common.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local
UID = root
GID = 0
BRANCH = $(shell git describe --all | cut -d/ -f2)
COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION:= $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD))
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)
all: $(tool).1 cmd/$(tool).go buildlocal
%.1: %.pod
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
cmd/%.go: %.pod
echo "package cmd" > cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go
pod2text $*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go
buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/lib.VERSION=$(VERSION)'"
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;)
./mkrel.sh $(tool) $(version)
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) releases
test:
go test -v ./...

View File

@@ -1,3 +1,6 @@
[![Actions](https://github.com/tlinden/tablizer/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/tablizer/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/tablizer/blob/master/LICENSE)
## tablizer - Manipulate tabular output of other programs
Tablizer can be used to re-format tabular output of other
@@ -68,12 +71,45 @@ 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`.
There are multiple ways to install **tablizer**:
- Go to the [latest release page](https://github.com/muesli/mango/releases/latest),
locate the binary for your operating system and platform.
Download it and put it into some directory within your `$PATH` variable.
- The release page also contains a tarball for every supported platform. Unpack it
to some temporary directory, extract it and execute the following command inside:
```
sudo make install
```
- You can also install from source. Issue the following commands in your shell:
```
git clone https://github.com/TLINDEN/tablizer.git
cd tablizer
make
sudo make install
```
If you do not find a binary release for your platform, please don't
hesitate to ask me about it, I'll add it.
## Documentation
The documentation is provided as a unix man-page. It will be
automatically installed if you install from source. However, you can
read the man-page online:
https://github.com/TLINDEN/tablizer/blob/main/tablizer.pod
Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc tablizer.pod`.
## Getting help
Although I'm happy to hear from udpxd users in private email,
Although I'm happy to hear from tablizer 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

3
TODO
View File

@@ -1,4 +1 @@
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

View File

@@ -1,109 +0,0 @@
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
}

View File

@@ -17,80 +17,63 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"fmt"
"github.com/alecthomas/repr"
"github.com/spf13/cobra"
"github.com/tlinden/tablizer/lib"
"log"
"os"
"strconv"
"strings"
"os/exec"
)
var version = "v1.0.0"
var ShowManual = false
func man() {
man := exec.Command("less", "-")
var b bytes.Buffer
b.Write([]byte(manpage))
man.Stdout = os.Stdout
man.Stdin = &b
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
log.Fatal(err)
}
}
var rootCmd = &cobra.Command{
Use: "tablizer [regex] [file, ...]",
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
Run: func(cmd *cobra.Command, args []string) {
if Version {
fmt.Printf("This is tablizer version %s\n", version)
return
RunE: func(cmd *cobra.Command, args []string) error {
if lib.ShowVersion {
fmt.Printf("This is tablizer version %s\n", lib.VERSION)
return nil
}
var pattern string
havefiles := false
if len(Columns) > 0 {
for _, use := range strings.Split(Columns, ",") {
usenum, err := strconv.Atoi(use)
if err != nil {
die(err)
}
UseColumns = append(UseColumns, usenum)
}
if ShowManual {
man()
return nil
}
if len(args) > 0 {
if _, err := os.Stat(args[0]); err != nil {
pattern = args[0]
args = args[1:]
}
if len(args) > 0 {
for _, file := range args {
fd, err := os.OpenFile(file, os.O_RDONLY, 0755)
if err != nil {
die(err)
}
data := parseFile(fd, pattern)
if Debug {
repr.Print(data)
}
printTable(data)
}
havefiles = true
}
err := lib.PrepareColumns()
if err != nil {
return err
}
if !havefiles {
data := parseFile(os.Stdin, pattern)
if Debug {
repr.Print(data)
}
printTable(data)
err = lib.PrepareModeFlags()
if err != nil {
return err
}
return lib.ProcessFiles(args)
},
}
var Debug bool
var XtendedOut bool
var NoNumbering bool
var Version bool
var Columns string
var UseColumns []int
var Separator string
func Execute() {
err := rootCmd.Execute()
if err != nil {
@@ -99,10 +82,25 @@ func Execute() {
}
func init() {
rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&XtendedOut, "extended", "x", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&Version, "version", "v", false, "Print program version")
rootCmd.PersistentFlags().StringVarP(&Separator, "separator", "s", "", "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
rootCmd.PersistentFlags().BoolVarP(&lib.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&lib.NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "V", false, "Print program version")
rootCmd.PersistentFlags().BoolVarP(&lib.InvertMatch, "invert-match", "v", false, "select non-matching rows")
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
// output flags, only 1 allowed, hidden, since just short cuts
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagShell, "shell", "S", false, "Enable shell mode output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell")
rootCmd.Flags().MarkHidden("extended")
rootCmd.Flags().MarkHidden("orgtbl")
rootCmd.Flags().MarkHidden("markdown")
rootCmd.Flags().MarkHidden("shell")
// same thing but more common, takes precedence over above group
rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)")
}

131
cmd/tablizer.go Normal file
View File

@@ -0,0 +1,131 @@
package cmd
var manpage = `
NAME
tablizer - Manipulate tabular output of other programs
SYNOPSIS
Usage:
tablizer [regex] [file, ...] [flags]
Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging
-h, --help help for tablizer
-v, --invert-match select non-matching rows
-m, --man Display manual page
-n, --no-numbering Disable header numbering
-o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default)
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator
-v, --version Print program version
DESCRIPTION
Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns or
you may want to filter for some pattern or you may need the output in
another program and need to parse it somehow. Standard unix tools such
as awk(1), grep(1) or column(1) may help, but sometimes it's a tedious
business.
Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by TAB characters. This is
not easy to process.
You can use tablizer to do these and more things.
tablizer analyses the header fiels of a table, registers the column
positions of each header field and separates columns by those positions.
Without any options it reads its input from "STDIN", but you can also
specify a file as a parameter. If you want to reduce the output by some
regular expression, just specify it as its first parameter. You may also
use the -v option to exclude all rows which match the pattern. Hence:
# read from STDIN
kubectl get pods | tablizer
# read a file
tablizer filename
# search for pattern in a file (works like grep)
tablizer regex filename
# search for pattern in STDIN
kubectl get pods | tablizer regex
The output looks like the original one but every header field will 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:
kubectl get pods | tablizer -c1,3
You can specify the numbers in any order but output will always follow
the original order.
The numbering can be suppressed by using the -n option.
Finally the -d option enables debugging output which is mostly usefull
for the developer.
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.
In such cases the -o extended or -X option can be usefull which enables
*extended mode*. In this mode, each row will be printed vertically,
header left, value right, aligned by the field widths. Here's an
example:
kubectl get pods | ./tablizer -o extended
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
You can of course still use a regex to reduce the number of rows
displayed.
The option -o shell can be used if the output has to be processed by the
shell, it prints variable assignments for each cell, one line per row:
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
NAME="repldepl-7bcd8d5b64-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table.
BUGS
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>.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2022 by Thomas von Dein
This software uses the following GO libraries:
repr (https://github.com/alecthomas/repr)
Released under the MIT License, Copyright (c) 2016 Alec Thomas
cobra (https://github.com/spf13/cobra)
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra
Authors
AUTHORS
Thomas von Dein tom AT vondein DOT org
`

10
go.mod
View File

@@ -1,12 +1,16 @@
module daemon.de/tablizer
module github.com/tlinden/tablizer
go 1.18
require github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897
require (
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.5.0
)
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.0 // indirect
)

4
go.sum
View File

@@ -6,6 +6,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

46
lib/common.go Normal file
View File

@@ -0,0 +1,46 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
var (
// command line flags
Debug bool
XtendedOut bool
NoNumbering bool
ShowVersion bool
Columns string
UseColumns []int
Separator string
OutflagExtended bool
OutflagMarkdown bool
OutflagOrgtable bool
OutflagShell bool
OutputMode string
InvertMatch bool
// used for validation
validOutputmodes = "(orgtbl|markdown|extended|ascii)"
// main program version
Version = "v1.0.5"
// generated version string, used by -v contains lib.Version on
// main branch, and lib.Version-$branch-$lastcommit-$date on
// development branch
VERSION string
)

91
lib/helpers.go Normal file
View File

@@ -0,0 +1,91 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func PrepareColumns() error {
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)
return errors.New(msg)
}
UseColumns = append(UseColumns, usenum)
}
}
return nil
}
func PrepareModeFlags() error {
if len(OutputMode) == 0 {
switch {
case OutflagExtended:
OutputMode = "extended"
case OutflagMarkdown:
OutputMode = "markdown"
case OutflagOrgtable:
OutputMode = "orgtbl"
case OutflagShell:
OutputMode = "shell"
NoNumbering = true
default:
OutputMode = "ascii"
}
} else {
r, err := regexp.Compile(validOutputmodes)
if err != nil {
return errors.New("Failed to validate output mode spec!")
}
match := r.MatchString(OutputMode)
if !match {
return errors.New("Invalid output mode!")
}
}
return nil
}
func trimRow(row []string) []string {
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()!
var fixedrow []string
for _, cell := range row {
fixedrow = append(fixedrow, strings.TrimSpace(cell))
}
return fixedrow
}

73
lib/helpers_test.go Normal file
View File

@@ -0,0 +1,73 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"fmt"
"reflect"
"testing"
)
func Testcontains(t *testing.T) {
var tests = []struct {
list []int
search int
want bool
}{
{[]int{1, 2, 3}, 2, true},
{[]int{2, 3, 4}, 5, false},
}
for _, tt := range tests {
testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want)
t.Run(testname, func(t *testing.T) {
answer := contains(tt.list, tt.search)
if answer != tt.want {
t.Errorf("got %t, want %t", answer, tt.want)
}
})
}
}
func TestPrepareColumns(t *testing.T) {
var tests = []struct {
input string
exp []int
wanterror bool // expect error
}{
{"1,2,3", []int{1, 2, 3}, false},
{"1,2,", []int{}, true},
}
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()
if err != nil {
if !tt.wanterror {
t.Errorf("got error: %v", err)
}
} else {
if !reflect.DeepEqual(UseColumns, tt.exp) {
t.Errorf("got: %v, expected: %v", UseColumns, tt.exp)
}
}
})
}
}

83
lib/io.go Normal file
View File

@@ -0,0 +1,83 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"errors"
"io"
"os"
)
func ProcessFiles(args []string) error {
fds, pattern, err := determineIO(args)
if err != nil {
return err
}
for _, fd := range fds {
data, err := parseFile(fd, pattern)
if err != nil {
return err
}
printData(&data)
}
return nil
}
func determineIO(args []string) ([]io.Reader, string, error) {
var pattern string
var fds []io.Reader
var havefiles bool
if len(args) > 0 {
// threre were args left, take a look
if _, err := os.Stat(args[0]); err != nil {
// first one is not a file, consider it as regexp and
// shift arg list
pattern = args[0]
args = args[1:]
}
if len(args) > 0 {
// only files
for _, file := range args {
fd, err := os.OpenFile(file, os.O_RDONLY, 0755)
if err != nil {
return nil, "", err
}
fds = append(fds, fd)
}
havefiles = true
}
}
if !havefiles {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
fds = append(fds, os.Stdin)
} else {
return nil, "", errors.New("No file specified and nothing to read on stdin!")
}
}
return fds, pattern, nil
}

View File

@@ -1,10 +1,28 @@
package cmd
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"bufio"
"errors"
"fmt"
"github.com/alecthomas/repr"
"io"
"os"
"regexp"
"strings"
)
@@ -19,11 +37,6 @@ type Tabdata struct {
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
@@ -31,7 +44,7 @@ func die(v ...interface{}) {
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 {
func parseFile(input io.Reader, pattern string) (Tabdata, error) {
data := Tabdata{}
var scanner *bufio.Scanner
@@ -48,12 +61,12 @@ func parseFile(input io.Reader, pattern string) Tabdata {
scanner = bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
line := strings.TrimSpace(scanner.Text())
values := []string{}
patternR, err := regexp.Compile(pattern)
if err != nil {
die(err)
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
}
if !hadFirst {
@@ -98,22 +111,22 @@ func parseFile(input io.Reader, pattern string) Tabdata {
// 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) {
if patternR.MatchString(line) == InvertMatch {
// by default -v is false, so if a line does NOT
// match the pattern, we will ignore it. However,
// if the user specified -v, the matching is inverted,
// so we ignore all lines, which DO match.
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 {
@@ -133,20 +146,21 @@ func parseFile(input io.Reader, pattern string) Tabdata {
// if Debug {
// fmt.Printf("<%s> ", value)
// }
values = append(values, value)
values = append(values, strings.TrimSpace(value))
idx++
}
if Debug {
fmt.Println()
}
data.entries = append(data.entries, values)
}
}
if scanner.Err() != nil {
die(scanner.Err())
return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err()))
}
return data
if Debug {
repr.Print(data)
}
return data, nil
}

137
lib/parser_test.go Normal file
View File

@@ -0,0 +1,137 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"fmt"
"reflect"
"strings"
"testing"
)
func TestParser(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3,
headerIndices: []map[string]int{
map[string]int{
"beg": 0,
"end": 6,
},
map[string]int{
"end": 13,
"beg": 7,
},
map[string]int{
"beg": 14,
"end": 0,
},
},
headers: []string{
"ONE",
"TWO",
"THREE",
},
entries: [][]string{
[]string{
"asd",
"igig",
"cxxxncnc",
},
[]string{
"19191",
"EDD 1",
"X",
},
},
}
table := `ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`
readFd := strings.NewReader(table)
gotdata, err := parseFile(readFd, "")
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\nExp: %+v\nGot: %+v\n", data, gotdata)
}
}
func TestParserPatternmatching(t *testing.T) {
var tests = []struct {
entries [][]string
pattern string
invert bool
}{
{
entries: [][]string{
[]string{
"asd",
"igig",
"cxxxncnc",
},
},
pattern: "ig",
invert: false,
},
{
entries: [][]string{
[]string{
"19191",
"EDD 1",
"X",
},
},
pattern: "ig",
invert: true,
},
}
table := `ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`
for _, tt := range tests {
testname := fmt.Sprintf("parse-with-inverted-pattern-%t", tt.invert)
t.Run(testname, func(t *testing.T) {
InvertMatch = tt.invert
readFd := strings.NewReader(table)
gotdata, err := parseFile(readFd, tt.pattern)
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
}
if !reflect.DeepEqual(tt.entries, gotdata.entries) {
t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n",
tt.pattern, tt.invert, tt.entries, gotdata.entries)
}
})
}
}

206
lib/printer.go Normal file
View File

@@ -0,0 +1,206 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"fmt"
"github.com/olekukonko/tablewriter"
"os"
"regexp"
"strings"
)
func printData(data *Tabdata) {
// prepare headers: add numbers to headers
numberedHeaders := []string{}
for i, head := range data.headers {
if len(Columns) > 0 {
// -c specified
if !contains(UseColumns, i+1) {
// ignore this one
continue
}
}
if NoNumbering {
numberedHeaders = append(numberedHeaders, head)
} else {
numberedHeaders = append(numberedHeaders, fmt.Sprintf("%s(%d)", head, i+1))
}
}
data.headers = numberedHeaders
// prepare data
if len(Columns) > 0 {
reducedEntries := [][]string{}
reducedEntry := []string{}
for _, entry := range data.entries {
reducedEntry = nil
for i, value := range entry {
if !contains(UseColumns, i+1) {
continue
}
reducedEntry = append(reducedEntry, value)
}
reducedEntries = append(reducedEntries, reducedEntry)
}
data.entries = reducedEntries
}
switch OutputMode {
case "extended":
printExtendedData(data)
case "ascii":
printAsciiData(data)
case "orgtbl":
printOrgmodeData(data)
case "markdown":
printMarkdownData(data)
case "shell":
printShellData(data)
default:
printAsciiData(data)
}
}
/*
Emacs org-mode compatible table (also orgtbl-mode)
*/
func printOrgmodeData(data *Tabdata) {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader(data.headers)
for _, row := range data.entries {
table.Append(trimRow(row))
}
table.Render()
/* fix output for org-mode (orgtbl)
tableWriter output:
+------+------+
| cell | cell |
+------+------+
Needed for org-mode compatibility:
|------+------|
| cell | cell |
|------+------|
*/
leftR := regexp.MustCompile("(?m)^\\+")
rightR := regexp.MustCompile("\\+(?m)$")
fmt.Print(rightR.ReplaceAllString(leftR.ReplaceAllString(tableString.String(), "|"), "|"))
}
/*
Markdown table
*/
func printMarkdownData(data *Tabdata) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(data.headers)
for _, row := range data.entries {
table.Append(trimRow(row))
}
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.Render()
}
/*
Simple ASCII table without any borders etc, just like the input we expect
*/
func printAsciiData(data *Tabdata) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader(data.headers)
table.AppendBulk(data.entries)
// for _, row := range data.entries {
// table.Append(trimRow(row))
// }
table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
table.Render()
}
/*
We simulate the \x command of psql (the PostgreSQL client)
*/
func printExtendedData(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()
}
}
}
/*
Shell output, ready to be eval'd. Just like FreeBSD stat(1)
*/
func printShellData(data *Tabdata) {
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("%s=\"%s\" ", data.headers[idx], value)
idx++
}
fmt.Println()
}
}
}

81
lib/printer_test.go Normal file
View File

@@ -0,0 +1,81 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"os"
"strings"
"testing"
)
func TestPrinter(t *testing.T) {
table := `ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`
expects := map[string]string{
"ascii": `ONE(1) TWO(2) THREE(3)
asd igig cxxxncnc
19191 EDD 1 X`,
"orgtbl": `|--------+--------+----------|
| ONE(1) | TWO(2) | THREE(3) |
|--------+--------+----------|
| asd | igig | cxxxncnc |
| 19191 | EDD 1 | X |
|--------+--------+----------|`,
"markdown": `| ONE(1) | TWO(2) | THREE(3) |
|--------|--------|----------|
| asd | igig | cxxxncnc |
| 19191 | EDD 1 | X |`,
}
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
origStdout := os.Stdout
os.Stdout = w
for mode, expect := range expects {
OutputMode = mode
fd := strings.NewReader(table)
data, err := parseFile(fd, "")
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, data)
}
printData(&data)
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
output := strings.TrimSpace(string(buf))
if output != expect {
t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect))
}
}
// Restore
os.Stdout = origStdout
}

19
main.go
View File

@@ -1,7 +1,24 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"daemon.de/tablizer/cmd"
"github.com/tlinden/tablizer/cmd"
)
func main() {

65
mkrel.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Copyright © 2022 Thomas von Dein
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# get list with: go tool dist list
DIST="darwin/amd64
freebsd/amd64
linux/amd64
netbsd/amd64
openbsd/amd64
windows/amd64"
tool="$1"
version="$2"
if test -z "$version"; then
echo "Usage: $0 <tool name> <release version>"
exit 1
fi
rm -rf releases
mkdir -p releases
for D in $DIST; do
os=${D/\/*/}
arch=${D/*\//}
binfile="releases/${tool}-${os}-${arch}-${version}"
tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
set -x
GOOS=${os} GOARCH=${arch} go build -o ${binfile}
mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = tablizer
PREFIX = /usr/local
UID = root
GID = 0
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
tar cpzf ${tarfile} ${tardir}
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
rm -rf ${tardir}
set +x
done

280
tablizer.1 Normal file
View File

@@ -0,0 +1,280 @@
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
.\" nothing in troff, for use with C<>.
.tr \(*W-
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
. ds C`
. ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
. if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. if !\nF==2 \{\
. nr % 0
. nr F 2
. \}
. \}
.\}
.rr rF
.\"
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
.\" Fear. Run. Save yourself. No user-serviceable parts.
. \" fudge factors for nroff and troff
.if n \{\
. ds #H 0
. ds #V .8m
. ds #F .3m
. ds #[ \f1
. ds #] \fP
.\}
.if t \{\
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
. ds #V .6m
. ds #F 0
. ds #[ \&
. ds #] \&
.\}
. \" simple accents for nroff and troff
.if n \{\
. ds ' \&
. ds ` \&
. ds ^ \&
. ds , \&
. ds ~ ~
. ds /
.\}
.if t \{\
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
.\}
. \" troff and (daisy-wheel) nroff accents
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
.ds ae a\h'-(\w'a'u*4/10)'e
.ds Ae A\h'-(\w'A'u*4/10)'E
. \" corrections for vroff
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
. \" for low resolution devices (crt and lpr)
.if \n(.H>23 .if \n(.V>19 \
\{\
. ds : e
. ds 8 ss
. ds o a
. ds d- d\h'-1'\(ga
. ds D- D\h'-1'\(hy
. ds th \o'bp'
. ds Th \o'LP'
. ds ae ae
. ds Ae AE
.\}
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
.TH TABLIZER 1 "2022-10-05" "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
.nh
.SH "NAME"
tablizer \- Manipulate tabular output of other programs
.SH "SYNOPSIS"
.IX Header "SYNOPSIS"
.Vb 2
\& Usage:
\& tablizer [regex] [file, ...] [flags]
\&
\& Flags:
\& \-c, \-\-columns string Only show the speficied columns (separated by ,)
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for tablizer
\& \-v, \-\-invert\-match select non\-matching rows
\& \-m, \-\-man Display manual page
\& \-n, \-\-no\-numbering Disable header numbering
\& \-o, \-\-output string Output mode \- one of: orgtbl, markdown, extended, ascii(default)
\& \-X, \-\-extended Enable extended output
\& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-s, \-\-separator string Custom field separator
\& \-v, \-\-version Print program version
.Ve
.SH "DESCRIPTION"
.IX Header "DESCRIPTION"
Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns
or you may want to filter for some pattern or you may need the output
in another program and need to parse it somehow. Standard unix tools
such as \fBawk\fR\|(1), \fBgrep\fR\|(1) or \fBcolumn\fR\|(1) may help, but sometimes it's a
tedious business.
.PP
Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by \s-1TAB\s0 characters. This is
not easy to process.
.PP
You can use \fBtablizer\fR to do these and more things.
.PP
\&\fBtablizer\fR analyses the header fiels of a table, registers the column
positions of each header field and separates columns by those
positions.
.PP
Without any options it reads its input from \f(CW\*(C`STDIN\*(C'\fR, but you can also
specify a file as a parameter. If you want to reduce the output by
some regular expression, just specify it as its first parameter. You
may also use the \fB\-v\fR option to exclude all rows which match the
pattern. Hence:
.PP
.Vb 2
\& # read from STDIN
\& kubectl get pods | tablizer
\&
\& # read a file
\& tablizer filename
\&
\& # search for pattern in a file (works like grep)
\& tablizer regex filename
\&
\& # search for pattern in STDIN
\& kubectl get pods | tablizer regex
.Ve
.PP
The output looks like the original one but every header field will
have a numer associated with it, e.g.:
.PP
.Vb 1
\& NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
.Ve
.PP
These numbers denote the column and you can use them to specify which
columns you want to have in your output:
.PP
.Vb 1
\& kubectl get pods | tablizer \-c1,3
.Ve
.PP
You can specify the numbers in any order but output will always follow
the original order.
.PP
The numbering can be suppressed by using the \fB\-n\fR option.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly
usefull for the developer.
.SS "\s-1OUTPUT MODES\s0"
.IX Subsection "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. In such cases the \fB\-o extended\fR or \fB\-X\fR option can be
usefull which enables \fIextended mode\fR. In this mode, each row will be
printed vertically, header left, value right, aligned by the field
widths. Here's an example:
.PP
.Vb 6
\& kubectl get pods | ./tablizer \-o extended
\& NAME: repldepl\-7bcd8d5b64\-7zq4l
\& READY: 1/1
\& STATUS: Running
\& RESTARTS: 1 (71m ago)
\& AGE: 5h28m
.Ve
.PP
You can of course still use a regex to reduce the number of rows
displayed.
.PP
The option \fB\-o shell\fR can be used if the output has to be processed
by the shell, it prints variable assignments for each cell, one line
per row:
.PP
.Vb 4
\& kubectl get pods | ./tablizer \-o extended ./tablizer \-o shell
\& NAME="repldepl\-7bcd8d5b64\-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
\& NAME="repldepl\-7bcd8d5b64\-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
\& NAME="repldepl\-7bcd8d5b64\-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
.Ve
.PP
You can use this in an eval loop.
.PP
Beside normal ascii mode (the default) and extended mode there are
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table.
.SH "BUGS"
.IX Header "BUGS"
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>.
.SH "LICENSE"
.IX Header "LICENSE"
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
.PP
Copyright (c) 2022 by Thomas von Dein
.PP
This software uses the following \s-1GO\s0 libraries:
.IP "repr (https://github.com/alecthomas/repr)" 4
.IX Item "repr (https://github.com/alecthomas/repr)"
Released under the \s-1MIT\s0 License, Copyright (c) 2016 Alec Thomas
.IP "cobra (https://github.com/spf13/cobra)" 4
.IX Item "cobra (https://github.com/spf13/cobra)"
Released under the Apache 2.0 license, Copyright 2013\-2022 The Cobra Authors
.SH "AUTHORS"
.IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR

145
tablizer.pod Normal file
View File

@@ -0,0 +1,145 @@
=head1 NAME
tablizer - Manipulate tabular output of other programs
=head1 SYNOPSIS
Usage:
tablizer [regex] [file, ...] [flags]
Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging
-h, --help help for tablizer
-v, --invert-match select non-matching rows
-m, --man Display manual page
-n, --no-numbering Disable header numbering
-o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default)
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator
-v, --version Print program version
=head1 DESCRIPTION
Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns
or you may want to filter for some pattern or you may need the output
in another program and need to parse it somehow. Standard unix tools
such as awk(1), grep(1) or column(1) may help, but sometimes it's a
tedious business.
Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by TAB characters. This is
not easy to process.
You can use B<tablizer> to do these and more things.
B<tablizer> analyses the header fiels of a table, registers the column
positions of each header field and separates columns by those
positions.
Without any options it reads its input from C<STDIN>, but you can also
specify a file as a parameter. If you want to reduce the output by
some regular expression, just specify it as its first parameter. You
may also use the B<-v> option to exclude all rows which match the
pattern. Hence:
# read from STDIN
kubectl get pods | tablizer
# read a file
tablizer filename
# search for pattern in a file (works like grep)
tablizer regex filename
# search for pattern in STDIN
kubectl get pods | tablizer regex
The output looks like the original one but every header field will
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:
kubectl get pods | tablizer -c1,3
You can specify the numbers in any order but output will always follow
the original order.
The numbering can be suppressed by using the B<-n> option.
Finally the B<-d> option enables debugging output which is mostly
usefull for the developer.
=head2 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. In such cases the B<-o extended> or B<-X> option can be
usefull which enables I<extended mode>. In this mode, each row will be
printed vertically, header left, value right, aligned by the field
widths. Here's an example:
kubectl get pods | ./tablizer -o extended
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
You can of course still use a regex to reduce the number of rows
displayed.
The option B<-o shell> can be used if the output has to be processed
by the shell, it prints variable assignments for each cell, one line
per row:
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
NAME="repldepl-7bcd8d5b64-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table.
=head1 BUGS
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
L<https://github.com/TLINDEN/tablizer/issues>.
=head1 LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2022 by Thomas von Dein
This software uses the following GO libraries:
=over 4
=item repr (https://github.com/alecthomas/repr)
Released under the MIT License, Copyright (c) 2016 Alec Thomas
=item cobra (https://github.com/spf13/cobra)
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra Authors
=back
=head1 AUTHORS
Thomas von Dein B<tom AT vondein DOT org>
=cut