Compare commits

...

150 Commits

Author SHA1 Message Date
1658512f07 tests don't work on windows 2025-01-14 12:59:06 +01:00
a282e347ec fix for windows 2025-01-14 12:51:51 +01:00
ab215156ac satisfy linter 2025-01-14 12:44:59 +01:00
675a7dcb52 added prior art, fixes #30 as well 2025-01-14 12:41:35 +01:00
06c7508c39 get rid of lisp interpreter, -R and -F are enough, fixes #30 2025-01-14 12:28:54 +01:00
4a41833b53 added testscript test to test the combination of all tasks 2025-01-14 12:22:35 +01:00
54c9e93301 changed file handling, use -r <file> or nothing to use stdin 2025-01-14 12:21:55 +01:00
d6ec8ae88b fixed transpose error message if count is incorrect 2025-01-14 12:20:55 +01:00
1906c3e43f fix #32: treat header filters case insensitively 2025-01-14 08:42:45 +01:00
cf5ee09805 add transpose stuff to README, bump version 2025-01-14 07:29:31 +01:00
5f8b6872ef added tests, reorganized Parse() by dismantling parsing and processing 2025-01-13 18:45:53 +01:00
a1de6b9d57 added transpose function (-T + -R) 2025-01-12 19:28:52 +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
T.v.Dein
6fccd1287b Feature additions (#11)
* add color table support (using alternating colorization of rows) using new flag `-L`
* add config file support (HCL format) using `~/.config/tablizer/config` or `-f <file>` so the user can customize colors
* removed golang 1.17 support
2023-11-22 14:16:43 +01: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
T.v.Dein
9eadb941da Release v1.0.17 (#9)
* add shortcut -H to --no-headers, it's too cumbersome to type
* added fuzzy search support
* Added basic lisp plugin facilities
* Lisp plugin Addidions:
- added process hook facilities
- added working example lisp plugin for filter hook
- load-path can now be a file as well
- added a couple of lisp helper functions (atoi, split), more may
  follow, see lisplib.go
* linting fixes
2023-10-02 18:15:41 +02:00
T.v.Dein
93800f81c1 release v1.0.16 (#8)
* add shortcut -H to --no-headers, it's too cumbersome to type

* bump version

* add -H to usage

* re-generated

---------

Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-05-03 18:31:28 +02:00
T.v.Dein
a94a4fd5b0 Merge pull request #7 from TLINDEN/development
added --no-headers flag to disable header display in tabular modes
2023-04-21 10:01:19 +02:00
1acbdbc674 added --no-headers flag to disable header display in tabular modes 2023-04-21 09:52:05 +02:00
195f685584 fix release maker 2023-01-23 13:40:50 +01:00
b72a99748f upd Changelog, bump version 2023-01-23 12:38:59 +01:00
3cf9310ef7 -D could not be used together with -a 2023-01-20 13:37:06 +01:00
ceae80c91c fix invalid arg handling (io, stdin) and add tests for this 2023-01-09 12:54:45 +01:00
54add2c801 only upd patch version if any 2022-11-04 20:27:22 +01:00
2d157bf2c0 force uniseg release version, see actions#3396457307/ 2022-11-04 20:22:51 +01:00
6f71a028f0 added licenses 2022-11-04 20:22:40 +01:00
dfc7c2e03e upd dep licenses, upd go modules 2022-11-04 20:10:54 +01:00
c443914222 fix spacing mess 2022-11-03 20:17:02 +01:00
eddd4e4180 add feature request template 2022-11-03 20:12:26 +01:00
0d05505493 Merge branch 'main' into development 2022-11-03 20:09:10 +01:00
T.v.Dein
a461dba10d Merge pull request #6 from TLINDEN/issue-template
Update issue templates
2022-11-03 20:08:42 +01:00
T.v.Dein
ca71f8a572 Update issue templates 2022-11-03 19:59:19 +01:00
60230eb1f6 added show-version target 2022-11-03 19:51:42 +01:00
315e8d5363 fix changelog 2022-11-03 19:30:55 +01:00
88d078a535 fix to be able to run 'make' on systems w/o perl 2022-11-03 19:26:57 +01:00
74ab3a1804 version bump 2022-11-03 13:16:02 +01:00
2d8614fa0f demo gif 2022-11-03 13:11:09 +01:00
c8bad4df1a check completion errors 2022-11-02 14:33:48 +01:00
335b2665f2 turned completion subcommand into option 2022-11-01 11:40:36 +01:00
8552270a68 added completion support 2022-10-31 16:19:12 +01:00
6f49b76607 added demo generator 2022-10-27 19:24:22 +02:00
4653eaca09 added demo generator 2022-10-27 19:23:48 +02:00
722eea7e7b add asciicast demo 2022-10-27 19:22:36 +02:00
304f2182ac js doesnt work, try image 2022-10-27 19:18:09 +02:00
73908b1661 added ascii cast 2022-10-27 19:16:41 +02:00
105ba96757 -A was not implemented, oops! 2022-10-27 18:38:46 +02:00
0681f67bc6 fix release link 2022-10-26 12:38:08 +02:00
066ddd0d98 re-organized pattern matching code 2022-10-25 18:34:28 +02:00
417faf3ff2 fixed #5, colorization now always works as expected 2022-10-25 14:24:05 +02:00
001021dac8 Workaround for issue#3: text containing tag like content is not colorized properly. 2022-10-24 17:55:31 +02:00
5c42f7ab9a check pattern on startup 2022-10-24 14:49:23 +02:00
5e65726cb0 cleanup 2022-10-24 14:05:50 +02:00
138ae51936 added CSV output mode, enhanced parser tests 2022-10-23 16:57:30 +02:00
b5c802403b added CSV parsing support, enabled with -s 2022-10-22 12:27:33 +02:00
e54435c2e4 added support for environment variables 2022-10-22 10:21:39 +02:00
975510c86a using enum modeflags, use my own usage template, generated from manpage so I don't have to maintain it twice, it's also nicer 2022-10-21 10:21:07 +02:00
9dd2a49d9b adapted version generation to cfg module, added and fixed unit tests 2022-10-19 19:32:41 +02:00
90872e0c60 fix linter errors 2022-10-19 12:57:50 +02:00
baac74eb47 yaml fix 2022-10-19 12:51:54 +02:00
360dd28e20 add linter 2022-10-19 12:50:20 +02:00
1e36c148ff get rid of global variables, makes testing way easier 2022-10-19 12:44:19 +02:00
399620de98 Added so we can use struct holding all configuration 2022-10-19 12:43:54 +02:00
5d10875a3f fix pointer bug, mockdata have been overwritten by go test everytime,
now use a const struct via func.
2022-10-18 19:45:09 +02:00
4481f59eda no need to return string when using io.Writer anyway 2022-10-17 23:40:53 +02:00
1b2f51dcaf Changed print funcs to use an io.Writer, reimplemented print tests 2022-10-17 20:04:05 +02:00
0d6de3fe5b add-fixes 2022-10-16 19:48:12 +02:00
ec23ae2e76 fix todo 2022-10-16 19:45:46 +02:00
76930ab45a added yaml output mode support (-o yaml or -Y) 2022-10-16 19:44:26 +02:00
a77e4dbc5a added NumberizeHeaders() unit test 2022-10-16 16:37:47 +02:00
9305f48639 added target to execute a single unit test manually 2022-10-16 16:37:26 +02:00
da276a1b50 replaced github.com/xhit/go-str2duration with my own func + tests 2022-10-16 15:30:34 +02:00
dfd3ab9b77 fixed version generation 2022-10-15 19:46:03 +02:00
d53b32b95e updated Changelog 2022-10-15 19:37:30 +02:00
3edbd53ef8 bump version 2022-10-15 19:33:07 +02:00
9c49b78593 added unit test + docs for the various sort modes. 2022-10-15 19:31:42 +02:00
ca87c339b0 added support to sort by time, duration, numerical 2022-10-15 17:05:15 +02:00
fd74628259 added unit test for descending order, fixed deduplication bug 2022-10-15 16:27:04 +02:00
839f33a7fc unit test missing 2022-10-15 14:25:38 +02:00
ebd391df63 added -D to alter sort order to descending order (default: ascending) 2022-10-15 14:24:43 +02:00
752406815c catch incomplete rows and fill them with empty string[s] 2022-10-15 14:15:36 +02:00
4ec6ccd0fd added support for regexp in -c parameter, added deduplication as well 2022-10-15 14:03:30 +02:00
aef545d51e added open todo items I still had on the list 2022-10-15 14:02:46 +02:00
3249e1719f some rewording of the NCOC and turn TODO into a md file 2022-10-15 14:02:09 +02:00
f830cc6256 updated 2022-10-14 19:56:55 +02:00
7e01d54b08 added. 2022-10-14 19:52:02 +02:00
487ba6253d Introduced changelog. 2022-10-14 19:51:45 +02:00
745d15b459 Made corrections to satisfy linter. 2022-10-14 19:51:19 +02:00
8e2ba58ddb added -k parameter to sort by columns 2022-10-13 18:56:34 +02:00
6eedb60a6a minor update to current state 2022-10-11 18:50:58 +02:00
81fac864f1 using gh for release generation, fixed mkrel.sh to add version 2022-10-11 18:42:10 +02:00
e868b50c0f Merge branch 'development' 2022-10-11 18:34:11 +02:00
b9ed7d8cb7 fixed -X output in combination with -c 2022-10-11 13:47:34 +02:00
6ae4a1b6d9 added test for -X output 2022-10-11 09:11:33 +02:00
f890596b4c added pattern highlighting support 2022-10-10 20:14:51 +02:00
22ee24cfdf Merge branch 'development' 2022-10-06 20:05:20 +02:00
34e2b8d855 fixed issuer #4, version string missing, and added some docs about pattern syntax 2022-10-06 20:02:40 +02:00
196833ed3c Merge branch 'development' 2022-10-05 19:17:34 +02:00
85277bbf5e more refactoring, fixed bug in shell mode output, fixed default
Separator and fixed #3
2022-10-05 16:43:51 +02:00
26e50cf908 fix #1: use scanner.Split() instead of splitting by header position
boundaries, since this splitting cuts utf-8 chars which causes
distorted output.
2022-10-05 12:55:33 +02:00
5be18e27c9 Merge branch 'development' 2022-10-05 09:20:02 +02:00
2c410e1cb3 added -v flag, replace 'help' subcommand wich -m, more tests 2022-10-05 09:12:46 +02:00
1b622284a1 Merge branch 'development' 2022-10-04 16:14:04 +02:00
404481c3dc fixed badge uri and workflow name 2022-10-04 16:12:12 +02:00
15f437314a Merge branch 'main' of github.com:TLINDEN/tablizer 2022-10-04 16:08:34 +02:00
3746c7f326 Merge branch 'development' 2022-10-04 16:06:59 +02:00
b7b638636d removed circleci, added badge 2022-10-04 16:05:02 +02:00
dd13300c8b add manpage to source (regen @ make) 2022-10-04 15:25:22 +02:00
a59a6cb7d8 fixed actions 2022-10-04 15:15:14 +02:00
d7ea0017b7 moved again 2022-10-04 15:14:21 +02:00
09dc1f3e60 mv 2022-10-04 15:12:55 +02:00
43dc4ff031 added dev version, changed go namespace, added inline manpage 2022-10-04 15:09:13 +02:00
4596d9d589 added gh workflow 2022-10-04 15:08:49 +02:00
55 changed files with 5148 additions and 533 deletions

View File

@@ -1,38 +0,0 @@
---
version: 2.1
jobs:
compile:
docker:
- image: cimg/go:1.18
steps:
- checkout
- run: make
test:
parameters:
go_version:
type: string
run_test:
type: boolean
default: true
docker:
- image: cimg/go:<< parameters.go_version >>
steps:
- checkout
- run: make test
workflows:
version: 2
unit-test:
jobs:
- compile
- test:
name: testing
matrix:
parameters:
go_version:
- "1.16"
- "1.17"
- "1.18"
- "1.19"

31
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: "[bug-report]"
labels: bug
assignees: TLINDEN
---
**Description**
<!-- Please provide a clear and concise description of the issue: -->
**Steps To Reproduce**
<!-- Please detail the steps to reproduce the behavior: -->
**Expected behavior**
<!-- What do you expected to happen instead? -->
**Version information**
<!--
Please provide as much version information as possible:
- if you have just installed a binary, provide the output of: tablizer --version
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->
**Additional informations**

View File

@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest a feature
title: "[feature-request]"
labels: feature-request
assignees: TLINDEN
---
**Description**
<!-- Please provide a clear and concise description of the feature you desire: -->
**Version information**
<!--
Just in case the feature is already present, please provide as
much version information as possible:
- if you have just installed a binary, provide the output of: tablizer --version
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->

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"

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

@@ -0,0 +1,40 @@
name: build-and-test-tablizer
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
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 ${{ matrix.version }}
uses: actions/setup-go@v5
with:
go-version: '${{ matrix.version }}'
id: go
- name: checkout
uses: actions/checkout@v4
- name: build
run: make
- name: test
run: make test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.22
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true

3
.gitignore vendored Normal file
View File

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

278
CHANGELOG.md Normal file
View File

@@ -0,0 +1,278 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
## [v1.0.14](https://github.com/TLINDEN/tablizer/tree/v1.0.14) - 2023-01-23
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.13...v1.0.14)
### Fixed
- The -D parameter could not be used together with -a.
- Fixed invalid argv handling: when the user wanted to read from stdin
but gave an argument which was meant as a pattern, but also existed
as a filename, then tablizer opened the file, ignored stdin.
- Makefile indentation
### Added
- added licens notes about dependencies
- using hard coded uniseq version, see actions#3396457307
- updated dependencies (go module versions)
## [v1.0.13](https://github.com/TLINDEN/tablizer/tree/v1.0.13) - 2022-11-03
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.12...v1.0.13)
### Added
- Added command line flag to generate shell completion code
- Added an animated demo gif to the README to demonstrate the tool
### Fixed
- The `-A` flag wasn't implemented (default output mode).
- Fixed building from source on systems w/o perls pod tools,
which is not requrired anyway since I always commit the latest
manpage.
## [v1.0.12](https://github.com/TLINDEN/tablizer/tree/v1.0.12) - 2022-10-25
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.11...v1.0.12)
### Added
- Added support to parse CSV input
- Added CSV output support
- Added support for environment variables
### Changed
- We do not use the generated help message anymore, instead we use the
usage from the manpage, which we have to maintain anyway. It looks
better and has flag groups, which cobra is still lacking as of this
writing.
- More refactoring and re-organization, runtime configuration now
lives in the cfg module.
### Fixed
- Fixed [Bug #5](https://github.com/TLINDEN/tablizer/issues/5), where
matches have not been highlighted correctly in some rare cases.
## [v1.0.11](https://github.com/TLINDEN/tablizer/tree/v1.0.11) - 2022-10-19
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.10...v1.0.11)
### Added
- Added CI job golinter to regularly check for common mistakes.
- Added YAML output mode.
- Added more unit tests, we're over 95% in the lib module.
### Changed
- do not use any global variables anymore, makes the code easier to
maintain, understand and test
- using io.Writer in print* functions, which is easier to test, also
re-implemented the print tests.
- replaced go-str2duration with my own implementation `duration2int()`.
## [v1.0.10](https://github.com/TLINDEN/tablizer/tree/v1.0.10) - 2022-10-15
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.9...v1.0.10)
### Added
- Added various sort modes: sort by time, by duration, numerical (-a -t -i)
- Added possibility to modify sort order to descending (-D)
- Added support to specify a regexp in column selector -c, which can
also be mixed with numerical column spec
- More unit tests
### Fixed
- Column specification allowed to specify duplicate columns like `-c
1,2,1,2` unchecked. Now this list will be deduplicated before use.
## [v1.0.9](https://github.com/TLINDEN/tablizer/tree/v1.0.9) - 2022-10-14
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.8...v1.0.9)
### Added
- Added Changelog, Contribution guidelines and no COC.
### Changed
- some minor changes to satisfy linter.
## [v1.0.8](https://github.com/TLINDEN/tablizer/tree/v1.0.8) - 2022-10-13
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.7...v1.0.8)
### Added
- Added sort support with the new parameter -k (like sort(1)).
## [v1.0.7](https://github.com/TLINDEN/tablizer/tree/v1.0.7) - 2022-10-11
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.6...v1.0.7)
### Added
- Added pattern highlighting support.
- Added more unit tests.
### Fixed
- Fixed extended more output in combination with -c.
- Fixed issue #4, the version string was missing.
## [v1.0.6](https://github.com/TLINDEN/tablizer/tree/v1.0.6) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.5...v1.0.6)
### Added
- Added documentation about regexp syntax in the manpage.
- Added more unit tests.
### Changed
- Rewrote the input parser.
- Some more refactoring work has been done.
## [v1.0.5](https://github.com/TLINDEN/tablizer/tree/v1.0.5) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.4...v1.0.5)
### Added
- A new option has been added: --invert-match -v which behaves like
the same option in grep(1): it inverts the pattern match.
- A few more unit tests have been added.
### Fixed
- Pattern matching did not work, because the (new) help subcommand
lead to cobra taking care of the first arg to the program
(argv[1]). So now there's a new parameter -m which displays the
manpage and no more subcommands.
## [v1.0.4](https://github.com/TLINDEN/tablizer/tree/v1.0.4) - 2022-10-04
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.3...v1.0.4)
### Added
- Development version of the compiled binary now uses git vars
in addition to program version.
- Added an option to display the manual page (compiled in) as text:
--help, for cases where a user just installed the binary.
### Changed
- Fixed go module namespace.
## [v1.0.3](https://github.com/TLINDEN/tablizer/tree/v1.0.3) - 2022-10-03
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.2...v1.0.3)
### Added
- Added a new output mode: shell mode, which allows the user
to use the output in a shell eval loop to further process
the data.
### Changed
- More refactoring work has been done.
## [v1.0.2](https://github.com/TLINDEN/tablizer/tree/v1.0.2) - 2022-10-02
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.1...v1.0.2)
### Added
- Added some basic unit tests.
### Changed
- Code has been refactored to be more efficient.
- Replaced table generation code with Tablewriter.
## [v1.0.1](https://github.com/TLINDEN/tablizer/tree/v1.0.1) - 2022-09-30
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.0...v1.0.1)
### Added
- Added a unix manual page.
- Added release builder to Makefile
### Changed
- Various minor fixes.
## [v1.0.0](https://github.com/TLINDEN/tablizer/tree/v1.0.0) - 2022-09-28
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/02a64a5c3fe4220df2c791ff1421d16ebd428c19...v1.0.0)
Initial release.

113
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,113 @@
# No Code of Conduct
*TL;DR:* This project does **NOT** have a so called Code of Conduct,
nor will it ever have one.
## The Rant
The reasons are somewhat complicated and I'll try my best to document
them here.
Ethical codes or rules come along like laws. But how is ethical or
moral behavior defined? And who defines which behavior is ethical and
which is not? Certainly not me.
Unless you live in a dictatorship (and more than half of the
population of planet earth do as of this writing), laws come into
existence by democratic procedures. Laws cover almost every aspect of
live in a society. Laws allow and forbid behavior and laws sanction
infringements.
A software project like this one on the other hand is not a society.
There are not enough people involved to form democratic
structures. And there will always be a minority of users who have the
right to commit or reject code. How could any maintainer of a software
project dare to decree rules upon others? Actually, am I, the current
maintainer of this very project authorized to do so?
I think the anser to this question clearly is NO.
The issue is being complicated by the fact, that open source
development these days happens on planetary scale. And this planet
houses hundreds if not thousands of different cultures, philosophies,
ideologies and worldviews. The answer to many ethical questions will
in most cases vague and nebulous.
Ones joke will always be another ones insult.
Then there is the problem of language. I myself am not an english
native, but I publish everyting using the english language. I am able
to communicate with most people in the open source community because
of that. But I am certainly not able to understand everything and
everyone. There might be nuances to a sentence I don't sense, there
might be sarcastic connotations I don't understand or references to
historical figures, events or traditions I don't know and never have
heard of.
Judging over other peoples online behavior looks like a titanic task
to me. It is just not my job to judge others, I am not legitimized or
authorized to do so and I am not interested in this kind of business.
Another huge problem with ethical rules is that you need to outline
and enforce sanctions on those who violate the rules. But since I am
not an elected authority how would I be able to do this? I don't
know. And what happens if someone complains about myself? Shall I
remove myself from my own project? Come on!
Last but not least there's the law. So, let's say someone in india
says something insulting to some other developer in an issue. Of
course german law does not apply to indian people. More, the insult
might actually not be an insult in india. In the end, nothing would
happen. Under normal circumstances, maintainers would delete the
posting, ban the user or remove push privileges etc.
But then, is there a way for the offending user to defend himself? Of
course not, since neither indian or german law alone applies. I cannot
go to a german court and sue the guy and he cannot do the same in
india. Or - we possibly could but the judges on both countries would
just laugh and close the case.
That being said, I don't have the power nor the tools, nor the
authority to enforce serious sanctions of any meaningful kind against
others. Therefore I cannot outline any rules whatsoever.
And let's not even start talking about there undemocratic "comitees"
many projects are forming to circumvent this problem. Some projects
even include external entities like a lawer or some bureaucrat
somewhere just to have the ability to complain against a comitee
member. What a mess!
## So, which are the ethical rules within this project then?
Well, there are none.
This project is about code, not society. It doesn't matter where you
come from, how you look, how you think, what you believe, who your
friends are, whay you said or did sometime in the past. I don't even
care if you are a human being. You are an alien so bored that you need
to submit code on github? Fine with me. You're a convicted criminal? I
don't give a shit!
**The only thing I am interested here is Code and only Code.**
So if anyhing happens here I don't like or I am obliged by law to act
on, I will decide on a case to case basis what to do. And
unfortunately, since this is the nature of a github project, you
cannot complain, object or protest. I am very sorry!
If you will, let's at least outline these:
- Please - just please - behave towards others as you'd expect others
to behave towards yourself.
- Don't judge others for any reason.
- Only judge the code.
But these are not rules, only a friendly appeal to you as a developer
and user.
Thanks a lot!

94
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,94 @@
## Project Goals
The goal of this project is to build a small tool which helps in day
to day work with tabular output of various commandline programs. It
should be small, fast and easy to understand. The idea is to replace
multiline shell pipes using awk, sed and grep with just one
binary.
There will be no GUI, no web interface, no public API of some sort, no
builtin interpreter.
The programming language used for this project will always be
[GOLANG](https://go.dev/) with the exception of the documentation
([Perl POD](https://perldoc.perl.org/perlpod)) and the Makefile.
# Contributing
You can contribute to this project in various ways:
## Open an issue
If you encounter a problem or don't understand how the program works
or if you think the documentation is unclear, please don't hesitate to
open an issue.
Please add as much information about the case as possible, such as:
- Your environment (operating system etc)
- tablizer version (`tablizer --version`)
- Input data. Please replace sensitive information with mock data!
- Actual program output.
- Expected program output.
- Error message - if any.
Be aware that I am working on this (and some other) project in my
spare time which is scarce. Therefore please don't expect me to
respond to your query within hours or even days. Be patient, but I
WILL respond.
## Pull Requests
Code and documentation help is always much appreciated! Please follow
thes guidelines to successfully contribute:
- Every pull request shall be based on latest `development`
branch. `main` is only used for releases.
- Execute the unit tests before committing: `make test`. There shall
be no errors.
- Strive to be backwards compatible so that users who are already
using the program don't have to change their habits - unless it is
really neccessary.
- Try to add a unit test for your addition.
- Don't ever change existing unit tests!
- Add a meaningful and comprehensive rationale about your contribution:
- Why do you think it might be useful for others?
- What did you actually change or add?
- Is there an open issue which this PR fixes and if so, please link
to that issue.
- [Re-]format your code with `gofmt -s`.
- Avoid unneccesary dependencies, especially for very small functions.
- **If** a new dependency is being added, it must be compatible with
our [license agreement](LICENSE).
- You need to accept that the code or documentation you contribute
will be redistributed under the terms of said license agreement. If
your contribution is considerably large or if you contribute
regularly, then feel free to add your name and if you want your
email address to the *AUTHORS* section of the
[manpage](tablizer.pod).
- Adhere to the above mentioned project goals.
- If you are unsure if your addition or change will be accepted,
better ask before starting coding. Open an issue about your proposal
and let's discuss it! That way we avoid doing unnessesary work on
both sides.
Each pull request will be carefully reviewed and if it is a useful
addition it will be accepted. However, please be prepared that
sometimes a PR will be rejected. The reasons may vary and will be
documented. Perhaps the above guidelines are not matched, or the
addition seems to be not so useful from my perspective, maybe there
are too much changes or there might be changes I don't even
understand.
But whatever happens: your contribution is always welcome!

View File

@@ -17,23 +17,44 @@
# #
# no need to modify anything below # no need to modify anything below
tool = tablizer tool = tablizer
version = $(shell egrep "^var Version = " lib/common.go | cut -d'=' -f2 | cut -d'"' -f 2) version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local PREFIX = /usr/local
UID = root UID = root
GID = 0 GID = 0
BRANCH = $(shell git branch --show-current)
COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
HAVE_POD := $(shell pod2text -h 2>/dev/null)
all: buildlocal $(tool).1 all: $(tool).1 cmd/$(tool).go buildlocal
%.1: %.pod %.1: %.pod
ifdef HAVE_POD
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1 pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
endif
cmd/%.go: %.pod
ifdef HAVE_POD
echo "package cmd" > cmd/$*.go
echo >> cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go
pod2text $*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go
echo "var usage = \`" >> cmd/$*.go
awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> cmd/$*.go
echo "\`" >> cmd/$*.go
endif
buildlocal: buildlocal:
go build go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"
release: release:
./mkrel.sh $(tool) $(version) ./mkrel.sh $(tool) $(version)
gh release create $(version) --generate-notes releases/*
install: buildlocal install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin install -d -o $(UID) -g $(GID) $(PREFIX)/bin
@@ -42,7 +63,38 @@ install: buildlocal
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean: clean:
rm -rf $(tool) $(tool).1 releases rm -rf $(tool) releases coverage.out
test: test:
go test -v ./... go test ./... $(OPTS)
singletest:
@echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'"
go test -run $(TEST) github.com/tlinden/tablizer/$(MOD)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
show-versions: buildlocal
@echo "### tablizer version:"
@./tablizer --version
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
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

158
README.md
View File

@@ -1,4 +1,6 @@
[![<ORG_NAME>](https://circleci.com/gh/TLINDEN/tablizer.svg?style=svg)](https://app.circleci.com/pipelines/github/TLINDEN/tablizer) [![Actions](https://github.com/tlinden/tablizer/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/tablizer/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/tablizer/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/tablizer)](https://goreportcard.com/report/github.com/tlinden/tablizer)
## tablizer - Manipulate tabular output of other programs ## tablizer - Manipulate tabular output of other programs
@@ -6,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 programs. While you could do this using standard unix tools, in some
cases it's a hard job. 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: Let's take this output:
``` ```
% kubectl get pods -o wide % kubectl get pods -o wide
@@ -19,13 +64,13 @@ But you're only interested in the NAME and STATUS columns. Here's how
to do this with tablizer: to do this with tablizer:
``` ```
% kubectl get pods | ./tablizer % kubectl get pods | tablizer
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-m48n8 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 repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
% kubectl get pods | ./tablizer -c 1,3 % kubectl get pods | tablizer -c 1,3
NAME(1) STATUS(3) NAME(1) STATUS(3)
repldepl-7bcd8d5b64-7zq4l Running repldepl-7bcd8d5b64-7zq4l Running
repldepl-7bcd8d5b64-m48n8 Running repldepl-7bcd8d5b64-m48n8 Running
@@ -33,27 +78,28 @@ repldepl-7bcd8d5b64-q2bf4 Running
``` ```
Another use case is when the tabular output is so wide that lines are Another use case is when the tabular output is so wide that lines are
being broken and the whole output is completely distorted. In such a being broken and the whole output is completely distorted. In such a
case you can use the `-x` flag to get an output similar to `\x` in `psql`: case you can use the `-o extended | -X` flag to get an output similar
to `\x` in `psql`:
``` ```
% kubectl get pods | ./tablizer -x % kubectl get pods | tablizer -X
NAME: repldepl-7bcd8d5b64-7zq4l NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
NAME: repldepl-7bcd8d5b64-m48n8 NAME: repldepl-7bcd8d5b64-m48n8
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
NAME: repldepl-7bcd8d5b64-q2bf4 NAME: repldepl-7bcd8d5b64-q2bf4
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
``` ```
@@ -62,17 +108,50 @@ Tablize can read one or more files or - if none specified - from STDIN.
You can also specify a regex pattern to reduce the output: You can also specify a regex pattern to reduce the output:
``` ```
% kubectl get pods | ./tablizer q2bf4 % kubectl get pods | tablizer q2bf4
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m 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)
## Installation ## Installation
There are multiple ways to install **tablizer**: There are multiple ways to install **tablizer**:
- Go to the [latest release page](https://github.com/muesli/mango/releases/latest), - Go to the [latest release page](https://github.com/tlinden/tablizer/releases/latest),
locate the binary for your operating system and platform. locate the binary for your operating system and platform.
Download it and put it into some directory within your `$PATH` variable. Download it and put it into some directory within your `$PATH` variable.
@@ -105,6 +184,10 @@ https://github.com/TLINDEN/tablizer/blob/main/tablizer.pod
Or if you cloned the repository you can read it this way (perl needs Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc tablizer.pod`. to be installed though): `perldoc tablizer.pod`.
If you have the binary installed, you can also read the man page with
this command:
tablizer --man
## Getting help ## Getting help
@@ -115,6 +198,41 @@ In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github: or to submit a patch, please open an issue on github:
https://github.com/TLINDEN/tablizer/issues. 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 ## Copyright and license
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3. This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.

1
TODO
View File

@@ -1 +0,0 @@

8
TODO.md Normal file
View File

@@ -0,0 +1,8 @@
## Fixes to be implemented
## Features to be implemented
- add comment support (csf.NewReader().Comment = '#')
- add --no-headers option

417
cfg/config.go Normal file
View File

@@ -0,0 +1,417 @@
/*
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
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 cfg
import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"github.com/gookit/color"
"github.com/hashicorp/hcl/v2/hclsimple"
)
const DefaultSeparator string = `(\s\s+|\t)`
const Version string = "v1.3.0"
const MAXPARTS = 2
var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
var VERSION string // maintained by -x
// public config, set via config file or using defaults
type Settings struct {
FG string `hcl:"FG"`
BG string `hcl:"BG"`
HighlightFG string `hcl:"HighlightFG"`
HighlightBG string `hcl:"HighlightBG"`
NoHighlightFG string `hcl:"NoHighlightFG"`
NoHighlightBG string `hcl:"NoHighlightBG"`
HighlightHdrFG string `hcl:"HighlightHdrFG"`
HighlightHdrBG string `hcl:"HighlightHdrBG"`
}
type Transposer struct {
Search regexp.Regexp
Replace string
}
// internal config
type Config struct {
Debug bool
NoNumbering bool
NoHeaders bool
Columns string
UseColumns []int
Separator string
OutputMode int
InvertMatch bool
Pattern string
PatternR *regexp.Regexp
UseFuzzySearch bool
UseHighlight bool
SortMode string
SortDescending bool
SortByColumn int
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
see https://github.com/gookit/color.
*/
ColorStyle color.Style
HighlightStyle color.Style
NoHighlightStyle color.Style
HighlightHdrStyle color.Style
NoColor bool
// config file, optional
Configfile string
Settings Settings
// used for field filtering
Rawfilters []string
Filters map[string]*regexp.Regexp
// -r <file>
InputFile string
}
// maps outputmode short flags to output mode, ie. -O => -o orgtbl
type Modeflag struct {
X bool
O bool
M bool
S bool
Y bool
A bool
C bool
}
// used for switching printers
const (
Extended = iota + 1
Orgtbl
Markdown
Shell
Yaml
CSV
ASCII
)
// various sort types
type Sortmode struct {
Numeric bool
Time bool
Age bool
}
// default color schemes
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,
"hlbg": color.BgGray, "hlfg": color.FgWhite,
},
color.Level256: {
"bg": color.BgLightGreen, "fg": color.FgWhite,
"hlbg": color.BgLightBlue, "hlfg": color.FgWhite,
},
color.LevelRgb: {
"bg": color.BgLightGreen, "fg": color.FgWhite,
"hlbg": color.BgHiGreen, "hlfg": color.FgWhite,
"nohlbg": color.BgWhite, "nohlfg": color.FgLightGreen,
"hdrbg": color.BgBlue, "hdrfg": color.FgWhite,
},
}
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(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(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(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(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(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(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(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 (conf *Config) DetermineColormode() {
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
colors := conf.Colors()
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()
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 {
return fmt.Sprintf("This is tablizer version %s", VERSION)
}
func (conf *Config) PrepareSortFlags(flag Sortmode) {
switch {
case flag.Numeric:
conf.SortMode = "numeric"
case flag.Age:
conf.SortMode = "duration"
case flag.Time:
conf.SortMode = "time"
default:
conf.SortMode = "string"
}
}
func (conf *Config) PrepareModeFlags(flag Modeflag) {
switch {
case flag.X:
conf.OutputMode = Extended
case flag.O:
conf.OutputMode = Orgtbl
case flag.M:
conf.OutputMode = Markdown
case flag.S:
conf.OutputMode = Shell
case flag.Y:
conf.OutputMode = Yaml
case flag.C:
conf.OutputMode = CSV
default:
conf.OutputMode = ASCII
}
}
func (conf *Config) PrepareFilters() error {
conf.Filters = make(map[string]*regexp.Regexp, len(conf.Rawfilters))
for _, filter := range conf.Rawfilters {
parts := strings.Split(filter, "=")
if len(parts) != MAXPARTS {
return errors.New("filter field and value must be separated by =")
}
reg, err := regexp.Compile(parts[1])
if err != nil {
return fmt.Errorf("failed to compile filter regex for field %s: %w",
parts[0], err)
}
conf.Filters[strings.ToLower(strings.ToLower(parts[0]))] = reg
}
return nil
}
// 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))
}
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(pattern string) error {
PatternR, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("regexp pattern %s is invalid: %w", conf.Pattern, err)
}
conf.PatternR = PatternR
conf.Pattern = pattern
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
}
// translate color string to internal color value
func ColorStringToColor(colorname string) color.Color {
for name, color := range color.FgColors {
if name == colorname {
return color
}
}
for name, color := range color.ExFgColors {
if name == colorname {
return color
}
}
return color.Normal
}
// same, for background colors
func ColorStringToBGColor(colorname string) color.Color {
for name, color := range color.BgColors {
if name == colorname {
return color
}
}
for name, color := range color.ExBgColors {
if name == colorname {
return color
}
}
return color.Normal
}

104
cfg/config_test.go Normal file
View File

@@ -0,0 +1,104 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cfg
import (
"fmt"
// "reflect"
"testing"
)
func TestPrepareModeFlags(t *testing.T) {
var tests = []struct {
flag Modeflag
expect int // output (constant enum)
}{
// short commandline flags like -M
{Modeflag{X: true}, Extended},
{Modeflag{S: true}, Shell},
{Modeflag{O: true}, Orgtbl},
{Modeflag{Y: true}, Yaml},
{Modeflag{M: true}, Markdown},
{Modeflag{}, ASCII},
}
// FIXME: use a map for easier printing
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareModeFlags-expect-%d", testdata.expect)
t.Run(testname, func(t *testing.T) {
conf := Config{}
conf.PrepareModeFlags(testdata.flag)
if conf.OutputMode != testdata.expect {
t.Errorf("got: %d, expect: %d", conf.OutputMode, testdata.expect)
}
})
}
}
func TestPrepareSortFlags(t *testing.T) {
var tests = []struct {
flag Sortmode
expect string // output
}{
// short commandline flags like -M
{Sortmode{Numeric: true}, "numeric"},
{Sortmode{Age: true}, "duration"},
{Sortmode{Time: true}, "time"},
{Sortmode{}, "string"},
}
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareSortFlags-expect-%s", testdata.expect)
t.Run(testname, func(t *testing.T) {
conf := Config{}
conf.PrepareSortFlags(testdata.flag)
if conf.SortMode != testdata.expect {
t.Errorf("got: %s, expect: %s", conf.SortMode, testdata.expect)
}
})
}
}
func TestPreparePattern(t *testing.T) {
var tests = []struct {
pattern string
wanterr bool
}{
{"[A-Z]+", false},
{"[a-z", true},
}
for _, testdata := range tests {
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t",
testdata.pattern, testdata.wanterr)
t.Run(testname, func(t *testing.T) {
conf := Config{}
err := conf.PreparePattern(testdata.pattern)
if err != nil {
if !testdata.wanterr {
t.Errorf("PreparePattern returned error: %s", err)
}
}
})
}
}

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 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 it under the terms of the GNU General Public License as published by
@@ -17,61 +17,191 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd package cmd
import ( import (
"daemon.de/tablizer/lib" "bytes"
"errors"
"fmt" "fmt"
"github.com/spf13/cobra" "log"
"os" "os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"github.com/tlinden/tablizer/cfg"
"github.com/tlinden/tablizer/lib"
) )
var rootCmd = &cobra.Command{ func man() {
Use: "tablizer [regex] [file, ...]", man := exec.Command("less", "-")
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
RunE: func(cmd *cobra.Command, args []string) error {
if lib.ShowVersion {
fmt.Printf("This is tablizer version %s\n", lib.Version)
return nil
}
err := lib.PrepareColumns() var buffer bytes.Buffer
if err != nil {
return err
}
err = lib.PrepareModeFlags() buffer.Write([]byte(manpage))
if err != nil {
return err
}
return lib.ProcessFiles(args) man.Stdout = os.Stdout
}, man.Stdin = &buffer
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
log.Fatal(err)
}
}
func completion(cmd *cobra.Command, mode string) error {
switch mode {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
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)
}
} }
func Execute() { func Execute() {
var (
conf cfg.Config
ShowManual bool
ShowVersion bool
ShowCompletion string
modeflag cfg.Modeflag
sortmode cfg.Sortmode
)
var rootCmd = &cobra.Command{
Use: "tablizer [regex] [file, ...]",
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
Run: func(cmd *cobra.Command, args []string) {
if ShowVersion {
fmt.Println(cfg.Getversion())
return
}
if ShowManual {
man()
return
}
if len(ShowCompletion) > 0 {
wrapE(completion(cmd, ShowCompletion))
return
}
// Setup
wrapE(conf.ParseConfigfile())
conf.CheckEnv()
conf.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode)
wrapE(conf.PrepareFilters())
conf.DetermineColormode()
conf.ApplyDefaults()
// actual execution starts here
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().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)")
// 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")
// 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")
// config file
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)")
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")
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
} }
func init() {
rootCmd.PersistentFlags().BoolVarP(&lib.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&lib.NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "v", false, "Print program version")
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
// output flags, only 1 allowed, hidden, since just short cuts
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagShell, "shell", "S", false, "Enable shell mode output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell")
rootCmd.Flags().MarkHidden("extended")
rootCmd.Flags().MarkHidden("orgtbl")
rootCmd.Flags().MarkHidden("markdown")
rootCmd.Flags().MarkHidden("shell")
// same thing but more common, takes precedence over above group
rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)")
}

426
cmd/tablizer.go Normal file
View File

@@ -0,0 +1,426 @@
package cmd
var manpage = `
NAME
tablizer - Manipulate tabular output of other programs
SYNOPSIS
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
DESCRIPTION
Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns or
you may want to filter for some pattern (See PATTERNS) or you may need
the output in another program and need to parse it somehow. Standard
unix tools such as awk(1), grep(1) or column(1) may help, but sometimes
it's a tedious business.
Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by TAB characters. This is
not easy to process.
You can use tablizer to do these and more things.
tablizer analyses the header fields of a table, registers the column
positions of each header field and separates columns by those positions.
Without any options it reads its input from "STDIN", but you can also
specify a file as a parameter. If you want to reduce the output by some
regular expression, just specify it as its first parameter. You may also
use the -v option to exclude all rows which match the pattern. Hence:
# read from STDIN
kubectl get pods | tablizer
# read a file
tablizer filename
# search for pattern in a file (works like grep)
tablizer regex filename
# search for pattern in STDIN
kubectl get pods | tablizer regex
The output looks like the original one but every header field will have
a numer associated with it, e.g.:
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which
columns you want to have in your output (see COLUMNS:
kubectl get pods | tablizer -c1,3
You can specify the numbers in any order but output will always follow
the original order.
The numbering can be suppressed by using the -n option.
By default tablizer shows a header containing the names of each column.
This can be disabled using the -H option. Be aware that this only
affects tabular output modes. Shell, Extended, Yaml and CSV output modes
always use the column names.
By default, if a pattern has been speficied, matches will be
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:
-a --sort-age
Sorts duration strings like "1d4h32m51s".
-i --sort-numeric
Sorts numeric fields.
-t --sort-time
Sorts timestamps.
Finally the -d option enables debugging output which is mostly useful
for the developer.
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: <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:
perldoc perlre
Or read it online: <https://perldoc.perl.org/perlre>.
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
(?MODIFIER)
The most important modifiers are:
"i" ignore case "m" multiline mode "s" single line mode
Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account"
You can 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).
If the option -v is specified, the filtering is inverted.
COLUMNS
The parameter -c can be used to specify, which columns to display. By
default tablizer numerizes the header names and these numbers can be
used to specify which header to display, see example above.
However, beside numbers, you can also use regular expressions with -c,
also separated by comma. And you can mix column numbers with regexps.
Lets take this table:
PID TTY TIME CMD
14001 pts/0 00:00:00 bash
42871 pts/0 00:00:00 ps
42872 pts/0 00:00:00 sed
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
tablizer
sed
where "C" is our regexp which matches CMD.
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.
In such cases the -o extended or -X option can be useful which enables
*extended mode*. In this mode, each row will be printed vertically,
header left, value right, aligned by the field widths. Here's an
example:
kubectl get pods | ./tablizer -o extended
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
RESTARTS: 1 (71m ago)
AGE: 5h28m
You can of course still use a regex to reduce the number of rows
displayed.
The option -o shell can be used if the output has to be processed by the
shell, it prints variable assignments for each cell, one line per row:
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
NAME="repldepl-7bcd8d5b64-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
NAME="repldepl-7bcd8d5b64-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table, yaml, which prints yaml encoding
and CSV mode, which prints a comma separated value file.
ENVIRONMENT VARIABLES
tablizer supports certain environment variables which use can use to
influence program behavior. Commandline flags have always precedence
over environment variables.
<T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n.
<T_COLUMNS> - comma separated list of columns to output, like -c
<NO_COLORS> - disable colorization of matches, like -N
COMPLETION
Shell completion for command line options can be enabled by using the
--completion flag. The required parameter is the name of your shell.
Currently supported are: bash, zsh, fish and powershell.
Detailed instructions:
Bash:
source <(tablizer --completion bash)
To load completions for each session, execute once:
# Linux:
$ tablizer --completion bash > /etc/bash_completion.d/tablizer
# macOS:
$ tablizer --completion bash > $(brew --prefix)/etc/bash_completion.d/tablizer
Zsh:
If shell completion is not already enabled in your environment, you
will need to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions for each session, execute once:
$ tablizer --completion zsh > "${fpath[1]}/_tablizer"
You will need to start a new shell for this setup to take effect.
fish:
tablizer --completion fish | source
To load completions for each session, execute once:
tablizer --completion fish > ~/.config/fish/completions/tablizer.fish
PowerShell:
tablizer --completion powershell | Out-String | Invoke-Expression
To load completions for every new session, run:
tablizer --completion powershell > tablizer.ps1
and source this file from your PowerShell profile.
CONFIGURATION AND COLORS
YOu can put certain configuration values into a configuration file in
HCL format. By default tablizer looks for
"$HOME/.config/tablizer/config", but you can provide one using the
parameter "-f".
In the configuration the following variables can be defined:
BG = "lightGreen"
FG = "white"
HighlightBG = "lightGreen"
HighlightFG = "white"
NoHighlightBG = "white"
NoHighlightFG = "lightGreen"
HighlightHdrBG = "red"
HighlightHdrFG = "white"
The following color definitions are available:
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta,
red, white, yellow
The Variables FG and BG are being used to highlight matches. The other
*FG and *BG variables are for colored table output (enabled with the
"-L" parameter).
Colorization can be turned off completely either by setting the
parameter "-N" or the environment variable NO_COLOR to a true value.
BUGS
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
<https://github.com/TLINDEN/tablizer/issues>.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2022-2024 by Thomas von Dein
This software uses the following GO modules:
repr (https://github.com/alecthomas/repr)
Released under the MIT License, Copyright (c) 2016 Alec Thomas
cobra (https://github.com/spf13/cobra)
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra
Authors
dateparse (github.com/araddon/dateparse)
Released under the MIT License, Copyright (c) 2015-2017 Aaron Raddon
color (github.com/gookit/color)
Released under the MIT License, Copyright (c) 2016 inhere
tablewriter (github.com/olekukonko/tablewriter)
Released under the MIT License, Copyright (c) 201 by Oleku Konko
yaml (gopkg.in/yaml.v3)
Released under the MIT License, Copyright (c) 2006-2011 Kirill
Simonov
AUTHORS
Thomas von Dein tom AT vondein DOT org
`
var usage = `
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
`

12
config.hcl Normal file
View File

@@ -0,0 +1,12 @@
# supported colors:
# black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
# lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
# magenta, red, white, yellow
BG = "lightGreen"
FG = "white"
HighlightBG = "lightGreen"
HighlightFG = "white"
NoHighlightBG = "white"
NoHighlightFG = "lightGreen"
HighlightHdrBG = "red"
HighlightHdrFG = "white"

3
demo/Makefile Normal file
View File

@@ -0,0 +1,3 @@
all:
LC_ALL=en_US.UTF-8 asciinema rec --cols 50 --row 30 -c ./demo.sh --overwrite tmp.cast
agg tmp.cast tmp.gif

31
demo/demo.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
prompt() {
if test -n "$1"; then
echo
echo -n "% $*"
sleep 1
echo
$*
echo
echo -n "% "
else
echo -n "% "
fi
}
PATH=..:$PATH
clear
while IFS=$'\t' read -r flags table msg source _; do
echo "#"
echo "# source tabular data:"
cat $table
echo
echo "#"
echo "# $msg:"
prompt "tablizer $flags $table"
sleep 4
clear
done < <(yq -r tables.yaml \
| yq -r '.tables[] | [.flags, .table, .msg, .source] | @tsv')

4
demo/table.demo1 Normal file
View File

@@ -0,0 +1,4 @@
NAME DURATION COUNT WHEN
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700

3
demo/table.demo2 Normal file
View File

@@ -0,0 +1,3 @@
PID TTY TIME CMD
30912 pts/0 00:00:00 bash
49526 pts/0 00:00:00 ps

54
demo/tables.yaml Normal file
View File

@@ -0,0 +1,54 @@
tables:
# OUTPUTS
- flags: -A
table: table.demo1
msg: default output mode
- flags: -O
table: table.demo1
msg: orgmode output mode
- flags: -M
table: table.demo1
msg: markdown output mode
- flags: -S
table: table.demo1
msg: shell output mode
- flags: -X
table: table.demo1
msg: extended output mode
- flags: -Y
table: table.demo1
msg: yaml output mode
- flags: -C
table: table.demo1
msg: CSV output mode
# SORTS
- flags: -A -k 3
table: table.demo1
msg: sort by column 3
- flags: -A -k 4 -t
table: table.demo1
msg: sort by column 4 and sort type time
- flags: -A -k 2 -a
table: table.demo1
msg: sort by column 2 and sort type duration
# REDUCE
- flags: -A -c 1,3
table: table.demo1
msg: only display column 1 and 3
- flags: -A -c AM,RA
table: table.demo1
msg: only display columns matching /(RA|AM)/
- flags: -X -c 1,3
table: table.demo1
msg: only display column 1 and 3 in extended mode
# SEARCH
- flags: /20 -A
table: table.demo1
msg: only show rows matching /20
- flags: /20 -A -v
table: table.demo1
msg: only show rows NOT matching /20

119
demo/tablizer-demo.cast Normal file
View File

@@ -0,0 +1,119 @@
{"version": 2, "width": 80, "height": 25, "timestamp": 1666890777, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}}
[0.004618, "o", "\u001b[H\u001b[2J\u001b[3J"]
[0.010297, "o", "#\r\n# source tabular data:\r\n"]
[0.010898, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[0.011125, "o", "\r\n#\r\n"]
[0.011177, "o", "# default output mode:\r\n"]
[0.011219, "o", "\r\n% tablizer -A table.demo1"]
[1.011851, "o", "\r\n"]
[1.013635, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nbeta \t1d10h5m1s \t33 \t3/1/2014 \t\r\nalpha \t4h35m \t170 \t2013-Feb-03 \t\r\nceta \t33d12h \t9 \t06/Jan/2008 15:04:05 -0700\t\r\n"]
[1.014021, "o", "\r\n% "]
[5.015241, "o", "\u001b[H\u001b[2J\u001b[3J"]
[5.015339, "o", "#\r\n# source tabular data:\r\n"]
[5.015688, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[5.015776, "o", "\r\n#\r\n# orgmode output mode:\r\n\r\n% tablizer -O table.demo1"]
[6.016322, "o", "\r\n"]
[6.01823, "o", "+---------+-------------+----------+----------------------------+\r\n| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |\r\n+---------+-------------+----------+----------------------------+\r\n| beta | 1d10h5m1s | 33 | 3/1/2014 |\r\n| alpha | 4h35m | 170 | 2013-Feb-03 |\r\n| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |\r\n+---------+-------------+----------+----------------------------+\r\n"]
[6.018497, "o", "\r\n% "]
[10.020014, "o", "\u001b[H\u001b[2J\u001b[3J"]
[10.020112, "o", "#\r\n# source tabular data:\r\n"]
[10.020573, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[10.020643, "o", "\r\n#\r\n"]
[10.02068, "o", "# markdown output mode:\r\n\r\n% tablizer -M table.demo1"]
[11.021559, "o", "\r\n"]
[11.023551, "o", "| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |\r\n|---------|-------------|----------|----------------------------|\r\n| beta | 1d10h5m1s | 33 | 3/1/2014 |\r\n| alpha | 4h35m | 170 | 2013-Feb-03 |\r\n| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |\r\n"]
[11.023838, "o", "\r\n% "]
[15.025244, "o", "\u001b[H\u001b[2J\u001b[3J"]
[15.025345, "o", "#\r\n# source tabular data:\r\n"]
[15.025829, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[15.025915, "o", "\r\n#\r\n# shell output mode:\r\n"]
[15.025931, "o", "\r\n"]
[15.025948, "o", "% tablizer -S table.demo1"]
[16.026714, "o", "\r\n"]
[16.028606, "o", "NAME(1)=\"beta\" DURATION(2)=\"1d10h5m1s\" COUNT(3)=\"33\" WHEN(4)=\"3/1/2014\"\r\nNAME(1)=\"alpha\" DURATION(2)=\"4h35m\" COUNT(3)=\"170\" WHEN(4)=\"2013-Feb-03\"\r\nNAME(1)=\"ceta\" DURATION(2)=\"33d12h\" COUNT(3)=\"9\" WHEN(4)=\"06/Jan/2008 15:04:05 -0700\"\r\n"]
[16.029144, "o", "\r\n% "]
[20.030593, "o", "\u001b[H\u001b[2J\u001b[3J"]
[20.030706, "o", "#\r\n# source tabular data:\r\n"]
[20.03121, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[20.031277, "o", "\r\n#\r\n# extended output mode:\r\n"]
[20.031327, "o", "\r\n% tablizer -X table.demo1"]
[21.032053, "o", "\r\n"]
[21.033787, "o", " NAME(1): beta\r\nDURATION(2): 1d10h5m1s\r\n COUNT(3): 33\r\n WHEN(4): 3/1/2014\r\n\r\n NAME(1): alpha\r\nDURATION(2): 4h35m\r\n COUNT(3): 170\r\n WHEN(4): 2013-Feb-03\r\n\r\n NAME(1): ceta\r\nDURATION(2): 33d12h\r\n COUNT(3): 9\r\n WHEN(4): 06/Jan/2008 15:04:05 -0700\r\n\r\n"]
[21.034132, "o", "\r\n% "]
[25.035531, "o", "\u001b[H\u001b[2J\u001b[3J"]
[25.035585, "o", "#\r\n"]
[25.035681, "o", "# source tabular data:\r\n"]
[25.036179, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[25.036232, "o", "\r\n#\r\n"]
[25.036274, "o", "# yaml output mode:\r\n\r\n% tablizer -Y table.demo1"]
[26.036928, "o", "\r\n"]
[26.038674, "o", "entries:\r\n - count: 33\r\n duration: \"1d10h5m1s\"\r\n name: \"beta\"\r\n when: \"3/1/2014\"\r\n - count: 170\r\n duration: \"4h35m\"\r\n name: \"alpha\"\r\n when: \"2013-Feb-03\"\r\n - count: 9\r\n duration: \"33d12h\"\r\n name: \"ceta\"\r\n when: \"06/Jan/2008 15:04:05 -0700\"\r\n"]
[26.038975, "o", "\r\n% "]
[30.040539, "o", "\u001b[H\u001b[2J\u001b[3J"]
[30.040659, "o", "#\r\n# source tabular data:\r\n"]
[30.041167, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[30.041246, "o", "\r\n#\r\n# CSV output mode:\r\n\r\n% tablizer -C table.demo1"]
[31.042088, "o", "\r\n"]
[31.043721, "o", "NAME,DURATION,COUNT,WHEN\r\nbeta,1d10h5m1s,33,3/1/2014\r\nalpha,4h35m,170,2013-Feb-03\r\nceta,33d12h,9,06/Jan/2008 15:04:05 -0700\r\n"]
[31.043997, "o", "\r\n% "]
[35.045523, "o", "\u001b[H\u001b[2J\u001b[3J"]
[35.04563, "o", "#\r\n# source tabular data:\r\n"]
[35.046209, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[35.046275, "o", "\r\n#\r\n# sort by column 3:\r\n\r\n% tablizer -A -k 3 table.demo1"]
[36.047083, "o", "\r\n"]
[36.048793, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nalpha \t4h35m \t170 \t2013-Feb-03 \t\r\nbeta \t1d10h5m1s \t33 \t3/1/2014 \t\r\nceta \t33d12h \t9 \t06/Jan/2008 15:04:05 -0700\t\r\n"]
[36.049077, "o", "\r\n% "]
[40.050739, "o", "\u001b[H\u001b[2J\u001b[3J"]
[40.050925, "o", "#\r\n# source tabular data:\r\n"]
[40.051481, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[40.051671, "o", "\r\n#\r\n# sort by column 4 and sort type time:\r\n\r\n% tablizer -A -k 4 -t table.demo1"]
[41.052486, "o", "\r\n"]
[41.05454, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nceta \t33d12h \t9 \t06/Jan/2008 15:04:05 -0700\t\r\nalpha \t4h35m \t170 \t2013-Feb-03 \t\r\nbeta \t1d10h5m1s \t33 \t3/1/2014 \t\r\n"]
[41.054864, "o", "\r\n% "]
[45.056297, "o", "\u001b[H\u001b[2J\u001b[3J"]
[45.056405, "o", "#\r\n# source tabular data:\r\n"]
[45.056895, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[45.056978, "o", "\r\n#\r\n"]
[45.057023, "o", "# sort by column 2 and sort type duration:\r\n"]
[45.057073, "o", "\r\n% tablizer -A -k 2 -a table.demo1"]
[46.057895, "o", "\r\n"]
[46.059684, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nalpha \t4h35m \t170 \t2013-Feb-03 \t\r\nbeta \t1d10h5m1s \t33 \t3/1/2014 \t\r\nceta \t33d12h \t9 \t06/Jan/2008 15:04:05 -0700\t\r\n"]
[46.059988, "o", "\r\n% "]
[50.061514, "o", "\u001b[H\u001b[2J\u001b[3J"]
[50.061622, "o", "#\r\n# source tabular data:\r\n"]
[50.062091, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[50.062188, "o", "\r\n#\r\n# only display column 1 and 3:\r\n\r\n% tablizer -A -c 1,3 table.demo1"]
[51.062985, "o", "\r\n"]
[51.066293, "o", "NAME(1)\tCOUNT(3) \r\nbeta \t33 \t\r\nalpha \t170 \t\r\nceta \t9 \t\r\n"]
[51.066843, "o", "\r\n% "]
[55.070781, "o", "\u001b[H\u001b[2J\u001b[3J"]
[55.071327, "o", "#\r\n# source tabular data:\r\n"]
[55.073499, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[55.073822, "o", "\r\n#\r\n# only display columns matching /(RA|AM)/:\r\n"]
[55.074188, "o", "\r\n% tablizer -A -c AM,RA table.demo1"]
[56.07636, "o", "\r\n"]
[56.078603, "o", "NAME(1)\tDURATION(2) \r\nbeta \t1d10h5m1s \t\r\nalpha \t4h35m \t\r\nceta \t33d12h \t\r\n"]
[56.078957, "o", "\r\n% "]
[60.080574, "o", "\u001b[H\u001b[2J\u001b[3J"]
[60.080734, "o", "#\r\n# source tabular data:\r\n"]
[60.081286, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[60.081418, "o", "\r\n#\r\n# only display column 1 and 3 in extended mode:\r\n\r\n% tablizer -X -c 1,3 table.demo1"]
[61.082844, "o", "\r\n"]
[61.089822, "o", " NAME(1): beta\r\nCOUNT(3): 33\r\n\r\n NAME(1): alpha\r\nCOUNT(3): 170\r\n\r\n NAME(1): ceta\r\nCOUNT(3): 9\r\n\r\n"]
[61.090969, "o", "\r\n% "]
[65.096092, "o", "\u001b[H\u001b[2J\u001b[3J"]
[65.096571, "o", "#\r\n# source tabular data:\r\n"]
[65.098736, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[65.099085, "o", "\r\n#\r\n# only show rows matching /20:\r\n"]
[65.099283, "o", "\r\n% tablizer /20 -A table.demo1"]
[66.101537, "o", "\r\n"]
[66.109112, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nbeta \t1d10h5m1s \t33 \t3/1\u001b[102;30m/20\u001b[0m14 \t\r\nceta \t33d12h \t9 \t06/Jan\u001b[102;30m/20\u001b[0m08 15:04:05 -0700\t\r\n"]
[66.109405, "o", "\r\n% "]
[70.11076, "o", "\u001b[H\u001b[2J\u001b[3J"]
[70.110873, "o", "#\r\n# source tabular data:\r\n"]
[70.111365, "o", "NAME DURATION COUNT WHEN\r\nbeta 1d10h5m1s 33 3/1/2014\r\nalpha 4h35m 170 2013-Feb-03\r\nceta 33d12h 9 06/Jan/2008 15:04:05 -0700\r\n"]
[70.111469, "o", "\r\n#\r\n# only show rows NOT matching /20:\r\n\r\n% tablizer /20 -A -v table.demo1"]
[71.112738, "o", "\r\n"]
[71.120032, "o", "NAME(1)\tDURATION(2)\tCOUNT(3)\tWHEN(4) \r\nalpha \t4h35m \t170 \t2013-Feb-03\t\r\n"]
[71.121127, "o", "\r\n% "]
[75.126199, "o", "\u001b[H\u001b[2J\u001b[3J"]

BIN
demo/tablizer-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

43
go.mod
View File

@@ -1,16 +1,45 @@
module daemon.de/tablizer module github.com/tlinden/tablizer
go 1.18 go 1.22
require ( require (
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 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.4
github.com/hashicorp/hcl/v2 v2.22.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.8.1
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect
github.com/mattn/go-runewidth v0.0.9 // 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.6.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // 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.2.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a // indirect
github.com/shurcooL/go-goon v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.0 // indirect github.com/tinylib/msgp v1.1.9 // indirect
github.com/ugorji/go/codec v1.2.12 // 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/sys v0.21.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.22.0 // indirect
) )

139
go.sum
View File

@@ -1,29 +1,140 @@
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.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.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 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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be/go.mod h1:OSCrScrFAjcBObrulk6BEQlytA462OkG1UGB5NYj9kE=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 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.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/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.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
github.com/hashicorp/hcl/v2 v2.22.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/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/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.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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 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 v0.0.0-20230706063926-5fe729b41b3a h1:ZHfoO7ZJhws9NU1kzZhStUnnVQiPtDe1PzpUnc6HirU=
github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a/go.mod h1:DNrlr0AR9NsHD/aoc2pPeu4uSBZ/71yCHkR42yrzW3M=
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.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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/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/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=
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/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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
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-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=
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.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.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
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=
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/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 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 it under the terms of the GNU General Public License as published by
@@ -17,19 +17,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
// command line flags // contains a whole parsed table
var Debug bool type Tabdata struct {
var XtendedOut bool maxwidthHeader int // longest header
var NoNumbering bool columns int // count
var ShowVersion bool headers []string // [ "ID", "NAME", ...]
var Columns string entries [][]string
var UseColumns []int }
var Separator string
var OutflagExtended bool
var OutflagMarkdown bool
var OutflagOrgtable bool
var OutflagShell bool
var OutputMode string
var Version = "v1.0.3" func (data *Tabdata) CloneEmpty() Tabdata {
var validOutputmodes = "(orgtbl|markdown|extended|ascii)" newdata := Tabdata{
maxwidthHeader: data.maxwidthHeader,
columns: data.columns,
headers: data.headers,
}
return newdata
}

154
lib/filter.go Normal file
View File

@@ -0,0 +1,154 @@
/*
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
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.
*/
func matchPattern(conf cfg.Config, line string) bool {
if conf.UseFuzzySearch {
return fuzzy.MatchFold(conf.Pattern, line)
}
return conf.PatternR.MatchString(line)
}
/*
* 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 {
if !Exists(conf.Filters, strings.ToLower(header)) {
// do not filter by unspecified field
continue
}
if !conf.Filters[strings.ToLower(header)].MatchString(row[idx]) {
// there IS a filter, but it doesn't 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
}
func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
if conf.Pattern == "" {
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 conf.Pattern != "" && 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
}

162
lib/filter_test.go Normal file
View File

@@ -0,0 +1,162 @@
/*
Copyright © 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
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
pattern string
line string
}{
{
name: "normal",
pattern: "haus",
line: "hausparty",
},
{
name: "fuzzy",
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.pattern)
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-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

@@ -20,9 +20,14 @@ package lib
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"github.com/gookit/color"
"github.com/tlinden/tablizer/cfg"
) )
func contains(s []int, e int) bool { func contains(s []int, e int) bool {
@@ -31,61 +36,258 @@ func contains(s []int, e int) bool {
return true return true
} }
} }
return false return false
} }
func PrepareColumns() error { func findindex(s []int, e int) (int, bool) {
if len(Columns) > 0 { for i, a := range s {
for _, use := range strings.Split(Columns, ",") { if a == e {
usenum, err := strconv.Atoi(use) return i, true
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", Columns, err)
return errors.New(msg)
}
UseColumns = append(UseColumns, usenum)
} }
} }
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 return nil
} }
func PrepareModeFlags() error { // parse columns list given with -c, modifies config.UseColumns based
if len(OutputMode) == 0 { // on eventually given regex.
switch { // This is an output filter, because -cN,N,... is being applied AFTER
case OutflagExtended: // processing of the input data.
OutputMode = "extended" func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
case OutflagMarkdown: // -c columns
OutputMode = "markdown" usecolumns, err := PrepareColumnVars(conf.Columns, data)
case OutflagOrgtable: if err != nil {
OutputMode = "orgtbl" return err
case OutflagShell: }
OutputMode = "shell"
NoNumbering = true
default:
OutputMode = "ascii"
}
} else {
r, err := regexp.Compile(validOutputmodes)
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
}
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 { if err != nil {
return errors.New("Failed to validate output mode spec!") // not a number
}
match := r.MatchString(OutputMode) 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)
if !match { for i, head := range data.headers {
return errors.New("Invalid output mode!") 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)
} }
} }
return nil // 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(conf cfg.Config, data *Tabdata) {
numberedHeaders := []string{}
maxwidth := 0 // start from scratch, so we only look at displayed column widths
for idx, head := range data.headers {
var headlen int
if len(conf.Columns) > 0 {
// -c specified
if !contains(conf.UseColumns, idx+1) {
// ignore this one
continue
}
}
if conf.NoNumbering {
numberedHeaders = append(numberedHeaders, head)
headlen = len(head)
} else {
numhead := fmt.Sprintf("%s(%d)", head, idx+1)
headlen = len(numhead)
numberedHeaders = append(numberedHeaders, numhead)
}
if headlen > maxwidth {
maxwidth = headlen
}
}
data.headers = numberedHeaders
if data.maxwidthHeader != maxwidth && maxwidth > 0 {
data.maxwidthHeader = maxwidth
}
}
// exclude columns, if any
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(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 { func trimRow(row []string) []string {
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()! var fixedrow = make([]string, len(row))
var fixedrow []string
for _, cell := range row { for idx, cell := range row {
fixedrow = append(fixedrow, strings.TrimSpace(cell)) fixedrow[idx] = strings.TrimSpace(cell)
} }
return fixedrow return fixedrow
} }
// 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
for _, line := range strings.Split(output, "\n") {
if highlight {
if first {
// we need to add two spaces to the header line
// because tablewriter omits them for some reason
// 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 conf.OutputMode == cfg.ASCII {
line += " "
}
line = conf.HighlightHdrStyle.Sprint(line)
first = false
} else {
line = conf.HighlightStyle.Sprint(line)
}
} else {
line = conf.NoHighlightStyle.Sprint(line)
}
highlight = !highlight
colorized += line + "\n"
}
return colorized
case len(conf.Pattern) > 0 && !conf.NoColor && color.IsConsole(os.Stdout):
r := regexp.MustCompile("(" + conf.Pattern + ")")
return r.ReplaceAllStringFunc(output, func(in string) string {
return conf.ColorStyle.Sprint(in)
})
default:
return output
}
}

View File

@@ -21,9 +21,11 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/tlinden/tablizer/cfg"
) )
func Testcontains(t *testing.T) { func TestContains(t *testing.T) {
var tests = []struct { var tests = []struct {
list []int list []int
search int search int
@@ -45,6 +47,19 @@ func Testcontains(t *testing.T) {
} }
func TestPrepareColumns(t *testing.T) { func TestPrepareColumns(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"2", "3", "4",
},
},
}
var tests = []struct { var tests = []struct {
input string input string
exp []int exp []int
@@ -52,22 +67,176 @@ func TestPrepareColumns(t *testing.T) {
}{ }{
{"1,2,3", []int{1, 2, 3}, false}, {"1,2,3", []int{1, 2, 3}, false},
{"1,2,", []int{}, true}, {"1,2,", []int{}, true},
{"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 { for _, testdata := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror) testname := fmt.Sprintf("PrepareColumns-%s-%t", testdata.input, testdata.wanterror)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
Columns = tt.input conf := cfg.Config{Columns: testdata.input}
err := PrepareColumns() err := PrepareColumns(&conf, &data)
if err != nil { if err != nil {
if !tt.wanterror { if !testdata.wanterror {
t.Errorf("got error: %v", err) t.Errorf("got error: %v", err)
} }
} else { } else {
if !reflect.DeepEqual(UseColumns, tt.exp) { if !reflect.DeepEqual(conf.UseColumns, testdata.exp) {
t.Errorf("got: %v, expected: %v", UseColumns, tt.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)
}
}
})
}
}
func TestReduceColumns(t *testing.T) {
var tests = []struct {
expect [][]string
columns []int
}{
{
expect: [][]string{{"a", "b"}},
columns: []int{1, 2},
},
{
expect: [][]string{{"a", "c"}},
columns: []int{1, 3},
},
{
expect: [][]string{{"a"}},
columns: []int{1},
},
{
expect: [][]string{nil},
columns: []int{4},
},
}
input := [][]string{{"a", "b", "c"}}
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: testdata.columns}
data := Tabdata{entries: input}
reduceColumns(c, &data)
if !reflect.DeepEqual(data.entries, testdata.expect) {
t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v",
data.entries, testdata.expect)
}
})
}
}
func TestNumberizeHeaders(t *testing.T) {
data := Tabdata{
headers: []string{"ONE", "TWO", "THREE"},
}
var tests = []struct {
expect []string
columns []int
nonum bool
}{
{[]string{"ONE(1)", "TWO(2)", "THREE(3)"}, []int{1, 2, 3}, false},
{[]string{"ONE(1)", "TWO(2)"}, []int{1, 2}, false},
{[]string{"ONE", "TWO"}, []int{1, 2}, true},
}
for _, testdata := range tests {
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t",
testdata.columns, testdata.nonum)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, NoNumbering: testdata.nonum}
usedata := data
numberizeAndReduceHeaders(conf, &usedata)
if !reflect.DeepEqual(usedata.headers, testdata.expect) {
t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v",
usedata.headers, testdata.expect)
}
})
}
}

View File

@@ -19,65 +19,82 @@ package lib
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"github.com/tlinden/tablizer/cfg"
) )
func ProcessFiles(args []string) error { const RWRR = 0755
fds, pattern, err := determineIO(args)
func ProcessFiles(conf *cfg.Config, args []string) error {
fd, pattern, err := determineIO(conf, args)
if err != nil { if err != nil {
return err return err
} }
for _, fd := range fds { if err := conf.PreparePattern(pattern); err != nil {
data, err := parseFile(fd, pattern) return err
if err != nil {
return err
}
printData(&data)
} }
data, err := Parse(*conf, fd)
if err != nil {
return err
}
if err = ValidateConsistency(&data); err != nil {
return err
}
err = PrepareColumns(conf, &data)
if err != nil {
return err
}
printData(os.Stdout, *conf, &data)
return nil return nil
} }
func determineIO(args []string) ([]io.Reader, string, error) { func determineIO(conf *cfg.Config, args []string) (io.Reader, string, error) {
var filehandle io.Reader
var pattern string var pattern string
var fds []io.Reader var haveio bool
var havefiles bool
if len(args) > 0 { switch {
// threre were args left, take a look case conf.InputFile == "-":
if _, err := os.Stat(args[0]); err != nil { filehandle = os.Stdin
// first one is not a file, consider it as regexp and haveio = true
// shift arg list case conf.InputFile != "":
pattern = args[0] fd, err := os.OpenFile(conf.InputFile, os.O_RDONLY, RWRR)
args = args[1:]
if err != nil {
return nil, "", fmt.Errorf("failed to read input file %s: %w", conf.InputFile, err)
} }
if len(args) > 0 { filehandle = fd
// only files haveio = true
for _, file := range args {
fd, err := os.OpenFile(file, os.O_RDONLY, 0755)
if err != nil {
return nil, "", err
}
fds = append(fds, fd)
}
havefiles = true
}
} }
if !havefiles { if !haveio {
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 { if (stat.Mode() & os.ModeCharDevice) == 0 {
fds = append(fds, os.Stdin) // we're reading from STDIN, which takes precedence over file args
} else { filehandle = os.Stdin
return nil, "", errors.New("No file specified and nothing to read on stdin!") haveio = true
} }
} }
return fds, pattern, nil if len(args) > 0 {
pattern = args[0]
conf.Pattern = args[0]
}
if !haveio {
return nil, "", errors.New("no file specified and nothing to read on stdin")
}
return filehandle, pattern, nil
} }

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 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 it under the terms of the GNU General Public License as published by
@@ -19,144 +19,192 @@ package lib
import ( import (
"bufio" "bufio"
"errors" "encoding/csv"
"fmt" "fmt"
"github.com/alecthomas/repr"
"io" "io"
"regexp" "regexp"
"strings" "strings"
"github.com/alecthomas/repr"
"github.com/tlinden/tablizer/cfg"
) )
// contains a whole parsed table /*
type Tabdata struct { Parser switch
maxwidthHeader int // longest header */
maxwidthPerCol []int // max width per column func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
columns int var data Tabdata
headerIndices []map[string]int // [ {beg=>0, end=>17}, ... ] var err error
headers []string // [ "ID", "NAME", ...]
entries [][]string // first step, parse the data
if len(conf.Separator) == 1 {
data, err = parseCSV(conf, input)
} else {
data, err = parseTabular(conf, 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 tabular input. We split the header (first line) by 2 or more Parse CSV input.
spaces, remember the positions of the header fields. We then split
the data (everything after the first line) by those positions. That
way we can turn "tabular data" (with fields containing whitespaces)
into real tabular data. We re-tabulate our input if you will.
*/ */
func parseFile(input io.Reader, pattern string) (Tabdata, error) { func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) {
data := Tabdata{}
// apply pattern, if any
content, err := FilterByPattern(conf, input)
if err != nil {
return data, err
}
csvreader := csv.NewReader(content)
csvreader.Comma = rune(conf.Separator[0])
records, err := csvreader.ReadAll()
if err != nil {
return data, fmt.Errorf("could not parse CSV input: %w", err)
}
if len(records) >= 1 {
data.headers = records[0]
data.columns = len(records)
for _, head := range data.headers {
// register widest header field
headerlen := len(head)
if headerlen > data.maxwidthHeader {
data.maxwidthHeader = headerlen
}
}
if len(records) > 1 {
data.entries = records[1:]
}
}
return data, nil
}
/*
Parse tabular input.
*/
func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
data := Tabdata{} data := Tabdata{}
var scanner *bufio.Scanner var scanner *bufio.Scanner
var spaces = `\s\s+|$`
if len(Separator) > 0 {
spaces = Separator
}
hadFirst := false hadFirst := false
spacefinder := regexp.MustCompile(spaces) separate := regexp.MustCompile(conf.Separator)
beg := 0
scanner = bufio.NewScanner(input) scanner = bufio.NewScanner(input)
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
values := []string{} parts := separate.Split(line, -1)
patternR, err := regexp.Compile(pattern)
if err != nil {
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
}
if !hadFirst { if !hadFirst {
// header processing // header processing
parts := spacefinder.FindAllStringIndex(line, -1)
data.columns = len(parts) data.columns = len(parts)
// if Debug {
// fmt.Println(parts)
// }
// process all header fields // process all header fields
for _, part := range parts { for _, part := range parts {
// if Debug {
// fmt.Printf("Part: <%s>\n", string(line[beg:part[0]]))
//}
// current field
head := string(line[beg:part[0]])
// register begin and end of field within line
indices := make(map[string]int)
indices["beg"] = beg
if part[0] == part[1] {
indices["end"] = 0
} else {
indices["end"] = part[1] - 1
}
// register widest header field // register widest header field
headerlen := len(head) headerlen := len(part)
if headerlen > data.maxwidthHeader { if headerlen > data.maxwidthHeader {
data.maxwidthHeader = headerlen data.maxwidthHeader = headerlen
} }
// register fields data // register fields data
data.headerIndices = append(data.headerIndices, indices) data.headers = append(data.headers, strings.TrimSpace(part))
data.headers = append(data.headers, head)
// end of current field == begin of next one
beg = part[1]
// done // done
hadFirst = true hadFirst = true
} }
} else { } else {
// data processing // data processing
if len(pattern) > 0 { if conf.Pattern != "" && matchPattern(conf, line) == conf.InvertMatch {
if !patternR.MatchString(line) { // by default -v is false, so if a line does NOT
continue // match the pattern, we will ignore it. However,
} // if the user specified -v, the matching is inverted,
// so we ignore all lines, which DO match.
continue
} }
idx := 0 // we cannot use the header index, because we could exclude columns idx := 0 // we cannot use the header index, because we could exclude columns
for _, index := range data.headerIndices { values := []string{}
value := "" for _, part := range parts {
if index["end"] == 0 {
value = string(line[index["beg"]:])
} else {
value = string(line[index["beg"]:index["end"]])
}
width := len(strings.TrimSpace(value))
if len(data.maxwidthPerCol)-1 < idx {
data.maxwidthPerCol = append(data.maxwidthPerCol, width)
} else {
if width > data.maxwidthPerCol[idx] {
data.maxwidthPerCol[idx] = width
}
}
// if Debug { // if Debug {
// fmt.Printf("<%s> ", value) // fmt.Printf("<%s> ", value)
// } // }
values = append(values, strings.TrimSpace(value)) values = append(values, strings.TrimSpace(part))
idx++ idx++
} }
// fill up missing fields, if any
for i := len(values); i < len(data.headers); i++ {
values = append(values, "")
}
data.entries = append(data.entries, values) data.entries = append(data.entries, values)
} }
} }
if scanner.Err() != nil { if scanner.Err() != nil {
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, scanner.Err())) return data, fmt.Errorf("failed to read from io.Reader: %w", scanner.Err())
}
if Debug {
repr.Print(data)
} }
return data, nil 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

@@ -18,65 +18,149 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/tlinden/tablizer/cfg"
) )
var input = []struct {
name string
text string
separator string
}{
{
name: "tabular-data",
separator: cfg.DefaultSeparator,
text: `
ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`,
},
{
name: "csv-data",
separator: ",",
text: `
ONE,TWO,THREE
asd,igig,cxxxncnc
19191,"EDD 1",X`,
},
}
func TestParser(t *testing.T) { func TestParser(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{ columns: 3,
5,
5,
8,
},
columns: 3,
headerIndices: []map[string]int{
map[string]int{
"beg": 0,
"end": 6,
},
map[string]int{
"end": 13,
"beg": 7,
},
map[string]int{
"beg": 14,
"end": 0,
},
},
headers: []string{ headers: []string{
"ONE", "ONE", "TWO", "THREE",
"TWO",
"THREE",
}, },
entries: [][]string{ entries: [][]string{
[]string{ {"asd", "igig", "cxxxncnc"},
"asd", {"19191", "EDD 1", "X"},
"igig",
"cxxxncnc",
},
[]string{
"19191",
"EDD 1",
"X",
},
}, },
} }
table := `ONE TWO THREE for _, testdata := range input {
asd igig cxxxncnc testname := fmt.Sprintf("parse-%s", testdata.name)
t.Run(testname, func(t *testing.T) {
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)
}
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n",
data, gotdata)
}
})
}
}
func TestParserPatternmatching(t *testing.T) {
var tests = []struct {
entries [][]string
pattern string
invert bool
want bool
}{
{
entries: [][]string{
{"asd", "igig", "cxxxncnc"},
},
pattern: "ig",
invert: false,
},
{
entries: [][]string{
{"19191", "EDD 1", "X"},
},
pattern: "ig",
invert: true,
},
}
for _, inputdata := range input {
for _, testdata := range tests {
testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
inputdata.name, testdata.pattern, testdata.invert)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{InvertMatch: testdata.invert, Pattern: testdata.pattern,
Separator: inputdata.separator}
_ = conf.PreparePattern(testdata.pattern)
readFd := strings.NewReader(strings.TrimSpace(inputdata.text))
gotdata, err := Parse(conf, readFd)
if err != nil {
if !testdata.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
}
} else {
if !reflect.DeepEqual(testdata.entries, gotdata.entries) {
t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n",
testdata.pattern, testdata.invert, testdata.entries, gotdata.entries)
}
}
})
}
}
}
func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"asd", "igig", ""},
{"19191", "EDD 1", "X"},
},
}
table := `
ONE TWO THREE
asd igig
19191 EDD 1 X` 19191 EDD 1 X`
readFd := strings.NewReader(table) readFd := strings.NewReader(strings.TrimSpace(table))
gotdata, err := parseFile(readFd, "") conf := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := Parse(conf, readFd)
if err != nil { if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
} }
if !reflect.DeepEqual(data, gotdata) { if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n", data, gotdata) t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
conf.Separator, data, gotdata)
} }
} }

View File

@@ -18,74 +18,64 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"encoding/csv"
"fmt" "fmt"
"github.com/olekukonko/tablewriter" "io"
"os" "log"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/gookit/color"
"github.com/olekukonko/tablewriter"
"github.com/tlinden/tablizer/cfg"
"gopkg.in/yaml.v3"
) )
func printData(data *Tabdata) { func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// prepare headers: add numbers to headers // add numbers to headers and remove this we're not interested in
numberedHeaders := []string{} numberizeAndReduceHeaders(conf, data)
for i, head := range data.headers {
if len(Columns) > 0 {
// -c specified
if !contains(UseColumns, i+1) {
// ignore this one
continue
}
}
if NoNumbering {
numberedHeaders = append(numberedHeaders, head)
} else {
numberedHeaders = append(numberedHeaders, fmt.Sprintf("%s(%d)", head, i+1))
}
}
data.headers = numberedHeaders
// prepare data // remove unwanted columns, if any
if len(Columns) > 0 { reduceColumns(conf, data)
reducedEntries := [][]string{}
reducedEntry := []string{}
for _, entry := range data.entries {
reducedEntry = nil
for i, value := range entry {
if !contains(UseColumns, i+1) {
continue
}
reducedEntry = append(reducedEntry, value) // sort the data
} sortTable(conf, data)
reducedEntries = append(reducedEntries, reducedEntry)
}
data.entries = reducedEntries
}
switch OutputMode { switch conf.OutputMode {
case "extended": case cfg.Extended:
printExtendedData(data) printExtendedData(writer, conf, data)
case "ascii": case cfg.ASCII:
printAsciiData(data) printASCIIData(writer, conf, data)
case "orgtbl": case cfg.Orgtbl:
printOrgmodeData(data) printOrgmodeData(writer, conf, data)
case "markdown": case cfg.Markdown:
printMarkdownData(data) printMarkdownData(writer, conf, data)
case "shell": case cfg.Shell:
printShellData(data) printShellData(writer, data)
case cfg.Yaml:
printYamlData(writer, data)
case cfg.CSV:
printCSVData(writer, data)
default: default:
printAsciiData(data) printASCIIData(writer, conf, data)
} }
} }
func output(writer io.Writer, str string) {
fmt.Fprint(writer, str)
}
/* /*
Emacs org-mode compatible table (also orgtbl-mode) Emacs org-mode compatible table (also orgtbl-mode)
*/ */
func printOrgmodeData(data *Tabdata) { func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString) table := tablewriter.NewWriter(tableString)
table.SetHeader(data.headers) if !conf.NoHeaders {
table.SetHeader(data.headers)
}
for _, row := range data.entries { for _, row := range data.entries {
table.Append(trimRow(row)) table.Append(trimRow(row))
@@ -104,19 +94,25 @@ func printOrgmodeData(data *Tabdata) {
| cell | cell | | cell | cell |
|------+------| |------+------|
*/ */
leftR := regexp.MustCompile("(?m)^\\+") leftR := regexp.MustCompile(`(?m)^\\+`)
rightR := regexp.MustCompile("\\+(?m)$") rightR := regexp.MustCompile(`\\+(?m)$`)
fmt.Print(rightR.ReplaceAllString(leftR.ReplaceAllString(tableString.String(), "|"), "|")) output(writer, color.Sprint(
colorizeData(conf,
rightR.ReplaceAllString(
leftR.ReplaceAllString(tableString.String(), "|"), "|"))))
} }
/* /*
Markdown table Markdown table
*/ */
func printMarkdownData(data *Tabdata) { func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
table := tablewriter.NewWriter(os.Stdout) tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader(data.headers) if !conf.NoHeaders {
table.SetHeader(data.headers)
}
for _, row := range data.entries { for _, row := range data.entries {
table.Append(trimRow(row)) table.Append(trimRow(row))
@@ -126,21 +122,22 @@ func printMarkdownData(data *Tabdata) {
table.SetCenterSeparator("|") table.SetCenterSeparator("|")
table.Render() table.Render()
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
} }
/* /*
Simple ASCII table without any borders etc, just like the input we expect Simple ASCII table without any borders etc, just like the input we expect
*/ */
func printAsciiData(data *Tabdata) { func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
table := tablewriter.NewWriter(os.Stdout) tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !conf.NoHeaders {
table.SetHeader(data.headers)
}
table.SetHeader(data.headers)
table.AppendBulk(data.entries) table.AppendBulk(data.entries)
// for _, row := range data.entries {
// table.Append(trimRow(row))
// }
table.SetAutoWrapText(false) table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true) table.SetAutoFormatHeaders(true)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
@@ -150,57 +147,116 @@ func printAsciiData(data *Tabdata) {
table.SetRowSeparator("") table.SetRowSeparator("")
table.SetHeaderLine(false) table.SetHeaderLine(false)
table.SetBorder(false) table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true) table.SetNoWhiteSpace(true)
if !conf.UseHighlight {
// the tabs destroy the highlighting
table.SetTablePadding("\t") // pad with tabs
} else {
table.SetTablePadding(" ")
}
table.Render() table.Render()
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
} }
/* /*
We simulate the \x command of psql (the PostgreSQL client) We simulate the \x command of psql (the PostgreSQL client)
*/ */
func printExtendedData(data *Tabdata) { func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// needed for data output // needed for data output
format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) // FIXME: re-calculate if -c has been set format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader)
out := ""
if len(data.entries) > 0 { if len(data.entries) > 0 {
var idx int
for _, entry := range data.entries { for _, entry := range data.entries {
idx = 0
for i, value := range entry { for i, value := range entry {
if len(Columns) > 0 { out += color.Sprintf(format, data.headers[i], value)
if !contains(UseColumns, i+1) {
continue
}
}
fmt.Printf(format, data.headers[idx], value)
idx++
} }
fmt.Println()
out += "\n"
} }
} }
output(writer, colorizeData(conf, out))
} }
/* /*
Shell output, ready to be eval'd. Just like FreeBSD stat(1) Shell output, ready to be eval'd. Just like FreeBSD stat(1)
*/ */
func printShellData(data *Tabdata) { func printShellData(writer io.Writer, data *Tabdata) {
if len(data.entries) > 0 { out := ""
var idx int
for _, entry := range data.entries {
idx = 0
for i, value := range entry {
if len(Columns) > 0 {
if !contains(UseColumns, i+1) {
continue
}
}
fmt.Printf("%s=\"%s\" ", data.headers[idx], value) if len(data.entries) > 0 {
idx++ for _, entry := range data.entries {
shentries := []string{}
for idx, value := range entry {
shentries = append(shentries, fmt.Sprintf("%s=\"%s\"",
data.headers[idx], value))
} }
fmt.Println()
out += strings.Join(shentries, " ") + "\n"
} }
} }
// no colorization here
output(writer, out)
}
func printYamlData(writer io.Writer, data *Tabdata) {
type Data struct {
Entries []map[string]interface{} `yaml:"entries"`
}
yamlout := Data{}
for _, entry := range data.entries {
yamldata := map[string]interface{}{}
for idx, entry := range entry {
style := yaml.TaggedStyle
_, err := strconv.Atoi(entry)
if err != nil {
style = yaml.DoubleQuotedStyle
}
yamldata[strings.ToLower(data.headers[idx])] =
&yaml.Node{
Kind: yaml.ScalarNode,
Style: style,
Value: entry}
}
yamlout.Entries = append(yamlout.Entries, yamldata)
}
yamlstr, err := yaml.Marshal(&yamlout)
if err != nil {
log.Fatal(err)
}
output(writer, string(yamlstr))
}
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)
}
for _, entry := range data.entries {
if err := csvout.Write(entry); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
csvout.Flush()
if err := csvout.Error(); err != nil {
log.Fatal(err)
}
} }

View File

@@ -18,64 +18,276 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"os" "bytes"
"fmt"
"strings" "strings"
"testing" "testing"
"github.com/tlinden/tablizer/cfg"
) )
func TestPrinter(t *testing.T) { func newData() Tabdata {
table := `ONE TWO THREE return Tabdata{
asd igig cxxxncnc maxwidthHeader: 8,
19191 EDD 1 X` columns: 4,
headers: []string{
expects := map[string]string{ "NAME",
"ascii": `ONE(1) TWO(2) THREE(3) "DURATION",
asd igig cxxxncnc "COUNT",
19191 EDD 1 X`, "WHEN",
"orgtbl": `|--------+--------+----------| },
| ONE(1) | TWO(2) | THREE(3) | entries: [][]string{
|--------+--------+----------| {
| asd | igig | cxxxncnc | "beta",
| 19191 | EDD 1 | X | "1d10h5m1s",
|--------+--------+----------|`, "33",
"markdown": `| ONE(1) | TWO(2) | THREE(3) | "3/1/2014",
|--------|--------|----------| },
| asd | igig | cxxxncnc | {
| 19191 | EDD 1 | X |`, "alpha",
"4h35m",
"170",
"2013-Feb-03",
},
{
"ceta",
"33d12h",
"9",
"06/Jan/2008 15:04:05 -0700",
},
},
}
}
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
desc bool // sort in descending order, default == ascending
nonum bool // hide numbering
mode int // shell, orgtbl, etc. empty == default: ascii
usecol []int // columns to display, empty == display all
usecolstr string // for testname, must match usecol
expect string // rendered output we expect
}{
// --------------------- Default settings mode tests ``
{
mode: cfg.ASCII,
name: "default",
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
{
mode: cfg.CSV,
name: "csv",
expect: `
NAME,DURATION,COUNT,WHEN
beta,1d10h5m1s,33,3/1/2014
alpha,4h35m,170,2013-Feb-03
ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
},
{
name: "default",
mode: cfg.Orgtbl,
expect: `
+---------+-------------+----------+----------------------------+
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
+---------+-------------+----------+----------------------------+
| beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |
+---------+-------------+----------+----------------------------+`,
},
{
name: "default",
mode: cfg.Markdown,
expect: `
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|---------|-------------|----------|----------------------------|
| beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`,
},
{
name: "default",
mode: cfg.Shell,
nonum: true,
expect: `
NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014"
NAME="alpha" DURATION="4h35m" COUNT="170" WHEN="2013-Feb-03"
NAME="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`,
},
{
name: "default",
mode: cfg.Yaml,
nonum: true,
expect: `
entries:
- count: 33
duration: "1d10h5m1s"
name: "beta"
when: "3/1/2014"
- count: 170
duration: "4h35m"
name: "alpha"
when: "2013-Feb-03"
- count: 9
duration: "33d12h"
name: "ceta"
when: "06/Jan/2008 15:04:05 -0700"`,
},
{
name: "default",
mode: cfg.Extended,
expect: `
NAME(1): beta
DURATION(2): 1d10h5m1s
COUNT(3): 33
WHEN(4): 3/1/2014
NAME(1): alpha
DURATION(2): 4h35m
COUNT(3): 170
WHEN(4): 2013-Feb-03
NAME(1): ceta
DURATION(2): 33d12h
COUNT(3): 9
WHEN(4): 06/Jan/2008 15:04:05 -0700`,
},
//------------------------ SORT TESTS
{
name: "sortbycolumn",
column: 3,
sortby: "numeric",
desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03`,
},
{
name: "sortbycolumn",
column: 4,
sortby: "time",
desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014`,
},
{
name: "sortbycolumn",
column: 2,
sortby: "duration",
desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
// ----------------------- UseColumns Tests
{
name: "usecolumns",
usecol: []int{1, 4},
usecolstr: "1,4",
expect: `
NAME(1) WHEN(4)
beta 3/1/2014
alpha 2013-Feb-03
ceta 06/Jan/2008 15:04:05 -0700`,
},
{
name: "usecolumns",
usecol: []int{2},
usecolstr: "2",
expect: `
DURATION(2)
1d10h5m1s
4h35m
33d12h`,
},
{
name: "usecolumns",
usecol: []int{3},
usecolstr: "3",
expect: `
COUNT(3)
33
170
9`,
},
{
name: "usecolumns",
column: 0,
usecol: []int{1, 3},
usecolstr: "1,3",
expect: `
NAME(1) COUNT(3)
beta 33
alpha 170
ceta 9`,
},
{
name: "usecolumns",
usecol: []int{2, 4},
usecolstr: "2,4",
expect: `
DURATION(2) WHEN(4)
1d10h5m1s 3/1/2014
4h35m 2013-Feb-03
33d12h 06/Jan/2008 15:04:05 -0700`,
},
}
func TestPrinter(t *testing.T) {
for _, testdata := range tests {
testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr)
t.Run(testname, func(t *testing.T) {
// replaces os.Stdout, but we ignore it
var writer bytes.Buffer
// cmd flags
conf := cfg.Config{
SortByColumn: testdata.column,
SortDescending: testdata.desc,
SortMode: testdata.sortby,
OutputMode: testdata.mode,
NoNumbering: testdata.nonum,
UseColumns: testdata.usecol,
NoColor: true,
}
conf.ApplyDefaults()
// the test checks the len!
if len(testdata.usecol) > 0 {
conf.Columns = "yes"
} else {
conf.Columns = ""
}
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",
got, exp)
}
})
} }
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
origStdout := os.Stdout
os.Stdout = w
for mode, expect := range expects {
OutputMode = mode
fd := strings.NewReader(table)
data, err := parseFile(fd, "")
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, data)
}
printData(&data)
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
output := strings.TrimSpace(string(buf))
if output != expect {
t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect))
}
}
// Restore
os.Stdout = origStdout
} }

127
lib/sort.go Normal file
View File

@@ -0,0 +1,127 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"regexp"
"sort"
"strconv"
"github.com/araddon/dateparse"
"github.com/tlinden/tablizer/cfg"
)
func sortTable(conf cfg.Config, data *Tabdata) {
if conf.SortByColumn <= 0 {
// no sorting wanted
return
}
// slightly modified here to match internal array indicies
col := conf.SortByColumn
col-- // ui starts counting by 1, but use 0 internally
// sanity checks
if len(data.entries) == 0 {
return
}
if col >= len(data.headers) {
// fall back to default column
col = 0
}
// actual sorting
sort.SliceStable(data.entries, func(i, j int) bool {
return compare(&conf, data.entries[i][col], data.entries[j][col])
})
}
// config is not modified here, but it would be inefficient to copy it every loop
func compare(conf *cfg.Config, left string, right string) bool {
var comp bool
switch conf.SortMode {
case "numeric":
left, err := strconv.Atoi(left)
if err != nil {
left = 0
}
right, err := strconv.Atoi(right)
if err != nil {
right = 0
}
comp = left < right
case "duration":
left := duration2int(left)
right := duration2int(right)
comp = left < right
case "time":
left, _ := dateparse.ParseAny(left)
right, _ := dateparse.ParseAny(right)
comp = left.Unix() < right.Unix()
default:
comp = left < right
}
if conf.SortDescending {
comp = !comp
}
return comp
}
/*
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 comparison.
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])`)
seconds := 0
for _, match := range re.FindAllStringSubmatch(duration, -1) {
if len(match) == 3 {
durationvalue, _ := strconv.Atoi(match[1])
switch match[2][0] {
case 'd':
seconds += durationvalue * 86400
case 'h':
seconds += durationvalue * 3600
case 'm':
seconds += durationvalue * 60
case 's':
seconds += durationvalue
}
}
}
return seconds
}

82
lib/sort_test.go Normal file
View File

@@ -0,0 +1,82 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"fmt"
"testing"
"github.com/tlinden/tablizer/cfg"
)
func TestDuration2Seconds(t *testing.T) {
var tests = []struct {
dur string
expect int
}{
{"1d", 60 * 60 * 24},
{"1h", 60 * 60},
{"10m", 60 * 10},
{"2h4m10s", (60 * 120) + (4 * 60) + 10},
{"88u", 0},
{"19t77X what?4s", 4},
}
for _, testdata := range tests {
testname := fmt.Sprintf("duration-%s", testdata.dur)
t.Run(testname, func(t *testing.T) {
seconds := duration2int(testdata.dur)
if seconds != testdata.expect {
t.Errorf("got %d, want %d", seconds, testdata.expect)
}
})
}
}
func TestCompare(t *testing.T) {
var tests = []struct {
mode string
a string
b string
want bool
desc bool
}{
// ascending
{"numeric", "10", "20", true, false},
{"duration", "2d4h5m", "45m", false, false},
{"time", "12/24/2022", "1/1/1970", false, false},
// descending
{"numeric", "10", "20", false, true},
{"duration", "2d4h5m", "45m", true, true},
{"time", "12/24/2022", "1/1/1970", true, true},
}
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: testdata.mode, SortDescending: testdata.desc}
got := compare(&c, testdata.a, testdata.b)
if got != testdata.want {
t.Errorf("got %t, want %t", got, testdata.want)
}
})
}
}

12
main.go
View File

@@ -18,9 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import ( import (
"daemon.de/tablizer/cmd" "os"
"github.com/tlinden/tablizer/cmd"
) )
func main() { 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",
})
}

View File

@@ -43,7 +43,7 @@ for D in $DIST; do
tardir="${tool}-${os}-${arch}-${version}" tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz" tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
set -x set -x
GOOS=${os} GOARCH=${arch} go build -o ${binfile} GOOS=${os} GOARCH=${arch} go build -o ${binfile} -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=${version}'"
mkdir -p ${tardir} mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/ cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = tablizer echo 'tool = tablizer

10
t/plugintest.zy Normal file
View File

@@ -0,0 +1,10 @@
/*
Simple filter hook function. Splits the argument by whitespace,
fetches the 2nd element, converts it to an int and returns true
if it s larger than 5, false otherwise.
*/
(defn uselarge [line]
(cond (> (atoi (second (resplit line `\s+`))) 5) true false))
/* Register the filter hook */
(addhook %filter %uselarge)

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

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

6
t/testtable Normal file
View File

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

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/testtable2 Normal file
View File

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

575
tablizer.1 Normal file
View File

@@ -0,0 +1,575 @@
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
.\" nothing in troff, for use with C<>.
.tr \(*W-
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
. ds C`
. ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
. if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. if !\nF==2 \{\
. nr % 0
. nr F 2
. \}
. \}
.\}
.rr rF
.\"
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
.\" Fear. Run. Save yourself. No user-serviceable parts.
. \" fudge factors for nroff and troff
.if n \{\
. ds #H 0
. ds #V .8m
. ds #F .3m
. ds #[ \f1
. ds #] \fP
.\}
.if t \{\
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
. ds #V .6m
. ds #F 0
. ds #[ \&
. ds #] \&
.\}
. \" simple accents for nroff and troff
.if n \{\
. ds ' \&
. ds ` \&
. ds ^ \&
. ds , \&
. ds ~ ~
. ds /
.\}
.if t \{\
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
.\}
. \" troff and (daisy-wheel) nroff accents
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
.ds ae a\h'-(\w'a'u*4/10)'e
.ds Ae A\h'-(\w'A'u*4/10)'E
. \" corrections for vroff
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
. \" for low resolution devices (crt and lpr)
.if \n(.H>23 .if \n(.V>19 \
\{\
. ds : e
. ds 8 ss
. ds o a
. ds d- d\h'-1'\(ga
. ds D- D\h'-1'\(hy
. ds th \o'bp'
. ds Th \o'LP'
. ds ae ae
. ds Ae AE
.\}
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
.TH TABLIZER 1 "2025-01-14" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
.nh
.SH "NAME"
tablizer \- Manipulate tabular output of other programs
.SH "SYNOPSIS"
.IX Header "SYNOPSIS"
.Vb 2
\& Usage:
\& tablizer [regex] [file, ...] [flags]
\&
\& 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
.Ve
.SH "DESCRIPTION"
.IX Header "DESCRIPTION"
Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns
or you may want to filter for some pattern (See \s-1PATTERNS\s0) or you
may need the output in another program and need to parse it somehow.
Standard unix tools such as \fBawk\fR\|(1), \fBgrep\fR\|(1) or \fBcolumn\fR\|(1) may help, but
sometimes it's a tedious business.
.PP
Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by \s-1TAB\s0 characters. This is
not easy to process.
.PP
You can use \fBtablizer\fR to do these and more things.
.PP
\&\fBtablizer\fR analyses the header fields of a table, registers the
column positions of each header field and separates columns by those
positions.
.PP
Without any options it reads its input from \f(CW\*(C`STDIN\*(C'\fR, but you can also
specify a file as a parameter. If you want to reduce the output by
some regular expression, just specify it as its first parameter. You
may also use the \fB\-v\fR option to exclude all rows which match the
pattern. Hence:
.PP
.Vb 2
\& # read from STDIN
\& kubectl get pods | tablizer
\&
\& # read a file
\& tablizer filename
\&
\& # search for pattern in a file (works like grep)
\& tablizer regex filename
\&
\& # search for pattern in STDIN
\& kubectl get pods | tablizer regex
.Ve
.PP
The output looks like the original one but every header field will
have a numer associated with it, e.g.:
.PP
.Vb 1
\& NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
.Ve
.PP
These numbers denote the column and you can use them to specify which
columns you want to have in your output (see \s-1COLUMNS\s0:
.PP
.Vb 1
\& kubectl get pods | tablizer \-c1,3
.Ve
.PP
You can specify the numbers in any order but output will always follow
the original order.
.PP
The numbering can be suppressed by using the \fB\-n\fR option.
.PP
By default tablizer shows a header containing the names of each
column. This can be disabled using the \fB\-H\fR option. Be aware that
this only affects tabular output modes. Shell, Extended, Yaml and \s-1CSV\s0
output modes always use the column names.
.PP
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:
.IP "\fB\-a \-\-sort\-age\fR" 4
.IX Item "-a --sort-age"
Sorts duration strings like \*(L"1d4h32m51s\*(R".
.IP "\fB\-i \-\-sort\-numeric\fR" 4
.IX Item "-i --sort-numeric"
Sorts numeric fields.
.IP "\fB\-t \-\-sort\-time\fR" 4
.IX Item "-t --sort-time"
Sorts timestamps.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly
useful for the developer.
.SS "\s-1PATTERNS AND FILTERING\s0"
.IX Subsection "PATTERNS AND FILTERING"
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:
.PP
.Vb 1
\& perldoc perlre
.Ve
.PP
Or read it online: <https://perldoc.perl.org/perlre>.
.PP
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
.PP
.Vb 1
\& (?MODIFIER)
.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"
.Ve
.PP
You can 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
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
display. By default tablizer numerizes the header names and these
numbers can be used to specify which header to display, see example
above.
.PP
However, beside numbers, you can also use regular expressions with
\&\fB\-c\fR, also separated by comma. And you can mix column numbers with
regexps.
.PP
Lets take this table:
.PP
.Vb 4
\& PID TTY TIME CMD
\& 14001 pts/0 00:00:00 bash
\& 42871 pts/0 00:00:00 ps
\& 42872 pts/0 00:00:00 sed
.Ve
.PP
We want to see only the \s-1CMD\s0 column and use a regex for this:
.PP
.Vb 6
\& ps | tablizer \-s \*(Aq\es+\*(Aq \-c C
\& CMD(4)
\& bash
\& ps
\& tablizer
\& sed
.Ve
.PP
where \*(L"C\*(R" is our regexp which matches \s-1CMD.\s0
.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
large for your current terminal but you still need to see every
column. In such cases the \fB\-o extended\fR or \fB\-X\fR option can be
useful which enables \fIextended mode\fR. In this mode, each row will be
printed vertically, header left, value right, aligned by the field
widths. Here's an example:
.PP
.Vb 6
\& kubectl get pods | ./tablizer \-o extended
\& NAME: repldepl\-7bcd8d5b64\-7zq4l
\& READY: 1/1
\& STATUS: Running
\& RESTARTS: 1 (71m ago)
\& AGE: 5h28m
.Ve
.PP
You can of course still use a regex to reduce the number of rows
displayed.
.PP
The option \fB\-o shell\fR can be used if the output has to be processed
by the shell, it prints variable assignments for each cell, one line
per row:
.PP
.Vb 4
\& kubectl get pods | ./tablizer \-o extended ./tablizer \-o shell
\& NAME="repldepl\-7bcd8d5b64\-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
\& NAME="repldepl\-7bcd8d5b64\-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
\& NAME="repldepl\-7bcd8d5b64\-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
.Ve
.PP
You can use this in an eval loop.
.PP
Beside normal ascii mode (the default) and extended mode there are
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
value file.
.SS "\s-1ENVIRONMENT VARIABLES\s0"
.IX Subsection "ENVIRONMENT VARIABLES"
\&\fBtablizer\fR supports certain environment variables which use can use
to influence program behavior. Commandline flags have always
precedence over environment variables.
.IP "<T_NO_HEADER_NUMBERING> \- disable numbering of header fields, like \fB\-n\fR." 4
.IX Item "<T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n."
.PD 0
.IP "<T_COLUMNS> \- comma separated list of columns to output, like \fB\-c\fR" 4
.IX Item "<T_COLUMNS> - comma separated list of columns to output, like -c"
.IP "<\s-1NO_COLORS\s0> \- disable colorization of matches, like \fB\-N\fR" 4
.IX Item "<NO_COLORS> - disable colorization of matches, like -N"
.PD
.SS "\s-1COMPLETION\s0"
.IX Subsection "COMPLETION"
Shell completion for command line options can be enabled by using the
\&\fB\-\-completion\fR flag. The required parameter is the name of your
shell. Currently supported are: bash, zsh, fish and powershell.
.PP
Detailed instructions:
.IP "Bash:" 4
.IX Item "Bash:"
.Vb 1
\& source <(tablizer \-\-completion bash)
.Ve
.Sp
To load completions for each session, execute once:
.Sp
.Vb 2
\& # Linux:
\& $ tablizer \-\-completion bash > /etc/bash_completion.d/tablizer
\&
\& # macOS:
\& $ tablizer \-\-completion bash > $(brew \-\-prefix)/etc/bash_completion.d/tablizer
.Ve
.IP "Zsh:" 4
.IX Item "Zsh:"
If shell completion is not already enabled in your environment,
you will need to enable it. You can execute the following once:
.Sp
.Vb 1
\& echo "autoload \-U compinit; compinit" >> ~/.zshrc
.Ve
.Sp
To load completions for each session, execute once:
.Sp
.Vb 1
\& $ tablizer \-\-completion zsh > "${fpath[1]}/_tablizer"
.Ve
.Sp
You will need to start a new shell for this setup to take effect.
.IP "fish:" 4
.IX Item "fish:"
.Vb 1
\& tablizer \-\-completion fish | source
.Ve
.Sp
To load completions for each session, execute once:
.Sp
.Vb 1
\& tablizer \-\-completion fish > ~/.config/fish/completions/tablizer.fish
.Ve
.IP "PowerShell:" 4
.IX Item "PowerShell:"
.Vb 1
\& tablizer \-\-completion powershell | Out\-String | Invoke\-Expression
.Ve
.Sp
To load completions for every new session, run:
.Sp
.Vb 1
\& tablizer \-\-completion powershell > tablizer.ps1
.Ve
.Sp
and source this file from your PowerShell profile.
.SH "CONFIGURATION AND COLORS"
.IX Header "CONFIGURATION AND COLORS"
YOu can put certain configuration values into a configuration file in
\&\s-1HCL\s0 format. By default tablizer looks for
\&\f(CW\*(C`$HOME/.config/tablizer/config\*(C'\fR, but you can provide one using the
parameter \f(CW\*(C`\-f\*(C'\fR.
.PP
In the configuration the following variables can be defined:
.PP
.Vb 8
\& BG = "lightGreen"
\& FG = "white"
\& HighlightBG = "lightGreen"
\& HighlightFG = "white"
\& NoHighlightBG = "white"
\& NoHighlightFG = "lightGreen"
\& HighlightHdrBG = "red"
\& HighlightHdrFG = "white"
.Ve
.PP
The following color definitions are available:
.PP
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
magenta, red, white, yellow
.PP
The Variables \fB\s-1FG\s0\fR and \fB\s-1BG\s0\fR are being used to highlight matches. The
other *FG and *BG variables are for colored table output (enabled with
the \f(CW\*(C`\-L\*(C'\fR parameter).
.PP
Colorization can be turned off completely either by setting the
parameter \f(CW\*(C`\-N\*(C'\fR or the environment variable \fB\s-1NO_COLOR\s0\fR to a true value.
.SH "BUGS"
.IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
<https://github.com/TLINDEN/tablizer/issues>.
.SH "LICENSE"
.IX Header "LICENSE"
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
.PP
Copyright (c) 2022\-2024 by Thomas von Dein
.PP
This software uses the following \s-1GO\s0 modules:
.IP "repr (https://github.com/alecthomas/repr)" 4
.IX Item "repr (https://github.com/alecthomas/repr)"
Released under the \s-1MIT\s0 License, Copyright (c) 2016 Alec Thomas
.IP "cobra (https://github.com/spf13/cobra)" 4
.IX Item "cobra (https://github.com/spf13/cobra)"
Released under the Apache 2.0 license, Copyright 2013\-2022 The Cobra Authors
.IP "dateparse (github.com/araddon/dateparse)" 4
.IX Item "dateparse (github.com/araddon/dateparse)"
Released under the \s-1MIT\s0 License, Copyright (c) 2015\-2017 Aaron Raddon
.IP "color (github.com/gookit/color)" 4
.IX Item "color (github.com/gookit/color)"
Released under the \s-1MIT\s0 License, Copyright (c) 2016 inhere
.IP "tablewriter (github.com/olekukonko/tablewriter)" 4
.IX Item "tablewriter (github.com/olekukonko/tablewriter)"
Released under the \s-1MIT\s0 License, Copyright (c) 201 by Oleku Konko
.IP "yaml (gopkg.in/yaml.v3)" 4
.IX Item "yaml (gopkg.in/yaml.v3)"
Released under the \s-1MIT\s0 License, Copyright (c) 2006\-2011 Kirill Simonov
.SH "AUTHORS"
.IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR

View File

@@ -7,27 +7,52 @@ tablizer - Manipulate tabular output of other programs
Usage: Usage:
tablizer [regex] [file, ...] [flags] tablizer [regex] [file, ...] [flags]
Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging -v, --invert-match select non-matching rows
-h, --help help for tablizer -n, --no-numbering Disable header numbering
-n, --no-numbering Disable header numbering -N, --no-color Disable pattern highlighting
-o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default) -H, --no-headers Disable headers display
-X, --extended Enable extended output -s, --separator string Custom field separator
-M, --markdown Enable markdown table output -k, --sort-by int Sort by column (default: 1)
-O, --orgtbl Enable org-mode table output -z, --fuzzy Use fuzzy search [experimental]
-s, --separator string Custom field separator -F, --filter field=reg Filter given field with regex, can be used multiple times
-v, --version Print program version -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
=head1 DESCRIPTION =head1 DESCRIPTION
Many programs generate tabular output. But sometimes you need to Many programs generate tabular output. But sometimes you need to
post-process these tables, you may need to remove one or more columns post-process these tables, you may need to remove one or more columns
or you may want to filter for some pattern or you may need the output or you may want to filter for some pattern (See L<PATTERNS>) or you
in another program and need to parse it somehow. Standard unix tools may need the output in another program and need to parse it somehow.
such as awk(1), grep(1) or column(1) may help, but sometimes it's a Standard unix tools such as awk(1), grep(1) or column(1) may help, but
tedious business. sometimes it's a tedious business.
Let's take the output of the tool kubectl. It contains cells with Let's take the output of the tool kubectl. It contains cells with
withespace and they do not separate columns by TAB characters. This is withespace and they do not separate columns by TAB characters. This is
@@ -35,14 +60,15 @@ not easy to process.
You can use B<tablizer> to do these and more things. You can use B<tablizer> to do these and more things.
B<tablizer> analyses the header fiels of a table, registers the column B<tablizer> analyses the header fields of a table, registers the
positions of each header field and separates columns by those column positions of each header field and separates columns by those
positions. positions.
Without any options it reads its input from C<STDIN>, but you can also Without any options it reads its input from C<STDIN>, but you can also
specify a file as a parameter. If you want to reduce the output by specify a file as a parameter. If you want to reduce the output by
some regular expression, just specify it as its first some regular expression, just specify it as its first parameter. You
parameters. Hence: may also use the B<-v> option to exclude all rows which match the
pattern. Hence:
# read from STDIN # read from STDIN
kubectl get pods | tablizer kubectl get pods | tablizer
@@ -62,7 +88,7 @@ have a numer associated with it, e.g.:
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which These numbers denote the column and you can use them to specify which
columns you want to have in your output: columns you want to have in your output (see L<COLUMNS>:
kubectl get pods | tablizer -c1,3 kubectl get pods | tablizer -c1,3
@@ -71,15 +97,160 @@ the original order.
The numbering can be suppressed by using the B<-n> option. The numbering can be suppressed by using the B<-n> option.
By default tablizer shows a header containing the names of each
column. This can be disabled using the B<-H> option. Be aware that
this only affects tabular output modes. Shell, Extended, Yaml and CSV
output modes always use the column names.
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:
=over
=item B<-a --sort-age>
Sorts duration strings like "1d4h32m51s".
=item B<-i --sort-numeric>
Sorts numeric fields.
=item B<-t --sort-time>
Sorts timestamps.
=back
Finally the B<-d> option enables debugging output which is mostly Finally the B<-d> option enables debugging output which is mostly
usefull for the developer. useful for the developer.
=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:
perldoc perlre
Or read it online: L<https://perldoc.perl.org/perlre>.
A note on modifiers: the regexp engine used in tablizer uses another
modifier syntax:
(?MODIFIER)
The most important modifiers are:
C<i> ignore case
C<m> multiline mode
C<s> single line mode
Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account"
You can 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).
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
display. By default tablizer numerizes the header names and these
numbers can be used to specify which header to display, see example
above.
However, beside numbers, you can also use regular expressions with
B<-c>, also separated by comma. And you can mix column numbers with
regexps.
Lets take this table:
PID TTY TIME CMD
14001 pts/0 00:00:00 bash
42871 pts/0 00:00:00 ps
42872 pts/0 00:00:00 sed
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
tablizer
sed
where "C" is our regexp which matches CMD.
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 =head2 OUTPUT MODES
There might be cases when the tabular output of a program is way too 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 large for your current terminal but you still need to see every
column. In such cases the B<-o extended> or B<-X> option can be column. In such cases the B<-o extended> or B<-X> option can be
usefull which enables I<extended mode>. In this mode, each row will be useful which enables I<extended mode>. In this mode, each row will be
printed vertically, header left, value right, aligned by the field printed vertically, header left, value right, aligned by the field
widths. Here's an example: widths. Here's an example:
@@ -106,7 +277,113 @@ You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table. table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding and CSV mode, which prints a comma separated
value file.
=head2 ENVIRONMENT VARIABLES
B<tablizer> supports certain environment variables which use can use
to influence program behavior. Commandline flags have always
precedence over environment variables.
=over
=item <T_NO_HEADER_NUMBERING> - disable numbering of header fields, like B<-n>.
=item <T_COLUMNS> - comma separated list of columns to output, like B<-c>
=item <NO_COLORS> - disable colorization of matches, like B<-N>
=back
=head2 COMPLETION
Shell completion for command line options can be enabled by using the
B<--completion> flag. The required parameter is the name of your
shell. Currently supported are: bash, zsh, fish and powershell.
Detailed instructions:
=over
=item Bash:
source <(tablizer --completion bash)
To load completions for each session, execute once:
# Linux:
$ tablizer --completion bash > /etc/bash_completion.d/tablizer
# macOS:
$ tablizer --completion bash > $(brew --prefix)/etc/bash_completion.d/tablizer
=item Zsh:
If shell completion is not already enabled in your environment,
you will need to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions for each session, execute once:
$ tablizer --completion zsh > "${fpath[1]}/_tablizer"
You will need to start a new shell for this setup to take effect.
=item fish:
tablizer --completion fish | source
To load completions for each session, execute once:
tablizer --completion fish > ~/.config/fish/completions/tablizer.fish
=item PowerShell:
tablizer --completion powershell | Out-String | Invoke-Expression
To load completions for every new session, run:
tablizer --completion powershell > tablizer.ps1
and source this file from your PowerShell profile.
=back
=head1 CONFIGURATION AND COLORS
YOu can put certain configuration values into a configuration file in
HCL format. By default tablizer looks for
C<$HOME/.config/tablizer/config>, but you can provide one using the
parameter C<-f>.
In the configuration the following variables can be defined:
BG = "lightGreen"
FG = "white"
HighlightBG = "lightGreen"
HighlightFG = "white"
NoHighlightBG = "white"
NoHighlightFG = "lightGreen"
HighlightHdrBG = "red"
HighlightHdrFG = "white"
The following color definitions are available:
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
magenta, red, white, yellow
The Variables B<FG> and B<BG> are being used to highlight matches. The
other *FG and *BG variables are for colored table output (enabled with
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 =head1 BUGS
@@ -118,9 +395,9 @@ L<https://github.com/TLINDEN/tablizer/issues>.
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3. This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2022 by Thomas von Dein Copyright (c) 2022-2024 by Thomas von Dein
This software uses the following GO libraries: This software uses the following GO modules:
=over 4 =over 4
@@ -132,6 +409,22 @@ Released under the MIT License, Copyright (c) 2016 Alec Thomas
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra Authors Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra Authors
=item dateparse (github.com/araddon/dateparse)
Released under the MIT License, Copyright (c) 2015-2017 Aaron Raddon
=item color (github.com/gookit/color)
Released under the MIT License, Copyright (c) 2016 inhere
=item tablewriter (github.com/olekukonko/tablewriter)
Released under the MIT License, Copyright (c) 201 by Oleku Konko
=item yaml (gopkg.in/yaml.v3)
Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov
=back =back
=head1 AUTHORS =head1 AUTHORS