Compare commits

...

50 Commits
v1.4.3 ... main

Author SHA1 Message Date
T. von Dein
46fde289f5 fix #46: check sort array index before using it for sorting (#48) 2025-12-08 22:30:09 +01:00
d331f0b2e6 not needed 2025-12-08 22:10:06 +01:00
T. von Dein
fc0352efa9 add support for template output mode with new option --templage <tmpl> (#47) 2025-12-08 22:01:15 +01:00
2bb0cdb0af fix link 2025-11-05 09:00:28 +01:00
fad8a59023 fix changelog link 2025-11-05 08:46:56 +01:00
T. von Dein
bd5ee90324 move to codeberg (#45) 2025-11-03 22:06:11 +01:00
bc717baa3f fix typo 2025-10-25 21:51:56 +02:00
c34f030914 add stew 2025-10-25 21:50:31 +02:00
T.v.Dein
f1aa9d0000 add json output mode (-J) (#87) 2025-10-14 07:18:30 +02:00
736dd37f16 fixed feature entry 2025-10-13 07:24:35 +02:00
e0dc6bb845 updated and added feature list 2025-10-13 07:23:54 +02:00
T.v.Dein
8bdb3db105 fix #85: add --auto-headers and --custom-headers (#86) 2025-10-10 13:08:16 +02:00
4ce6c30f54 fix short usage formatting 2025-10-09 23:16:07 +02:00
T.v.Dein
ec0b210167 add some handy builtin character classes as split separators (#84) 2025-10-09 23:03:57 +02:00
253ef8262e fix builder go version 2025-10-08 10:36:09 +02:00
da48994744 fix comment 2025-10-06 23:27:48 +02:00
39f06fddc8 md fix 2025-10-06 23:02:28 +02:00
T.v.Dein
50a9378d92 use column order of -c when specified (#81) 2025-10-06 22:55:04 +02:00
T.v.Dein
35b726fee4 Fix json parser (#80)
* fix #77: parse floats and nils as well and convert them to string
2025-10-06 22:54:31 +02:00
T.v.Dein
8c87da34f2 show short help with -h (#76) 2025-10-02 21:34:38 +02:00
dependabot[bot]
6f0f5afb27 Bump actions/setup-go from 5 to 6 (#68) 2025-10-01 21:16:48 +02:00
T.v.Dein
62b606e7da use 1.24 for CI (#75) 2025-10-01 21:14:18 +02:00
dependabot[bot]
567d23b175 Bump github.com/alecthomas/repr from 0.5.1 to 0.5.2 (#69) 2025-10-01 21:08:36 +02:00
dependabot[bot]
14f24533f0 Bump github.com/spf13/cobra from 1.9.1 to 1.10.1 (#70) 2025-10-01 21:04:25 +02:00
dependabot[bot]
4e413c02b5 Bump github.com/charmbracelet/bubbletea from 1.3.6 to 1.3.10 (#71) 2025-10-01 21:01:04 +02:00
dependabot[bot]
6d8c0c0936 Bump github.com/evertras/bubble-table from 0.19.0 to 0.19.2 (#72) 2025-10-01 20:57:58 +02:00
dependabot[bot]
21b607af7c Bump github.com/olekukonko/tablewriter from 1.0.9 to 1.1.0 (#73) 2025-10-01 20:54:31 +02:00
T.v.Dein
06a5d74fb6 Add JSON input support (#74)
* added basic json input support
* add coverage to make test
* enhanced unit tests, switch to testify/assert
* reduce ci runs
2025-10-01 20:48:49 +02:00
T.v.Dein
5f3f7c417c Improve ascii table, add --ofs flag, enhance documentation (#67)
* enhanced documentation
* added --ofs parameter
  use 2 spaces for ascii output but it is customizable with --ofs,
  which is also being used by CSV mode (whose defaults remains unchanged)
* improve ascii table output, use 2 spaces as OFS by default
2025-09-30 11:21:49 +02:00
T.v.Dein
687f4b7bb2 Fix excess spaces on normal ascii table output (#66) 2025-09-29 20:46:33 +02:00
24b66b8a6b mv demo to top 2025-09-11 19:12:49 +02:00
d87c6878a4 bump version 2025-09-11 19:04:38 +02:00
4cdc4c8e18 switched to vhs demo creator 2025-09-11 19:04:02 +02:00
9cb9a66332 fix #64: documented -r parameter 2025-09-11 19:01:11 +02:00
24277cd716 use stdout if it is a tty 2025-09-11 19:00:37 +02:00
e51b141032 bump version 2025-09-10 12:17:09 +02:00
7af7304529 header cosmetics 2025-09-10 12:16:53 +02:00
b4c833a0ba also style selected row 2025-09-10 12:13:34 +02:00
1c36d93d65 add gh-dash config 2025-09-09 22:06:07 +02:00
T.v.Dein
ec864f42d6 added styled help buffer (#63) 2025-09-09 22:01:45 +02:00
dependabot[bot]
4eaa676510 Bump github.com/gookit/color from 1.5.4 to 1.6.0 (#60) 2025-09-09 20:36:44 +02:00
dependabot[bot]
c600fb1136 Bump actions/checkout from 4 to 5 (#59) 2025-09-09 20:10:03 +02:00
dependabot[bot]
abf9fac5c7 Bump github.com/charmbracelet/bubbletea from 1.3.4 to 1.3.6 (#61) 2025-09-09 20:09:35 +02:00
T.v.Dein
80dd6849ae Add interactive filter table (#62) 2025-09-09 20:09:08 +02:00
e2b82515f5 bump version 2025-08-28 21:09:32 +02:00
T.v.Dein
1976b4046e Add interactive filter/selection tool (#58) 2025-08-28 21:08:28 +02:00
dependabot[bot]
b1a2b3059e Bump github.com/alecthomas/repr from 0.4.0 to 0.5.1 (#55) 2025-08-26 10:44:10 +02:00
dependabot[bot]
e3d6ef130c Bump github.com/hashicorp/hcl/v2 from 2.23.0 to 2.24.0 (#56) 2025-08-26 10:34:24 +02:00
dependabot[bot]
92fffaae9a Bump github.com/olekukonko/tablewriter from 1.0.6 to 1.0.9 (#54) 2025-08-26 10:28:54 +02:00
T.v.Dein
f1c5ee5797 replace pipe to less with bubbletea internal pager (#57) 2025-08-26 10:22:03 +02:00
51 changed files with 2755 additions and 933 deletions

96
.gh-dash.yml Normal file
View File

@@ -0,0 +1,96 @@
prSections:
- title: Responsible PRs
filters: repo:tlinden/tablizer is:open NOT dependabot
layout:
repoName:
hidden: true
- title: Responsible Dependabot PRs
filters: repo:tlinden/tablizer is:open dependabot
layout:
repoName:
hidden: true
issuesSections:
- title: Responsible Issues
filters: is:open repo:tlinden/tablizer -author:@me
layout:
repoName:
hidden: true
- title: Note-to-Self Issues
filters: is:open repo:tlinden/tablizer author:@me
layout:
creator:
hidden: true
repoName:
hidden: true
defaults:
preview:
open: false
width: 100
keybindings:
universal:
- key: "shift+down"
builtin: pageDown
- key: "shift+up"
builtin: pageUp
prs:
- key: g
name: gitu
command: >
cd {{.RepoPath}} && /home/scip/bin/gitu
- key: M
name: squash-merge
command: gh pr merge --rebase --squash --admin --repo {{.RepoName}} {{.PrNumber}}
- key: i
name: show ci checks
command: gh pr checks --repo {{.RepoName}} {{.PrNumber}} | glow -p
- key: e
name: edit pr
command: ~/.config/gh-dash/edit-gh-pr {{.RepoName}} {{.PrNumber}}
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
issues:
- key: v
name: view
command: gh issue view --repo {{.RepoName}} {{.IssueNumber}} | glow -p
- key: l
name: add label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --add-label $(gum choose bug enhancement question dependencies wontfix)
- key: L
name: remove label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --remove-label $(gum choose bug enhancement question dependencies wontfix)
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
theme:
ui:
sectionsShowCount: true
table:
compact: false
showSeparator: true
colors:
text:
primary: "#E2E1ED"
secondary: "#6770cb"
inverted: "#242347"
faint: "#b0793b"
warning: "#E0AF68"
success: "#3DF294"
background:
selected: "#1B1B33"
border:
primary: "#383B5B"
secondary: "#39386B"
faint: "#8d3e0b"
repoPaths:
:owner/:repo: ~/dev/:repo
pager:
diff: delta

View File

@@ -1,38 +0,0 @@
name: build-and-test-tablizer
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: ['1.23']
os: [ubuntu-latest, macos-latest, windows-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.23
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true

View File

@@ -1,87 +0,0 @@
name: build-release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
name: Build Release Assets
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22.11
- name: Build the executables
run: ./mkrel.sh tablizer ${{ github.ref_name}}
- name: List the executables
run: ls -l ./releases
- name: Upload the binaries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
file: ./releases/*
file_glob: true
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
mode: "PR"
configurationJson: |
{
"template": "#{{CHANGELOG}}\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
"pr_template": "- #{{TITLE}} (##{{NUMBER}}) by #{{AUTHOR}}\n#{{BODY}}",
"empty_template": "- no changes",
"categories": [
{
"title": "## New Features",
"labels": ["add", "feature"]
},
{
"title": "## Bug Fixes",
"labels": ["fix", "bug", "revert"]
},
{
"title": "## Documentation Enhancements",
"labels": ["doc"]
},
{
"title": "## Refactoring Efforts",
"labels": ["refactor"]
},
{
"title": "## Miscellaneus Changes",
"labels": []
}
],
"ignore_labels": [
"duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix"
],
"label_extractor": [
{
"pattern": "(.) (.+)",
"target": "$1"
},
{
"pattern": "(.) (.+)",
"target": "$1",
"on_property": "title"
}
]
}
- name: Create Release
uses: softprops/action-gh-release@v2
with:
body: ${{steps.github_release.outputs.changelog}}

69
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,69 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: "docs/*"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/tablizer/compare/{{ .PreviousTag }}...{{ .Tag }})

36
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,36 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.24
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
linter:
when:
event: [push]
image: golang:${goversion}
commands:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
- golangci-lint --version
- golangci-lint run ./...
depends_on: [build]
test:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go test -v -cover
depends_on: [build,linter]

15
.woodpecker/release.yaml Normal file
View File

@@ -0,0 +1,15 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -4,9 +4,9 @@ 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
## [v1.0.14](https://codeberg.org/scip/tablizer/tree/v1.0.14) - 2023-01-23
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.13...v1.0.14)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.13...v1.0.14)
### Fixed
@@ -28,9 +28,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
- updated dependencies (go module versions)
## [v1.0.13](https://github.com/TLINDEN/tablizer/tree/v1.0.13) - 2022-11-03
## [v1.0.13](https://codeberg.org/scip/tablizer/tree/v1.0.13) - 2022-11-03
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.12...v1.0.13)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.12...v1.0.13)
### Added
@@ -47,9 +47,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
manpage.
## [v1.0.12](https://github.com/TLINDEN/tablizer/tree/v1.0.12) - 2022-10-25
## [v1.0.12](https://codeberg.org/scip/tablizer/tree/v1.0.12) - 2022-10-25
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.11...v1.0.12)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.11...v1.0.12)
### Added
@@ -72,14 +72,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
### Fixed
- Fixed [Bug #5](https://github.com/TLINDEN/tablizer/issues/5), where
- Fixed [Bug #5](https://codeberg.org/scip/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
## [v1.0.11](https://codeberg.org/scip/tablizer/tree/v1.0.11) - 2022-10-19
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.10...v1.0.11)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.10...v1.0.11)
### Added
@@ -101,9 +101,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.10](https://github.com/TLINDEN/tablizer/tree/v1.0.10) - 2022-10-15
## [v1.0.10](https://codeberg.org/scip/tablizer/tree/v1.0.10) - 2022-10-15
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.9...v1.0.10)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.9...v1.0.10)
### Added
@@ -123,9 +123,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.9](https://github.com/TLINDEN/tablizer/tree/v1.0.9) - 2022-10-14
## [v1.0.9](https://codeberg.org/scip/tablizer/tree/v1.0.9) - 2022-10-14
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.8...v1.0.9)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.8...v1.0.9)
### Added
@@ -137,9 +137,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.8](https://github.com/TLINDEN/tablizer/tree/v1.0.8) - 2022-10-13
## [v1.0.8](https://codeberg.org/scip/tablizer/tree/v1.0.8) - 2022-10-13
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.7...v1.0.8)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.7...v1.0.8)
### Added
@@ -147,9 +147,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.7](https://github.com/TLINDEN/tablizer/tree/v1.0.7) - 2022-10-11
## [v1.0.7](https://codeberg.org/scip/tablizer/tree/v1.0.7) - 2022-10-11
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.6...v1.0.7)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.6...v1.0.7)
### Added
@@ -165,9 +165,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.6](https://github.com/TLINDEN/tablizer/tree/v1.0.6) - 2022-10-05
## [v1.0.6](https://codeberg.org/scip/tablizer/tree/v1.0.6) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.5...v1.0.6)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.5...v1.0.6)
### Added
@@ -183,9 +183,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.5](https://github.com/TLINDEN/tablizer/tree/v1.0.5) - 2022-10-05
## [v1.0.5](https://codeberg.org/scip/tablizer/tree/v1.0.5) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.4...v1.0.5)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.4...v1.0.5)
### Added
@@ -203,9 +203,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.4](https://github.com/TLINDEN/tablizer/tree/v1.0.4) - 2022-10-04
## [v1.0.4](https://codeberg.org/scip/tablizer/tree/v1.0.4) - 2022-10-04
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.3...v1.0.4)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.3...v1.0.4)
### Added
@@ -221,9 +221,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.3](https://github.com/TLINDEN/tablizer/tree/v1.0.3) - 2022-10-03
## [v1.0.3](https://codeberg.org/scip/tablizer/tree/v1.0.3) - 2022-10-03
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.2...v1.0.3)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.2...v1.0.3)
### Added
@@ -237,9 +237,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.2](https://github.com/TLINDEN/tablizer/tree/v1.0.2) - 2022-10-02
## [v1.0.2](https://codeberg.org/scip/tablizer/tree/v1.0.2) - 2022-10-02
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.1...v1.0.2)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.1...v1.0.2)
### Added
@@ -255,9 +255,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.1](https://github.com/TLINDEN/tablizer/tree/v1.0.1) - 2022-09-30
## [v1.0.1](https://codeberg.org/scip/tablizer/tree/v1.0.1) - 2022-09-30
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.0...v1.0.1)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.0...v1.0.1)
### Added
@@ -271,8 +271,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [v1.0.0](https://github.com/TLINDEN/tablizer/tree/v1.0.0) - 2022-09-28
## [v1.0.0](https://codeberg.org/scip/tablizer/tree/v1.0.0) - 2022-09-28
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/02a64a5c3fe4220df2c791ff1421d16ebd428c19...v1.0.0)
[Full Changelog](https://codeberg.org/scip/tablizer/compare/02a64a5c3fe4220df2c791ff1421d16ebd428c19...v1.0.0)
Initial release.

View File

@@ -50,7 +50,7 @@ ifdef HAVE_POD
endif
buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"
go build -ldflags "-X 'codeberg.org/scip/tablizer/cfg.VERSION=$(VERSION)'"
release:
gh release create $(version) --generate-notes
@@ -65,11 +65,11 @@ clean:
rm -rf $(tool) releases coverage.out
test: clean
go test ./... $(OPTS)
go test -count=1 -cover ./... $(OPTS)
singletest:
@echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'"
go test -run $(TEST) github.com/tlinden/tablizer/$(MOD) $(OPTS)
go test -run $(TEST) codeberg.org/scip/tablizer/$(MOD) $(OPTS)
cover-report:
go test ./... -cover -coverprofile=coverage.out

20
Makefile.dist Normal file
View File

@@ -0,0 +1,20 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -1,30 +1,61 @@
[![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)
[![status-badge](https://ci.codeberg.org/api/badges/15519/status.svg)](https://ci.codeberg.org/repos/15519)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/tablizer/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/tablizer)](https://goreportcard.com/report/codeberg.org/scip/tablizer)
## tablizer - Manipulate tabular output of other programs
Tablizer can be used to re-format tabular output of other
programs. While you could do this using standard unix tools, in some
cases it's a hard job.
cases it's a hard job. With tablizer you can filter by column[s],
ignore certain column[s] by regex, name or number. It can output the
tabular data in a range of formats (see below). There's even an
interactive filter/selection tool available.
## FEATURES
- supports csv, json or ascii format input from files or stdin
- split any tabular input data by character or regular expression into columns
- add headers if input data doesn't contain them (automatically or manually)
- print tabular data as ascii table, org-mode, markdown, csv, shell-evaluable or yaml format
- filter rows by regular expression (saves a call to `| grep ...`)
- filter rows by column filter
- filters may also be negations eg `-Fname!=cow.*` or `-v`
- modify cells wih regular expressions
- reduce columns by specifying which columns to show, with regex support
- color support
- sort by any field[s], multiple sort modes are supported
- shell completion for options
- regular used options can be put into a config file
- filter TUI where where you can interactively sort and filter rows
## Demo
![demo cast](vhsdemo/demo.gif)
## Usage
Usage:
```default
Usage:
tablizer [regex] [file, ...] [flags]
tablizer [regex,...] [file, ...] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows
-n, --no-numbering Disable header numbering
-n, --numbering Enable 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)
-s, --separator <string> Custom field separator
-k, --sort-by <int|name> Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-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
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
--auto-headers Generate headers if there are none present in input
--custom-headers a,b,... Use custom headers, separated by comma
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -37,6 +68,7 @@ Output Flags (mutually exclusive):
-L, --hightlight-lines Use alternating background colors for tables
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
--ofs <char> Output field separator, used by -A and -C.
Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string
@@ -45,6 +77,7 @@ Sort Mode Flags (mutually exclusive):
-t, --sort-time sort according to time string
Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
@@ -67,13 +100,13 @@ to do this with tablizer:
```
% kubectl get pods | tablizer
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
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
% kubectl get pods | tablizer -c 1,3
NAME(1) STATUS(3)
NAME STATUS
repldepl-7bcd8d5b64-7zq4l Running
repldepl-7bcd8d5b64-m48n8 Running
repldepl-7bcd8d5b64-q2bf4 Running
@@ -111,14 +144,14 @@ You can also specify a regex pattern to reduce the output:
```
% kubectl get pods | tablizer q2bf4
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
NAME READY STATUS RESTARTS AGE
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
% kubectl get pods | tablizer -Fname=2
NAME READY STATUS RESTARTS AGE
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
```
@@ -132,7 +165,7 @@ You can also use it to modify certain cells using regular expression
matching. For example:
```shell
kubectl get pods | tablizer -n -T4 -R '/ /-/'
kubectl get pods | tablizer -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
@@ -143,17 +176,23 @@ 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 '| |-|'`.
There's also an interactive mode, invoked with the option B<-I>, where
you can interactively filter and select rows:
<img width="937" height="293" alt="interactive" src="https://github.com/user-attachments/assets/0d4d65e2-d156-43ed-8021-39047c7939ed" />
## Demo
[![asciicast](demo/tablizer-demo.gif)](https://asciinema.org/a/9FKc3HPnlg8D2X8otheleEa9t)
## Installation
There are multiple ways to install **tablizer**:
- Go to the [latest release page](https://github.com/tlinden/tablizer/releases/latest),
- You can use [stew](https://github.com/marwanhawari/stew) to install tablizer:
```default
stew install tlinden/tablizer
```
- Go to the [latest release page](https://codeberg.org/scip/tablizer/releases),
locate the binary for your operating system and platform.
Download it and put it into some directory within your `$PATH` variable.
@@ -166,7 +205,7 @@ There are multiple ways to install **tablizer**:
- You can also install from source. Issue the following commands in your shell:
```
git clone https://github.com/TLINDEN/tablizer.git
git clone https://codeberg.org/scip/tablizer.git
cd tablizer
make
sudo make install
@@ -178,10 +217,9 @@ hesitate to ask me about it, I'll add it.
## Documentation
The documentation is provided as a unix man-page. It will be
automatically installed if you install from source. However, you can
read the man-page online:
automatically installed if you install from source.
https://github.com/TLINDEN/tablizer/blob/main/tablizer.pod
[However, you can read the man-page online](https://codeberg.org/scip/tablizer/raw/branch/main/tablizer.pod).
Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc tablizer.pod`.
@@ -198,7 +236,7 @@ that's the best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
https://github.com/TLINDEN/tablizer/issues.
https://codeberg.org/scip/tablizer/issues.
## Prior Art
@@ -245,4 +283,4 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage
https://github.com/TLINDEN/tablizer
https://codeberg.org/scip/tablizer

View File

@@ -27,13 +27,26 @@ import (
"github.com/hashicorp/hcl/v2/hclsimple"
)
const DefaultSeparator string = `(\s\s+|\t)`
const Version string = "v1.4.3"
const MAXPARTS = 2
const (
Version = "v1.5.12"
MAXPARTS = 2
)
var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
var (
DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
VERSION string // maintained by -x
var VERSION string // maintained by -x
SeparatorTemplates = map[string]string{
":tab:": `\s*\t\s*`, // tab but eats spaces around
":spaces:": `\s{2,}`, // 2 or more spaces
":pipe:": `\s*\|\s*`, // one pipe eating spaces around
":default:": `(\s\s+|\t)`, // 2 or more spaces or tab
":nonword:": `\W`, // word boundary
":nondigit:": `\D`, // same for numbers
":special:": `[\*\+\-_\(\)\[\]\{\}?\\/<>=&$§"':,\^]+`, // match any special char
":nonprint:": `[[:^print:]]+`, // non printables
}
)
// public config, set via config file or using defaults
type Settings struct {
@@ -78,6 +91,11 @@ type Config struct {
Patterns []*Pattern
UseFuzzySearch bool
UseHighlight bool
Interactive bool
InputJSON bool
AutoHeaders bool
CustomHeaders []string
Template string
SortMode string
SortDescending bool
@@ -111,6 +129,8 @@ type Config struct {
// -r <file>
InputFile string
OFS string
}
// maps outputmode short flags to output mode, ie. -O => -o orgtbl
@@ -122,6 +142,8 @@ type Modeflag struct {
Y bool
A bool
C bool
J bool
P bool // template
}
// used for switching printers
@@ -133,6 +155,8 @@ const (
Yaml
CSV
ASCII
Json
Template
)
// various sort types
@@ -271,6 +295,10 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) {
conf.OutputMode = Yaml
case flag.C:
conf.OutputMode = CSV
case flag.J:
conf.OutputMode = Json
case conf.Template != "":
conf.OutputMode = Template
default:
conf.OutputMode = ASCII
}
@@ -352,6 +380,13 @@ func (conf *Config) ApplyDefaults() {
if conf.OutputMode == Yaml || conf.OutputMode == CSV {
conf.Numbering = false
}
if conf.Separator[0] == ':' && conf.Separator[len(conf.Separator)-1] == ':' {
separator, ok := SeparatorTemplates[conf.Separator]
if ok {
conf.Separator = separator
}
}
}
func (conf *Config) PreparePattern(patterns []*Pattern) error {
@@ -389,6 +424,12 @@ func (conf *Config) PreparePattern(patterns []*Pattern) error {
return nil
}
func (conf *Config) PrepareCustomHeaders(custom string) {
if len(custom) > 0 {
conf.CustomHeaders = strings.Split(custom, ",")
}
}
// 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 {

View File

@@ -21,6 +21,8 @@ import (
"fmt"
// "reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPrepareModeFlags(t *testing.T) {
@@ -44,9 +46,8 @@ func TestPrepareModeFlags(t *testing.T) {
conf := Config{}
conf.PrepareModeFlags(testdata.flag)
if conf.OutputMode != testdata.expect {
t.Errorf("got: %d, expect: %d", conf.OutputMode, testdata.expect)
}
assert.EqualValues(t, testdata.expect, conf.OutputMode)
})
}
}
@@ -70,9 +71,7 @@ func TestPrepareSortFlags(t *testing.T) {
conf.PrepareSortFlags(testdata.flag)
if conf.SortMode != testdata.expect {
t.Errorf("got: %s, expect: %s", conf.SortMode, testdata.expect)
}
assert.EqualValues(t, testdata.expect, conf.SortMode)
})
}
}
@@ -81,7 +80,7 @@ func TestPreparePattern(t *testing.T) {
var tests = []struct {
patterns []*Pattern
name string
wanterr bool
wanterror bool
wanticase bool
wantneg bool
}{
@@ -123,16 +122,16 @@ func TestPreparePattern(t *testing.T) {
}
for _, testdata := range tests {
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", testdata.name, testdata.wanterr)
testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", testdata.name, testdata.wanterror)
t.Run(testname, func(t *testing.T) {
conf := Config{}
err := conf.PreparePattern(testdata.patterns)
if err != nil {
if !testdata.wanterr {
t.Errorf("PreparePattern returned error: %s", err)
}
if testdata.wanterror {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}

View File

@@ -17,37 +17,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"slices"
"strings"
"codeberg.org/scip/tablizer/cfg"
"codeberg.org/scip/tablizer/lib"
"github.com/spf13/cobra"
"github.com/tlinden/tablizer/cfg"
"github.com/tlinden/tablizer/lib"
)
func man() {
man := exec.Command("less", "-")
var buffer bytes.Buffer
buffer.Write([]byte(manpage))
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":
@@ -79,6 +59,7 @@ func Execute() {
ShowCompletion string
modeflag cfg.Modeflag
sortmode cfg.Sortmode
headers string
)
var rootCmd = &cobra.Command{
@@ -94,7 +75,7 @@ func Execute() {
}
if ShowManual {
man()
lib.Pager("tablizer manual page", manpage)
return
}
@@ -111,6 +92,7 @@ func Execute() {
conf.CheckEnv()
conf.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode)
conf.PrepareCustomHeaders(headers)
wrapE(conf.PrepareFilters())
@@ -143,7 +125,7 @@ func Execute() {
"Use alternating background colors")
rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "",
"Display completion code")
rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator,
rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.SeparatorTemplates[":default:"],
"Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "",
"Only show the speficied columns (separated by ,)")
@@ -151,6 +133,16 @@ func Execute() {
"Yank the speficied columns (separated by ,) to the clipboard")
rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "",
"Transpose the speficied columns (separated by ,)")
rootCmd.PersistentFlags().BoolVarP(&conf.Interactive, "interactive", "I", false,
"interactive mode")
rootCmd.PersistentFlags().StringVarP(&conf.OFS, "ofs", "o", "",
"Output field separator (' ' for ascii table, ',' for CSV)")
rootCmd.PersistentFlags().BoolVarP(&conf.InputJSON, "json", "j", false,
"JSON input mode")
rootCmd.PersistentFlags().BoolVarP(&conf.AutoHeaders, "auto-headers", "g", false,
"Generate headers automatically")
rootCmd.PersistentFlags().StringVarP(&headers, "custom-headers", "x", "",
"Custom headers")
// sort options
rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "",
@@ -179,12 +171,16 @@ func Execute() {
"Enable shell mode output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false,
"Enable yaml output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.J, "jsonout", "J", false,
"Enable json 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")
rootCmd.PersistentFlags().StringVarP(&conf.Template, "template", "P", "",
"template for template output mode")
// config file
rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile,
@@ -202,6 +198,11 @@ func Execute() {
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")
if slices.Contains(os.Args, "-h") {
fmt.Println(shortusage)
os.Exit(0)
}
err := rootCmd.Execute()
if err != nil {
os.Exit(1)

18
cmd/shortusage.go Normal file
View File

@@ -0,0 +1,18 @@
package cmd
const shortusage = `tablizer [regex,...] [-r file] [flags]
-c col,... show specified columns -L highlight matching lines
-k col,... sort by specified columns -j read JSON input
-F col=reg filter field with regexp -v invert match
-T col,... transpose specified columns -n numberize columns
-R /from/to/ apply replacement to columns in -T -N do not use colors
-y col,... yank columns to clipboard -H do not show headers
--ofs char output field separator -s specify field separator
-r file read input from file -z use fuzzy search
-f file read config from file -I interactive filter mode
-x col,... use custom headers -d debug
-o char use char as output separator -g auto generate headers
-O org -C CSV -M md -X ext -S shell -Y yaml -J json -P template
-a sort by age -i sort numerically -t sort by time -D sort descending order
-m show manual -v show version --help show detailed help`

View File

@@ -6,7 +6,7 @@ NAME
SYNOPSIS
Usage:
tablizer [regex,...] [file, ...] [flags]
tablizer [regex,...] [-r file] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
@@ -14,12 +14,16 @@ SYNOPSIS
-n, --numbering Enable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-k, --sort-by int|name Sort by column (default: 1)
-s, --separator <string> Custom field separator (maybe char, string or :class:)
-k, --sort-by <int|name> Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-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
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
-g, --auto-headers Generate headers if there are none present in input
-x, --custom-headers a,b,... Use custom headers, separated by comma
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -27,9 +31,12 @@ SYNOPSIS
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-J, --jsonout Enable JSON output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-P, --template <tpl> Enable template mode with template <tpl>
-L, --hightlight-lines Use alternating background colors for tables
-o, --ofs <char> Output field separator, used by -A and -C.
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
@@ -40,6 +47,7 @@ SYNOPSIS
-t, --sort-time sort according to time string
Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
@@ -70,31 +78,37 @@ DESCRIPTION
use the -v option to exclude all rows which match the pattern. Hence:
# read from STDIN
kubectl get pods | tablizer
> kubectl get pods | tablizer
# read a file
tablizer filename
> tablizer -r filename
# search for pattern in a file (works like grep)
tablizer regex filename
> tablizer regex -r filename
# search for pattern in STDIN
kubectl get pods | tablizer regex
> 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.:
The output looks like the original one. You can add the option -n, then
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
> 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.
However, you may also just use the header names instead of numbers, eg:
> kubectl get pods | tablizer -cname,status
You can also use regular expressions with -c, eg:
> kubectl get pods | tablizer -c '[ae]'
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
@@ -131,6 +145,57 @@ DESCRIPTION
Finally the -d option enables debugging output which is mostly useful
for the developer.
SEPARATOR
The option -s can be a single character, in which case the CSV parser
will be invoked. You can also specify a string as separator. The string
will be interpreted as literal string unless it is a valid go regular
expression. For example:
-s '\t{2,}\'
is being used as a regexp and will match two or more consecutive tabs.
-s 'foo'
on the other hand is no regular expression and will be used literally.
To make live easier, there are a couple of predefined regular
expressions, which you can specify as classes:
* :tab:
Matches a tab and eats spaces around it.
* :spaces:
Matches 2 or more spaces.
* :pipe:
Matches a pipe character and eats spaces around it.
* :default:
Matches 2 or more spaces or tab. This is the default separator if
none is specified.
* :nonword:
Matches a non-word character.
* :nondigit:
Matches a non-digit character.
* :special:
Matches one or more special chars like brackets, dollar sign,
slashes etc.
* :nonprint:
Matches one or more non-printable characters.
PATTERNS AND FILTERING
You can reduce the rows being displayed by using one or more regular
expression patterns. The regexp language being used is the one of
@@ -154,7 +219,7 @@ DESCRIPTION
Example for a case insensitive search:
kubectl get pods -A | tablizer "/account/i"
> kubectl get pods -A | tablizer "/account/i"
If you use the "!" flag, then the regex match will be negated, that is,
if a line in the input matches the given regex, but "!" is supplied,
@@ -189,6 +254,20 @@ DESCRIPTION
If the option -v is specified, the filtering is inverted.
INTERACTIVE FILTERING
You can also use the interactive mode, enabled with "-I" to filter and
select rows. This mode is complementary, that is, other filter options
are still being respected.
To enter e filter, hit "/", enter a filter string and finish with
"ENTER". Use "SPACE" to select/deselect rows, use "a" to select all
(visible) rows.
Commit your selection with "q". The selected rows are being fed to the
requested output mode as usual. Abort with "CTRL-c", in which case the
results of the interactive mode are being ignored and all rows are being
fed to output.
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
@@ -206,7 +285,7 @@ DESCRIPTION
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
> ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
@@ -237,7 +316,7 @@ DESCRIPTION
Example:
cat t/testtable2
> cat t/testtable2
NAME DURATION
x 10
a 100
@@ -245,7 +324,7 @@ DESCRIPTION
u 4
k 6
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
> cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
NAME DURATION
x 40
a 400
@@ -261,7 +340,7 @@ DESCRIPTION
header left, value right, aligned by the field widths. Here's an
example:
kubectl get pods | ./tablizer -o extended
> kubectl get pods | ./tablizer -o extended
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
@@ -274,7 +353,7 @@ DESCRIPTION
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
> 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"
@@ -286,6 +365,18 @@ DESCRIPTION
markdown which prints a Markdown table, yaml, which prints yaml encoding
and CSV mode, which prints a comma separated value file.
A special output mode ist the Template mode, activated with the option
"--template". Template language is the Golang template language:
<https://pkg.go.dev/text/template>. You can also use lot's of additional
functions from: <https://masterminds.github.io/sprig/>. Here's an
example:
> kubectl get pods | tablizer --template "{{.name}} is {{.status}}"
alertmanager-kube-prometheus-alertmanager-0 is Running
grafana-fcc54cbc9-bk7s8 is Running
You can use header names as variables.
PUT FIELDS TO CLIPBOARD
You can let tablizer put fields to the clipboard using the option "-y".
This best fits the use-case when the result of your filtering yields
@@ -386,7 +477,7 @@ CONFIGURATION AND COLORS
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>.
<https://codeberg.org/scip/tablizer/issues>.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
@@ -416,6 +507,9 @@ LICENSE
Released under the MIT License, Copyright (c) 2006-2011 Kirill
Simonov
bubble-table (https://github.com/Evertras/bubble-table)
Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
AUTHORS
Thomas von Dein tom AT vondein DOT org
@@ -423,7 +517,7 @@ AUTHORS
var usage = `
Usage:
tablizer [regex,...] [file, ...] [flags]
tablizer [regex,...] [-r file] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
@@ -431,12 +525,16 @@ Operational Flags:
-n, --numbering Enable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-k, --sort-by int|name Sort by column (default: 1)
-s, --separator <string> Custom field separator (maybe char, string or :class:)
-k, --sort-by <int|name> Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-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
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
-g, --auto-headers Generate headers if there are none present in input
-x, --custom-headers a,b,... Use custom headers, separated by comma
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -444,9 +542,12 @@ Output Flags (mutually exclusive):
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-J, --jsonout Enable JSON output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-P, --template <tpl> Enable template mode with template <tpl>
-L, --hightlight-lines Use alternating background colors for tables
-o, --ofs <char> Output field separator, used by -A and -C.
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
@@ -457,6 +558,7 @@ Sort Mode Flags (mutually exclusive):
-t, --sort-time sort according to time string
Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging

View File

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

View File

@@ -1,31 +0,0 @@
#!/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')

View File

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

View File

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

View File

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

View File

@@ -1,119 +0,0 @@
{"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"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

65
go.mod
View File

@@ -1,43 +1,72 @@
module github.com/tlinden/tablizer
module codeberg.org/scip/tablizer
go 1.23.0
toolchain go1.23.5
go 1.24.0
require (
github.com/alecthomas/repr v0.4.0
github.com/alecthomas/repr v0.5.2
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/gookit/color v1.5.4
github.com/hashicorp/hcl/v2 v2.23.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/evertras/bubble-table v0.19.2
github.com/gookit/color v1.6.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/olekukonko/tablewriter v1.0.6
github.com/mattn/go-isatty v0.0.20
github.com/olekukonko/tablewriter v1.1.0
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/cobra v1.9.1
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
github.com/tiagomelo/go-clipboard v0.1.2
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.13.3 // indirect
github.com/zclconf/go-cty v1.16.3 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.26.0 // indirect
)

140
go.sum
View File

@@ -1,70 +1,102 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
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/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw=
github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/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/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.6-0.20250511102614-9564773e9d27 h1:LgDwLQDELPB6wMOx1x4DSXnH2pjQNDKFgqv2inJuiAU=
github.com/olekukonko/ll v0.0.6-0.20250511102614-9564773e9d27/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
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 v1.0.2 h1:nxz/j28kPYQUuc4veIv3Ymmef7gHKn8rhr42aauENnk=
github.com/olekukonko/tablewriter v1.0.2/go.mod h1:eUa4ArVhHJYomS27xrJB/GyLtnzKKVkZeLM6/MNO+pA=
github.com/olekukonko/tablewriter v1.0.4 h1:Lnz32TW+q/MQhA4qwhIyLA+j5hZ3dcNpZrcpPC+4iaM=
github.com/olekukonko/tablewriter v1.0.4/go.mod h1:eUa4ArVhHJYomS27xrJB/GyLtnzKKVkZeLM6/MNO+pA=
github.com/olekukonko/tablewriter v1.0.6 h1:/T45mIHc5hcEvibgzBzvMy7ruT+RjgoQRvkHbnl6OWA=
github.com/olekukonko/tablewriter v1.0.6/go.mod h1:SJ0MV1aHb/89fLcsBMXMp30Xg3g5eGoOUu0RptEk4AU=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -72,27 +104,33 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tiagomelo/go-clipboard v0.1.2 h1:Ph2icR0vZRIj3v5ExvsGweBwsbbDUTlS6HoF40MkQD8=
github.com/tiagomelo/go-clipboard v0.1.2/go.mod h1:kXtjJBIMimZaGbxmcKZ8+JqK+acSNf5tAJiChlZBOr8=
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.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 v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
@@ -104,20 +142,18 @@ 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20210809222454-d867a43fc93e/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@@ -126,8 +162,8 @@ 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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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=

View File

@@ -23,7 +23,7 @@ import (
"strings"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
/*

View File

@@ -19,10 +19,10 @@ package lib
import (
"fmt"
"reflect"
"testing"
"github.com/tlinden/tablizer/cfg"
"github.com/stretchr/testify/assert"
"codeberg.org/scip/tablizer/cfg"
)
func TestMatchPattern(t *testing.T) {
@@ -56,13 +56,11 @@ func TestMatchPattern(t *testing.T) {
}
err := conf.PreparePattern(inputdata.patterns)
if err != nil {
t.Errorf("PreparePattern returned error: %s", err)
}
if !matchPattern(conf, inputdata.line) {
t.Errorf("matchPattern() did not match\nExp: true\nGot: false\n")
}
assert.NoError(t, err)
res := matchPattern(conf, inputdata.line)
assert.EqualValues(t, true, res)
})
}
}
@@ -163,14 +161,12 @@ func TestFilterByFields(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)
}
assert.NoError(t, 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)
}
assert.EqualValues(t, inputdata.expect, *data)
})
}
}

View File

@@ -22,24 +22,14 @@ import (
"fmt"
"os"
"regexp"
"sort"
"slices"
"strconv"
"strings"
"github.com/gookit/color"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
func contains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func findindex(s []int, e int) (int, bool) {
for i, a := range s {
if a == e {
@@ -172,48 +162,32 @@ func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) {
}
}
// 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))
// deduplicate columns, preserve order
deduped := []int{}
for _, i := range usecolumns {
imap[i] = 0
if !slices.Contains(deduped, i) {
deduped = append(deduped, i)
}
}
// fill with deduplicated columns
usecolumns = nil
for k := range imap {
usecolumns = append(usecolumns, k)
}
sort.Ints(usecolumns)
return usecolumns, nil
return deduped, nil
}
// prepare headers: add numbers to headers
func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
numberedHeaders := []string{}
numberedHeaders := make([]string, len(data.headers))
maxwidth := 0 // start from scratch, so we only look at displayed column widths
// add numbers to headers if needed, get widest cell width
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.Numbering {
numhead := fmt.Sprintf("%s(%d)", head, idx+1)
headlen = len(numhead)
numberedHeaders = append(numberedHeaders, numhead)
newhead := fmt.Sprintf("%s(%d)", head, idx+1)
numberedHeaders[idx] = newhead
headlen = len(newhead)
} else {
numberedHeaders = append(numberedHeaders, head)
headlen = len(head)
}
@@ -222,7 +196,24 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
}
}
if conf.Numbering {
data.headers = numberedHeaders
}
if len(conf.UseColumns) > 0 {
// re-align headers based on user requested column list
headers := make([]string, len(conf.UseColumns))
for i, col := range conf.UseColumns {
for idx := range data.headers {
if col-1 == idx {
headers[i] = data.headers[col-1]
}
}
}
data.headers = headers
}
if data.maxwidthHeader != maxwidth && maxwidth > 0 {
data.maxwidthHeader = maxwidth
@@ -234,18 +225,18 @@ func reduceColumns(conf cfg.Config, data *Tabdata) {
if len(conf.Columns) > 0 {
reducedEntries := [][]string{}
for _, entry := range data.entries {
var reducedEntry []string
for _, entry := range data.entries {
reducedEntry = nil
for i, value := range entry {
if !contains(conf.UseColumns, i+1) {
continue
}
for _, col := range conf.UseColumns {
col--
for idx, value := range entry {
if idx == col {
reducedEntry = append(reducedEntry, value)
}
}
}
reducedEntries = append(reducedEntries, reducedEntry)
}

View File

@@ -19,10 +19,11 @@ package lib
import (
"fmt"
"reflect"
"slices"
"testing"
"github.com/tlinden/tablizer/cfg"
"github.com/stretchr/testify/assert"
"codeberg.org/scip/tablizer/cfg"
)
func TestContains(t *testing.T) {
@@ -38,10 +39,9 @@ func TestContains(t *testing.T) {
for _, tt := range tests {
testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want)
t.Run(testname, func(t *testing.T) {
answer := contains(tt.list, tt.search)
if answer != tt.want {
t.Errorf("got %t, want %t", answer, tt.want)
}
answer := slices.Contains(tt.list, tt.search)
assert.EqualValues(t, tt.want, answer)
})
}
}
@@ -73,18 +73,17 @@ func TestPrepareColumns(t *testing.T) {
}
for _, testdata := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", testdata.input, testdata.wanterror)
testname := fmt.Sprintf("PrepareColumns-%s-%t",
testdata.input, testdata.wanterror)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{Columns: testdata.input}
err := PrepareColumns(&conf, &data)
if err != nil {
if !testdata.wanterror {
t.Errorf("got error: %v", err)
}
if testdata.wanterror {
assert.Error(t, err)
} else {
if !reflect.DeepEqual(conf.UseColumns, testdata.exp) {
t.Errorf("got: %v, expected: %v", conf.UseColumns, testdata.exp)
}
assert.NoError(t, err)
assert.EqualValues(t, testdata.exp, conf.UseColumns)
}
})
}
@@ -153,18 +152,13 @@ func TestPrepareTransposerColumns(t *testing.T) {
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)
}
if testdata.wanterror {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.EqualValues(t, testdata.exp, len(conf.UseTransposeColumns))
assert.EqualValues(t, len(conf.UseTransposeColumns), len(conf.Transposers))
}
})
}
@@ -202,10 +196,8 @@ func TestReduceColumns(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)
}
assert.EqualValues(t, testdata.expect, data.entries)
})
}
}
@@ -233,10 +225,8 @@ func TestNumberizeHeaders(t *testing.T) {
conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, Numbering: testdata.numberize}
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)
}
assert.EqualValues(t, testdata.expect, usedata.headers)
})
}
}

View File

@@ -23,7 +23,7 @@ import (
"io"
"os"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
const RWRR = 0755
@@ -58,6 +58,15 @@ func ProcessFiles(conf *cfg.Config, args []string) error {
return err
}
if conf.Interactive {
newdata, err := tableEditor(conf, &data)
if err != nil {
return err
}
data = *newdata
}
printData(os.Stdout, *conf, &data)
return nil

120
lib/pager.go Normal file
View File

@@ -0,0 +1,120 @@
package lib
// pager setup using bubbletea
// file shamlelessly copied from:
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.BorderStyle(b)
}()
)
type Doc struct {
content string
title string
ready bool
viewport viewport.Model
}
func (m Doc) Init() tea.Cmd {
return nil
}
func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.ready {
// Since this program is using the full size of the viewport we
// need to wait until we've received the window dimensions before
// we can initialize the viewport. The initial dimensions come in
// quickly, though asynchronously, which is why we wait for them
// here.
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.SetContent(m.content)
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m Doc) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
func (m Doc) headerView() string {
// title := titleStyle.Render("RPN Help Overview")
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m Doc) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func Pager(title, message string) {
p := tea.NewProgram(
Doc{content: message, title: title},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
)
if _, err := p.Run(); err != nil {
fmt.Println("could not run pager:", err)
os.Exit(1)
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022-2024 Thomas von Dein
Copyright © 2022-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -20,13 +20,17 @@ package lib
import (
"bufio"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"regexp"
"strings"
"github.com/alecthomas/repr"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
/*
@@ -39,6 +43,8 @@ func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
// first step, parse the data
if len(conf.Separator) == 1 {
data, err = parseCSV(conf, input)
} else if conf.InputJSON {
data, err = parseJSON(conf, input)
} else {
data, err = parseTabular(conf, input)
}
@@ -60,6 +66,43 @@ func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
return data, err
}
/*
* Setup headers, given headers might be usable headers or just the
* first row, which we use to determine how many headers to generate,
* if enabled.
*/
func SetHeaders(conf cfg.Config, headers []string) []string {
if !conf.AutoHeaders && len(conf.CustomHeaders) == 0 {
return headers
}
if conf.AutoHeaders {
heads := make([]string, len(headers))
for idx := range headers {
heads[idx] = fmt.Sprintf("%d", idx+1)
}
return heads
}
if len(conf.CustomHeaders) == len(headers) {
return conf.CustomHeaders
}
// use as much custom ones we have, generate the remainder
heads := make([]string, len(headers))
for idx := range headers {
if idx < len(conf.CustomHeaders) {
heads[idx] = conf.CustomHeaders[idx]
} else {
heads[idx] = fmt.Sprintf("%d", idx+1)
}
}
return heads
}
/*
Parse CSV input.
*/
@@ -81,7 +124,7 @@ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) {
}
if len(records) >= 1 {
data.headers = records[0]
data.headers = SetHeaders(conf, records[0])
data.columns = len(records)
for _, head := range data.headers {
@@ -92,11 +135,16 @@ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) {
}
}
if len(records) > 1 {
if len(records) >= 1 {
if conf.AutoHeaders || len(conf.CustomHeaders) > 0 {
data.entries = records
} else {
data.entries = records[1:]
}
}
}
return data, nil
}
@@ -122,7 +170,9 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
data.columns = len(parts)
// process all header fields
for _, part := range parts {
firstrow := make([]string, len(parts))
for idx, part := range parts {
// register widest header field
headerlen := len(part)
if headerlen > data.maxwidthHeader {
@@ -130,11 +180,22 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
}
// register fields data
data.headers = append(data.headers, strings.TrimSpace(part))
firstrow[idx] = strings.TrimSpace(part)
// done
hadFirst = true
}
data.headers = SetHeaders(conf, firstrow)
if conf.AutoHeaders || len(conf.CustomHeaders) > 0 {
// we do not use generated headers, consider as row
if matchPattern(conf, line) == conf.InvertMatch {
continue
}
data.entries = append(data.entries, firstrow)
}
} else {
// data processing
if matchPattern(conf, line) == conf.InvertMatch {
@@ -172,6 +233,137 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
return data, nil
}
/*
Parse JSON input. We only support an array of maps.
*/
func parseRawJSON(conf cfg.Config, input io.Reader) (Tabdata, error) {
dec := json.NewDecoder(input)
headers := []string{}
idxmap := map[string]int{}
data := [][]string{}
row := []string{}
iskey := true
haveheaders := false
var currentfield string
var idx int
var isjson bool
for {
t, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
switch val := t.(type) {
case string:
if iskey {
if !haveheaders {
// consider only the keys of the first item as headers
headers = append(headers, val)
}
currentfield = val
} else {
if !haveheaders {
// the first row uses the order as it comes in
row = append(row, val)
} else {
// use the pre-determined order, that way items
// can be in any order as long as they contain all
// neccessary fields. They may also contain less
// fields than the first item, these will contain
// the empty string
row[idxmap[currentfield]] = val
}
}
case float64:
var value string
// we set precision to 0 if the float is a whole number
if val == math.Trunc(val) {
value = fmt.Sprintf("%.f", val)
} else {
value = fmt.Sprintf("%f", val)
}
if !haveheaders {
row = append(row, value)
} else {
row[idxmap[currentfield]] = value
}
case nil:
// we ignore here if a value shall be an int or a string,
// because tablizer only works with strings anyway
if !haveheaders {
row = append(row, "")
} else {
row[idxmap[currentfield]] = ""
}
case json.Delim:
if val.String() == "}" {
data = append(data, row)
row = make([]string, len(headers))
idx++
if !haveheaders {
// remember the array position of header fields,
// which we use to assign elements to the correct
// row index
for i, header := range headers {
idxmap[header] = i
}
}
haveheaders = true
}
isjson = true
default:
fmt.Printf("unknown token: %v type: %T\n", t, t)
}
iskey = !iskey
}
if isjson && (len(headers) == 0 || len(data) == 0) {
return Tabdata{}, errors.New("failed to parse JSON, input did not contain array of hashes")
}
return Tabdata{headers: headers, entries: data, columns: len(headers)}, nil
}
func parseJSON(conf cfg.Config, input io.Reader) (Tabdata, error) {
// parse raw json
data, err := parseRawJSON(conf, input)
if err != nil {
return data, err
}
// apply filter, if any
filtered := [][]string{}
var line string
for _, row := range data.entries {
line = strings.Join(row, " ")
if matchPattern(conf, line) == conf.InvertMatch {
continue
}
filtered = append(filtered, row)
}
if len(filtered) != len(data.entries) {
data.entries = filtered
}
return data, nil
}
func PostProcess(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
var modified bool

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022 Thomas von Dein
Copyright © 2022-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -19,11 +19,12 @@ package lib
import (
"fmt"
"reflect"
"io"
"strings"
"testing"
"github.com/tlinden/tablizer/cfg"
"github.com/stretchr/testify/assert"
"codeberg.org/scip/tablizer/cfg"
)
var input = []struct {
@@ -33,7 +34,7 @@ var input = []struct {
}{
{
name: "tabular-data",
separator: cfg.DefaultSeparator,
separator: cfg.SeparatorTemplates[":default:"],
text: `
ONE TWO THREE
asd igig cxxxncnc
@@ -67,16 +68,10 @@ func TestParser(t *testing.T) {
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)
gotdata, err := wrapValidateParser(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)
}
assert.NoError(t, err)
assert.EqualValues(t, data, gotdata)
})
}
}
@@ -87,7 +82,7 @@ func TestParserPatternmatching(t *testing.T) {
entries [][]string
patterns []*cfg.Pattern
invert bool
want bool
wanterror bool
}{
{
name: "match",
@@ -121,18 +116,13 @@ func TestParserPatternmatching(t *testing.T) {
_ = conf.PreparePattern(testdata.patterns)
readFd := strings.NewReader(strings.TrimSpace(inputdata.text))
gotdata, err := Parse(conf, readFd)
data, err := wrapValidateParser(conf, readFd)
if err != nil {
if !testdata.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
}
if testdata.wanterror {
assert.Error(t, err)
} else {
if !reflect.DeepEqual(testdata.entries, gotdata.entries) {
t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n",
testdata.name, testdata.invert, testdata.entries, gotdata.entries)
}
assert.NoError(t, err)
assert.EqualValues(t, testdata.entries, data.entries)
}
})
}
@@ -158,15 +148,282 @@ asd igig
19191 EDD 1 X`
readFd := strings.NewReader(strings.TrimSpace(table))
conf := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := Parse(conf, readFd)
conf := cfg.Config{Separator: cfg.SeparatorTemplates[":default:"]}
gotdata, err := wrapValidateParser(conf, readFd)
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
assert.NoError(t, err)
assert.EqualValues(t, data, gotdata)
}
func TestParserJSONInput(t *testing.T) {
var tests = []struct {
name string
input string
expect Tabdata
wanterror bool // true: expect fail, false: expect success
}{
{
// too deep nesting
name: "invalidjson",
wanterror: true,
input: `[
{
"item": {
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
}
}
`,
expect: Tabdata{},
},
{
// contains nil, int and float values
name: "niljson",
wanterror: false,
input: `[
{
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": 0,
"AGE": null,
"X": 12,
"Y": 34.222
}
]`,
expect: Tabdata{
columns: 7,
headers: []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE", "X", "Y"},
entries: [][]string{
[]string{
"postgres-operator-7f4c7c8485-ntlns",
"1/1",
"Running",
"0",
"",
"12",
"34.222000",
},
},
},
},
{
// one field missing + different order
// but shall not fail
name: "kgpfail",
wanterror: false,
input: `[
{
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
},
{
"NAME": "wal-g-exporter-778dcd95f5-wcjzn",
"RESTARTS": "0",
"READY": "1/1",
"AGE": "24h"
}
]`,
expect: Tabdata{
columns: 5,
headers: []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"},
entries: [][]string{
[]string{
"postgres-operator-7f4c7c8485-ntlns",
"1/1",
"Running",
"0",
"24h",
},
[]string{
"wal-g-exporter-778dcd95f5-wcjzn",
"1/1",
"",
"0",
"24h",
},
},
},
},
{
name: "kgp",
wanterror: false,
input: `[
{
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
},
{
"NAME": "wal-g-exporter-778dcd95f5-wcjzn",
"STATUS": "Running",
"READY": "1/1",
"RESTARTS": "0",
"AGE": "24h"
}
]`,
expect: Tabdata{
columns: 5,
headers: []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"},
entries: [][]string{
[]string{
"postgres-operator-7f4c7c8485-ntlns",
"1/1",
"Running",
"0",
"24h",
},
[]string{
"wal-g-exporter-778dcd95f5-wcjzn",
"1/1",
"Running",
"0",
"24h",
},
},
},
},
}
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
conf.Separator, data, gotdata)
for _, testdata := range tests {
testname := fmt.Sprintf("parse-json-%s", testdata.name)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{InputJSON: true}
readFd := strings.NewReader(strings.TrimSpace(testdata.input))
data, err := wrapValidateParser(conf, readFd)
if testdata.wanterror {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.EqualValues(t, testdata.expect, data)
}
})
}
}
func TestParserSeparators(t *testing.T) {
list := []string{"alpha", "beta", "delta"}
tests := []struct {
input string
sep string
}{
{
input: `🎲`,
sep: ":nonprint:",
},
{
input: `|`,
sep: ":pipe:",
},
{
input: ` `,
sep: ":spaces:",
},
{
input: " \t ",
sep: ":tab:",
},
{
input: `-`,
sep: ":nonword:",
},
{
input: `//$`,
sep: ":special:",
},
}
for _, testdata := range tests {
testname := fmt.Sprintf("parse-%s", testdata.sep)
t.Run(testname, func(t *testing.T) {
header := strings.Join(list, testdata.input)
row := header
content := header + "\n" + row
readFd := strings.NewReader(strings.TrimSpace(content))
conf := cfg.Config{Separator: testdata.sep}
conf.ApplyDefaults()
gotdata, err := wrapValidateParser(conf, readFd)
assert.NoError(t, err)
assert.EqualValues(t, [][]string{list}, gotdata.entries)
})
}
}
func TestParserSetHeaders(t *testing.T) {
row := []string{"c", "b", "c", "d", "e"}
tests := []struct {
name string
custom []string
expect []string
auto bool
}{
{
name: "default",
expect: row,
},
{
name: "auto",
expect: strings.Split("1 2 3 4 5", " "),
auto: true,
},
{
name: "custom-complete",
custom: strings.Split("A B C D E", " "),
expect: strings.Split("A B C D E", " "),
},
{
name: "custom-too-short",
custom: strings.Split("A B", " "),
expect: strings.Split("A B 3 4 5", " "),
},
{
name: "custom-too-long",
custom: strings.Split("A B C D E F G", " "),
expect: strings.Split("A B C D E", " "),
},
}
for _, testdata := range tests {
testname := fmt.Sprintf("parse-%s", testdata.name)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{
AutoHeaders: testdata.auto,
CustomHeaders: testdata.custom,
}
headers := SetHeaders(conf, row)
assert.NotNil(t, headers)
assert.EqualValues(t, testdata.expect, headers)
})
}
}
func wrapValidateParser(conf cfg.Config, input io.Reader) (Tabdata, error) {
data, err := Parse(conf, input)
if err != nil {
return data, err
}
err = ValidateConsistency(&data)
return data, err
}

View File

@@ -19,17 +19,20 @@ package lib
import (
"encoding/csv"
"encoding/json"
"fmt"
"io"
"log"
"strconv"
"strings"
"text/template"
"codeberg.org/scip/tablizer/cfg"
"github.com/Masterminds/sprig/v3"
"github.com/gookit/color"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/tlinden/tablizer/cfg"
"gopkg.in/yaml.v3"
)
@@ -61,15 +64,22 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
printShellData(writer, data)
case cfg.Yaml:
printYamlData(writer, data)
case cfg.Json:
printJsonData(writer, data)
case cfg.CSV:
printCSVData(writer, data)
printCSVData(writer, conf, data)
case cfg.Template:
printTemplateData(writer, conf, data)
default:
printASCIIData(writer, conf, data)
}
}
func output(writer io.Writer, str string) {
fmt.Fprint(writer, str)
_, err := fmt.Fprint(writer, str)
if err != nil {
log.Fatalf("failed to print output: %s", err)
}
}
/*
@@ -194,6 +204,11 @@ func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
Simple ASCII table without any borders etc, just like the input we expect
*/
func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
OFS := " "
if conf.OFS != "" {
OFS = conf.OFS
}
tableString := &strings.Builder{}
styleTSV := tw.NewSymbolCustom("space").WithColumn("\t")
@@ -204,8 +219,8 @@ func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
Borders: tw.BorderNone,
Symbols: styleTSV,
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.Off, BetweenColumns: tw.On},
Lines: tw.Lines{ShowFooterLine: tw.Off, ShowHeaderLine: tw.Off},
Separators: tw.SeparatorsNone,
Lines: tw.LinesNone,
},
})),
tablewriter.WithConfig(tablewriter.Config{
@@ -213,23 +228,18 @@ func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
Padding: tw.CellPadding{
Global: tw.Padding{Left: "", Right: ""},
},
Padding: tw.CellPadding{Global: tw.Padding{Left: "", Right: OFS}},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
Alignment: tw.AlignLeft,
},
Padding: tw.CellPadding{
Global: tw.Padding{Left: "", Right: ""},
},
Padding: tw.CellPadding{Global: tw.Padding{Right: OFS}},
},
Debug: true,
}),
tablewriter.WithPadding(tw.PaddingNone),
)
if !conf.NoHeaders {
@@ -291,6 +301,35 @@ func printShellData(writer io.Writer, data *Tabdata) {
output(writer, out)
}
func printJsonData(writer io.Writer, data *Tabdata) {
objlist := make([]map[string]any, len(data.entries))
if len(data.entries) > 0 {
for i, entry := range data.entries {
obj := make(map[string]any, len(entry))
for idx, value := range entry {
num, err := strconv.Atoi(value)
if err == nil {
obj[data.headers[idx]] = num
} else {
obj[data.headers[idx]] = value
}
}
objlist[i] = obj
}
}
jsonstr, err := json.MarshalIndent(&objlist, "", " ")
if err != nil {
log.Fatal(err)
}
output(writer, string(jsonstr))
}
func printYamlData(writer io.Writer, data *Tabdata) {
type Data struct {
Entries []map[string]interface{} `yaml:"entries"`
@@ -328,8 +367,14 @@ func printYamlData(writer io.Writer, data *Tabdata) {
output(writer, string(yamlstr))
}
func printCSVData(writer io.Writer, data *Tabdata) {
func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) {
OFS := ","
if conf.OFS != "" {
OFS = conf.OFS
}
csvout := csv.NewWriter(writer)
csvout.Comma = []rune(OFS)[0]
if err := csvout.Write(data.headers); err != nil {
log.Fatalln("error writing record to csv:", err)
@@ -347,3 +392,25 @@ func printCSVData(writer io.Writer, data *Tabdata) {
log.Fatal(err)
}
}
func printTemplateData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tmpl, err := template.New("printer").Funcs(sprig.TxtFuncMap()).Parse(conf.Template)
if err != nil {
log.Fatalf("failed to parse template: %s", err)
}
buf := strings.Builder{}
for line, dict := range data.ToMap() {
err = tmpl.Execute(&buf, dict)
if err != nil {
log.Fatalf("failed to execute template in line %d: %s", line, err)
}
buf.WriteString("\n")
}
if _, err := fmt.Fprintln(writer, buf.String()); err != nil {
log.Fatalf("failed to print output: %s", err)
}
}

View File

@@ -23,7 +23,8 @@ import (
"strings"
"testing"
"github.com/tlinden/tablizer/cfg"
"github.com/stretchr/testify/assert"
"codeberg.org/scip/tablizer/cfg"
)
func newData() Tabdata {
@@ -124,6 +125,31 @@ ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
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: "json",
mode: cfg.Json,
numberize: false,
expect: `[
{
"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: "yaml",
@@ -216,7 +242,7 @@ alpha 2013-Feb-03
ceta 06/Jan/2008 15:04:05 -0700`,
},
{
name: "usecolumns",
name: "usecolumns2",
usecol: []int{2},
numberize: true,
usecolstr: "2",
@@ -227,7 +253,7 @@ DURATION(2)
33d12h`,
},
{
name: "usecolumns",
name: "usecolumns3",
usecol: []int{3},
numberize: true,
usecolstr: "3",
@@ -238,7 +264,7 @@ COUNT(3)
9`,
},
{
name: "usecolumns",
name: "usecolumns4",
column: 0,
usecol: []int{1, 3},
numberize: true,
@@ -280,12 +306,18 @@ func TestPrinter(t *testing.T) {
Numbering: testdata.numberize,
UseColumns: testdata.usecol,
NoColor: true,
OFS: " ",
}
if conf.OutputMode == cfg.CSV {
conf.OFS = ","
}
if testdata.column > 0 {
conf.UseSortByColumn = []int{testdata.column}
}
conf.Separator = cfg.SeparatorTemplates[":default:"]
conf.ApplyDefaults()
// the test checks the len!
@@ -302,10 +334,7 @@ func TestPrinter(t *testing.T) {
got := strings.TrimSpace(writer.String())
if got != exp {
t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s",
got, exp)
}
assert.EqualValues(t, exp, got)
})
}
}

View File

@@ -23,8 +23,8 @@ import (
"sort"
"strconv"
"codeberg.org/scip/tablizer/cfg"
"github.com/araddon/dateparse"
"github.com/tlinden/tablizer/cfg"
)
func sortTable(conf cfg.Config, data *Tabdata) {
@@ -38,6 +38,12 @@ func sortTable(conf cfg.Config, data *Tabdata) {
return
}
for _, column := range conf.UseSortByColumn {
if column > len(data.headers) {
return
}
}
// actual sorting
sort.SliceStable(data.entries, func(i, j int) bool {
// holds the result of a sort of one column

View File

@@ -21,7 +21,8 @@ import (
"fmt"
"testing"
"github.com/tlinden/tablizer/cfg"
"github.com/stretchr/testify/assert"
"codeberg.org/scip/tablizer/cfg"
)
func TestDuration2Seconds(t *testing.T) {
@@ -41,9 +42,7 @@ func TestDuration2Seconds(t *testing.T) {
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)
}
assert.EqualValues(t, testdata.expect, seconds)
})
}
}
@@ -74,9 +73,7 @@ func TestCompare(t *testing.T) {
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 %d, want %d", got, testdata.want)
}
assert.EqualValues(t, testdata.want, got)
})
}
}

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2022-2024 Thomas von Dein
Copyright © 2022-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -17,6 +17,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
import (
"strconv"
"strings"
)
// contains a whole parsed table
type Tabdata struct {
maxwidthHeader int // longest header
@@ -34,3 +39,41 @@ func (data *Tabdata) CloneEmpty() Tabdata {
return newdata
}
// convert our internal data to a list of maps
func (data *Tabdata) ToMap() []map[string]any {
dictlist := make([]map[string]any, len(data.entries))
headers := make([]string, len(data.headers))
for idx, header := range data.headers {
headers[idx] = strings.ToLower(header)
}
for line, row := range data.entries {
dict := make(map[string]any, len(data.headers))
for col, cell := range row {
n, ok := strconv.Atoi(cell)
if ok == nil {
dict[headers[col]] = n
continue
}
if strings.ToLower(cell) == "true" {
dict[headers[col]] = true
continue
}
if strings.ToLower(cell) == "false" {
dict[headers[col]] = false
continue
}
dict[headers[col]] = cell
}
dictlist[line] = dict
}
return dictlist
}

524
lib/tableeditor.go Normal file
View File

@@ -0,0 +1,524 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/evertras/bubble-table/table"
"github.com/mattn/go-isatty"
"codeberg.org/scip/tablizer/cfg"
)
// The context exists outside of the bubble loop, and is being used as
// pointer reciever. That way we can use it as our primary storage
// container.
type Context struct {
selectedColumn int
showHelp bool
descending bool
data *Tabdata
// Window dimensions
totalWidth int
totalHeight int
// Table dimensions
horizontalMargin int
verticalMargin int
}
// Execute tablizer sort function, feed it with fresh config, we do
// NOT use the existing runtime config, because sorting is
// configurable in the UI separately.
func (ctx *Context) Sort(mode string) {
conf := cfg.Config{
SortMode: mode,
SortDescending: ctx.descending,
UseSortByColumn: []int{ctx.selectedColumn + 1},
}
ctx.descending = !ctx.descending
sortTable(conf, ctx.data)
}
// The actual table model, holds the context pointer, a copy of the
// pre-processed data and some flags
type FilterTable struct {
Table table.Model
Rows int
quitting bool
unchanged bool
maxColumns int
headerIdx map[string]int
ctx *Context
columns []table.Column
}
type HelpLine []string
type HelpColumn []HelpLine
const (
// header+footer
ExtraRows = 5
HelpFooter = "?:help | "
)
var (
// we use our own custom border style
customBorder = table.Border{
Top: "─",
Left: "│",
Right: "│",
Bottom: "─",
TopRight: "╮",
TopLeft: "╭",
BottomRight: "╯",
BottomLeft: "╰",
TopJunction: "┬",
LeftJunction: "├",
RightJunction: "┤",
BottomJunction: "┴",
InnerJunction: "┼",
InnerDivider: "│",
}
// Cells in selected columns will be highlighted
StyleSelected = lipgloss.NewStyle().
Background(lipgloss.Color("#696969")).
Foreground(lipgloss.Color("#ffffff")).
Align(lipgloss.Left)
StyleHeader = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ff4500")).
Align(lipgloss.Left).Bold(true)
// help buffer styles
StyleKey = lipgloss.NewStyle().Bold(true)
StyleHelp = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff4500"))
// the default style
NoStyle = lipgloss.NewStyle().Align(lipgloss.Left)
HelpData = []HelpColumn{
{
HelpLine{"up", "navigate up"},
HelpLine{"down", "navigate down"},
HelpLine{"tab", "navigate columns"},
},
{
HelpLine{"s", "sort alpha-numerically"},
HelpLine{"n", "sort numerically"},
HelpLine{"t", "sort by time"},
HelpLine{"d", "sort by duration"},
},
{
HelpLine{"spc", "[de]select a row"},
HelpLine{"a", "[de]select all visible rows"},
HelpLine{"f", "enter fuzzy filter"},
HelpLine{"esc", "finish filter input"},
},
{
HelpLine{"?", "show help buffer"},
HelpLine{"q", "commit and quit"},
HelpLine{"c-c", "discard and quit"},
},
}
// rendered from Help above
Help = ""
// number of lines taken by help below, adjust accordingly!
HelpRows = 0
)
// generate a lipgloss styled help buffer consisting of various
// columns
func generateHelp() {
help := strings.Builder{}
helpcols := []string{}
maxrows := 0
for _, col := range HelpData {
help.Reset()
// determine max key width to avoid excess spaces between keys and help
keylen := 0
for _, line := range col {
if len(line[0]) > keylen {
keylen = len(line[0])
}
}
keylenstr := fmt.Sprintf("%d", keylen)
for _, line := range col {
// 0: key, 1: help text
help.WriteString(StyleKey.Render(fmt.Sprintf("%-"+keylenstr+"s", line[0])))
help.WriteString(" " + StyleHelp.Render(line[1]) + " \n")
}
helpcols = append(helpcols, help.String())
if len(col) > maxrows {
maxrows = len(col)
}
}
HelpRows = maxrows + 1
Help = "\n" + lipgloss.JoinHorizontal(lipgloss.Top, helpcols...)
}
// initializes the table model
func NewModel(data *Tabdata, ctx *Context) FilterTable {
columns := make([]table.Column, len(data.headers))
lengths := make([]int, len(data.headers))
hidx := make(map[string]int, len(data.headers))
// give columns at least the header width
for idx, header := range data.headers {
lengths[idx] = len(header)
hidx[strings.ToLower(header)] = idx
}
// determine max width per column
for _, entry := range data.entries {
for i, cell := range entry {
if len(cell) > lengths[i] {
lengths[i] = len(cell)
}
}
}
// determine flexFactor with base 10, used by flexColumns
for i, len := range lengths {
if len <= 10 {
lengths[i] = 1
} else {
lengths[i] = len / 10
}
}
// setup column data with flexColumns
for idx, header := range data.headers {
columns[idx] = table.NewFlexColumn(
strings.ToLower(header),
StyleHeader.Render(header),
lengths[idx]).WithFiltered(true).WithStyle(NoStyle)
}
// separate variable so we can share the row filling code
filtertbl := FilterTable{
maxColumns: len(data.headers),
Rows: len(data.entries),
headerIdx: hidx,
ctx: ctx,
columns: columns,
}
filtertbl.Table = table.New(columns)
filtertbl.fillRows()
// finally construct help buffer
generateHelp()
return filtertbl
}
// Applied to every cell on every change (TAB,up,down key, resize
// event etc)
func CellController(input table.StyledCellFuncInput, m FilterTable) lipgloss.Style {
if m.headerIdx[input.Column.Key()] == m.ctx.selectedColumn {
return StyleSelected
}
return NoStyle
}
// Selects or deselects ALL rows
func (m *FilterTable) ToggleAllSelected() {
rows := m.Table.GetVisibleRows()
selected := m.Table.SelectedRows()
if len(selected) > 0 {
for i, row := range selected {
rows[i] = row.Selected(false)
}
} else {
for i, row := range rows {
rows[i] = row.Selected(true)
}
}
m.Table.WithRows(rows)
}
// ? pressed, display help message
func (m FilterTable) ToggleHelp() {
m.ctx.showHelp = !m.ctx.showHelp
}
func (m FilterTable) Init() tea.Cmd {
return nil
}
// Forward call to context sort
func (m *FilterTable) Sort(mode string) {
m.ctx.Sort(mode)
m.fillRows()
}
// Fills the table rows with our data. Called once on startup and
// repeatedly if the user changes the sort order in some way
func (m *FilterTable) fillRows() {
// required to be able to feed the model to the controller
controllerWrapper := func(input table.StyledCellFuncInput) lipgloss.Style {
return CellController(input, *m)
}
// fill the rows with style
rows := make([]table.Row, len(m.ctx.data.entries))
for idx, entry := range m.ctx.data.entries {
rowdata := make(table.RowData, len(entry))
for i, cell := range entry {
rowdata[strings.ToLower(m.ctx.data.headers[i])] =
table.NewStyledCellWithStyleFunc(cell+" ", controllerWrapper)
}
rows[idx] = table.NewRow(rowdata)
}
m.Table = m.Table.
WithRows(rows).
Filtered(true).
WithFuzzyFilter().
Focused(true).
SelectableRows(true).
WithSelectedText(" ", "✓").
WithFooterVisibility(true).
WithHeaderVisibility(true).
HighlightStyle(StyleSelected).
Border(customBorder)
}
// Part of the bubbletea event loop, called every tick
func (m FilterTable) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.Table, cmd = m.Table.Update(msg)
cmds = append(cmds, cmd)
// If the user is about to enter filter text, do NOT respond to
// key bindings, as they might be part of the filter!
if !m.Table.GetIsFilterInputFocused() {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q":
m.quitting = true
m.unchanged = false
cmds = append(cmds, tea.Quit)
case "ctrl+c":
m.quitting = true
m.unchanged = true
cmds = append(cmds, tea.Quit)
case "a":
m.ToggleAllSelected()
case "tab":
m.SelectNextColumn()
case "?":
m.ToggleHelp()
m.recalculateTable()
case "s":
m.Sort("alphanumeric")
case "n":
m.Sort("numeric")
case "d":
m.Sort("duration")
case "t":
m.Sort("time")
}
}
}
// Happens when the terminal window has been resized
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.ctx.totalWidth = msg.Width
m.ctx.totalHeight = msg.Height
m.recalculateTable()
}
m.updateFooter()
return m, tea.Batch(cmds...)
}
// Add some info to the footer
func (m *FilterTable) updateFooter() {
selected := m.Table.SelectedRows()
footer := fmt.Sprintf("selected: %d ", len(selected))
if m.Table.GetIsFilterInputFocused() {
footer = fmt.Sprintf("/%s %s", m.Table.GetCurrentFilter(), footer)
} else if m.Table.GetIsFilterActive() {
footer = fmt.Sprintf("Filter: %s %s", m.Table.GetCurrentFilter(), footer)
}
m.Table = m.Table.WithStaticFooter(HelpFooter + footer)
}
// Called on resize event (or if help has been toggled)
func (m *FilterTable) recalculateTable() {
m.Table = m.Table.
WithTargetWidth(m.calculateWidth()).
WithMinimumHeight(m.calculateHeight()).
WithPageSize(m.calculateHeight() - ExtraRows)
}
func (m *FilterTable) calculateWidth() int {
return m.ctx.totalWidth - m.ctx.horizontalMargin
}
// Take help height into account, if enabled
func (m *FilterTable) calculateHeight() int {
height := m.Rows + ExtraRows
if height >= m.ctx.totalHeight {
height = m.ctx.totalHeight - m.ctx.verticalMargin
} else {
height = m.ctx.totalHeight
}
if m.ctx.showHelp {
height = height - HelpRows
}
return height
}
// Part of the bubbletable event view, called every tick
func (m FilterTable) View() string {
body := strings.Builder{}
if !m.quitting {
body.WriteString(m.Table.View())
if m.ctx.showHelp {
body.WriteString(Help)
}
}
return body.String()
}
// User hit the TAB key
func (m *FilterTable) SelectNextColumn() {
if m.ctx.selectedColumn == m.maxColumns-1 {
m.ctx.selectedColumn = 0
} else {
m.ctx.selectedColumn++
}
}
// entry point from outside tablizer into table editor
func tableEditor(conf *cfg.Config, data *Tabdata) (*Tabdata, error) {
// we render to STDERR to avoid dead lock when the user redirects STDOUT
// see https://github.com/charmbracelet/bubbletea/issues/860
//
// TODO: doesn't work with libgloss v2 anymore!
out := os.Stderr
if isatty.IsTerminal(os.Stdout.Fd()) {
out = os.Stdout
}
lipgloss.SetDefaultRenderer(lipgloss.NewRenderer(out))
ctx := &Context{data: data}
// Output to STDERR because there's a known bubbletea/lipgloss
// issue: if a program with a tui is expected to write something
// to STDOUT when the tui is finished, then the styles do not
// work. So we write to STDERR (which works) and tablizer can
// still be used inside pipes.
program := tea.NewProgram(
NewModel(data, ctx),
tea.WithOutput(out),
tea.WithAltScreen())
m, err := program.Run()
if err != nil {
return nil, err
}
if m.(FilterTable).unchanged {
return data, err
}
// Data has been modified. Extract it, put it back into our own
// structure and give control back to cmdline tablizer.
filteredtable := m.(FilterTable)
data.entries = make([][]string, len(filteredtable.Table.SelectedRows()))
for pos, row := range m.(FilterTable).Table.SelectedRows() {
entry := make([]string, len(data.headers))
for idx, field := range data.headers {
cell := row.Data[strings.ToLower(field)]
switch value := cell.(type) {
case string:
entry[idx] = value
case table.StyledCell:
entry[idx] = value.Data.(string)
}
}
data.entries[pos] = entry
}
return data, err
}

View File

@@ -22,7 +22,7 @@ import (
"strings"
"github.com/tiagomelo/go-clipboard/clipboard"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
func yankColumns(conf cfg.Config, data *Tabdata) {

View File

@@ -22,8 +22,9 @@ import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tiagomelo/go-clipboard/clipboard"
"github.com/tlinden/tablizer/cfg"
"codeberg.org/scip/tablizer/cfg"
)
var yanktests = []struct {
@@ -59,14 +60,9 @@ func DISABLED_TestYankColumns(t *testing.T) {
printData(&writer, conf, &data)
got, err := cb.PasteText()
if err != nil {
t.Errorf("failed to fetch yanked text from clipboard")
}
if got != testdata.expect {
t.Errorf("not yanked correctly:\n+++ got:\n%s\n+++ want:\n%s",
got, testdata.expect)
}
assert.NoError(t, err)
assert.EqualValues(t, testdata.expect, got)
})
}
}

View File

@@ -20,7 +20,7 @@ package main
import (
"os"
"github.com/tlinden/tablizer/cmd"
"codeberg.org/scip/tablizer/cmd"
)
func main() {

View File

@@ -49,7 +49,7 @@ for D in $DIST; do
fi
set -x
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static -w -X 'github.com/tlinden/tablizer/cfg.VERSION=${version}'" --trimpath $pie -o ${binfile}
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static -w -X 'codeberg.org/scip/tablizer/cfg.VERSION=${version}'" --trimpath $pie -o ${binfile}
strip --strip-all ${binfile}
mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/

View File

@@ -1,15 +1,14 @@
# usage
exec tablizer -h
exec tablizer --help
stdout Usage
exec tablizer -h
stdout show
# version
exec tablizer -V
stdout version
# manpage
exec tablizer -m
stdout SYNOPSIS
# completion
exec tablizer --completion bash
stdout __tablizer_init_completion

16
t/test-template.txtar Normal file
View File

@@ -0,0 +1,16 @@
exec tablizer -r testtable.txt -P '{{.species | title}}s are {{.type }}'
stdout 'Humans are invasive'
exec tablizer -r testtable.txt -P '{{.species | sha256sum}}'
stdout '79a5478768d2447431a90f7f4549df735f50ad541371464c248abc7522dc3a01'
exec tablizer -r testtable.txt -P '{{add 100 .count}}'
stdout 104
-- testtable.txt --
SPECIES TYPE HOME COUNT STAGE
human invasive earth 4 brink
riedl peaceful keauna 33 civilized
namak invasive namak 123 imperium
heduu peaceful iu 66 imperium
kenaha peaceful kohi 3333 hunter-gatherer

View File

@@ -133,7 +133,7 @@
.\" ========================================================================
.\"
.IX Title "TABLIZER 1"
.TH TABLIZER 1 "2025-03-06" "1" "User Commands"
.TH TABLIZER 1 "2025-12-08" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
@@ -144,7 +144,7 @@ tablizer \- Manipulate tabular output of other programs
.IX Header "SYNOPSIS"
.Vb 2
\& Usage:
\& tablizer [regex,...] [file, ...] [flags]
\& tablizer [regex,...] [\-r file] [flags]
\&
\& Operational Flags:
\& \-c, \-\-columns string Only show the speficied columns (separated by ,)
@@ -152,12 +152,16 @@ tablizer \- Manipulate tabular output of other programs
\& \-n, \-\-numbering Enable header numbering
\& \-N, \-\-no\-color Disable pattern highlighting
\& \-H, \-\-no\-headers Disable headers display
\& \-s, \-\-separator string Custom field separator
\& \-k, \-\-sort\-by int|name Sort by column (default: 1)
\& \-s, \-\-separator <string> Custom field separator (maybe char, string or :class:)
\& \-k, \-\-sort\-by <int|name> Sort by column (default: 1)
\& \-z, \-\-fuzzy Use fuzzy search [experimental]
\& \-F, \-\-filter field[!]=reg Filter given field with regex, can be used multiple times
\& \-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
\& \-R, \-\-regex\-transposer </from/to/> Apply /search/replace/ regexp to fields given in \-T
\& \-j, \-\-json Read JSON input (must be array of hashes)
\& \-I, \-\-interactive Interactively filter and select rows
\& \-g, \-\-auto\-headers Generate headers if there are none present in input
\& \-x, \-\-custom\-headers a,b,... Use custom headers, separated by comma
\&
\& Output Flags (mutually exclusive):
\& \-X, \-\-extended Enable extended output
@@ -165,9 +169,12 @@ tablizer \- Manipulate tabular output of other programs
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-S, \-\-shell Enable shell evaluable output
\& \-Y, \-\-yaml Enable yaml output
\& \-J, \-\-jsonout Enable JSON output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular
\& \-P, \-\-template <tpl> Enable template mode with template <tpl>
\& \-L, \-\-hightlight\-lines Use alternating background colors for tables
\& \-o, \-\-ofs <char> Output field separator, used by \-A and \-C.
\& \-y, \-\-yank\-columns Yank specified columns (separated by ,) to clipboard,
\& space separated
\&
@@ -178,6 +185,7 @@ tablizer \- Manipulate tabular output of other programs
\& \-t, \-\-sort\-time sort according to time string
\&
\& Other Flags:
\& \-r \-\-read\-file <file> Use <file> as input instead of STDIN
\& \-\-completion <shell> Generate the autocompletion script for <shell>
\& \-f, \-\-config <file> Configuration file (default: ~/.config/tablizer/config)
\& \-d, \-\-debug Enable debugging
@@ -212,20 +220,20 @@ pattern. Hence:
.PP
.Vb 2
\& # read from STDIN
\& kubectl get pods | tablizer
\& > kubectl get pods | tablizer
\&
\& # read a file
\& tablizer filename
\& > tablizer \-r filename
\&
\& # search for pattern in a file (works like grep)
\& tablizer regex filename
\& > tablizer regex \-r filename
\&
\& # search for pattern in STDIN
\& kubectl get pods | tablizer regex
\& > 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.:
The output looks like the original one. You can add the option \fB\-n\fR,
then 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)
@@ -235,13 +243,24 @@ 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
\& > 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.
However, you may also just use the header names instead of numbers,
eg:
.PP
.Vb 1
\& > kubectl get pods | tablizer \-cname,status
.Ve
.PP
You can also use regular expressions with \fB\-c\fR, eg:
.PP
.Vb 1
\& > kubectl get pods | tablizer \-c \*(Aq[ae]\*(Aq
.Ve
.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
@@ -278,6 +297,62 @@ Sorts timestamps.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly
useful for the developer.
.SS "\s-1SEPARATOR\s0"
.IX Subsection "SEPARATOR"
The option \fB\-s\fR can be a single character, in which case the \s-1CSV\s0
parser will be invoked. You can also specify a string as
separator. The string will be interpreted as literal string unless it
is a valid go regular expression. For example:
.PP
.Vb 1
\& \-s \*(Aq\et{2,}\e\*(Aq
.Ve
.PP
is being used as a regexp and will match two or more consecutive tabs.
.PP
.Vb 1
\& \-s \*(Aqfoo\*(Aq
.Ve
.PP
on the other hand is no regular expression and will be used literally.
.PP
To make live easier, there are a couple of predefined regular
expressions, which you can specify as classes:
.Sp
.RS 4
* :tab:
.Sp
Matches a tab and eats spaces around it.
.Sp
* :spaces:
.Sp
Matches 2 or more spaces.
.Sp
* :pipe:
.Sp
Matches a pipe character and eats spaces around it.
.Sp
* :default:
.Sp
Matches 2 or more spaces or tab. This is the default separator if none
is specified.
.Sp
* :nonword:
.Sp
Matches a non-word character.
.Sp
* :nondigit:
.Sp
Matches a non-digit character.
.Sp
* :special:
.Sp
Matches one or more special chars like brackets, dollar sign, slashes etc.
.Sp
* :nonprint:
.Sp
Matches one or more non-printable characters.
.RE
.SS "\s-1PATTERNS AND FILTERING\s0"
.IX Subsection "PATTERNS AND FILTERING"
You can reduce the rows being displayed by using one or more regular
@@ -307,7 +382,7 @@ and append the flag. The following flags are supported:
Example for a case insensitive search:
.PP
.Vb 1
\& kubectl get pods \-A | tablizer "/account/i"
\& > kubectl get pods \-A | tablizer "/account/i"
.Ve
.PP
If you use the \f(CW\*(C`!\*(C'\fR flag, then the regex match will be negated, that
@@ -349,6 +424,20 @@ These field filters can also be negated:
.Ve
.PP
If the option \fB\-v\fR is specified, the filtering is inverted.
.SS "\s-1INTERACTIVE FILTERING\s0"
.IX Subsection "INTERACTIVE FILTERING"
You can also use the interactive mode, enabled with \f(CW\*(C`\-I\*(C'\fR to filter
and select rows. This mode is complementary, that is, other filter
options are still being respected.
.PP
To enter e filter, hit \f(CW\*(C`/\*(C'\fR, enter a filter string and finish with
\&\f(CW\*(C`ENTER\*(C'\fR. Use \f(CW\*(C`SPACE\*(C'\fR to select/deselect rows, use \f(CW\*(C`a\*(C'\fR to select all
(visible) rows.
.PP
Commit your selection with \f(CW\*(C`q\*(C'\fR. The selected rows are being fed to
the requested output mode as usual. Abort with \f(CW\*(C`CTRL\-c\*(C'\fR, in which
case the results of the interactive mode are being ignored and all
rows are being fed to output.
.SS "\s-1COLUMNS\s0"
.IX Subsection "COLUMNS"
The parameter \fB\-c\fR can be used to specify, which columns to
@@ -372,7 +461,7 @@ Lets take this table:
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
\& > ps | tablizer \-s \*(Aq\es+\*(Aq \-c C
\& CMD(4)
\& bash
\& ps
@@ -409,7 +498,7 @@ use a regexp containing the \f(CW\*(C`/\*(C'\fR character, eg:
Example:
.PP
.Vb 7
\& cat t/testtable2
\& > cat t/testtable2
\& NAME DURATION
\& x 10
\& a 100
@@ -417,7 +506,7 @@ Example:
\& u 4
\& k 6
\&
\& cat t/testtable2 | tablizer \-T2 \-R \*(Aq/^\ed/4/\*(Aq \-n
\& > cat t/testtable2 | tablizer \-T2 \-R \*(Aq/^\ed/4/\*(Aq \-n
\& NAME DURATION
\& x 40
\& a 400
@@ -435,7 +524,7 @@ printed vertically, header left, value right, aligned by the field
widths. Here's an example:
.PP
.Vb 6
\& kubectl get pods | ./tablizer \-o extended
\& > kubectl get pods | ./tablizer \-o extended
\& NAME: repldepl\-7bcd8d5b64\-7zq4l
\& READY: 1/1
\& STATUS: Running
@@ -451,7 +540,7 @@ 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
\& > 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"
@@ -462,8 +551,22 @@ You can use this in an eval loop.
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
prints yaml encoding and \fB\s-1CSV\s0\fR mode, which prints a comma separated
value file.
.PP
A special output mode ist the \fBTemplate\fR mode, activated with the
option \f(CW\*(C`\-\-template\*(C'\fR. Template language is the Golang template
language: <https://pkg.go.dev/text/template>. You can also use lot's
of additional functions from:
<https://masterminds.github.io/sprig/>. Here's an example:
.PP
.Vb 3
\& > kubectl get pods | tablizer \-\-template "{{.name}} is {{.status}}"
\& alertmanager\-kube\-prometheus\-alertmanager\-0 is Running
\& grafana\-fcc54cbc9\-bk7s8 is Running
.Ve
.PP
You can use header names as variables.
.SS "\s-1PUT FIELDS TO CLIPBOARD\s0"
.IX Subsection "PUT FIELDS TO CLIPBOARD"
You can let tablizer put fields to the clipboard using the option
@@ -589,7 +692,7 @@ parameter \f(CW\*(C`\-N\*(C'\fR or the environment variable \fB\s-1NO_COLOR\s0\f
.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>.
<https://codeberg.org/scip/tablizer/issues>.
.SH "LICENSE"
.IX Header "LICENSE"
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
@@ -615,6 +718,9 @@ 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
.IP "bubble-table (https://github.com/Evertras/bubble\-table)" 4
.IX Item "bubble-table (https://github.com/Evertras/bubble-table)"
Released under the \s-1MIT\s0 License, Copyright (c) 2022 Brandon Fulljames
.SH "AUTHORS"
.IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR

View File

@@ -5,7 +5,7 @@ tablizer - Manipulate tabular output of other programs
=head1 SYNOPSIS
Usage:
tablizer [regex,...] [file, ...] [flags]
tablizer [regex,...] [-r file] [flags]
Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,)
@@ -13,12 +13,16 @@ tablizer - Manipulate tabular output of other programs
-n, --numbering Enable header numbering
-N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display
-s, --separator string Custom field separator
-k, --sort-by int|name Sort by column (default: 1)
-s, --separator <string> Custom field separator (maybe char, string or :class:)
-k, --sort-by <int|name> Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental]
-F, --filter field[!]=reg Filter given field with regex, can be used multiple times
-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
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
-g, --auto-headers Generate headers if there are none present in input
-x, --custom-headers a,b,... Use custom headers, separated by comma
Output Flags (mutually exclusive):
-X, --extended Enable extended output
@@ -26,9 +30,12 @@ tablizer - Manipulate tabular output of other programs
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output
-J, --jsonout Enable JSON output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
-P, --template <tpl> Enable template mode with template <tpl>
-L, --hightlight-lines Use alternating background colors for tables
-o, --ofs <char> Output field separator, used by -A and -C.
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
@@ -39,6 +46,7 @@ tablizer - Manipulate tabular output of other programs
-t, --sort-time sort according to time string
Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN
--completion <shell> Generate the autocompletion script for <shell>
-f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-d, --debug Enable debugging
@@ -73,31 +81,38 @@ may also use the B<-v> option to exclude all rows which match the
pattern. Hence:
# read from STDIN
kubectl get pods | tablizer
> kubectl get pods | tablizer
# read a file
tablizer filename
> tablizer -r filename
# search for pattern in a file (works like grep)
tablizer regex filename
> tablizer regex -r filename
# search for pattern in STDIN
kubectl get pods | tablizer regex
> 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.:
The output looks like the original one. You can add the option B<-n>,
then 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 L<COLUMNS>:
kubectl get pods | tablizer -c1,3
> kubectl get pods | tablizer -c1,3
You can specify the numbers in any order but output will always follow
the original order.
The numbering can be suppressed by using the B<-n> option.
However, you may also just use the header names instead of numbers,
eg:
> kubectl get pods | tablizer -cname,status
You can also use regular expressions with B<-c>, eg:
> kubectl get pods | tablizer -c '[ae]'
By default tablizer shows a header containing the names of each
column. This can be disabled using the B<-H> option. Be aware that
@@ -142,6 +157,62 @@ Sorts timestamps.
Finally the B<-d> option enables debugging output which is mostly
useful for the developer.
=head2 SEPARATOR
The option B<-s> can be a single character, in which case the CSV
parser will be invoked. You can also specify a string as
separator. The string will be interpreted as literal string unless it
is a valid go regular expression. For example:
-s '\t{2,}\'
is being used as a regexp and will match two or more consecutive tabs.
-s 'foo'
on the other hand is no regular expression and will be used literally.
To make live easier, there are a couple of predefined regular
expressions, which you can specify as classes:
=over
* :tab:
Matches a tab and eats spaces around it.
* :spaces:
Matches 2 or more spaces.
* :pipe:
Matches a pipe character and eats spaces around it.
* :default:
Matches 2 or more spaces or tab. This is the default separator if none
is specified.
* :nonword:
Matches a non-word character.
* :nondigit:
Matches a non-digit character.
* :special:
Matches one or more special chars like brackets, dollar sign, slashes etc.
* :nonprint:
Matches one or more non-printable characters.
=back
=head2 PATTERNS AND FILTERING
You can reduce the rows being displayed by using one or more regular
@@ -166,7 +237,7 @@ and append the flag. The following flags are supported:
Example for a case insensitive search:
kubectl get pods -A | tablizer "/account/i"
> kubectl get pods -A | tablizer "/account/i"
If you use the C<!> flag, then the regex match will be negated, that
is, if a line in the input matches the given regex, but C<!> is
@@ -202,6 +273,20 @@ These field filters can also be negated:
If the option B<-v> is specified, the filtering is inverted.
=head2 INTERACTIVE FILTERING
You can also use the interactive mode, enabled with C<-I> to filter
and select rows. This mode is complementary, that is, other filter
options are still being respected.
To enter e filter, hit C</>, enter a filter string and finish with
C<ENTER>. Use C<SPACE> to select/deselect rows, use C<a> to select all
(visible) rows.
Commit your selection with C<q>. The selected rows are being fed to
the requested output mode as usual. Abort with C<CTRL-c>, in which
case the results of the interactive mode are being ignored and all
rows are being fed to output.
=head2 COLUMNS
@@ -223,7 +308,7 @@ Lets take this table:
We want to see only the CMD column and use a regex for this:
ps | tablizer -s '\s+' -c C
> ps | tablizer -s '\s+' -c C
CMD(4)
bash
ps
@@ -256,7 +341,7 @@ use a regexp containing the C</> character, eg:
Example:
cat t/testtable2
> cat t/testtable2
NAME DURATION
x 10
a 100
@@ -264,7 +349,7 @@ Example:
u 4
k 6
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
> cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
NAME DURATION
x 40
a 400
@@ -281,7 +366,7 @@ useful which enables I<extended mode>. In this mode, each row will be
printed vertically, header left, value right, aligned by the field
widths. Here's an example:
kubectl get pods | ./tablizer -o extended
> kubectl get pods | ./tablizer -o extended
NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1
STATUS: Running
@@ -295,7 +380,7 @@ The option B<-o shell> can be used if the output has to be processed
by the shell, it prints variable assignments for each cell, one line
per row:
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
> 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"
@@ -305,9 +390,22 @@ You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding and CSV mode, which prints a comma separated
prints yaml encoding and B<CSV> mode, which prints a comma separated
value file.
A special output mode ist the B<Template> mode, activated with the
option C<--template>. Template language is the Golang template
language: L<https://pkg.go.dev/text/template>. You can also use lot's
of additional functions from:
L<https://masterminds.github.io/sprig/>. Here's an example:
> kubectl get pods | tablizer --template "{{.name}} is {{.status}}"
alertmanager-kube-prometheus-alertmanager-0 is Running
grafana-fcc54cbc9-bk7s8 is Running
You can use header names as variables.
=head2 PUT FIELDS TO CLIPBOARD
You can let tablizer put fields to the clipboard using the option
@@ -429,7 +527,7 @@ parameter C<-N> or the environment variable B<NO_COLOR> to a true value.
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
L<https://github.com/TLINDEN/tablizer/issues>.
L<https://codeberg.org/scip/tablizer/issues>.
=head1 LICENSE
@@ -465,6 +563,10 @@ Released under the MIT License, Copyright (c) 201 by Oleku Konko
Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov
=item bubble-table (https://github.com/Evertras/bubble-table)
Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
=back
=head1 AUTHORS

15
vhsdemo/Makefile Normal file
View File

@@ -0,0 +1,15 @@
.PHONY: demo check clean-demo
VHS = vhs
clean-demo:
%.gif: %.tape
@echo "vhs $<"
env PATH=..:$(PATH) vhs $<
check:
ls -l ../tablizer
demo: check clean-demo demo.gif

BIN
vhsdemo/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

157
vhsdemo/demo.tape Normal file
View File

@@ -0,0 +1,157 @@
# -*-sh-*-
Output demo.gif
Set FontSize 20
Set Width 1200
Set Height 1000
Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#29283b", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" }
Set WindowBar Colorful
Set BorderRadius 10
Set Shell zsh
Set FontFamily "IBM Plex Mono"
Set CursorBlink false
Set PlaybackSpeed 1
Set TypingSpeed .05
# initialize
Hide
Type `PROMPT=''`
Enter
Type "setopt interactivecomments"
Enter
Type "autoload -U colors && colors"
Enter
Type `PS1="%{$fg[magenta]%}demo> %{$reset_color%}"`
Enter
Type "clear"
Enter
Show
Type "# Our input data"
Enter
Sleep 1s
Type "cat input | head -10"
Enter
Sleep 2s
Enter
Type "# Filter over all rows"
Enter
Sleep 1s
Type "tablizer Central < input"
Enter
Sleep 2s
Enter
Type "# Filter over all rows case insensitive"
Enter
Sleep 1s
Type "tablizer '/penc/i' < input"
Enter
Sleep 2s
Enter
Type "# Filter over specific column"
Enter
Sleep 1s
Type "tablizer -Fcost=4.99 < input"
Enter
Sleep 2s
Enter
Type "# Filter by regex on specific column"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. < input"
Enter
Sleep 2s
Enter
Type "# Output as markdown"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -M < input"
Enter
Sleep 2s
Enter
Type "# Output as CSV"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -C < input"
Enter
Sleep 2s
Enter
Type "# Output as shell evaluable"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -S < input"
Enter
Sleep 2s
Type "bat eval.sh"
Enter
Sleep 2s
Type "tablizer -Funits=Pen. -S < input | ./eval.sh"
Enter
Sleep 2s
Enter
Type "# Reduce columns"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -c region,customer,units,count < input"
Enter
Sleep 2s
Enter
Type "# Sort by COUNT column numerically "
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -c region,customer,units,count -kcount -i < input"
Enter
Sleep 2s
Enter
Type "# Do further filtering interactively"
Enter
Sleep 1s
Type "tablizer -Funits=Pen. -c region,customer,units,count -I -O < input"
Enter
Sleep 2s
Type "?"
Sleep 2s
Type "/"
Sleep 2s
Type "J"
Sleep 1s
Type "o"
Sleep 1s
Type "n"
Sleep 1s
Type "e"
Sleep 1s
Type "s"
Sleep 1s
Enter
Sleep 2s
Tab
Sleep 1s
Tab
Sleep 1s
Tab
Sleep 1s
Tab
Type "n"
Sleep 2s
Space
Sleep 1s
Down
Sleep 1s
Down
Sleep 1s
Space
Sleep 2s
Type "q"
Sleep 10s

5
vhsdemo/eval.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
while read LINE; do
eval "$LINE"; echo "$Customer ordered $Count ${Units}s"
done

44
vhsdemo/input Normal file
View File

@@ -0,0 +1,44 @@
Date Region Customer Units Count Cost Total
2016-01-06 East Jones Pencil 95 1.99 189.05
2016-01-23 Central Kivell Binder 50 19.99 999.50
2016-02-09 Central Jardine Pencil 36 4.99 179.64
2016-02-26 Central Gill Pen 27 19.99 539.73
2016-03-15 West Sorvino Pencil 56 2.99 167.44
2016-04-01 East Jones Binder 60 4.99 299.40
2016-04-18 Central Andrews Pencil 75 1.99 149.25
2016-05-05 Central Jardine Pencil 90 4.99 449.10
2016-05-22 West Thompson Pencil 32 1.99 63.68
2016-06-08 East Jones Binder 60 8.99 539.40
2016-06-25 Central Morgan Pencil 90 4.99 449.10
2016-07-12 East Howard Binder 29 1.99 57.71
2016-07-29 East Parent Binder 81 19.99 1619.19
2016-08-15 East Jones Pencil 35 4.99 174.65
2016-09-01 Central Smith Desk 2 125.00 250.00
2016-09-18 East Jones Pen Set 16 15.99 255.84
2016-10-05 Central Morgan Binder 28 8.99 251.72
2016-10-22 East Jones Pen 64 8.99 575.36
2016-11-08 East Parent Pen 15 19.99 299.85
2016-11-25 Central Kivell Pen Set 96 4.99 479.04
2016-12-12 Central Smith Pencil 67 1.29 86.43
2016-12-29 East Parent Pen Set 74 15.99 1183.26
2017-01-15 Central Gill Binder 46 8.99 413.54
2017-02-01 Central Smith Binder 87 15.00 1305.00
2017-02-18 East Jones Binder 4 4.99 19.96
2017-03-07 West Sorvino Binder 7 19.99 139.93
2017-03-24 Central Jardine Pen Set 50 4.99 249.50
2017-04-10 Central Andrews Pencil 66 1.99 131.34
2017-04-27 East Howard Pen 96 4.99 479.04
2017-05-14 Central Gill Pencil 53 1.29 68.37
2017-05-31 Central Gill Binder 80 8.99 719.20
2017-06-17 Central Kivell Desk 5 125.00 625.00
2017-07-04 East Jones Pen Set 62 4.99 309.38
2017-07-21 Central Morgan Pen Set 55 12.49 686.95
2017-08-07 Central Kivell Pen Set 42 23.95 1005.90
2017-08-24 West Sorvino Desk 3 275.00 825.00
2017-09-10 Central Gill Pencil 7 1.29 9.03
2017-09-27 West Sorvino Pen 76 1.99 151.24
2017-10-14 West Thompson Binder 57 19.99 1139.43
2017-10-31 Central Andrews Pencil 14 1.29 18.06
2017-11-17 Central Jardine Binder 11 4.99 54.89
2017-12-04 Central Jardine Binder 94 19.99 1879.06
2017-12-21 Central Andrews Binder 28 4.99 139.72