Compare commits

..

61 Commits

Author SHA1 Message Date
a455f6b79a bump version 2025-01-30 17:31:56 +01:00
2c08687c29 add support for negative filters (-F field!=regex) 2025-01-30 17:31:26 +01:00
6566dd66f0 fixed pattern regex, fixed pattern AND operation 2025-01-22 17:53:10 +01:00
1593799c03 added multi pattern tests 2025-01-22 17:53:10 +01:00
ea3dd75fec fix linting error 2025-01-22 17:53:10 +01:00
a306f2c601 implement multiple regex support and icase and negate flags 2025-01-22 17:53:10 +01:00
03f3225f24 build release binaries using ci workflow 2025-01-18 10:51:28 +01:00
63c7ef26b6 add -k<name> and sort by multiple columns support, fixes #34 2025-01-15 18:53:34 +01:00
dependabot[bot]
c2e7d8037a Bump github.com/hashicorp/hcl/v2 from 2.22.0 to 2.23.0
Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.22.0 to 2.23.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.22.0...v2.23.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hcl/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 13:12:06 +01:00
323c070caa tests don't work on windows 2025-01-14 13:10:09 +01:00
53cf1e2ebe fix for windows 2025-01-14 13:10:09 +01:00
16c5053752 satisfy linter 2025-01-14 13:10:09 +01:00
7d2d9a55d3 added prior art, fixes #30 as well 2025-01-14 13:10:09 +01:00
14c50b4e63 get rid of lisp interpreter, -R and -F are enough, fixes #30 2025-01-14 13:10:09 +01:00
0e68dc585d added testscript test to test the combination of all tasks 2025-01-14 13:10:09 +01:00
6ca835add1 changed file handling, use -r <file> or nothing to use stdin 2025-01-14 13:10:09 +01:00
306f583522 fixed transpose error message if count is incorrect 2025-01-14 13:10:09 +01:00
9f971ed3b9 fix #32: treat header filters case insensitively 2025-01-14 13:10:09 +01:00
2ae2d2b33d add transpose stuff to README, bump version 2025-01-14 13:10:09 +01:00
cf1a555b9b added tests, reorganized Parse() by dismantling parsing and processing 2025-01-14 13:10:09 +01:00
4d894a728b added transpose function (-T + -R) 2025-01-14 13:10:09 +01:00
8792c5a40f fix regex in example 2025-01-10 18:33:55 +01:00
7ab1a1178a add zygo reference 2025-01-10 18:27:41 +01:00
1e44da4f6e added documentation about current state of lisp support 2025-01-10 18:26:33 +01:00
59171f0fab bump versions 2024-12-13 10:37:44 +01:00
8098ccf000 fix #29: fix stat() error checking 2024-12-13 10:35:56 +01:00
4dc87ac22e fix #27: check if parsed headers and columns match 2024-11-04 11:13:53 +01:00
ef5211e45f only works on 1.22 2024-09-27 11:15:23 +02:00
1a80e72737 fix version quoting 2024-09-27 11:04:32 +02:00
8e765b167f ok, only test with 1.22.1 2024-09-25 18:55:04 +02:00
30f4b67538 bump version and add current go versions for testing 2024-09-25 18:52:47 +02:00
383b5db47e try 1.22.1 2024-09-25 18:40:13 +02:00
f7d812b372 try to quote go version 2024-09-25 18:38:06 +02:00
480f5f617d only try 1.20 2024-09-25 18:33:46 +02:00
586e36c181 update to go 1.22 2024-09-25 18:24:35 +02:00
dependabot[bot]
13c789b800 Bump github.com/alecthomas/repr from 0.1.1 to 0.4.0
Bumps [github.com/alecthomas/repr](https://github.com/alecthomas/repr) from 0.1.1 to 0.4.0.
- [Commits](https://github.com/alecthomas/repr/compare/v0.1.1...v0.4.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/repr
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 18:22:10 +02:00
dependabot[bot]
81e1394fd2 Bump github.com/gookit/color from 1.5.2 to 1.5.4
Bumps [github.com/gookit/color](https://github.com/gookit/color) from 1.5.2 to 1.5.4.
- [Release notes](https://github.com/gookit/color/releases)
- [Commits](https://github.com/gookit/color/compare/v1.5.2...v1.5.4)

---
updated-dependencies:
- dependency-name: github.com/gookit/color
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 18:22:00 +02:00
dependabot[bot]
b8099fe389 Bump github.com/spf13/cobra from 1.6.1 to 1.8.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.6.1 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.6.1...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 18:21:46 +02:00
dependabot[bot]
1dc072aa67 Bump github.com/lithammer/fuzzysearch from 1.1.7 to 1.1.8
Bumps [github.com/lithammer/fuzzysearch](https://github.com/lithammer/fuzzysearch) from 1.1.7 to 1.1.8.
- [Release notes](https://github.com/lithammer/fuzzysearch/releases)
- [Commits](https://github.com/lithammer/fuzzysearch/compare/v1.1.7...v1.1.8)

---
updated-dependencies:
- dependency-name: github.com/lithammer/fuzzysearch
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 18:21:23 +02:00
dependabot[bot]
d92f63ca30 Bump github.com/hashicorp/hcl/v2 from 2.19.1 to 2.22.0
Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.19.1 to 2.22.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.19.1...v2.22.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/hcl/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 18:13:50 +02:00
78ccb8f54b use non format logger 2024-09-25 18:06:00 +02:00
a29104aeab fix typos in issue templates 2024-09-25 18:06:00 +02:00
dependabot[bot]
45d9e219a5 Bump golangci/golangci-lint-action from 3 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 16:40:10 +02:00
dependabot[bot]
3eda59beeb Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 16:39:58 +02:00
dependabot[bot]
7ada75c1d6 Bump actions/setup-go from 3 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-25 16:39:36 +02:00
83d5628430 add dependabot config 2024-09-25 16:33:54 +02:00
a9bb79b01c merge corrections 2024-05-07 18:39:38 +02:00
a718fa388d Merge remote-tracking branch 'origin/development' 2024-05-07 18:29:01 +02:00
473feff451 refactored and un-go-criticed 2024-05-07 18:01:12 +02:00
9e2e45715e added -F docs 2024-05-07 18:00:57 +02:00
39609425e5 refactoring and gouncritic, 1st part 2024-05-07 15:19:54 +02:00
ba2a2e8460 add -F filter by column flag (closes #13) 2024-05-07 13:30:07 +02:00
96f7881c16 fix #12: only consider -v if there's a pattern, ignore it otherwise 2024-05-07 13:29:41 +02:00
0f22457961 remove go 1.17 support 2023-11-22 14:09:49 +01:00
ddfbecaa35 +docs, try linter v1.18 2023-11-22 14:08:36 +01:00
3632de10d7 try to fix linter 2023-11-22 13:57:57 +01:00
76b98fb8ad upd mods 2023-11-22 13:48:32 +01:00
f045adf441 added config file support to set custom colors 2023-11-22 13:33:26 +01:00
811173ddb4 fixed alternating highlighting, now looks reasonable 2023-11-22 10:30:40 +01:00
3c910ca08f works but is ugly :( 2023-11-21 17:41:04 +01:00
a8c9ede77e added -L flag to highligh lines in alternating bg color 2023-11-21 11:40:55 +01:00
46 changed files with 2353 additions and 1332 deletions

View File

@@ -7,7 +7,7 @@ assignees: TLINDEN
---
**Describtion**
**Description**
<!-- Please provide a clear and concise description of the issue: -->

View File

@@ -7,7 +7,7 @@ assignees: TLINDEN
---
**Describtion**
**Description**
<!-- Please provide a clear and concise description of the feature you desire: -->

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -4,19 +4,21 @@ jobs:
build:
strategy:
matrix:
version: [1.18, 1.19]
os: [ubuntu-latest, windows-latest, macos-latest]
version: ['1.22']
# windows-latest removed, see:
# https://github.com/rogpeppe/go-internal/issues/284
os: [ubuntu-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go 1.18
uses: actions/setup-go@v3
- name: Set up Go ${{ matrix.version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.version }}
go-version: '${{ matrix.version }}'
id: go
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: build
run: make
@@ -28,11 +30,11 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v5
with:
go-version: 1.18
- uses: actions/checkout@v3
go-version: 1.22
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true

32
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: build-and-test
on:
push:
tags:
- "*"
jobs:
release:
name: Build Release Assets
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.22.11
- name: Build the executables
run: ./mkrel.sh tablizer ${{ github.ref_name}}
- name: List the executables
run: ls -l ./releases
- name: Upload the binaries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
file: ./releases/*
file_glob: true

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
releases
tablizer
*.out

View File

@@ -53,8 +53,7 @@ buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"
release:
./mkrel.sh $(tool) $(version)
gh release create $(version) --generate-notes releases/*
gh release create $(version) --generate-notes
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
@@ -66,11 +65,10 @@ clean:
rm -rf $(tool) releases coverage.out
test:
go test -v ./...
bash t/test.sh
go test ./... $(OPTS)
singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib"
@echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'"
go test -run $(TEST) github.com/tlinden/tablizer/$(MOD)
cover-report:
@@ -91,3 +89,11 @@ show-versions: buildlocal
goupdate:
go get -t -u=patch ./...
lint:
golangci-lint run
# keep til ireturn
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn,stylecheck,testpackage,mirror,nestif,revive,goerr113,gomnd
gocritic check -enableAll *.go

106
README.md
View File

@@ -8,6 +8,49 @@ 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.
Usage:
```default
Usage:
tablizer [regex] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-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)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive):
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
-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
Other Flags:
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
```
Let's take this output:
```
% kubectl get pods -o wide
@@ -70,8 +113,36 @@ NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
```
Sometimes a filter regex is to broad and you wish to filter only on a
particular column. This is possible using `-F`:
```
% kubectl get pods | tablizer -n -Fname=2
NAME READY STATUS RESTARTS AGE
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
```
Here we filtered the `NAME` column for `2`, which would have matched
otherwise on all rows.
There are more output modes like org-mode (orgtbl) and markdown.
You can also use it to modify certain cells using regular expression
matching. For example:
```shell
kubectl get pods | tablizer -n -T4 -R '/ /-/'
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
```
Here, we modified the 4th column (`-T4`) by replacing every space with
a dash. If you need to work with `/` characters, you can also use any
other separator, for instance: `-R '| |-|'`.
## Demo
[![asciicast](demo/tablizer-demo.gif)](https://asciinema.org/a/9FKc3HPnlg8D2X8otheleEa9t)
@@ -127,6 +198,41 @@ 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.
## Prior Art
When I started with tablizer I was not aware that other tools
exist. Here is a non-exhausive list of the ones I find especially
awesome:
### [miller](https://github.com/johnkerl/miller)
This is a really powerful tool to work with tabular data and it also
allows other inputs as json, csv etc. You can filter, manipulate,
create pipelines, there's even a programming language builtin to do
even more amazing things.
### [csvq](https://github.com/mithrandie/csvq)
Csvq allows you to query CSV and TSV data using SQL queries. How nice
is that? Highly recommended if you have to work with a large (and
wide) dataset and need to apply a complicated set of rules.
### [goawk](https://github.com/benhoyt/goawk)
Goawk is a 100% POSIX compliant AWK implementation in GO, which also
supports CSV and TSV data as input (using `-i csv` for example). You
can apply any kind of awk code to your tabular data, there are no
limit to your creativity!
### [teip](https://github.com/greymd/teip)
I particularly like teip, it's a real gem. You can use it to drill
"holes" into your tabular data and modify these "holes" using small
external unix commands such as grep or sed. The possibilities are
endless, you can even use teip to modify data inside a hole created by
teip. Highly recommended.
## Copyright and license
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.

10
TODO.md
View File

@@ -6,13 +6,3 @@
- add --no-headers option
### Lisp Plugin Infrastructure using zygo
Hooks:
| Filter | Purpose | Args | Return |
|-----------|-------------------------------------------------------------|---------------------|--------|
| filter | include or exclude lines | row as hash | bool |
| process | do calculations with data, store results in global lisp env | whole dataset | nil |
| transpose | modify a cell | headername and cell | cell |
| append | add one or more rows to the dataset (use this to add stats) | nil | rows |

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022 Thomas von Dein
Copyright © 2022-2025 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
@@ -19,25 +19,24 @@ package cfg
import (
"errors"
"fmt"
"log"
"os"
"regexp"
"strings"
"github.com/glycerine/zygomys/zygo"
"github.com/gookit/color"
"github.com/hashicorp/hcl/v2/hclsimple"
)
const DefaultSeparator string = `(\s\s+|\t)`
const Version string = "v1.1.0"
const Version string = "v1.3.2"
const MAXPARTS = 2
var DefaultLoadPath string = os.Getenv("HOME") + "/.config/tablizer/lisp"
var DefaultConfigfile string = os.Getenv("HOME") + "/.config/tablizer/config"
var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
var VERSION string // maintained by -x
// public config, set via config file or using defaults
type Configuration struct {
type Settings struct {
FG string `hcl:"FG"`
BG string `hcl:"BG"`
HighlightFG string `hcl:"HighlightFG"`
@@ -48,6 +47,22 @@ type Configuration struct {
HighlightHdrBG string `hcl:"HighlightHdrBG"`
}
type Transposer struct {
Search regexp.Regexp
Replace string
}
type Pattern struct {
Pattern string
PatternRe *regexp.Regexp
Negate bool
}
type Filter struct {
Regex *regexp.Regexp
Negate bool
}
// internal config
type Config struct {
Debug bool
@@ -58,14 +73,19 @@ type Config struct {
Separator string
OutputMode int
InvertMatch bool
Pattern string
PatternR *regexp.Regexp
Patterns []*Pattern
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}
Transposers []string // []string{"/ /-/", "/foo/bar/"}
UseTransposers []Transposer // {Search: re, Replace: string}
/*
FIXME: make configurable somehow, config file or ENV
@@ -78,17 +98,17 @@ type Config struct {
NoColor bool
// special case: we use the config struct to transport the lisp
// env trough the program
Lisp *zygo.Zlisp
// a path containing lisp scripts to be loaded on startup
LispLoadPath string
// config file, optional
Configfile string
Configuration Configuration
Settings Settings
// used for field filtering
Rawfilters []string
Filters map[string]Filter //map[string]*regexp.Regexp
// -r <file>
InputFile string
}
// maps outputmode short flags to output mode, ie. -O => -o orgtbl
@@ -110,7 +130,7 @@ const (
Shell
Yaml
CSV
Ascii
ASCII
)
// various sort types
@@ -120,11 +140,8 @@ type Sortmode struct {
Age bool
}
// valid lisp hooks
var ValidHooks []string
// default color schemes
func (c *Config) Colors() map[color.Level]map[string]color.Color {
func (conf *Config) Colors() map[color.Level]map[string]color.Color {
colors := map[color.Level]map[string]color.Color{
color.Level16: {
"bg": color.BgGreen, "fg": color.FgWhite,
@@ -142,89 +159,86 @@ func (c *Config) Colors() map[color.Level]map[string]color.Color {
},
}
if len(c.Configuration.BG) > 0 {
colors[color.Level16]["bg"] = ColorStringToBGColor(c.Configuration.BG)
colors[color.Level256]["bg"] = ColorStringToBGColor(c.Configuration.BG)
colors[color.LevelRgb]["bg"] = ColorStringToBGColor(c.Configuration.BG)
if len(conf.Settings.BG) > 0 {
colors[color.Level16]["bg"] = ColorStringToBGColor(conf.Settings.BG)
colors[color.Level256]["bg"] = ColorStringToBGColor(conf.Settings.BG)
colors[color.LevelRgb]["bg"] = ColorStringToBGColor(conf.Settings.BG)
}
if len(c.Configuration.FG) > 0 {
colors[color.Level16]["fg"] = ColorStringToColor(c.Configuration.FG)
colors[color.Level256]["fg"] = ColorStringToColor(c.Configuration.FG)
colors[color.LevelRgb]["fg"] = ColorStringToColor(c.Configuration.FG)
if len(conf.Settings.FG) > 0 {
colors[color.Level16]["fg"] = ColorStringToColor(conf.Settings.FG)
colors[color.Level256]["fg"] = ColorStringToColor(conf.Settings.FG)
colors[color.LevelRgb]["fg"] = ColorStringToColor(conf.Settings.FG)
}
if len(c.Configuration.HighlightBG) > 0 {
colors[color.Level16]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG)
colors[color.Level256]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG)
colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG)
if len(conf.Settings.HighlightBG) > 0 {
colors[color.Level16]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
colors[color.Level256]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
}
if len(c.Configuration.HighlightFG) > 0 {
colors[color.Level16]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG)
colors[color.Level256]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG)
colors[color.LevelRgb]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG)
if len(conf.Settings.HighlightFG) > 0 {
colors[color.Level16]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
colors[color.Level256]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
colors[color.LevelRgb]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
}
if len(c.Configuration.NoHighlightBG) > 0 {
colors[color.Level16]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG)
colors[color.Level256]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG)
colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG)
if len(conf.Settings.NoHighlightBG) > 0 {
colors[color.Level16]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
colors[color.Level256]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
}
if len(c.Configuration.NoHighlightFG) > 0 {
colors[color.Level16]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG)
colors[color.Level256]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG)
colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG)
if len(conf.Settings.NoHighlightFG) > 0 {
colors[color.Level16]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
colors[color.Level256]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
}
if len(c.Configuration.HighlightHdrBG) > 0 {
colors[color.Level16]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG)
colors[color.Level256]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG)
colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG)
if len(conf.Settings.HighlightHdrBG) > 0 {
colors[color.Level16]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
colors[color.Level256]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
}
if len(c.Configuration.HighlightHdrFG) > 0 {
colors[color.Level16]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG)
colors[color.Level256]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG)
colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG)
if len(conf.Settings.HighlightHdrFG) > 0 {
colors[color.Level16]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
colors[color.Level256]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
}
return colors
}
// find supported color mode, modifies config based on constants
func (c *Config) DetermineColormode() {
func (conf *Config) DetermineColormode() {
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
colors := c.Colors()
colors := conf.Colors()
c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"])
c.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"])
c.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"])
c.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"])
conf.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"])
conf.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"])
conf.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"])
conf.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"])
}
}
// Return true if current terminal is interactive
func isTerminal(f *os.File) bool {
o, _ := f.Stat()
if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
return true
} else {
return false
}
return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice
}
// main program version
// generated version string, used by -v contains lib.Version on
//
// main branch, and lib.Version-$branch-$lastcommit-$date on
//
// development branch
func Getversion() string {
// main program version
// generated version string, used by -v contains lib.Version on
// main branch, and lib.Version-$branch-$lastcommit-$date on
// development branch
return fmt.Sprintf("This is tablizer version %s", VERSION)
}
@@ -256,66 +270,155 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) {
case flag.C:
conf.OutputMode = CSV
default:
conf.OutputMode = Ascii
conf.OutputMode = ASCII
}
}
func (c *Config) CheckEnv() {
// check for environment vars, command line flags have precedence,
// NO_COLOR is being checked by the color module itself.
if !c.NoNumbering {
_, set := os.LookupEnv("T_NO_HEADER_NUMBERING")
if set {
c.NoNumbering = true
func (conf *Config) PrepareFilters() error {
conf.Filters = make(map[string]Filter, len(conf.Rawfilters))
for _, rawfilter := range conf.Rawfilters {
filter := Filter{}
parts := strings.Split(rawfilter, "!=")
if len(parts) != MAXPARTS {
parts = strings.Split(rawfilter, "=")
if len(parts) != MAXPARTS {
return errors.New("filter field and value must be separated by '=' or '!='")
}
} else {
filter.Negate = true
}
}
if len(c.Columns) == 0 {
cols := os.Getenv("T_COLUMNS")
if len(cols) > 1 {
c.Columns = cols
reg, err := regexp.Compile(parts[1])
if err != nil {
return fmt.Errorf("failed to compile filter regex for field %s: %w",
parts[0], err)
}
filter.Regex = reg
conf.Filters[strings.ToLower(parts[0])] = filter
}
}
func (c *Config) ApplyDefaults() {
// mode specific defaults
if c.OutputMode == Yaml || c.OutputMode == CSV {
c.NoNumbering = true
}
ValidHooks = []string{"filter", "process", "transpose", "append"}
}
func (c *Config) PreparePattern(pattern string) error {
PatternR, err := regexp.Compile(pattern)
if err != nil {
return errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", c.Pattern, err))
}
c.PatternR = PatternR
c.Pattern = pattern
return nil
}
func (c *Config) ParseConfigfile() error {
if path, err := os.Stat(c.Configfile); !os.IsNotExist(err) {
if !path.IsDir() {
configstring, err := os.ReadFile(path.Name())
if err != nil {
return err
}
// check if transposers match transposer columns and prepare transposer structs
func (conf *Config) PrepareTransposers() error {
if len(conf.Transposers) != len(conf.UseTransposeColumns) {
return fmt.Errorf("the number of transposers needs to correspond to the number of transpose columns: %d != %d",
len(conf.Transposers), len(conf.UseTransposeColumns))
}
err = hclsimple.Decode(
path.Name(), []byte(configstring),
nil, &c.Configuration,
)
if err != nil {
log.Fatalf("Failed to load configuration: %s", err)
for _, transposer := range conf.Transposers {
parts := strings.Split(transposer, string(transposer[0]))
if len(parts) != 4 {
return fmt.Errorf("transposer function must have the format /regexp/replace-string/")
}
conf.UseTransposers = append(conf.UseTransposers,
Transposer{
Search: *regexp.MustCompile(parts[1]),
Replace: parts[2]},
)
}
return nil
}
func (conf *Config) CheckEnv() {
// check for environment vars, command line flags have precedence,
// NO_COLOR is being checked by the color module itself.
if !conf.NoNumbering {
_, set := os.LookupEnv("T_NO_HEADER_NUMBERING")
if set {
conf.NoNumbering = true
}
}
if len(conf.Columns) == 0 {
cols := os.Getenv("T_COLUMNS")
if len(cols) > 1 {
conf.Columns = cols
}
}
}
func (conf *Config) ApplyDefaults() {
// mode specific defaults
if conf.OutputMode == Yaml || conf.OutputMode == CSV {
conf.NoNumbering = true
}
}
func (conf *Config) PreparePattern(patterns []*Pattern) error {
// regex checks if a pattern looks like /$pattern/[i!]
flagre := regexp.MustCompile(`^/(.*)/([i!]*)$`)
for _, pattern := range patterns {
matches := flagre.FindAllStringSubmatch(pattern.Pattern, -1)
// we have a regex with flags
for _, match := range matches {
pattern.Pattern = match[1] // the inner part is our actual pattern
flags := match[2] // the flags
for _, flag := range flags {
switch flag {
case 'i':
pattern.Pattern = `(?i)` + pattern.Pattern
case '!':
pattern.Negate = true
}
}
}
PatternRe, err := regexp.Compile(pattern.Pattern)
if err != nil {
return fmt.Errorf("regexp pattern %s is invalid: %w", pattern.Pattern, err)
}
pattern.PatternRe = PatternRe
}
conf.Patterns = patterns
return nil
}
// Parse config file. Ignore if the file doesn't exist but return an
// error if it exists but fails to read or parse
func (conf *Config) ParseConfigfile() error {
path, err := os.Stat(conf.Configfile)
if err != nil {
if os.IsNotExist(err) {
// ignore non-existent files
return nil
}
return fmt.Errorf("failed to stat config file: %w", err)
}
if path.IsDir() {
// ignore non-existent or dirs
return nil
}
configstring, err := os.ReadFile(path.Name())
if err != nil {
return fmt.Errorf("failed to read config file %s: %w", path.Name(), err)
}
err = hclsimple.Decode(
path.Name(),
configstring,
nil,
&conf.Settings)
if err != nil {
return fmt.Errorf("failed to load configuration file %s: %w",
path.Name(), err)
}
return nil

View File

@@ -34,18 +34,18 @@ func TestPrepareModeFlags(t *testing.T) {
{Modeflag{O: true}, Orgtbl},
{Modeflag{Y: true}, Yaml},
{Modeflag{M: true}, Markdown},
{Modeflag{}, Ascii},
{Modeflag{}, ASCII},
}
// FIXME: use a map for easier printing
for _, tt := range tests {
testname := fmt.Sprintf("PrepareModeFlags-expect-%d", tt.expect)
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareModeFlags-expect-%d", testdata.expect)
t.Run(testname, func(t *testing.T) {
c := Config{}
conf := Config{}
c.PrepareModeFlags(tt.flag)
if c.OutputMode != tt.expect {
t.Errorf("got: %d, expect: %d", c.OutputMode, tt.expect)
conf.PrepareModeFlags(testdata.flag)
if conf.OutputMode != testdata.expect {
t.Errorf("got: %d, expect: %d", conf.OutputMode, testdata.expect)
}
})
}
@@ -63,15 +63,15 @@ func TestPrepareSortFlags(t *testing.T) {
{Sortmode{}, "string"},
}
for _, tt := range tests {
testname := fmt.Sprintf("PrepareSortFlags-expect-%s", tt.expect)
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareSortFlags-expect-%s", testdata.expect)
t.Run(testname, func(t *testing.T) {
c := Config{}
conf := Config{}
c.PrepareSortFlags(tt.flag)
conf.PrepareSortFlags(testdata.flag)
if c.SortMode != tt.expect {
t.Errorf("got: %s, expect: %s", c.SortMode, tt.expect)
if conf.SortMode != testdata.expect {
t.Errorf("got: %s, expect: %s", conf.SortMode, testdata.expect)
}
})
}
@@ -79,22 +79,58 @@ func TestPrepareSortFlags(t *testing.T) {
func TestPreparePattern(t *testing.T) {
var tests = []struct {
pattern string
wanterr bool
patterns []*Pattern
name string
wanterr bool
wanticase bool
wantneg bool
}{
{"[A-Z]+", false},
{"[a-z", true},
{
[]*Pattern{{Pattern: "[A-Z]+"}},
"simple",
false,
false,
false,
},
{
[]*Pattern{{Pattern: "[a-z"}},
"regfail",
true,
false,
false,
},
{
[]*Pattern{{Pattern: "/[A-Z]+/i"}},
"icase",
false,
true,
false,
},
{
[]*Pattern{{Pattern: "/[A-Z]+/!"}},
"negate",
false,
false,
true,
},
{
[]*Pattern{{Pattern: "/[A-Z]+/!i"}},
"negicase",
false,
true,
true,
},
}
for _, tt := range tests {
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", tt.pattern, tt.wanterr)
for _, testdata := range tests {
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", testdata.name, testdata.wanterr)
t.Run(testname, func(t *testing.T) {
c := Config{}
conf := Config{}
err := c.PreparePattern(tt.pattern)
err := conf.PreparePattern(testdata.patterns)
if err != nil {
if !tt.wanterr {
if !testdata.wanterr {
t.Errorf("PreparePattern returned error: %s", err)
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022 Thomas von Dein
Copyright © 2022-2025 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
@@ -33,11 +33,12 @@ import (
func man() {
man := exec.Command("less", "-")
var b bytes.Buffer
b.Write([]byte(manpage))
var buffer bytes.Buffer
buffer.Write([]byte(manpage))
man.Stdout = os.Stdout
man.Stdin = &b
man.Stdin = &buffer
man.Stderr = os.Stderr
err := man.Run()
@@ -58,7 +59,15 @@ func completion(cmd *cobra.Command, mode string) error {
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return errors.New("Invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
return errors.New("invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
}
}
// we die with exit 1 if there's an error
func wrapE(err error) {
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}
@@ -76,83 +85,118 @@ func Execute() {
Use: "tablizer [regex] [file, ...]",
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) {
if ShowVersion {
fmt.Println(cfg.Getversion())
return nil
return
}
if ShowManual {
man()
return nil
return
}
if len(ShowCompletion) > 0 {
return completion(cmd, ShowCompletion)
wrapE(completion(cmd, ShowCompletion))
return
}
// Setup
err := conf.ParseConfigfile()
if err != nil {
return err
}
wrapE(conf.ParseConfigfile())
conf.CheckEnv()
conf.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode)
wrapE(conf.PrepareFilters())
conf.DetermineColormode()
conf.ApplyDefaults()
// setup lisp env, load plugins etc
err = lib.SetupLisp(&conf)
if err != nil {
return err
}
// actual execution starts here
return lib.ProcessFiles(&conf, args)
wrapE(lib.ProcessFiles(&conf, args))
},
}
// options
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false, "Disable header display")
rootCmd.PersistentFlags().BoolVarP(&conf.NoColor, "no-color", "N", false, "Disable pattern highlighting")
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "V", false, "Print program version")
rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false, "select non-matching rows")
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false, "Use fuzzy searching")
rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false, "Use alternating background colors")
rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "", "Display completion code")
rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator, "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false,
"Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false,
"Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false,
"Disable header display")
rootCmd.PersistentFlags().BoolVarP(&conf.NoColor, "no-color", "N", false,
"Disable pattern highlighting")
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "V", false,
"Print program version")
rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false,
"select non-matching rows")
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false,
"Display manual page")
rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false,
"Use fuzzy searching")
rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false,
"Use alternating background colors")
rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "",
"Display completion code")
rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator,
"Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "",
"Only show the speficied columns (separated by ,)")
rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "",
"Transpose the speficied columns (separated by ,)")
// sort options
rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)")
rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "",
"Sort by column (default: 1)")
// sort mode, only 1 allowed
rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false, "sort according to string numerical value")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false, "sort according to time string")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false, "sort according to age (duration) string")
rootCmd.MarkFlagsMutuallyExclusive("sort-numeric", "sort-time", "sort-age")
rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false,
"Sort in descending order (default: ascending)")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false,
"sort according to string numerical value")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false,
"sort according to time string")
rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false,
"sort according to age (duration) string")
rootCmd.MarkFlagsMutuallyExclusive("sort-numeric", "sort-time",
"sort-age")
// output flags, only 1 allowed
rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false, "Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false, "Enable shell mode output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, "Enable CSV output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.A, "ascii", "A", false, "Enable ASCII output (default)")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv")
// lisp options
rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, "Load path for lisp plugins (expects *.zy files)")
rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false,
"Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false,
"Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false,
"Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false,
"Enable shell mode output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false,
"Enable yaml output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false,
"Enable CSV output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.A, "ascii", "A", false,
"Enable ASCII output (default)")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl",
"shell", "yaml", "csv")
// config file
rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile, "config file (default: ~/.config/tablizer/config)")
rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile,
"config file (default: ~/.config/tablizer/config)")
// filters
rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters,
"filter", "F", nil, "Filter by field (field=regexp || field!=regexp)")
rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers,
"regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T")
// input
rootCmd.PersistentFlags().StringVarP(&conf.InputFile, "read-file", "r", "",
"Read input data from file")
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")

View File

@@ -6,41 +6,44 @@ NAME
SYNOPSIS
Usage:
tablizer [regex] [file, ...] [flags]
tablizer [regex,...] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-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)
-z, --fuzzy Use fuzzy seach [experimental]
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive):
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
-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
-a, --sort-age sort according to age (duration) string
-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
Other Flags:
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
DESCRIPTION
Many programs generate tabular output. But sometimes you need to
@@ -100,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".
@@ -117,33 +129,63 @@ DESCRIPTION
Finally the -d option enables debugging output which is mostly useful
for the developer.
PATTERNS
You can reduce the rows being displayed by using a regular expression
pattern. The regexp is PCRE compatible, refer to the syntax cheat sheet
here: <https://github.com/google/re2/wiki/Syntax>. If you want to read a
more comprehensive documentation about the topic and have perl installed
you can read it with:
PATTERNS AND FILTERING
You can reduce the rows being displayed by using one or more regular
expression patterns. The regexp language being used is the one of
GOLANG, refer to the syntax cheat sheet here:
<https://pkg.go.dev/regexp/syntax>.
If you want to read a more comprehensive documentation about the topic
and have perl installed you can read it with:
perldoc perlre
Or read it online: <https://perldoc.perl.org/perlre>.
Or read it online: <https://perldoc.perl.org/perlre>. But please note
that the GO regexp engine does NOT support all perl regex terms,
especially look-ahead and look-behind.
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
If you want to supply flags to a regex, then surround it with slashes
and append the flag. The following flags are supported:
(?MODIFIER)
The most important modifiers are:
"i" ignore case "m" multiline mode "s" single line mode
i => case insensitive
! => negative match
Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account"
kubectl get pods -A | tablizer "/account/i"
You can use the experimental fuzzy seach feature by providing the option
-z, in which case the pattern is regarded as a fuzzy search term, not a
regexp.
If you use the "!" flag, then the regex match will be negated, that is,
if a line in the input matches the given regex, but "!" is supplied,
tablizer will NOT include it in the output.
For example, here we want to get all lines matching "foo" but not "bar":
cat table | tablizer foo '/bar/!'
This would match a line "foo zorro" but not "foo bar".
The flags can also be combined.
You can also use the experimental fuzzy search feature by providing the
option -z, in which case the pattern is regarded as a fuzzy search term,
not a regexp.
Sometimes you want to filter by one or more columns. You can do that
using the -F option. The option can be specified multiple times and has
the following format:
fieldname=regexp
Fieldnames (== columns headers) are case insensitive.
If you specify more than one filter, both filters have to match (AND
operation).
These field filters can also be negated:
fieldname!=regexp
If the option -v is specified, the filtering is inverted.
COLUMNS
The parameter -c can be used to specify, which columns to display. By
@@ -171,6 +213,44 @@ DESCRIPTION
where "C" is our regexp which matches CMD.
If a column specifier doesn't look like a regular expression, matching
against header fields will be case insensitive. So, if you have a field
with the name "ID" then these will all match: "-c id", "-c Id". The same
rule applies to the options "-T" and "-F".
TRANSPOSE FIELDS USING REGEXPS
You can manipulate field contents using regular expressions. You have to
tell tablizer which field[s] to operate on using the option "-T" and the
search/replace pattern using "-R". The number of columns and patterns
must match.
A search/replace pattern consists of the following elements:
/search-regexp/replace-string/
The separator can be any valid character. Especially if you want to use
a regexp containing the "/" character, eg:
|search-regexp|replace-string|
Example:
cat t/testtable2
NAME DURATION
x 10
a 100
z 0
u 4
k 6
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
NAME DURATION
x 40
a 400
z 4
u 4
k 4
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.
@@ -298,7 +378,7 @@ LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2023 by Thomas von Dein
Copyright (c) 2022-2024 by Thomas von Dein
This software uses the following GO modules:
@@ -329,41 +409,44 @@ AUTHORS
var usage = `
Usage:
tablizer [regex] [file, ...] [flags]
tablizer [regex,...] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-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)
-z, --fuzzy Use fuzzy seach [experimental]
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive):
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
-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
-a, --sort-age sort according to age (duration) string
-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
Other Flags:
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
`

45
go.mod
View File

@@ -1,43 +1,34 @@
module github.com/tlinden/tablizer
go 1.18
go 1.22
require (
github.com/alecthomas/repr v0.1.1
github.com/alecthomas/repr v0.4.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/glycerine/zygomys v5.1.2+incompatible
github.com/gookit/color v1.5.2
github.com/hashicorp/hcl/v2 v2.19.1
github.com/lithammer/fuzzysearch v1.1.7
github.com/gookit/color v1.5.4
github.com/hashicorp/hcl/v2 v2.23.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.6.1
github.com/rogpeppe/go-internal v1.13.1
github.com/spf13/cobra v1.8.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/agext/levenshtein v1.2.1 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be // indirect
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect
github.com/glycerine/greenpack v5.1.1+incompatible // indirect
github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
github.com/shurcooL/go-goon v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tinylib/msgp v1.1.9 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zclconf/go-cty v1.13.0 // indirect
golang.org/x/sys v0.13.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.13.3 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/tools v0.22.0 // indirect
)

105
go.sum
View File

@@ -1,88 +1,69 @@
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs=
github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
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/cpuguy83/go-md2man/v2 v2.0.4/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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be h1:XBJdPGgA3qqhW+p9CANCAVdF7ZIXdu3pZAkypMkKAjE=
github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be/go.mod h1:OSCrScrFAjcBObrulk6BEQlytA462OkG1UGB5NYj9kE=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8=
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
github.com/glycerine/greenpack v5.1.1+incompatible h1:fDr9i6MkSGZmAy4VXPfJhW+SyK2/LNnzIp5nHyDiaIM=
github.com/glycerine/greenpack v5.1.1+incompatible/go.mod h1:us0jVISAESGjsEuLlAfCd5nkZm6W6WQF18HPuOecIg4=
github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 h1:4ZegphJXBTc4uFQ08UVoWYmQXorGa+ipXetUj83sMBc=
github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0/go.mod h1:AqJLs6UeoC65dnHxyCQ6MO31P5STpjcmgaANAU+No8Q=
github.com/glycerine/zygomys v5.1.2+incompatible h1:jmcdmA3XPxgfOunAXFpipE9LQoUL6eX6d2mhYyjV4GE=
github.com/glycerine/zygomys v5.1.2+incompatible/go.mod h1:i3SPKZpmy9dwF/3iWrXJ/ZLyzZucegwypwOmqRkUUaQ=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI=
github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/lithammer/fuzzysearch v1.1.7 h1:q8rZNmBIUkqxsxb/IlwsXVbCoPIH/0juxjFHY0UIwhU=
github.com/lithammer/fuzzysearch v1.1.7/go.mod h1:ZhIlfRGxnD8qa9car/yplC6GmnM14CS07BYAKJJBK2I=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
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/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E=
github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
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/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0=
github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
github.com/zclconf/go-cty v1.13.3 h1:m+b9q3YDbg6Bec5rr+KGy1MzEVzY/jC2X+YX4yqKtHI=
github.com/zclconf/go-cty v1.13.3/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -90,15 +71,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -106,17 +88,16 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022 Thomas von Dein
Copyright © 2022-2024 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
@@ -24,3 +24,13 @@ type Tabdata struct {
headers []string // [ "ID", "NAME", ...]
entries [][]string
}
func (data *Tabdata) CloneEmpty() Tabdata {
newdata := Tabdata{
maxwidthHeader: data.maxwidthHeader,
columns: data.columns,
headers: data.headers,
}
return newdata
}

192
lib/filter.go Normal file
View File

@@ -0,0 +1,192 @@
/*
Copyright © 2022-2025 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"
"io"
"strings"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/tlinden/tablizer/cfg"
)
/*
* [!]Match a line, use fuzzy search for normal pattern strings and
* regexp otherwise.
'foo bar' foo, /bar/! => false => line contains foo and not (not bar)
'foo nix' foo, /bar/! => ture => line contains foo and (not bar)
'foo bar' foo, /bar/ => true => line contains both foo and bar
'foo nix' foo, /bar/ => false => line does not contain bar
'foo bar' foo, /nix/ => false => line does not contain nix
*/
func matchPattern(conf cfg.Config, line string) bool {
if len(conf.Patterns) == 0 {
// any line always matches ""
return true
}
if conf.UseFuzzySearch {
// fuzzy search only considers the 1st pattern
return fuzzy.MatchFold(conf.Patterns[0].Pattern, line)
}
var match int
//fmt.Printf("<%s>\n", line)
for _, re := range conf.Patterns {
patmatch := re.PatternRe.MatchString(line)
if re.Negate {
// toggle the meaning of match
patmatch = !patmatch
}
if patmatch {
match++
}
//fmt.Printf("patmatch: %t, match: %d, pattern: %s, negate: %t\n", patmatch, match, re.Pattern, re.Negate)
}
// fmt.Printf("result: %t\n", match == len(conf.Patterns))
//fmt.Println()
return match == len(conf.Patterns)
}
/*
* Filter parsed data by fields. The filter is positive, so if one or
* more filters match on a row, it will be kept, otherwise it will be
* excluded.
*/
func FilterByFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
if len(conf.Filters) == 0 {
// no filters, no checking
return nil, false, nil
}
newdata := data.CloneEmpty()
for _, row := range data.entries {
keep := true
for idx, header := range data.headers {
lcheader := strings.ToLower(header)
if !Exists(conf.Filters, lcheader) {
// do not filter by unspecified field
continue
}
match := conf.Filters[lcheader].Regex.MatchString(row[idx])
if conf.Filters[lcheader].Negate {
match = !match
}
if !match {
keep = false
break
}
}
if keep == !conf.InvertMatch {
// also apply -v
newdata.entries = append(newdata.entries, row)
}
}
return &newdata, true, nil
}
/*
* Transpose fields using search/replace regexp.
*/
func TransposeFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
if len(conf.UseTransposers) == 0 {
// nothing to be done
return nil, false, nil
}
newdata := data.CloneEmpty()
transposed := false
for _, row := range data.entries {
transposedrow := false
for idx := range data.headers {
transposeidx, hasone := findindex(conf.UseTransposeColumns, idx+1)
if hasone {
row[idx] =
conf.UseTransposers[transposeidx].Search.ReplaceAllString(
row[idx],
conf.UseTransposers[transposeidx].Replace,
)
transposedrow = true
}
}
if transposedrow {
// also apply -v
newdata.entries = append(newdata.entries, row)
transposed = true
}
}
return &newdata, transposed, nil
}
/* generic map.Exists(key) */
func Exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}
/*
* Filters the whole input lines, returns filtered lines
*/
func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
if len(conf.Patterns) == 0 {
return input, nil
}
scanner := bufio.NewScanner(input)
lines := []string{}
hadFirst := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if hadFirst {
// don't match 1st line, it's the header
if matchPattern(conf, line) == conf.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
}
}
lines = append(lines, line)
hadFirst = true
}
return strings.NewReader(strings.Join(lines, "\n")), nil
}

176
lib/filter_test.go Normal file
View File

@@ -0,0 +1,176 @@
/*
Copyright © 2024-2025 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"
"github.com/tlinden/tablizer/cfg"
)
func TestMatchPattern(t *testing.T) {
var input = []struct {
name string
fuzzy bool
patterns []*cfg.Pattern
line string
}{
{
name: "normal",
patterns: []*cfg.Pattern{{Pattern: "haus"}},
line: "hausparty",
},
{
name: "fuzzy",
patterns: []*cfg.Pattern{{Pattern: "hpt"}},
line: "haus-party-termin",
fuzzy: true,
},
}
for _, inputdata := range input {
testname := fmt.Sprintf("match-pattern-%s", inputdata.name)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{}
if inputdata.fuzzy {
conf.UseFuzzySearch = true
}
err := conf.PreparePattern(inputdata.patterns)
if err != nil {
t.Errorf("PreparePattern returned error: %s", err)
}
if !matchPattern(conf, inputdata.line) {
t.Errorf("matchPattern() did not match\nExp: true\nGot: false\n")
}
})
}
}
func TestFilterByFields(t *testing.T) {
data := Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"asd", "igig", "cxxxncnc"},
{"19191", "EDD 1", "x"},
{"8d8", "AN 1", "y"},
},
}
var input = []struct {
name string
filter []string
expect Tabdata
invert bool
}{
{
name: "one-field",
filter: []string{"one=19"},
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"19191", "EDD 1", "x"},
},
},
},
{
name: "one-field-negative",
filter: []string{"one!=asd"},
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"19191", "EDD 1", "x"},
{"8d8", "AN 1", "y"},
},
},
},
{
name: "one-field-inverted",
filter: []string{"one=19"},
invert: true,
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"asd", "igig", "cxxxncnc"},
{"8d8", "AN 1", "y"},
},
},
},
{
name: "many-fields",
filter: []string{"one=19", "two=DD"},
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"19191", "EDD 1", "x"},
},
},
},
{
name: "many-fields-inverted",
filter: []string{"one=19", "two=DD"},
invert: true,
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"asd", "igig", "cxxxncnc"},
{"8d8", "AN 1", "y"},
},
},
},
}
for _, inputdata := range input {
testname := fmt.Sprintf("filter-by-fields-%s", inputdata.name)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{Rawfilters: inputdata.filter, InvertMatch: inputdata.invert}
err := conf.PrepareFilters()
if err != nil {
t.Errorf("PrepareFilters returned error: %s", err)
}
data, _, _ := FilterByFields(conf, &data)
if !reflect.DeepEqual(*data, inputdata.expect) {
t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect)
}
})
}
}

View File

@@ -36,79 +36,175 @@ func contains(s []int, e int) bool {
return true
}
}
return false
}
// parse columns list given with -c, modifies config.UseColumns based
// on eventually given regex
func PrepareColumns(c *cfg.Config, data *Tabdata) error {
if len(c.Columns) > 0 {
for _, use := range strings.Split(c.Columns, ",") {
if len(use) == 0 {
msg := fmt.Sprintf("Could not parse columns list %s: empty column", c.Columns)
return errors.New(msg)
}
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", c.Columns, err)
return errors.New(msg)
}
// find matching header fields
for i, head := range data.headers {
if colPattern.MatchString(head) {
c.UseColumns = append(c.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.
c.UseColumns = append(c.UseColumns, usenum)
}
func findindex(s []int, e int) (int, bool) {
for i, a := range s {
if a == e {
return i, true
}
// 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(c.UseColumns))
for _, i := range c.UseColumns {
imap[i] = 0
}
c.UseColumns = nil
for k := range imap {
c.UseColumns = append(c.UseColumns, k)
}
sort.Ints(c.UseColumns)
}
return 0, false
}
// validate the consitency of parsed data
func ValidateConsistency(data *Tabdata) error {
expectedfields := len(data.headers)
for idx, row := range data.entries {
if len(row) != expectedfields {
return fmt.Errorf("row %d does not contain expected %d elements, but %d",
idx, expectedfields, len(row))
}
}
return nil
}
// parse columns list given with -c, modifies config.UseColumns based
// on eventually given regex.
// This is an output filter, because -cN,N,... is being applied AFTER
// processing of the input data.
func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
// -c columns
usecolumns, err := PrepareColumnVars(conf.Columns, data)
if err != nil {
return err
}
conf.UseColumns = usecolumns
return nil
}
// Same thing as above but for -T option, which is an input option,
// because transposers are being applied before output.
func PrepareTransposerColumns(conf *cfg.Config, data *Tabdata) error {
// -T columns
usetransposecolumns, err := PrepareColumnVars(conf.TransposeColumns, data)
if err != nil {
return err
}
conf.UseTransposeColumns = usetransposecolumns
// verify that columns and transposers match and prepare transposer structs
if err := conf.PrepareTransposers(); err != nil {
return err
}
return nil
}
// 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
}
usecolumns := []int{}
isregex := regexp.MustCompile(`\W`)
for _, columnpattern := range strings.Split(columns, ",") {
if len(columnpattern) == 0 {
return nil, fmt.Errorf("could not parse columns list %s: empty column", columns)
}
usenum, err := strconv.Atoi(columnpattern)
if err != nil {
// not a number
if !isregex.MatchString(columnpattern) {
// is not a regexp (contains no non-word chars)
// lc() it so that word searches are case insensitive
columnpattern = strings.ToLower(columnpattern)
for i, head := range data.headers {
if columnpattern == strings.ToLower(head) {
usecolumns = append(usecolumns, i+1)
}
}
} else {
colPattern, err := regexp.Compile("(?i)" + columnpattern)
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", columns, err)
return nil, errors.New(msg)
}
// find matching header fields, ignoring case
for i, head := range data.headers {
if colPattern.MatchString(strings.ToLower(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
}
// fill with deduplicated columns
usecolumns = nil
for k := range imap {
usecolumns = append(usecolumns, k)
}
sort.Ints(usecolumns)
return usecolumns, nil
}
// prepare headers: add numbers to headers
func numberizeAndReduceHeaders(c cfg.Config, data *Tabdata) {
func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
numberedHeaders := []string{}
maxwidth := 0 // start from scratch, so we only look at displayed column widths
for i, head := range data.headers {
headlen := 0
if len(c.Columns) > 0 {
for idx, head := range data.headers {
var headlen int
if len(conf.Columns) > 0 {
// -c specified
if !contains(c.UseColumns, i+1) {
if !contains(conf.UseColumns, idx+1) {
// ignore this one
continue
}
}
if c.NoNumbering {
if conf.NoNumbering {
numberedHeaders = append(numberedHeaders, head)
headlen = len(head)
} else {
numhead := fmt.Sprintf("%s(%d)", head, i+1)
numhead := fmt.Sprintf("%s(%d)", head, idx+1)
headlen = len(numhead)
numberedHeaders = append(numberedHeaders, numhead)
}
@@ -117,44 +213,54 @@ func numberizeAndReduceHeaders(c cfg.Config, data *Tabdata) {
maxwidth = headlen
}
}
data.headers = numberedHeaders
if data.maxwidthHeader != maxwidth && maxwidth > 0 {
data.maxwidthHeader = maxwidth
}
}
// exclude columns, if any
func reduceColumns(c cfg.Config, data *Tabdata) {
if len(c.Columns) > 0 {
func reduceColumns(conf cfg.Config, data *Tabdata) {
if len(conf.Columns) > 0 {
reducedEntries := [][]string{}
var reducedEntry []string
for _, entry := range data.entries {
reducedEntry = nil
for i, value := range entry {
if !contains(c.UseColumns, i+1) {
if !contains(conf.UseColumns, i+1) {
continue
}
reducedEntry = append(reducedEntry, value)
}
reducedEntries = append(reducedEntries, reducedEntry)
}
data.entries = reducedEntries
}
}
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()!
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))
var fixedrow = make([]string, len(row))
for idx, cell := range row {
fixedrow[idx] = strings.TrimSpace(cell)
}
return fixedrow
}
func colorizeData(c cfg.Config, output string) string {
if c.UseHighlight && color.IsConsole(os.Stdout) {
// FIXME: refactor this beast!
func colorizeData(conf cfg.Config, output string) string {
switch {
case conf.UseHighlight && color.IsConsole(os.Stdout):
highlight := true
colorized := ""
first := true
@@ -167,30 +273,42 @@ func colorizeData(c cfg.Config, output string) string {
// in pprint mode. This doesn't matter as long as
// we don't use colorization. But with colors the
// missing spaces can be seen.
if c.OutputMode == cfg.Ascii {
line = line + " "
if conf.OutputMode == cfg.ASCII {
line += " "
}
line = c.HighlightHdrStyle.Sprint(line)
line = conf.HighlightHdrStyle.Sprint(line)
first = false
} else {
line = c.HighlightStyle.Sprint(line)
line = conf.HighlightStyle.Sprint(line)
}
} else {
line = c.NoHighlightStyle.Sprint(line)
line = conf.NoHighlightStyle.Sprint(line)
}
highlight = !highlight
colorized += line + "\n"
}
return colorized
} else if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) {
r := regexp.MustCompile("(" + c.Pattern + ")")
return r.ReplaceAllStringFunc(output, func(in string) string {
return c.ColorStyle.Sprint(in)
})
} else {
case len(conf.Patterns) > 0 && !conf.NoColor && color.IsConsole(os.Stdout):
out := output
for _, re := range conf.Patterns {
if !re.Negate {
r := regexp.MustCompile("(" + re.Pattern + ")")
out = r.ReplaceAllStringFunc(out, func(in string) string {
return conf.ColorStyle.Sprint(in)
})
}
}
return out
default:
return output
}
}

View File

@@ -19,9 +19,10 @@ package lib
import (
"fmt"
"github.com/tlinden/tablizer/cfg"
"reflect"
"testing"
"github.com/tlinden/tablizer/cfg"
)
func TestContains(t *testing.T) {
@@ -66,23 +67,103 @@ func TestPrepareColumns(t *testing.T) {
}{
{"1,2,3", []int{1, 2, 3}, false},
{"1,2,", []int{}, true},
{"T", []int{2, 3}, false},
{"T,2,3", []int{2, 3}, false},
{"T.", []int{2, 3}, false},
{"T.,2,3", []int{2, 3}, false},
{"[a-z,4,5", []int{4, 5}, true}, // invalid regexp
}
for _, tt := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror)
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", testdata.input, testdata.wanterror)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{Columns: tt.input}
err := PrepareColumns(&c, &data)
conf := cfg.Config{Columns: testdata.input}
err := PrepareColumns(&conf, &data)
if err != nil {
if !tt.wanterror {
if !testdata.wanterror {
t.Errorf("got error: %v", err)
}
} else {
if !reflect.DeepEqual(c.UseColumns, tt.exp) {
t.Errorf("got: %v, expected: %v", c.UseColumns, tt.exp)
if !reflect.DeepEqual(conf.UseColumns, testdata.exp) {
t.Errorf("got: %v, expected: %v", conf.UseColumns, testdata.exp)
}
}
})
}
}
func TestPrepareTransposerColumns(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"2", "3", "4",
},
},
}
var tests = []struct {
input string
transp []string
exp int
wanterror bool // expect error
}{
{
"1",
[]string{`/\d/x/`},
1,
false,
},
{
"T.", // will match [T]WO and [T]HREE
[]string{`/\d/x/`, `/.//`},
2,
false,
},
{
"TH.,2",
[]string{`/\d/x/`, `/.//`},
2,
false,
},
{
"1",
[]string{},
1,
true,
},
{
"",
[]string{`|.|N|`},
0,
true,
},
{
"1",
[]string{`|.|N|`},
1,
false,
},
}
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareTransposerColumns-%s-%t", testdata.input, testdata.wanterror)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{TransposeColumns: testdata.input, Transposers: testdata.transp}
err := PrepareTransposerColumns(&conf, &data)
if err != nil {
if !testdata.wanterror {
t.Errorf("got error: %v", err)
}
} else {
if len(conf.UseTransposeColumns) != testdata.exp {
t.Errorf("got %d, want %d", conf.UseTransposeColumns, testdata.exp)
}
if len(conf.Transposers) != len(conf.UseTransposeColumns) {
t.Errorf("got %d, want %d", conf.UseTransposeColumns, testdata.exp)
}
}
})
@@ -114,14 +195,16 @@ func TestReduceColumns(t *testing.T) {
input := [][]string{{"a", "b", "c"}}
for _, tt := range tests {
testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns)
for _, testdata := range tests {
testname := fmt.Sprintf("reduce-columns-by-%+v", testdata.columns)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{Columns: "x", UseColumns: tt.columns}
c := cfg.Config{Columns: "x", UseColumns: testdata.columns}
data := Tabdata{entries: input}
reduceColumns(c, &data)
if !reflect.DeepEqual(data.entries, tt.expect) {
t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect)
if !reflect.DeepEqual(data.entries, testdata.expect) {
t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v",
data.entries, testdata.expect)
}
})
}
@@ -142,15 +225,17 @@ func TestNumberizeHeaders(t *testing.T) {
{[]string{"ONE", "TWO"}, []int{1, 2}, true},
}
for _, tt := range tests {
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", tt.columns, tt.nonum)
for _, testdata := range tests {
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t",
testdata.columns, testdata.nonum)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{Columns: "x", UseColumns: tt.columns, NoNumbering: tt.nonum}
conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, NoNumbering: testdata.nonum}
usedata := data
numberizeAndReduceHeaders(c, &usedata)
if !reflect.DeepEqual(usedata.headers, tt.expect) {
numberizeAndReduceHeaders(conf, &usedata)
if !reflect.DeepEqual(usedata.headers, testdata.expect) {
t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v",
usedata.headers, tt.expect)
usedata.headers, testdata.expect)
}
})
}

112
lib/io.go
View File

@@ -19,91 +19,89 @@ package lib
import (
"errors"
"github.com/tlinden/tablizer/cfg"
"fmt"
"io"
"os"
"github.com/tlinden/tablizer/cfg"
)
func ProcessFiles(c *cfg.Config, args []string) error {
fds, pattern, err := determineIO(c, args)
const RWRR = 0755
func ProcessFiles(conf *cfg.Config, args []string) error {
fd, patterns, err := determineIO(conf, args)
if err != nil {
return err
}
if err := c.PreparePattern(pattern); err != nil {
if err := conf.PreparePattern(patterns); err != nil {
return err
}
for _, fd := range fds {
data, err := Parse(*c, fd)
if err != nil {
return err
}
err = PrepareColumns(c, &data)
if err != nil {
return err
}
printData(os.Stdout, *c, &data)
data, err := Parse(*conf, fd)
if err != nil {
return err
}
if err = ValidateConsistency(&data); err != nil {
return err
}
err = PrepareSortColumns(conf, &data)
if err != nil {
return err
}
err = PrepareColumns(conf, &data)
if err != nil {
return err
}
printData(os.Stdout, *conf, &data)
return nil
}
func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) {
var pattern string
var fds []io.Reader
func determineIO(conf *cfg.Config, args []string) (io.Reader, []*cfg.Pattern, error) {
var filehandle io.Reader
var patterns []*cfg.Pattern
var haveio bool
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
// we're reading from STDIN, which takes precedence over file args
fds = append(fds, os.Stdin)
if len(args) > 0 {
// ignore any args > 1
pattern = args[0]
c.Pattern = args[0] // used for colorization by printData()
}
switch {
case conf.InputFile == "-":
filehandle = os.Stdin
haveio = true
} else {
if len(args) > 0 {
// threre were args left, take a look
if args[0] == "-" {
// in traditional unix programs a dash denotes STDIN (forced)
fds = append(fds, os.Stdin)
haveio = true
} else {
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]
c.Pattern = args[0] // used for colorization by printData()
args = args[1:]
}
case conf.InputFile != "":
fd, err := os.OpenFile(conf.InputFile, os.O_RDONLY, RWRR)
if len(args) > 0 {
// consider any other args as files
for _, file := range args {
if err != nil {
return nil, nil, fmt.Errorf("failed to read input file %s: %w", conf.InputFile, err)
}
fd, err := os.OpenFile(file, os.O_RDONLY, 0755)
filehandle = fd
haveio = true
}
if err != nil {
return nil, "", err
}
if !haveio {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
// we're reading from STDIN, which takes precedence over file args
filehandle = os.Stdin
haveio = true
}
}
fds = append(fds, fd)
haveio = true
}
}
}
if len(args) > 0 {
patterns = make([]*cfg.Pattern, len(args))
for i, arg := range args {
patterns[i] = &cfg.Pattern{Pattern: arg}
}
}
if !haveio {
return nil, "", errors.New("No file specified and nothing to read on stdin!")
return nil, nil, errors.New("no file specified and nothing to read on stdin")
}
return fds, pattern, nil
return filehandle, patterns, nil
}

View File

@@ -1,293 +0,0 @@
/*
Copyright © 2023 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"
"log"
"os"
"strings"
"github.com/glycerine/zygomys/zygo"
"github.com/tlinden/tablizer/cfg"
)
/*
needs to be global because we can't feed an cfg object to AddHook()
which is being called from user lisp code
*/
var Hooks map[string][]*zygo.SexpSymbol
/*
AddHook() (called addhook from lisp code) can be used by the user to
add a function to one of the available hooks provided by tablizer.
*/
func AddHook(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
var hookname string
if len(args) < 2 {
return zygo.SexpNull, errors.New("argument of %add-hook should be: %hook-name %your-function")
}
switch t := args[0].(type) {
case *zygo.SexpSymbol:
if !HookExists(t.Name()) {
return zygo.SexpNull, errors.New("Unknown hook " + t.Name())
}
hookname = t.Name()
default:
return zygo.SexpNull, errors.New("hook name must be a symbol!")
}
switch t := args[1].(type) {
case *zygo.SexpSymbol:
_, exists := Hooks[hookname]
if !exists {
Hooks[hookname] = []*zygo.SexpSymbol{t}
} else {
Hooks[hookname] = append(Hooks[hookname], t)
}
default:
return zygo.SexpNull, errors.New("hook function must be a symbol!")
}
return zygo.SexpNull, nil
}
/*
Check if a hook exists
*/
func HookExists(key string) bool {
for _, hook := range cfg.ValidHooks {
if hook == key {
return true
}
}
return false
}
/*
* Basic sanity checks and load lisp file
*/
func LoadFile(env *zygo.Zlisp, path string) error {
if strings.HasSuffix(path, `.zy`) {
code, err := os.ReadFile(path)
if err != nil {
return err
}
// FIXME: check what res (_ here) could be and mean
_, err = env.EvalString(string(code))
if err != nil {
log.Fatalf(env.GetStackTrace(err))
}
}
return nil
}
/*
* Setup lisp interpreter environment
*/
func SetupLisp(c *cfg.Config) error {
Hooks = make(map[string][]*zygo.SexpSymbol)
env := zygo.NewZlispSandbox()
env.AddFunction("addhook", AddHook)
// iterate over load-path and evaluate all *.zy files there, if any
// we ignore if load-path does not exist, which is the default anyway
if path, err := os.Stat(c.LispLoadPath); !os.IsNotExist(err) {
if !path.IsDir() {
err := LoadFile(env, c.LispLoadPath)
if err != nil {
return err
}
} else {
dir, err := os.ReadDir(c.LispLoadPath)
if err != nil {
return err
}
for _, entry := range dir {
if !entry.IsDir() {
err := LoadFile(env, c.LispLoadPath+"/"+entry.Name())
if err != nil {
return err
}
}
}
}
}
RegisterLib(env)
c.Lisp = env
return nil
}
/*
Execute every user lisp function registered as filter hook.
Each function is given the current line as argument and is expected to
return a boolean. True indicates to keep the line, false to skip
it.
If there are multiple such functions registered, then the first one
returning false wins, that is if each function returns true the line
will be kept, if at least one of them returns false, it will be
skipped.
*/
func RunFilterHooks(c cfg.Config, line string) (bool, error) {
for _, hook := range Hooks["filter"] {
var result bool
c.Lisp.Clear()
res, err := c.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line))
if err != nil {
return false, err
}
switch t := res.(type) {
case *zygo.SexpBool:
result = t.Val
default:
return false, errors.New("filter hook shall return BOOL!")
}
if !result {
// the first hook which returns false leads to complete false
return result, nil
}
}
// if no hook returned false, we succeed and accept the given line
return true, nil
}
/*
These hooks get the data (Tabdata) readily processed by tablizer as
argument. They are expected to return a SexpPair containing a boolean
denoting if the data has been modified and the actual modified
data. Columns must be the same, rows may differ. Cells may also have
been modified.
Replaces the internal data structure Tabdata with the user supplied
version.
Only one process hook function is supported.
The somewhat complicated code is being caused by the fact, that we
need to convert our internal structure to a lisp variable and vice
versa afterwards.
*/
func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) {
var userdata Tabdata
lisplist := []zygo.Sexp{}
if len(Hooks["process"]) == 0 {
return userdata, false, nil
}
if len(Hooks["process"]) > 1 {
fmt.Println("Warning: only one process hook is allowed!")
}
// there are hook[s] installed, convert the go data structure 'data to lisp
for _, row := range data.entries {
var entry zygo.SexpHash
for idx, cell := range row {
err := entry.HashSet(&zygo.SexpStr{S: data.headers[idx]}, &zygo.SexpStr{S: cell})
if err != nil {
return userdata, false, err
}
}
lisplist = append(lisplist, &entry)
}
// we need to add it to the env so that the function can use the struct directly
c.Lisp.AddGlobal("data", &zygo.SexpArray{Val: lisplist, Env: c.Lisp})
// execute the actual hook
hook := Hooks["process"][0]
var result bool
c.Lisp.Clear()
res, err := c.Lisp.EvalString(fmt.Sprintf("(%s data)", hook.Name()))
if err != nil {
return userdata, false, err
}
// we expect (bool, array(hash)) as return from the function
switch t := res.(type) {
case *zygo.SexpPair:
switch th := t.Head.(type) {
case *zygo.SexpBool:
result = th.Val
default:
return userdata, false, errors.New("Expect (bool, array(hash)) as return value!")
}
switch tt := t.Tail.(type) {
case *zygo.SexpArray:
lisplist = tt.Val
default:
return userdata, false, errors.New("Expect (bool, array(hash)) as return value!")
}
default:
return userdata, false, errors.New("filter hook shall return array of hashes!")
}
if !result {
// no further processing required
return userdata, result, nil
}
// finally convert lispdata back to Tabdata
for _, item := range lisplist {
row := []string{}
switch hash := item.(type) {
case *zygo.SexpHash:
for _, header := range data.headers {
entry, err := hash.HashGetDefault(c.Lisp, &zygo.SexpStr{S: header}, &zygo.SexpStr{S: ""})
if err != nil {
return userdata, false, err
}
switch t := entry.(type) {
case *zygo.SexpStr:
row = append(row, t.S)
default:
return userdata, false, errors.New("Hash values should be string!")
}
}
default:
return userdata, false, errors.New("Returned array should contain hashes!")
}
userdata.entries = append(userdata.entries, row)
}
userdata.headers = data.headers
return userdata, result, nil
}

View File

@@ -1,84 +0,0 @@
/*
Copyright © 2023 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"
"regexp"
"strconv"
"github.com/glycerine/zygomys/zygo"
)
func Splice2SexpList(list []string) zygo.Sexp {
slist := []zygo.Sexp{}
for _, item := range list {
slist = append(slist, &zygo.SexpStr{S: item})
}
return zygo.MakeList(slist)
}
func StringReSplit(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
if len(args) < 2 {
return zygo.SexpNull, errors.New("expecting 2 arguments!")
}
var separator string
var input string
switch t := args[0].(type) {
case *zygo.SexpStr:
input = t.S
default:
return zygo.SexpNull, errors.New("second argument must be a string!")
}
switch t := args[1].(type) {
case *zygo.SexpStr:
separator = t.S
default:
return zygo.SexpNull, errors.New("first argument must be a string!")
}
sep := regexp.MustCompile(separator)
return Splice2SexpList(sep.Split(input, -1)), nil
}
func String2Int(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) {
var number int
switch t := args[0].(type) {
case *zygo.SexpStr:
num, err := strconv.Atoi(t.S)
if err != nil {
return zygo.SexpNull, err
}
number = num
default:
return zygo.SexpNull, errors.New("argument must be a string!")
}
return &zygo.SexpInt{Val: int64(number)}, nil
}
func RegisterLib(env *zygo.Zlisp) {
env.AddFunction("resplit", StringReSplit)
env.AddFunction("atoi", String2Int)
}

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022 Thomas von Dein
Copyright © 2022-2024 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
@@ -20,91 +20,64 @@ package lib
import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"io"
"regexp"
"strings"
"github.com/alecthomas/repr"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/tlinden/tablizer/cfg"
)
/*
* [!]Match a line, use fuzzy search for normal pattern strings and
* regexp otherwise.
*/
func matchPattern(c cfg.Config, line string) bool {
if len(c.Pattern) > 0 {
if c.UseFuzzySearch {
return fuzzy.MatchFold(c.Pattern, line)
} else {
return c.PatternR.MatchString(line)
}
}
return true
}
/*
Parser switch
*/
func Parse(c cfg.Config, input io.Reader) (Tabdata, error) {
if len(c.Separator) == 1 {
return parseCSV(c, input)
func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
var data Tabdata
var err error
// first step, parse the data
if len(conf.Separator) == 1 {
data, err = parseCSV(conf, input)
} else {
data, err = parseTabular(conf, input)
}
return parseTabular(c, input)
if err != nil {
return data, err
}
// 2nd step, apply filters, code or transposers, if any
postdata, changed, err := PostProcess(conf, &data)
if err != nil {
return data, err
}
if changed {
return *postdata, nil
}
return data, err
}
/*
Parse CSV input.
*/
func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) {
var content io.Reader = input
func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) {
data := Tabdata{}
if len(c.Pattern) > 0 {
scanner := bufio.NewScanner(input)
lines := []string{}
hadFirst := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if hadFirst {
// don't match 1st line, it's the header
if matchPattern(c, line) == c.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
}
// apply user defined lisp filters, if any
accept, err := RunFilterHooks(c, line)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err))
}
if !accept {
// IF there are filter hook[s] and IF one of them
// returns false on the current line, reject it
continue
}
}
lines = append(lines, line)
hadFirst = true
}
content = strings.NewReader(strings.Join(lines, "\n"))
// apply pattern, if any
content, err := FilterByPattern(conf, input)
if err != nil {
return data, err
}
csvreader := csv.NewReader(content)
csvreader.Comma = rune(c.Separator[0])
csvreader.Comma = rune(conf.Separator[0])
records, err := csvreader.ReadAll()
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Could not parse CSV input: %w", err))
return data, fmt.Errorf("could not parse CSV input: %w", err)
}
if len(records) >= 1 {
@@ -124,28 +97,19 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) {
}
}
// apply user defined lisp process hooks, if any
userdata, changed, err := RunProcessHooks(c, data)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err))
}
if changed {
data = userdata
}
return data, nil
}
/*
Parse tabular input.
*/
func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
data := Tabdata{}
var scanner *bufio.Scanner
hadFirst := false
separate := regexp.MustCompile(c.Separator)
separate := regexp.MustCompile(conf.Separator)
scanner = bufio.NewScanner(input)
@@ -156,16 +120,9 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
if !hadFirst {
// header processing
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]]))
//}
// register widest header field
headerlen := len(part)
if headerlen > data.maxwidthHeader {
@@ -180,7 +137,7 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
}
} else {
// data processing
if matchPattern(c, line) == c.InvertMatch {
if matchPattern(conf, line) == conf.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,
@@ -188,18 +145,6 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
continue
}
// apply user defined lisp filters, if any
accept, err := RunFilterHooks(c, line)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err))
}
if !accept {
// IF there are filter hook[s] and IF one of them
// returns false on the current line, reject it
continue
}
idx := 0 // we cannot use the header index, because we could exclude columns
values := []string{}
for _, part := range parts {
@@ -221,21 +166,45 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) {
}
if scanner.Err() != nil {
return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err()))
}
// apply user defined lisp process hooks, if any
userdata, changed, err := RunProcessHooks(c, data)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err))
}
if changed {
data = userdata
}
if c.Debug {
repr.Print(data)
return data, fmt.Errorf("failed to read from io.Reader: %w", scanner.Err())
}
return data, nil
}
func PostProcess(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
var modified bool
// filter by field filters, if any
filtereddata, changed, err := FilterByFields(conf, data)
if err != nil {
return data, false, fmt.Errorf("failed to filter fields: %w", err)
}
if changed {
data = filtereddata
modified = true
}
// check if transposers are valid and turn into Transposer structs
if err := PrepareTransposerColumns(&conf, data); err != nil {
return data, false, err
}
// transpose if demanded
modifieddata, changed, err := TransposeFields(conf, data)
if err != nil {
return data, false, fmt.Errorf("failed to transpose fields: %w", err)
}
if changed {
data = modifieddata
modified = true
}
if conf.Debug {
repr.Print(data)
}
return data, modified, nil
}

View File

@@ -62,12 +62,12 @@ func TestParser(t *testing.T) {
},
}
for _, in := range input {
testname := fmt.Sprintf("parse-%s", in.name)
for _, testdata := range input {
testname := fmt.Sprintf("parse-%s", testdata.name)
t.Run(testname, func(t *testing.T) {
readFd := strings.NewReader(strings.TrimSpace(in.text))
c := cfg.Config{Separator: in.separator}
gotdata, err := Parse(c, readFd)
readFd := strings.NewReader(strings.TrimSpace(testdata.text))
conf := cfg.Config{Separator: testdata.separator}
gotdata, err := Parse(conf, readFd)
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
@@ -83,49 +83,55 @@ func TestParser(t *testing.T) {
func TestParserPatternmatching(t *testing.T) {
var tests = []struct {
entries [][]string
pattern string
invert bool
want bool
name string
entries [][]string
patterns []*cfg.Pattern
invert bool
want bool
}{
{
name: "match",
entries: [][]string{
{"asd", "igig", "cxxxncnc"},
},
pattern: "ig",
invert: false,
patterns: []*cfg.Pattern{{Pattern: "ig"}},
invert: false,
},
{
name: "invert",
entries: [][]string{
{"19191", "EDD 1", "X"},
},
pattern: "ig",
invert: true,
patterns: []*cfg.Pattern{{Pattern: "ig"}},
invert: true,
},
}
for _, in := range input {
for _, tt := range tests {
for _, inputdata := range input {
for _, testdata := range tests {
testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
in.name, tt.pattern, tt.invert)
inputdata.name, testdata.name, testdata.invert)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern,
Separator: in.separator}
conf := cfg.Config{
InvertMatch: testdata.invert,
Patterns: testdata.patterns,
Separator: inputdata.separator,
}
_ = c.PreparePattern(tt.pattern)
_ = conf.PreparePattern(testdata.patterns)
readFd := strings.NewReader(strings.TrimSpace(in.text))
gotdata, err := Parse(c, readFd)
readFd := strings.NewReader(strings.TrimSpace(inputdata.text))
gotdata, err := Parse(conf, readFd)
if err != nil {
if !tt.want {
if !testdata.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
}
} else {
if !reflect.DeepEqual(tt.entries, gotdata.entries) {
if !reflect.DeepEqual(testdata.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)
testdata.name, testdata.invert, testdata.entries, gotdata.entries)
}
}
})
@@ -152,8 +158,8 @@ asd igig
19191 EDD 1 X`
readFd := strings.NewReader(strings.TrimSpace(table))
c := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := Parse(c, readFd)
conf := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := Parse(conf, readFd)
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
@@ -161,6 +167,6 @@ asd igig
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
c.Separator, data, gotdata)
conf.Separator, data, gotdata)
}
}

View File

@@ -32,51 +32,50 @@ import (
"gopkg.in/yaml.v3"
)
func printData(w io.Writer, c cfg.Config, data *Tabdata) {
// some output preparations:
func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// 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 this we're not interested in
numberizeAndReduceHeaders(c, data)
// add numbers to headers and remove those we're not interested in
numberizeAndReduceHeaders(conf, data)
// remove unwanted columns, if any
reduceColumns(c, data)
reduceColumns(conf, data)
// sort the data
sortTable(c, data)
switch c.OutputMode {
switch conf.OutputMode {
case cfg.Extended:
printExtendedData(w, c, data)
case cfg.Ascii:
printAsciiData(w, c, data)
printExtendedData(writer, conf, data)
case cfg.ASCII:
printASCIIData(writer, conf, data)
case cfg.Orgtbl:
printOrgmodeData(w, c, data)
printOrgmodeData(writer, conf, data)
case cfg.Markdown:
printMarkdownData(w, c, data)
printMarkdownData(writer, conf, data)
case cfg.Shell:
printShellData(w, c, data)
printShellData(writer, data)
case cfg.Yaml:
printYamlData(w, c, data)
printYamlData(writer, data)
case cfg.CSV:
printCSVData(w, c, data)
printCSVData(writer, data)
default:
printAsciiData(w, c, data)
printASCIIData(writer, conf, data)
}
}
func output(w io.Writer, str string) {
fmt.Fprint(w, str)
func output(writer io.Writer, str string) {
fmt.Fprint(writer, str)
}
/*
Emacs org-mode compatible table (also orgtbl-mode)
*/
func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) {
func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !c.NoHeaders {
if !conf.NoHeaders {
table.SetHeader(data.headers)
}
@@ -100,8 +99,8 @@ func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) {
leftR := regexp.MustCompile(`(?m)^\\+`)
rightR := regexp.MustCompile(`\\+(?m)$`)
output(w, color.Sprint(
colorizeData(c,
output(writer, color.Sprint(
colorizeData(conf,
rightR.ReplaceAllString(
leftR.ReplaceAllString(tableString.String(), "|"), "|"))))
}
@@ -109,11 +108,11 @@ func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) {
/*
Markdown table
*/
func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) {
func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !c.NoHeaders {
if !conf.NoHeaders {
table.SetHeader(data.headers)
}
@@ -125,19 +124,20 @@ func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) {
table.SetCenterSeparator("|")
table.Render()
output(w, color.Sprint(colorizeData(c, tableString.String())))
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
}
/*
Simple ASCII table without any borders etc, just like the input we expect
*/
func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) {
func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !c.NoHeaders {
if !conf.NoHeaders {
table.SetHeader(data.headers)
}
table.AppendBulk(data.entries)
table.SetAutoWrapText(false)
@@ -151,7 +151,7 @@ func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) {
table.SetBorder(false)
table.SetNoWhiteSpace(true)
if !c.UseHighlight {
if !conf.UseHighlight {
// the tabs destroy the highlighting
table.SetTablePadding("\t") // pad with tabs
} else {
@@ -159,16 +159,17 @@ func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) {
}
table.Render()
output(w, color.Sprint(colorizeData(c, tableString.String())))
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
}
/*
We simulate the \x command of psql (the PostgreSQL client)
*/
func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) {
func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// needed for data output
format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader)
out := ""
if len(data.entries) > 0 {
for _, entry := range data.entries {
for i, value := range entry {
@@ -179,67 +180,71 @@ func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) {
}
}
output(w, colorizeData(c, out))
output(writer, colorizeData(conf, out))
}
/*
Shell output, ready to be eval'd. Just like FreeBSD stat(1)
*/
func printShellData(w io.Writer, c cfg.Config, data *Tabdata) {
func printShellData(writer io.Writer, data *Tabdata) {
out := ""
if len(data.entries) > 0 {
for _, entry := range data.entries {
shentries := []string{}
for i, value := range entry {
for idx, value := range entry {
shentries = append(shentries, fmt.Sprintf("%s=\"%s\"",
data.headers[i], value))
data.headers[idx], value))
}
out += fmt.Sprint(strings.Join(shentries, " ")) + "\n"
out += strings.Join(shentries, " ") + "\n"
}
}
// no colorization here
output(w, out)
output(writer, out)
}
func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) {
type D struct {
func printYamlData(writer io.Writer, data *Tabdata) {
type Data struct {
Entries []map[string]interface{} `yaml:"entries"`
}
d := D{}
yamlout := Data{}
for _, entry := range data.entries {
ml := map[string]interface{}{}
yamldata := map[string]interface{}{}
for i, entry := range entry {
for idx, entry := range entry {
style := yaml.TaggedStyle
_, err := strconv.Atoi(entry)
if err != nil {
style = yaml.DoubleQuotedStyle
}
ml[strings.ToLower(data.headers[i])] =
yamldata[strings.ToLower(data.headers[idx])] =
&yaml.Node{
Kind: yaml.ScalarNode,
Style: style,
Value: entry}
}
d.Entries = append(d.Entries, ml)
yamlout.Entries = append(yamlout.Entries, yamldata)
}
yamlstr, err := yaml.Marshal(&d)
yamlstr, err := yaml.Marshal(&yamlout)
if err != nil {
log.Fatal(err)
}
output(w, string(yamlstr))
output(writer, string(yamlstr))
}
func printCSVData(w io.Writer, c cfg.Config, data *Tabdata) {
csvout := csv.NewWriter(w)
func printCSVData(writer io.Writer, data *Tabdata) {
csvout := csv.NewWriter(writer)
if err := csvout.Write(data.headers); err != nil {
log.Fatalln("error writing record to csv:", err)

View File

@@ -20,10 +20,10 @@ package lib
import (
"bytes"
"fmt"
//"github.com/alecthomas/repr"
"github.com/tlinden/tablizer/cfg"
"strings"
"testing"
"github.com/tlinden/tablizer/cfg"
)
func newData() Tabdata {
@@ -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
@@ -73,7 +73,7 @@ var tests = []struct {
}{
// --------------------- Default settings mode tests ``
{
mode: cfg.Ascii,
mode: cfg.ASCII,
name: "default",
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
@@ -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,
@@ -250,39 +250,42 @@ DURATION(2) WHEN(4)
}
func TestPrinter(t *testing.T) {
for _, tt := range tests {
testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr)
for _, testdata := range tests {
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 w bytes.Buffer
var writer bytes.Buffer
// cmd flags
c := cfg.Config{
SortByColumn: tt.column,
SortDescending: tt.desc,
SortMode: tt.sortby,
OutputMode: tt.mode,
NoNumbering: tt.nonum,
UseColumns: tt.usecol,
conf := cfg.Config{
SortDescending: testdata.desc,
SortMode: testdata.sortby,
OutputMode: testdata.mode,
NoNumbering: testdata.nonum,
UseColumns: testdata.usecol,
NoColor: true,
}
c.ApplyDefaults()
// the test checks the len!
if len(tt.usecol) > 0 {
c.Columns = "yes"
} else {
c.Columns = ""
if testdata.column > 0 {
conf.UseSortByColumn = []int{testdata.column}
}
testdata := newData()
exp := strings.TrimSpace(tt.expect)
conf.ApplyDefaults()
printData(&w, c, &testdata)
// the test checks the len!
if len(testdata.usecol) > 0 {
conf.Columns = "yes"
} else {
conf.Columns = ""
}
got := strings.TrimSpace(w.String())
data := newData()
exp := strings.TrimSpace(testdata.expect)
printData(&writer, conf, &data)
got := strings.TrimSpace(writer.String())
if got != exp {
t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s",

View File

@@ -18,84 +18,102 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
import (
"github.com/araddon/dateparse"
"github.com/tlinden/tablizer/cfg"
"cmp"
"regexp"
"sort"
"strconv"
"github.com/araddon/dateparse"
"github.com/tlinden/tablizer/cfg"
)
func sortTable(c cfg.Config, data *Tabdata) {
if c.SortByColumn <= 0 {
func sortTable(conf cfg.Config, data *Tabdata) {
if len(conf.UseSortByColumn) == 0 {
// no sorting wanted
return
}
// slightly modified here to match internal array indicies
col := c.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(&c, 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(c *cfg.Config, a string, b string) bool {
func compare(conf *cfg.Config, left string, right string) int {
var comp bool
switch c.SortMode {
switch conf.SortMode {
case "numeric":
left, err := strconv.Atoi(a)
left, err := strconv.Atoi(left)
if err != nil {
left = 0
}
right, err := strconv.Atoi(b)
right, err := strconv.Atoi(right)
if err != nil {
right = 0
}
comp = left < right
case "duration":
left := duration2int(a)
right := duration2int(b)
left := duration2int(left)
right := duration2int(right)
comp = left < right
case "time":
left, _ := dateparse.ParseAny(a)
right, _ := dateparse.ParseAny(b)
left, _ := dateparse.ParseAny(left)
right, _ := dateparse.ParseAny(right)
comp = left.Unix() < right.Unix()
default:
comp = a < b
comp = left < right
}
if c.SortDescending {
if conf.SortDescending {
comp = !comp
}
return comp
switch comp {
case true:
return 0
default:
return 1
}
}
/*
We could use time.ParseDuration(), but this doesn't support days.
We could use time.ParseDuration(), but this doesn't support days.
We could also use github.com/xhit/go-str2duration/v2, which does
the job, but it's just another dependency, just for this little
gem. And we don't need a time.Time value. And int is good enough
for duration comparision.
We could also use github.com/xhit/go-str2duration/v2, which does
the job, but it's just another dependency, just for this little
gem. And we don't need a time.Time value. And int is good enough
for duration comparison.
Convert a durartion into an integer. Valid time units are "s",
"m", "h" and "d".
Convert a duration into an integer. Valid time units are "s",
"m", "h" and "d".
*/
func duration2int(duration string) int {
re := regexp.MustCompile(`(\d+)([dhms])`)
@@ -103,16 +121,17 @@ func duration2int(duration string) int {
for _, match := range re.FindAllStringSubmatch(duration, -1) {
if len(match) == 3 {
v, _ := strconv.Atoi(match[1])
durationvalue, _ := strconv.Atoi(match[1])
switch match[2][0] {
case 'd':
seconds += v * 86400
seconds += durationvalue * 86400
case 'h':
seconds += v * 3600
seconds += durationvalue * 3600
case 'm':
seconds += v * 60
seconds += durationvalue * 60
case 's':
seconds += v
seconds += durationvalue
}
}
}

View File

@@ -19,8 +19,9 @@ package lib
import (
"fmt"
"github.com/tlinden/tablizer/cfg"
"testing"
"github.com/tlinden/tablizer/cfg"
)
func TestDuration2Seconds(t *testing.T) {
@@ -36,12 +37,12 @@ func TestDuration2Seconds(t *testing.T) {
{"19t77X what?4s", 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("duration-%s", tt.dur)
for _, testdata := range tests {
testname := fmt.Sprintf("duration-%s", testdata.dur)
t.Run(testname, func(t *testing.T) {
seconds := duration2int(tt.dur)
if seconds != tt.expect {
t.Errorf("got %d, want %d", seconds, tt.expect)
seconds := duration2int(testdata.dur)
if seconds != testdata.expect {
t.Errorf("got %d, want %d", seconds, testdata.expect)
}
})
}
@@ -52,27 +53,29 @@ 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 _, tt := range tests {
testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t", tt.mode, tt.a, tt.b, tt.desc)
for _, testdata := range tests {
testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t",
testdata.mode, testdata.a, testdata.b, testdata.desc)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{SortMode: tt.mode, SortDescending: tt.desc}
got := compare(&c, tt.a, tt.b)
if got != tt.want {
t.Errorf("got %t, want %t", got, tt.want)
c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc}
got := compare(&c, testdata.a, testdata.b)
if got != testdata.want {
t.Errorf("got %d, want %d", got, testdata.want)
}
})
}

10
main.go
View File

@@ -18,9 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"os"
"github.com/tlinden/tablizer/cmd"
)
func main() {
cmd.Execute()
os.Exit(Main())
}
func Main() int {
cmd.Execute()
return 0 // cmd takes care of exit 1 itself
}

20
main_test.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"os"
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"tablizer": Main,
}))
}
func TestTablizer(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

43
t/test-basics.txtar Normal file
View File

@@ -0,0 +1,43 @@
# usage
exec tablizer -h
stdout Usage
# version
exec tablizer -V
stdout version
# manpage
exec tablizer -m
stdout SYNOPSIS
# completion
exec tablizer --completion bash
stdout __tablizer_init_completion
# use config (configures colors, but these are not being used, since
# this env doesn't support it, but at least it should succeed.
exec tablizer -f config.hcl -r testtable.txt Runn
stdout Runn
# will be automatically created in work dir
-- testtable.txt --
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
-- config.hcl --
BG = "lightGreen"
FG = "white"
HighlightBG = "lightGreen"
HighlightFG = "white"
NoHighlightBG = "white"
NoHighlightFG = "lightGreen"
HighlightHdrBG = "red"
HighlightHdrFG = "white"

26
t/test-csv.txtar Normal file
View File

@@ -0,0 +1,26 @@
# reading from file and matching with lowercase words
exec tablizer -c name,status -r testtable.csv -s,
stdout grafana.*Runn
# matching mixed case
exec tablizer -c NAME,staTUS -r testtable.csv -s,
stdout grafana.*Runn
# matching using numbers
exec tablizer -c 1,3 -r testtable.csv -s,
stdout grafana.*Runn
# matching using regex
exec tablizer -c 'na.*,stat.' -r testtable.csv -s,
stdout grafana.*Runn
# will be automatically created in work dir
-- testtable.csv --
NAME,READY,STATUS,RESTARTS,AGE
alertmanager-kube-prometheus-alertmanager-0,2/2,Running,35 (45m ago),11d
grafana-fcc54cbc9-bk7s8,1/1,Running,17 (45m ago),1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7,1/1,Running,17 (45m ago),1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f,1/1,Running,20 (45m ago),45m
kube-prometheus-node-exporter-bfzpl,1/1,Running,17 (45m ago),54s

21
t/test-filtering.txtar Normal file
View File

@@ -0,0 +1,21 @@
# filtering
exec tablizer -r testtable.txt -F name=grafana
stdout grafana.*Runn
# filtering two columns
exec tablizer -r testtable.txt -F name=prometh -F age=1h
stdout blackbox.*Runn
# filtering two same columns
exec tablizer -r testtable.txt -F name=prometh -F name=alert
stdout prometheus-alertmanager.*Runn
# will be automatically created in work dir
-- testtable.txt --
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s

View File

@@ -0,0 +1,25 @@
# reading from file and matching with lowercase words
exec tablizer -c name,status -r testtable.txt
stdout grafana.*Runn
# matching mixed case
exec tablizer -c NAME,staTUS -r testtable.txt
stdout grafana.*Runn
# matching using numbers
exec tablizer -c 1,3 -r testtable.txt
stdout grafana.*Runn
# matching using regex
exec tablizer -c 'na.*,stat.' -r testtable.txt
stdout grafana.*Runn
# will be automatically created in work dir
-- testtable.txt --
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s

View File

@@ -0,0 +1,46 @@
# filtering
# a AND b
exec tablizer -r testtable.txt -H -cspecies invasive imperium
stdout 'namak'
! stdout human
# a AND !b
exec tablizer -r testtable.txt -H -cspecies invasive '/imperium/!'
stdout 'human'
! stdout namak
# a AND !b AND c
exec tablizer -r testtable.txt -H -cspecies peaceful '/imperium/!' planetary
stdout 'kenaha'
! stdout 'namak|heduu|riedl'
# case insensitive
exec tablizer -r testtable.txt -H -cspecies '/REGIONAL/i'
stdout namak
! stdout 'human|riedl|heduu|kenaa'
# case insensitive negated
exec tablizer -r testtable.txt -H -cspecies '/REGIONAL/!i'
stdout 'human|riedl|heduu|kenaa'
! stdout namak
# !a AND !b
exec tablizer -r testtable.txt -H -cspecies '/galactic/!' '/planetary/!'
stdout namak
! stdout 'human|riedl|heduu|kenaa'
# same case insensitive
exec tablizer -r testtable.txt -H -cspecies '/GALACTIC/i!' '/PLANETARY/!i'
stdout namak
! stdout 'human|riedl|heduu|kenaa'
# will be automatically created in work dir
-- testtable.txt --
SPECIES TYPE HOME STAGE SPREAD
human invasive earth brink planetary
riedl peaceful keauna civilized pangalactic
namak invasive namak imperium regional
heduu peaceful iu imperium galactic
kenaha peaceful kohi hunter-gatherer planetary

49
t/test-sort.txtar Normal file
View File

@@ -0,0 +1,49 @@
# sort by name
exec tablizer -r testtable.txt -k 1
stdout '^alert.*\n^grafana.*\n^kube'
# sort by name reversed
exec tablizer -r testtable.txt -k 1 -D
stdout 'kube.*\n^grafana.*\n^alert'
# sort by starts numerically
exec tablizer -r testtable.txt -k 4 -i -c4
stdout '17\s*\n^20\s*\n^35'
# sort by starts numerically reversed
exec tablizer -r testtable.txt -k 4 -i -c4 -D
stdout '35\s*\n^20\s*\n^17'
# sort by age
exec tablizer -r testtable.txt -k 5 -a
stdout '45m\s*\n.*1h44m'
# sort by age reverse
exec tablizer -r testtable.txt -k 5 -a -D
stdout '1h44m\s*\n.*45m'
# sort by time
exec tablizer -r timetable.txt -k 2 -t
stdout '^sel.*\n^foo.*\nbar'
# sort by time reverse
exec tablizer -r timetable.txt -k 2 -t -D
stdout '^bar.*\n^foo.*\nsel'
# will be automatically created in work dir
-- testtable.txt --
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
-- timetable.txt --
NAME TIME
foo 2024-11-18T12:00:00+01:00
bar 2024-11-18T12:45:00+01:00
sel 2024-07-18T12:00:00+01:00

18
t/test-stdin.txtar Normal file
View File

@@ -0,0 +1,18 @@
# reading from stdin and matching with lowercase words
stdin testtable.txt
exec tablizer -c name,status
stdout grafana.*Runn
# reading from -r stdin and matching with lowercase words
stdin testtable.txt
exec tablizer -c name,status -r -
stdout grafana.*Runn
# will be automatically created in work dir
-- testtable.txt --
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s

21
t/test-transpose.txtar Normal file
View File

@@ -0,0 +1,21 @@
# transpose one field
exec tablizer -r testtable.txt -T status -R '/Running/OK/'
stdout grafana.*OK
# transpose two fields
exec tablizer -r testtable.txt -T name,status -R '/alertmanager-//' -R '/Running/OK/'
stdout prometheus-0.*OK
# transpose one field and show one column
exec tablizer -r testtable.txt -T status -R '/Running/OK/' -c name
! stdout grafana.*OK
# will be automatically created in work dir
-- testtable.txt --
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s

View File

@@ -1,45 +0,0 @@
#!/bin/sh
# simple commandline unit test script
t="../tablizer"
fail=0
ex() {
# execute a test, report+exit on error, stay silent otherwise
log="/tmp/test-tablizer.$$.log"
name=$1
shift
echo -n "TEST $name "
$* > $log 2>&1
if test $? -ne 0; then
echo "failed, see $log"
fail=1
else
echo "ok"
rm -f $log
fi
}
# only use files in test dir
cd $(dirname $0)
echo "Executing commandline tests ..."
# io pattern tests
ex io-pattern-and-file $t bk7 testtable
cat testtable | ex io-pattern-and-stdin $t bk7
cat testtable | ex io-pattern-and-stdin-dash $t bk7 -
# same w/o pattern
ex io-just-file $t testtable
cat testtable | ex io-just-stdin $t
cat testtable | ex io-just-stdin-dash $t -
if test $fail -ne 0; then
echo "!!! Some tests failed !!!"
exit 1
fi

6
t/testtable.csv Normal file
View File

@@ -0,0 +1,6 @@
NAME,DURATION
x,10
a,100
z,0
u,4
k,6
1 NAME DURATION
2 x 10
3 a 100
4 z 0
5 u 4
6 k 6

6
t/testtable3 Normal file
View File

@@ -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

4
t/testtable4 Normal file
View File

@@ -0,0 +1,4 @@
ONE TWO
1 4
3 1
5 2

6
t/testtable5 Normal file
View File

@@ -0,0 +1,6 @@
SPECIES TYPE HOME STAGE
human invasive earth brink
riedl peaceful keauna civilized
namak invasive namak imperium
heduu peaceful iu imperium
kenaha peaceful kohi hunter-gatherer

View File

@@ -133,7 +133,7 @@
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
.TH TABLIZER 1 "2023-11-22" "1" "User Commands"
.TH TABLIZER 1 "2025-01-30" "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
@@ -144,41 +144,44 @@ tablizer \- Manipulate tabular output of other programs
.IX Header "SYNOPSIS"
.Vb 2
\& Usage:
\& tablizer [regex] [file, ...] [flags]
\& tablizer [regex,...] [file, ...] [flags]
\&
\& Operational Flags:
\& \-c, \-\-columns string Only show the speficied columns (separated by ,)
\& \-v, \-\-invert\-match select non\-matching rows
\& \-n, \-\-no\-numbering Disable header numbering
\& \-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)
\& \-z, \-\-fuzzy Use fuzzy seach [experimental]
\& \-c, \-\-columns string Only show the speficied columns (separated by ,)
\& \-v, \-\-invert\-match select non\-matching rows
\& \-n, \-\-no\-numbering Disable header numbering
\& \-N, \-\-no\-color Disable pattern highlighting
\& \-H, \-\-no\-headers Disable headers display
\& \-s, \-\-separator string Custom field separator
\& \-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 ,)
\& \-R, \-\-regex\-transposer /from/to/ Apply /search/replace/ regexp to fields given in \-T
\&
\& Output Flags (mutually exclusive):
\& \-X, \-\-extended Enable extended output
\& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-S, \-\-shell Enable shell evaluable output
\& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular
\& \-L, \-\-hightlight\-lines Use alternating background colors for tables
\& \-X, \-\-extended Enable extended output
\& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-S, \-\-shell Enable shell evaluable output
\& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular
\& \-L, \-\-hightlight\-lines Use alternating background colors for tables
\&
\& Sort Mode Flags (mutually exclusive):
\& \-a, \-\-sort\-age sort according to age (duration) string
\& \-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
\& \-a, \-\-sort\-age sort according to age (duration) string
\& \-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
\&
\& Other Flags:
\& \-\-completion <shell> Generate the autocompletion script for <shell>
\& \-f, \-\-config <file> Configuration file (default: ~/.config/tablizer/config)
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for tablizer
\& \-m, \-\-man Display manual page
\& \-V, \-\-version Print program version
\& \-\-completion <shell> Generate the autocompletion script for <shell>
\& \-f, \-\-config <file> Configuration file (default: ~/.config/tablizer/config)
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for tablizer
\& \-m, \-\-man Display manual page
\& \-V, \-\-version Print program version
.Ve
.SH "DESCRIPTION"
.IX Header "DESCRIPTION"
@@ -247,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".
@@ -264,42 +276,77 @@ Sorts timestamps.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly
useful for the developer.
.SS "\s-1PATTERNS\s0"
.IX Subsection "PATTERNS"
You can reduce the rows being displayed by using a regular expression
pattern. The regexp is \s-1PCRE\s0 compatible, refer to the syntax cheat
sheet here: <https://github.com/google/re2/wiki/Syntax>. If you want
to read a more comprehensive documentation about the topic and have
perl installed you can read it with:
.SS "\s-1PATTERNS AND FILTERING\s0"
.IX Subsection "PATTERNS AND FILTERING"
You can reduce the rows being displayed by using one or more regular
expression patterns. The regexp language being used is the one of
\&\s-1GOLANG,\s0 refer to the syntax cheat sheet here:
<https://pkg.go.dev/regexp/syntax>.
.PP
If you want to read a more comprehensive documentation about the
topic and have perl installed you can read it with:
.PP
.Vb 1
\& perldoc perlre
.Ve
.PP
Or read it online: <https://perldoc.perl.org/perlre>.
Or read it online: <https://perldoc.perl.org/perlre>. But please note
that the \s-1GO\s0 regexp engine does \s-1NOT\s0 support all perl regex terms,
especially look-ahead and look-behind.
.PP
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
If you want to supply flags to a regex, then surround it with slashes
and append the flag. The following flags are supported:
.PP
.Vb 1
\& (?MODIFIER)
.Vb 2
\& i => case insensitive
\& ! => negative match
.Ve
.PP
The most important modifiers are:
.PP
\&\f(CW\*(C`i\*(C'\fR ignore case
\&\f(CW\*(C`m\*(C'\fR multiline mode
\&\f(CW\*(C`s\*(C'\fR single line mode
.PP
Example for a case insensitive search:
.PP
.Vb 1
\& kubectl get pods \-A | tablizer "(?i)account"
\& kubectl get pods \-A | tablizer "/account/i"
.Ve
.PP
You can use the experimental fuzzy seach feature by providing the
If you use the \f(CW\*(C`!\*(C'\fR flag, then the regex match will be negated, that
is, if a line in the input matches the given regex, but \f(CW\*(C`!\*(C'\fR is
supplied, tablizer will \s-1NOT\s0 include it in the output.
.PP
For example, here we want to get all lines matching \*(L"foo\*(R" but not
\&\*(L"bar\*(R":
.PP
.Vb 1
\& cat table | tablizer foo \*(Aq/bar/!\*(Aq
.Ve
.PP
This would match a line \*(L"foo zorro\*(R" but not \*(L"foo bar\*(R".
.PP
The flags can also be combined.
.PP
You can also use the experimental fuzzy search feature by providing the
option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search
term, not a regexp.
.PP
Sometimes you want to filter by one or more columns. You can do that
using the \fB\-F\fR option. The option can be specified multiple times and
has the following format:
.PP
.Vb 1
\& fieldname=regexp
.Ve
.PP
Fieldnames (== columns headers) are case insensitive.
.PP
If you specify more than one filter, both filters have to match (\s-1AND\s0
operation).
.PP
These field filters can also be negated:
.PP
.Vb 1
\& fieldname!=regexp
.Ve
.PP
If the option \fB\-v\fR is specified, the filtering is inverted.
.SS "\s-1COLUMNS\s0"
.IX Subsection "COLUMNS"
The parameter \fB\-c\fR can be used to specify, which columns to
@@ -332,6 +379,50 @@ We want to see only the \s-1CMD\s0 column and use a regex for this:
.Ve
.PP
where \*(L"C\*(R" is our regexp which matches \s-1CMD.\s0
.PP
If a column specifier doesn't look like a regular expression, matching
against header fields will be case insensitive. So, if you have a
field with the name \f(CW\*(C`ID\*(C'\fR then these will all match: \f(CW\*(C`\-c id\*(C'\fR, \f(CW\*(C`\-c
Id\*(C'\fR. The same rule applies to the options \f(CW\*(C`\-T\*(C'\fR and \f(CW\*(C`\-F\*(C'\fR.
.SS "\s-1TRANSPOSE FIELDS USING REGEXPS\s0"
.IX Subsection "TRANSPOSE FIELDS USING REGEXPS"
You can manipulate field contents using regular expressions. You have
to tell tablizer which field[s] to operate on using the option \f(CW\*(C`\-T\*(C'\fR
and the search/replace pattern using \f(CW\*(C`\-R\*(C'\fR. The number of columns and
patterns must match.
.PP
A search/replace pattern consists of the following elements:
.PP
.Vb 1
\& /search\-regexp/replace\-string/
.Ve
.PP
The separator can be any valid character. Especially if you want to
use a regexp containing the \f(CW\*(C`/\*(C'\fR character, eg:
.PP
.Vb 1
\& |search\-regexp|replace\-string|
.Ve
.PP
Example:
.PP
.Vb 7
\& cat t/testtable2
\& NAME DURATION
\& x 10
\& a 100
\& z 0
\& u 4
\& k 6
\&
\& cat t/testtable2 | tablizer \-T2 \-R \*(Aq/^\ed/4/\*(Aq \-n
\& NAME DURATION
\& x 40
\& a 400
\& z 4
\& u 4
\& k 4
.Ve
.SS "\s-1OUTPUT MODES\s0"
.IX Subsection "OUTPUT MODES"
There might be cases when the tabular output of a program is way too
@@ -487,7 +578,7 @@ or to submit a patch, please open an issue on github:
.IX Header "LICENSE"
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
.PP
Copyright (c) 2023 by Thomas von Dein
Copyright (c) 2022\-2024 by Thomas von Dein
.PP
This software uses the following \s-1GO\s0 modules:
.IP "repr (https://github.com/alecthomas/repr)" 4

View File

@@ -5,41 +5,44 @@ tablizer - Manipulate tabular output of other programs
=head1 SYNOPSIS
Usage:
tablizer [regex] [file, ...] [flags]
tablizer [regex,...] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-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)
-z, --fuzzy Use fuzzy seach [experimental]
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-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 ,)
-R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
Output Flags (mutually exclusive):
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
-X, --extended Enable extended output
-M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
-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
-a, --sort-age sort according to age (duration) string
-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
Other Flags:
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
-h, --help help for tablizer
-m, --man Display manual page
-V, --version Print program version
=head1 DESCRIPTION
@@ -103,11 +106,20 @@ By default, if a B<pattern> 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
@@ -128,37 +140,67 @@ Sorts timestamps.
Finally the B<-d> option enables debugging output which is mostly
useful for the developer.
=head2 PATTERNS
=head2 PATTERNS AND FILTERING
You can reduce the rows being displayed by using a regular expression
pattern. The regexp is PCRE compatible, refer to the syntax cheat
sheet here: L<https://github.com/google/re2/wiki/Syntax>. If you want
to read a more comprehensive documentation about the topic and have
perl installed you can read it with:
You can reduce the rows being displayed by using one or more regular
expression patterns. The regexp language being used is the one of
GOLANG, refer to the syntax cheat sheet here:
L<https://pkg.go.dev/regexp/syntax>.
If you want to read a more comprehensive documentation about the
topic and have perl installed you can read it with:
perldoc perlre
Or read it online: L<https://perldoc.perl.org/perlre>.
Or read it online: L<https://perldoc.perl.org/perlre>. But please note
that the GO regexp engine does NOT support all perl regex terms,
especially look-ahead and look-behind.
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
If you want to supply flags to a regex, then surround it with slashes
and append the flag. The following flags are supported:
(?MODIFIER)
The most important modifiers are:
C<i> ignore case
C<m> multiline mode
C<s> single line mode
i => case insensitive
! => negative match
Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account"
kubectl get pods -A | tablizer "/account/i"
You can use the experimental fuzzy seach feature by providing the
If you use the C<!> flag, then the regex match will be negated, that
is, if a line in the input matches the given regex, but C<!> is
supplied, tablizer will NOT include it in the output.
For example, here we want to get all lines matching "foo" but not
"bar":
cat table | tablizer foo '/bar/!'
This would match a line "foo zorro" but not "foo bar".
The flags can also be combined.
You can also use the experimental fuzzy search feature by providing the
option B<-z>, in which case the pattern is regarded as a fuzzy search
term, not a regexp.
Sometimes you want to filter by one or more columns. You can do that
using the B<-F> option. The option can be specified multiple times and
has the following format:
fieldname=regexp
Fieldnames (== columns headers) are case insensitive.
If you specify more than one filter, both filters have to match (AND
operation).
These field filters can also be negated:
fieldname!=regexp
If the option B<-v> is specified, the filtering is inverted.
=head2 COLUMNS
The parameter B<-c> can be used to specify, which columns to
@@ -188,6 +230,46 @@ We want to see only the CMD column and use a regex for this:
where "C" is our regexp which matches CMD.
If a column specifier doesn't look like a regular expression, matching
against header fields will be case insensitive. So, if you have a
field with the name C<ID> then these will all match: C<-c id>, C<-c
Id>. The same rule applies to the options C<-T> and C<-F>.
=head2 TRANSPOSE FIELDS USING REGEXPS
You can manipulate field contents using regular expressions. You have
to tell tablizer which field[s] to operate on using the option C<-T>
and the search/replace pattern using C<-R>. The number of columns and
patterns must match.
A search/replace pattern consists of the following elements:
/search-regexp/replace-string/
The separator can be any valid character. Especially if you want to
use a regexp containing the C</> character, eg:
|search-regexp|replace-string|
Example:
cat t/testtable2
NAME DURATION
x 10
a 100
z 0
u 4
k 6
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
NAME DURATION
x 40
a 400
z 4
u 4
k 4
=head2 OUTPUT MODES
There might be cases when the tabular output of a program is way too
@@ -326,6 +408,8 @@ the C<-L> parameter).
Colorization can be turned off completely either by setting the
parameter C<-N> or the environment variable B<NO_COLOR> to a true value.
=head1 BUGS
In order to report a bug, unexpected behavior, feature requests
@@ -336,7 +420,7 @@ L<https://github.com/TLINDEN/tablizer/issues>.
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2023 by Thomas von Dein
Copyright (c) 2022-2024 by Thomas von Dein
This software uses the following GO modules: