mirror of
https://codeberg.org/scip/tablizer.git
synced 2025-12-17 04:30:56 +01:00
moved to codeberg
This commit is contained in:
96
.gh-dash.yml
96
.gh-dash.yml
@@ -1,96 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# 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/epuppy/compare/{{ .PreviousTag }}...{{ .Tag }})
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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]
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# https://woodpecker-ci.org/plugins/docker-buildx
|
|
||||||
# enable Package unit and go to /scip/-/packages after building to link to proj
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- &repo codeberg.org/${CI_REPO_OWNER}/tablizer
|
|
||||||
|
|
||||||
steps:
|
|
||||||
dryrun:
|
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
|
|
||||||
settings:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
dry_run: true
|
|
||||||
repo: *repo
|
|
||||||
tags: latest
|
|
||||||
when:
|
|
||||||
event: [pull_request]
|
|
||||||
|
|
||||||
publish:
|
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
|
|
||||||
settings:
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
repo: *repo
|
|
||||||
registry: codeberg.org
|
|
||||||
tags: latest,${CI_COMMIT_SHA:0:8},${CI_COMMIT_TAG}
|
|
||||||
username: ${CI_REPO_OWNER}
|
|
||||||
password:
|
|
||||||
from_secret: REGISTRY_TOKEN
|
|
||||||
when:
|
|
||||||
event: [tag]
|
|
||||||
branch: main
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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
|
|
||||||
278
CHANGELOG.md
278
CHANGELOG.md
@@ -1,278 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
|
|
||||||
|
|
||||||
## [v1.0.14](https://codeberg.org/scip/tablizer/tree/v1.0.14) - 2023-01-23
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.13...v1.0.14)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- The -D parameter could not be used together with -a.
|
|
||||||
|
|
||||||
- Fixed invalid argv handling: when the user wanted to read from stdin
|
|
||||||
but gave an argument which was meant as a pattern, but also existed
|
|
||||||
as a filename, then tablizer opened the file, ignored stdin.
|
|
||||||
|
|
||||||
- Makefile indentation
|
|
||||||
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- added licens notes about dependencies
|
|
||||||
|
|
||||||
- using hard coded uniseq version, see actions#3396457307
|
|
||||||
|
|
||||||
- updated dependencies (go module versions)
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.13](https://codeberg.org/scip/tablizer/tree/v1.0.13) - 2022-11-03
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.12...v1.0.13)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added command line flag to generate shell completion code
|
|
||||||
|
|
||||||
- Added an animated demo gif to the README to demonstrate the tool
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- The `-A` flag wasn't implemented (default output mode).
|
|
||||||
|
|
||||||
- Fixed building from source on systems w/o perls pod tools,
|
|
||||||
which is not requrired anyway since I always commit the latest
|
|
||||||
manpage.
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.12](https://codeberg.org/scip/tablizer/tree/v1.0.12) - 2022-10-25
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.11...v1.0.12)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added support to parse CSV input
|
|
||||||
|
|
||||||
- Added CSV output support
|
|
||||||
|
|
||||||
- Added support for environment variables
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- We do not use the generated help message anymore, instead we use the
|
|
||||||
usage from the manpage, which we have to maintain anyway. It looks
|
|
||||||
better and has flag groups, which cobra is still lacking as of this
|
|
||||||
writing.
|
|
||||||
|
|
||||||
- More refactoring and re-organization, runtime configuration now
|
|
||||||
lives in the cfg module.
|
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fixed [Bug #5](https://codeberg.org/scip/tablizer/issues/5), where
|
|
||||||
matches have not been highlighted correctly in some rare cases.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.11](https://codeberg.org/scip/tablizer/tree/v1.0.11) - 2022-10-19
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.10...v1.0.11)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added CI job golinter to regularly check for common mistakes.
|
|
||||||
|
|
||||||
- Added YAML output mode.
|
|
||||||
|
|
||||||
- Added more unit tests, we're over 95% in the lib module.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- do not use any global variables anymore, makes the code easier to
|
|
||||||
maintain, understand and test
|
|
||||||
|
|
||||||
- using io.Writer in print* functions, which is easier to test, also
|
|
||||||
re-implemented the print tests.
|
|
||||||
|
|
||||||
- replaced go-str2duration with my own implementation `duration2int()`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.10](https://codeberg.org/scip/tablizer/tree/v1.0.10) - 2022-10-15
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.9...v1.0.10)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added various sort modes: sort by time, by duration, numerical (-a -t -i)
|
|
||||||
|
|
||||||
- Added possibility to modify sort order to descending (-D)
|
|
||||||
|
|
||||||
- Added support to specify a regexp in column selector -c, which can
|
|
||||||
also be mixed with numerical column spec
|
|
||||||
|
|
||||||
- More unit tests
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Column specification allowed to specify duplicate columns like `-c
|
|
||||||
1,2,1,2` unchecked. Now this list will be deduplicated before use.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.9](https://codeberg.org/scip/tablizer/tree/v1.0.9) - 2022-10-14
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.8...v1.0.9)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added Changelog, Contribution guidelines and no COC.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- some minor changes to satisfy linter.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.8](https://codeberg.org/scip/tablizer/tree/v1.0.8) - 2022-10-13
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.7...v1.0.8)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added sort support with the new parameter -k (like sort(1)).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.7](https://codeberg.org/scip/tablizer/tree/v1.0.7) - 2022-10-11
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.6...v1.0.7)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added pattern highlighting support.
|
|
||||||
|
|
||||||
- Added more unit tests.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fixed extended more output in combination with -c.
|
|
||||||
|
|
||||||
- Fixed issue #4, the version string was missing.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.6](https://codeberg.org/scip/tablizer/tree/v1.0.6) - 2022-10-05
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.5...v1.0.6)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added documentation about regexp syntax in the manpage.
|
|
||||||
|
|
||||||
- Added more unit tests.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Rewrote the input parser.
|
|
||||||
|
|
||||||
- Some more refactoring work has been done.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.5](https://codeberg.org/scip/tablizer/tree/v1.0.5) - 2022-10-05
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.4...v1.0.5)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- A new option has been added: --invert-match -v which behaves like
|
|
||||||
the same option in grep(1): it inverts the pattern match.
|
|
||||||
|
|
||||||
- A few more unit tests have been added.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Pattern matching did not work, because the (new) help subcommand
|
|
||||||
lead to cobra taking care of the first arg to the program
|
|
||||||
(argv[1]). So now there's a new parameter -m which displays the
|
|
||||||
manpage and no more subcommands.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.4](https://codeberg.org/scip/tablizer/tree/v1.0.4) - 2022-10-04
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.3...v1.0.4)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Development version of the compiled binary now uses git vars
|
|
||||||
in addition to program version.
|
|
||||||
|
|
||||||
- Added an option to display the manual page (compiled in) as text:
|
|
||||||
--help, for cases where a user just installed the binary.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Fixed go module namespace.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.3](https://codeberg.org/scip/tablizer/tree/v1.0.3) - 2022-10-03
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.2...v1.0.3)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added a new output mode: shell mode, which allows the user
|
|
||||||
to use the output in a shell eval loop to further process
|
|
||||||
the data.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- More refactoring work has been done.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.2](https://codeberg.org/scip/tablizer/tree/v1.0.2) - 2022-10-02
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.1...v1.0.2)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added some basic unit tests.
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Code has been refactored to be more efficient.
|
|
||||||
|
|
||||||
- Replaced table generation code with Tablewriter.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.1](https://codeberg.org/scip/tablizer/tree/v1.0.1) - 2022-09-30
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/v1.0.0...v1.0.1)
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Added a unix manual page.
|
|
||||||
|
|
||||||
- Added release builder to Makefile
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Various minor fixes.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.0.0](https://codeberg.org/scip/tablizer/tree/v1.0.0) - 2022-09-28
|
|
||||||
|
|
||||||
[Full Changelog](https://codeberg.org/scip/tablizer/compare/02a64a5c3fe4220df2c791ff1421d16ebd428c19...v1.0.0)
|
|
||||||
|
|
||||||
Initial release.
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# No Code of Conduct
|
|
||||||
|
|
||||||
*TL;DR:* This project does **NOT** have a so called Code of Conduct,
|
|
||||||
nor will it ever have one.
|
|
||||||
|
|
||||||
## The Rant
|
|
||||||
|
|
||||||
The reasons are somewhat complicated and I'll try my best to document
|
|
||||||
them here.
|
|
||||||
|
|
||||||
Ethical codes or rules come along like laws. But how is ethical or
|
|
||||||
moral behavior defined? And who defines which behavior is ethical and
|
|
||||||
which is not? Certainly not me.
|
|
||||||
|
|
||||||
Unless you live in a dictatorship (and more than half of the
|
|
||||||
population of planet earth do as of this writing), laws come into
|
|
||||||
existence by democratic procedures. Laws cover almost every aspect of
|
|
||||||
live in a society. Laws allow and forbid behavior and laws sanction
|
|
||||||
infringements.
|
|
||||||
|
|
||||||
A software project like this one on the other hand is not a society.
|
|
||||||
There are not enough people involved to form democratic
|
|
||||||
structures. And there will always be a minority of users who have the
|
|
||||||
right to commit or reject code. How could any maintainer of a software
|
|
||||||
project dare to decree rules upon others? Actually, am I, the current
|
|
||||||
maintainer of this very project authorized to do so?
|
|
||||||
|
|
||||||
I think the anser to this question clearly is NO.
|
|
||||||
|
|
||||||
The issue is being complicated by the fact, that open source
|
|
||||||
development these days happens on planetary scale. And this planet
|
|
||||||
houses hundreds if not thousands of different cultures, philosophies,
|
|
||||||
ideologies and worldviews. The answer to many ethical questions will
|
|
||||||
in most cases vague and nebulous.
|
|
||||||
|
|
||||||
Ones joke will always be another ones insult.
|
|
||||||
|
|
||||||
Then there is the problem of language. I myself am not an english
|
|
||||||
native, but I publish everyting using the english language. I am able
|
|
||||||
to communicate with most people in the open source community because
|
|
||||||
of that. But I am certainly not able to understand everything and
|
|
||||||
everyone. There might be nuances to a sentence I don't sense, there
|
|
||||||
might be sarcastic connotations I don't understand or references to
|
|
||||||
historical figures, events or traditions I don't know and never have
|
|
||||||
heard of.
|
|
||||||
|
|
||||||
Judging over other peoples online behavior looks like a titanic task
|
|
||||||
to me. It is just not my job to judge others, I am not legitimized or
|
|
||||||
authorized to do so and I am not interested in this kind of business.
|
|
||||||
|
|
||||||
Another huge problem with ethical rules is that you need to outline
|
|
||||||
and enforce sanctions on those who violate the rules. But since I am
|
|
||||||
not an elected authority how would I be able to do this? I don't
|
|
||||||
know. And what happens if someone complains about myself? Shall I
|
|
||||||
remove myself from my own project? Come on!
|
|
||||||
|
|
||||||
Last but not least there's the law. So, let's say someone in india
|
|
||||||
says something insulting to some other developer in an issue. Of
|
|
||||||
course german law does not apply to indian people. More, the insult
|
|
||||||
might actually not be an insult in india. In the end, nothing would
|
|
||||||
happen. Under normal circumstances, maintainers would delete the
|
|
||||||
posting, ban the user or remove push privileges etc.
|
|
||||||
|
|
||||||
But then, is there a way for the offending user to defend himself? Of
|
|
||||||
course not, since neither indian or german law alone applies. I cannot
|
|
||||||
go to a german court and sue the guy and he cannot do the same in
|
|
||||||
india. Or - we possibly could but the judges on both countries would
|
|
||||||
just laugh and close the case.
|
|
||||||
|
|
||||||
That being said, I don't have the power nor the tools, nor the
|
|
||||||
authority to enforce serious sanctions of any meaningful kind against
|
|
||||||
others. Therefore I cannot outline any rules whatsoever.
|
|
||||||
|
|
||||||
And let's not even start talking about there undemocratic "comitees"
|
|
||||||
many projects are forming to circumvent this problem. Some projects
|
|
||||||
even include external entities like a lawer or some bureaucrat
|
|
||||||
somewhere just to have the ability to complain against a comitee
|
|
||||||
member. What a mess!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## So, which are the ethical rules within this project then?
|
|
||||||
|
|
||||||
Well, there are none.
|
|
||||||
|
|
||||||
This project is about code, not society. It doesn't matter where you
|
|
||||||
come from, how you look, how you think, what you believe, who your
|
|
||||||
friends are, whay you said or did sometime in the past. I don't even
|
|
||||||
care if you are a human being. You are an alien so bored that you need
|
|
||||||
to submit code on github? Fine with me. You're a convicted criminal? I
|
|
||||||
don't give a shit!
|
|
||||||
|
|
||||||
**The only thing I am interested here is Code and only Code.**
|
|
||||||
|
|
||||||
So if anyhing happens here I don't like or I am obliged by law to act
|
|
||||||
on, I will decide on a case to case basis what to do. And
|
|
||||||
unfortunately, since this is the nature of a github project, you
|
|
||||||
cannot complain, object or protest. I am very sorry!
|
|
||||||
|
|
||||||
If you will, let's at least outline these:
|
|
||||||
|
|
||||||
- Please - just please - behave towards others as you'd expect others
|
|
||||||
to behave towards yourself.
|
|
||||||
|
|
||||||
- Don't judge others for any reason.
|
|
||||||
|
|
||||||
- Only judge the code.
|
|
||||||
|
|
||||||
But these are not rules, only a friendly appeal to you as a developer
|
|
||||||
and user.
|
|
||||||
|
|
||||||
|
|
||||||
Thanks a lot!
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
## Project Goals
|
|
||||||
|
|
||||||
The goal of this project is to build a small tool which helps in day
|
|
||||||
to day work with tabular output of various commandline programs. It
|
|
||||||
should be small, fast and easy to understand. The idea is to replace
|
|
||||||
multiline shell pipes using awk, sed and grep with just one
|
|
||||||
binary.
|
|
||||||
|
|
||||||
There will be no GUI, no web interface, no public API of some sort, no
|
|
||||||
builtin interpreter.
|
|
||||||
|
|
||||||
The programming language used for this project will always be
|
|
||||||
[GOLANG](https://go.dev/) with the exception of the documentation
|
|
||||||
([Perl POD](https://perldoc.perl.org/perlpod)) and the Makefile.
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
You can contribute to this project in various ways:
|
|
||||||
|
|
||||||
## Open an issue
|
|
||||||
|
|
||||||
If you encounter a problem or don't understand how the program works
|
|
||||||
or if you think the documentation is unclear, please don't hesitate to
|
|
||||||
open an issue.
|
|
||||||
|
|
||||||
Please add as much information about the case as possible, such as:
|
|
||||||
|
|
||||||
- Your environment (operating system etc)
|
|
||||||
- tablizer version (`tablizer --version`)
|
|
||||||
- Input data. Please replace sensitive information with mock data!
|
|
||||||
- Actual program output.
|
|
||||||
- Expected program output.
|
|
||||||
- Error message - if any.
|
|
||||||
|
|
||||||
Be aware that I am working on this (and some other) project in my
|
|
||||||
spare time which is scarce. Therefore please don't expect me to
|
|
||||||
respond to your query within hours or even days. Be patient, but I
|
|
||||||
WILL respond.
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
Code and documentation help is always much appreciated! Please follow
|
|
||||||
thes guidelines to successfully contribute:
|
|
||||||
|
|
||||||
- Every pull request shall be based on latest `development`
|
|
||||||
branch. `main` is only used for releases.
|
|
||||||
|
|
||||||
- Execute the unit tests before committing: `make test`. There shall
|
|
||||||
be no errors.
|
|
||||||
|
|
||||||
- Strive to be backwards compatible so that users who are already
|
|
||||||
using the program don't have to change their habits - unless it is
|
|
||||||
really neccessary.
|
|
||||||
|
|
||||||
- Try to add a unit test for your addition.
|
|
||||||
|
|
||||||
- Don't ever change existing unit tests!
|
|
||||||
|
|
||||||
- Add a meaningful and comprehensive rationale about your contribution:
|
|
||||||
- Why do you think it might be useful for others?
|
|
||||||
- What did you actually change or add?
|
|
||||||
- Is there an open issue which this PR fixes and if so, please link
|
|
||||||
to that issue.
|
|
||||||
|
|
||||||
- [Re-]format your code with `gofmt -s`.
|
|
||||||
|
|
||||||
- Avoid unneccesary dependencies, especially for very small functions.
|
|
||||||
|
|
||||||
- **If** a new dependency is being added, it must be compatible with
|
|
||||||
our [license agreement](LICENSE).
|
|
||||||
|
|
||||||
- You need to accept that the code or documentation you contribute
|
|
||||||
will be redistributed under the terms of said license agreement. If
|
|
||||||
your contribution is considerably large or if you contribute
|
|
||||||
regularly, then feel free to add your name and if you want your
|
|
||||||
email address to the *AUTHORS* section of the
|
|
||||||
[manpage](tablizer.pod).
|
|
||||||
|
|
||||||
- Adhere to the above mentioned project goals.
|
|
||||||
|
|
||||||
- If you are unsure if your addition or change will be accepted,
|
|
||||||
better ask before starting coding. Open an issue about your proposal
|
|
||||||
and let's discuss it! That way we avoid doing unnessesary work on
|
|
||||||
both sides.
|
|
||||||
|
|
||||||
Each pull request will be carefully reviewed and if it is a useful
|
|
||||||
addition it will be accepted. However, please be prepared that
|
|
||||||
sometimes a PR will be rejected. The reasons may vary and will be
|
|
||||||
documented. Perhaps the above guidelines are not matched, or the
|
|
||||||
addition seems to be not so useful from my perspective, maybe there
|
|
||||||
are too much changes or there might be changes I don't even
|
|
||||||
understand.
|
|
||||||
|
|
||||||
But whatever happens: your contribution is always welcome!
|
|
||||||
99
Makefile
99
Makefile
@@ -1,99 +0,0 @@
|
|||||||
|
|
||||||
# Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# no need to modify anything below
|
|
||||||
tool = tablizer
|
|
||||||
version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
|
|
||||||
archs = android darwin freebsd linux netbsd openbsd windows
|
|
||||||
PREFIX = /usr/local
|
|
||||||
UID = root
|
|
||||||
GID = 0
|
|
||||||
BRANCH = $(shell git branch --show-current)
|
|
||||||
COMMIT = $(shell git rev-parse --short=8 HEAD)
|
|
||||||
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
|
|
||||||
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
|
|
||||||
HAVE_POD := $(shell pod2text -h 2>/dev/null)
|
|
||||||
|
|
||||||
all: $(tool).1 cmd/$(tool).go buildlocal
|
|
||||||
|
|
||||||
%.1: %.pod
|
|
||||||
ifdef HAVE_POD
|
|
||||||
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
|
|
||||||
endif
|
|
||||||
|
|
||||||
cmd/%.go: %.pod
|
|
||||||
ifdef HAVE_POD
|
|
||||||
echo "package cmd" > cmd/$*.go
|
|
||||||
echo >> cmd/$*.go
|
|
||||||
echo "var manpage = \`" >> cmd/$*.go
|
|
||||||
pod2text $*.pod >> cmd/$*.go
|
|
||||||
echo "\`" >> cmd/$*.go
|
|
||||||
|
|
||||||
echo "var usage = \`" >> cmd/$*.go
|
|
||||||
awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> cmd/$*.go
|
|
||||||
echo "\`" >> cmd/$*.go
|
|
||||||
endif
|
|
||||||
|
|
||||||
buildlocal:
|
|
||||||
go build -ldflags "-X 'codeberg.org/scip/tablizer/cfg.VERSION=$(VERSION)'"
|
|
||||||
|
|
||||||
release:
|
|
||||||
gh release create $(version) --generate-notes
|
|
||||||
|
|
||||||
install: buildlocal
|
|
||||||
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
|
|
||||||
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
|
|
||||||
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
|
|
||||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf $(tool) releases coverage.out
|
|
||||||
|
|
||||||
test: clean
|
|
||||||
go test -count=1 -cover ./... $(OPTS)
|
|
||||||
|
|
||||||
singletest:
|
|
||||||
@echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'"
|
|
||||||
go test -run $(TEST) codeberg.org/scip/tablizer/$(MOD) $(OPTS)
|
|
||||||
|
|
||||||
cover-report:
|
|
||||||
go test ./... -cover -coverprofile=coverage.out
|
|
||||||
go tool cover -html=coverage.out
|
|
||||||
|
|
||||||
show-versions: buildlocal
|
|
||||||
@echo "### tablizer version:"
|
|
||||||
@./tablizer --version
|
|
||||||
|
|
||||||
@echo
|
|
||||||
@echo "### go module versions:"
|
|
||||||
@go list -m all
|
|
||||||
|
|
||||||
@echo
|
|
||||||
@echo "### go version used for building:"
|
|
||||||
@grep -m 1 go go.mod
|
|
||||||
|
|
||||||
goupdate:
|
|
||||||
go get -t -u=patch ./...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
golangci-lint run
|
|
||||||
|
|
||||||
# keep til ireturn
|
|
||||||
lint-full:
|
|
||||||
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn,stylecheck,testpackage,mirror,nestif,revive,goerr113,gomnd
|
|
||||||
gocritic check -enableAll *.go
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# -*-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/
|
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
## tablizer - Manipulate tabular output of other programs
|
## tablizer - Manipulate tabular output of other programs
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This software is now being maintained on [Codeberg](https://codeberg.org/scip/tablizer/).
|
||||||
|
|
||||||
Tablizer can be used to re-format tabular output of other
|
Tablizer can be used to re-format tabular output of other
|
||||||
programs. While you could do this using standard unix tools, in some
|
programs. While you could do this using standard unix tools, in some
|
||||||
cases it's a hard job. With tablizer you can filter by column[s],
|
cases it's a hard job. With tablizer you can filter by column[s],
|
||||||
|
|||||||
8
TODO.md
8
TODO.md
@@ -1,8 +0,0 @@
|
|||||||
## Fixes to be implemented
|
|
||||||
|
|
||||||
## Features to be implemented
|
|
||||||
|
|
||||||
- add comment support (csf.NewReader().Comment = '#')
|
|
||||||
|
|
||||||
- add --no-headers option
|
|
||||||
|
|
||||||
497
cfg/config.go
497
cfg/config.go
@@ -1,497 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gookit/color"
|
|
||||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Version = "v1.5.11"
|
|
||||||
MAXPARTS = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
|
|
||||||
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 {
|
|
||||||
FG string `hcl:"FG"`
|
|
||||||
BG string `hcl:"BG"`
|
|
||||||
HighlightFG string `hcl:"HighlightFG"`
|
|
||||||
HighlightBG string `hcl:"HighlightBG"`
|
|
||||||
NoHighlightFG string `hcl:"NoHighlightFG"`
|
|
||||||
NoHighlightBG string `hcl:"NoHighlightBG"`
|
|
||||||
HighlightHdrFG string `hcl:"HighlightHdrFG"`
|
|
||||||
HighlightHdrBG string `hcl:"HighlightHdrBG"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Transposer struct {
|
|
||||||
Search regexp.Regexp
|
|
||||||
Replace string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Pattern struct {
|
|
||||||
Pattern string
|
|
||||||
PatternRe *regexp.Regexp
|
|
||||||
Negate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Filter struct {
|
|
||||||
Regex *regexp.Regexp
|
|
||||||
Negate bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal config
|
|
||||||
type Config struct {
|
|
||||||
Debug bool
|
|
||||||
Numbering bool
|
|
||||||
NoHeaders bool
|
|
||||||
Columns string
|
|
||||||
UseColumns []int
|
|
||||||
YankColumns string
|
|
||||||
UseYankColumns []int
|
|
||||||
Separator string
|
|
||||||
OutputMode int
|
|
||||||
InvertMatch bool
|
|
||||||
Patterns []*Pattern
|
|
||||||
UseFuzzySearch bool
|
|
||||||
UseHighlight bool
|
|
||||||
Interactive bool
|
|
||||||
InputJSON bool
|
|
||||||
AutoHeaders bool
|
|
||||||
CustomHeaders []string
|
|
||||||
|
|
||||||
SortMode string
|
|
||||||
SortDescending bool
|
|
||||||
SortByColumn string // 1,2
|
|
||||||
UseSortByColumn []int // []int{1,2}
|
|
||||||
|
|
||||||
TransposeColumns string // 1,2
|
|
||||||
UseTransposeColumns []int // []int{1,2}
|
|
||||||
Transposers []string // []string{"/ /-/", "/foo/bar/"}
|
|
||||||
UseTransposers []Transposer // {Search: re, Replace: string}
|
|
||||||
|
|
||||||
/*
|
|
||||||
FIXME: make configurable somehow, config file or ENV
|
|
||||||
see https://github.com/gookit/color.
|
|
||||||
*/
|
|
||||||
ColorStyle color.Style
|
|
||||||
HighlightStyle color.Style
|
|
||||||
NoHighlightStyle color.Style
|
|
||||||
HighlightHdrStyle color.Style
|
|
||||||
|
|
||||||
NoColor bool
|
|
||||||
|
|
||||||
// config file, optional
|
|
||||||
Configfile string
|
|
||||||
|
|
||||||
Settings Settings
|
|
||||||
|
|
||||||
// used for field filtering
|
|
||||||
Rawfilters []string
|
|
||||||
Filters map[string]Filter //map[string]*regexp.Regexp
|
|
||||||
|
|
||||||
// -r <file>
|
|
||||||
InputFile string
|
|
||||||
|
|
||||||
OFS string
|
|
||||||
}
|
|
||||||
|
|
||||||
// maps outputmode short flags to output mode, ie. -O => -o orgtbl
|
|
||||||
type Modeflag struct {
|
|
||||||
X bool
|
|
||||||
O bool
|
|
||||||
M bool
|
|
||||||
S bool
|
|
||||||
Y bool
|
|
||||||
A bool
|
|
||||||
C bool
|
|
||||||
J bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for switching printers
|
|
||||||
const (
|
|
||||||
Extended = iota + 1
|
|
||||||
Orgtbl
|
|
||||||
Markdown
|
|
||||||
Shell
|
|
||||||
Yaml
|
|
||||||
CSV
|
|
||||||
ASCII
|
|
||||||
Json
|
|
||||||
)
|
|
||||||
|
|
||||||
// various sort types
|
|
||||||
type Sortmode struct {
|
|
||||||
Numeric bool
|
|
||||||
Time bool
|
|
||||||
Age bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// default color schemes
|
|
||||||
func (conf *Config) Colors() map[color.Level]map[string]color.Color {
|
|
||||||
colors := map[color.Level]map[string]color.Color{
|
|
||||||
color.Level16: {
|
|
||||||
"bg": color.BgGreen, "fg": color.FgWhite,
|
|
||||||
"hlbg": color.BgGray, "hlfg": color.FgWhite,
|
|
||||||
},
|
|
||||||
color.Level256: {
|
|
||||||
"bg": color.BgLightGreen, "fg": color.FgWhite,
|
|
||||||
"hlbg": color.BgLightBlue, "hlfg": color.FgWhite,
|
|
||||||
},
|
|
||||||
color.LevelRgb: {
|
|
||||||
"bg": color.BgLightGreen, "fg": color.FgWhite,
|
|
||||||
"hlbg": color.BgHiGreen, "hlfg": color.FgWhite,
|
|
||||||
"nohlbg": color.BgWhite, "nohlfg": color.FgLightGreen,
|
|
||||||
"hdrbg": color.BgBlue, "hdrfg": color.FgWhite,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.BG) > 0 {
|
|
||||||
colors[color.Level16]["bg"] = ColorStringToBGColor(conf.Settings.BG)
|
|
||||||
colors[color.Level256]["bg"] = ColorStringToBGColor(conf.Settings.BG)
|
|
||||||
colors[color.LevelRgb]["bg"] = ColorStringToBGColor(conf.Settings.BG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.FG) > 0 {
|
|
||||||
colors[color.Level16]["fg"] = ColorStringToColor(conf.Settings.FG)
|
|
||||||
colors[color.Level256]["fg"] = ColorStringToColor(conf.Settings.FG)
|
|
||||||
colors[color.LevelRgb]["fg"] = ColorStringToColor(conf.Settings.FG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.HighlightBG) > 0 {
|
|
||||||
colors[color.Level16]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
|
|
||||||
colors[color.Level256]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
|
|
||||||
colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.HighlightFG) > 0 {
|
|
||||||
colors[color.Level16]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
|
|
||||||
colors[color.Level256]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
|
|
||||||
colors[color.LevelRgb]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.NoHighlightBG) > 0 {
|
|
||||||
colors[color.Level16]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
|
|
||||||
colors[color.Level256]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
|
|
||||||
colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.NoHighlightFG) > 0 {
|
|
||||||
colors[color.Level16]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
|
|
||||||
colors[color.Level256]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
|
|
||||||
colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.HighlightHdrBG) > 0 {
|
|
||||||
colors[color.Level16]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
|
|
||||||
colors[color.Level256]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
|
|
||||||
colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Settings.HighlightHdrFG) > 0 {
|
|
||||||
colors[color.Level16]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
|
|
||||||
colors[color.Level256]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
|
|
||||||
colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG)
|
|
||||||
}
|
|
||||||
|
|
||||||
return colors
|
|
||||||
}
|
|
||||||
|
|
||||||
// find supported color mode, modifies config based on constants
|
|
||||||
func (conf *Config) DetermineColormode() {
|
|
||||||
if !isTerminal(os.Stdout) {
|
|
||||||
color.Disable()
|
|
||||||
} else {
|
|
||||||
level := color.TermColorLevel()
|
|
||||||
colors := conf.Colors()
|
|
||||||
|
|
||||||
conf.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"])
|
|
||||||
conf.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"])
|
|
||||||
conf.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"])
|
|
||||||
conf.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if current terminal is interactive
|
|
||||||
func isTerminal(f *os.File) bool {
|
|
||||||
o, _ := f.Stat()
|
|
||||||
|
|
||||||
return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
// main program version
|
|
||||||
// generated version string, used by -v contains lib.Version on
|
|
||||||
//
|
|
||||||
// main branch, and lib.Version-$branch-$lastcommit-$date on
|
|
||||||
//
|
|
||||||
// development branch
|
|
||||||
func Getversion() string {
|
|
||||||
return fmt.Sprintf("This is tablizer version %s", VERSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *Config) PrepareSortFlags(flag Sortmode) {
|
|
||||||
switch {
|
|
||||||
case flag.Numeric:
|
|
||||||
conf.SortMode = "numeric"
|
|
||||||
case flag.Age:
|
|
||||||
conf.SortMode = "duration"
|
|
||||||
case flag.Time:
|
|
||||||
conf.SortMode = "time"
|
|
||||||
default:
|
|
||||||
conf.SortMode = "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *Config) PrepareModeFlags(flag Modeflag) {
|
|
||||||
switch {
|
|
||||||
case flag.X:
|
|
||||||
conf.OutputMode = Extended
|
|
||||||
case flag.O:
|
|
||||||
conf.OutputMode = Orgtbl
|
|
||||||
case flag.M:
|
|
||||||
conf.OutputMode = Markdown
|
|
||||||
case flag.S:
|
|
||||||
conf.OutputMode = Shell
|
|
||||||
case flag.Y:
|
|
||||||
conf.OutputMode = Yaml
|
|
||||||
case flag.C:
|
|
||||||
conf.OutputMode = CSV
|
|
||||||
case flag.J:
|
|
||||||
conf.OutputMode = Json
|
|
||||||
default:
|
|
||||||
conf.OutputMode = ASCII
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *Config) PrepareFilters() error {
|
|
||||||
conf.Filters = make(map[string]Filter, len(conf.Rawfilters))
|
|
||||||
|
|
||||||
for _, rawfilter := range conf.Rawfilters {
|
|
||||||
filter := Filter{}
|
|
||||||
|
|
||||||
parts := strings.Split(rawfilter, "!=")
|
|
||||||
if len(parts) != MAXPARTS {
|
|
||||||
parts = strings.Split(rawfilter, "=")
|
|
||||||
|
|
||||||
if len(parts) != MAXPARTS {
|
|
||||||
return errors.New("filter field and value must be separated by '=' or '!='")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filter.Negate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
reg, err := regexp.Compile(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to compile filter regex for field %s: %w",
|
|
||||||
parts[0], err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.Regex = reg
|
|
||||||
conf.Filters[strings.ToLower(parts[0])] = filter
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if transposers match transposer columns and prepare transposer structs
|
|
||||||
func (conf *Config) PrepareTransposers() error {
|
|
||||||
if len(conf.Transposers) != len(conf.UseTransposeColumns) {
|
|
||||||
return fmt.Errorf("the number of transposers needs to correspond to the number of transpose columns: %d != %d",
|
|
||||||
len(conf.Transposers), len(conf.UseTransposeColumns))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, transposer := range conf.Transposers {
|
|
||||||
parts := strings.Split(transposer, string(transposer[0]))
|
|
||||||
if len(parts) != 4 {
|
|
||||||
return fmt.Errorf("transposer function must have the format /regexp/replace-string/")
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.UseTransposers = append(conf.UseTransposers,
|
|
||||||
Transposer{
|
|
||||||
Search: *regexp.MustCompile(parts[1]),
|
|
||||||
Replace: parts[2]},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *Config) CheckEnv() {
|
|
||||||
// check for environment vars, command line flags have precedence,
|
|
||||||
// NO_COLOR is being checked by the color module itself.
|
|
||||||
if !conf.Numbering {
|
|
||||||
_, set := os.LookupEnv("T_HEADER_NUMBERING")
|
|
||||||
if set {
|
|
||||||
conf.Numbering = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.Columns) == 0 {
|
|
||||||
cols := os.Getenv("T_COLUMNS")
|
|
||||||
if len(cols) > 1 {
|
|
||||||
conf.Columns = cols
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (conf *Config) ApplyDefaults() {
|
|
||||||
// mode specific defaults
|
|
||||||
if conf.OutputMode == Yaml || conf.OutputMode == CSV {
|
|
||||||
conf.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 {
|
|
||||||
// regex checks if a pattern looks like /$pattern/[i!]
|
|
||||||
flagre := regexp.MustCompile(`^/(.*)/([i!]*)$`)
|
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
matches := flagre.FindAllStringSubmatch(pattern.Pattern, -1)
|
|
||||||
|
|
||||||
// we have a regex with flags
|
|
||||||
for _, match := range matches {
|
|
||||||
pattern.Pattern = match[1] // the inner part is our actual pattern
|
|
||||||
flags := match[2] // the flags
|
|
||||||
|
|
||||||
for _, flag := range flags {
|
|
||||||
switch flag {
|
|
||||||
case 'i':
|
|
||||||
pattern.Pattern = `(?i)` + pattern.Pattern
|
|
||||||
case '!':
|
|
||||||
pattern.Negate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternRe, err := regexp.Compile(pattern.Pattern)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("regexp pattern %s is invalid: %w", pattern.Pattern, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern.PatternRe = PatternRe
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.Patterns = patterns
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
path, err := os.Stat(conf.Configfile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
// ignore non-existent files
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to stat config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if path.IsDir() {
|
|
||||||
// ignore non-existent or dirs
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
configstring, err := os.ReadFile(path.Name())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read config file %s: %w", path.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = hclsimple.Decode(
|
|
||||||
path.Name(),
|
|
||||||
configstring,
|
|
||||||
nil,
|
|
||||||
&conf.Settings)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load configuration file %s: %w",
|
|
||||||
path.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// translate color string to internal color value
|
|
||||||
func ColorStringToColor(colorname string) color.Color {
|
|
||||||
for name, color := range color.FgColors {
|
|
||||||
if name == colorname {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, color := range color.ExFgColors {
|
|
||||||
if name == colorname {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return color.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
// same, for background colors
|
|
||||||
func ColorStringToBGColor(colorname string) color.Color {
|
|
||||||
for name, color := range color.BgColors {
|
|
||||||
if name == colorname {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, color := range color.ExBgColors {
|
|
||||||
if name == colorname {
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return color.Normal
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cfg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
// "reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrepareModeFlags(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
flag Modeflag
|
|
||||||
expect int // output (constant enum)
|
|
||||||
}{
|
|
||||||
// short commandline flags like -M
|
|
||||||
{Modeflag{X: true}, Extended},
|
|
||||||
{Modeflag{S: true}, Shell},
|
|
||||||
{Modeflag{O: true}, Orgtbl},
|
|
||||||
{Modeflag{Y: true}, Yaml},
|
|
||||||
{Modeflag{M: true}, Markdown},
|
|
||||||
{Modeflag{}, ASCII},
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: use a map for easier printing
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("PrepareModeFlags-expect-%d", testdata.expect)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := Config{}
|
|
||||||
|
|
||||||
conf.PrepareModeFlags(testdata.flag)
|
|
||||||
|
|
||||||
assert.EqualValues(t, testdata.expect, conf.OutputMode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepareSortFlags(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
flag Sortmode
|
|
||||||
expect string // output
|
|
||||||
}{
|
|
||||||
// short commandline flags like -M
|
|
||||||
{Sortmode{Numeric: true}, "numeric"},
|
|
||||||
{Sortmode{Age: true}, "duration"},
|
|
||||||
{Sortmode{Time: true}, "time"},
|
|
||||||
{Sortmode{}, "string"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("PrepareSortFlags-expect-%s", testdata.expect)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := Config{}
|
|
||||||
|
|
||||||
conf.PrepareSortFlags(testdata.flag)
|
|
||||||
|
|
||||||
assert.EqualValues(t, testdata.expect, conf.SortMode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPreparePattern(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
patterns []*Pattern
|
|
||||||
name string
|
|
||||||
wanterror bool
|
|
||||||
wanticase bool
|
|
||||||
wantneg bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
[]*Pattern{{Pattern: "[A-Z]+"}},
|
|
||||||
"simple",
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]*Pattern{{Pattern: "[a-z"}},
|
|
||||||
"regfail",
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]*Pattern{{Pattern: "/[A-Z]+/i"}},
|
|
||||||
"icase",
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]*Pattern{{Pattern: "/[A-Z]+/!"}},
|
|
||||||
"negate",
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[]*Pattern{{Pattern: "/[A-Z]+/!i"}},
|
|
||||||
"negicase",
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
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 testdata.wanterror {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
208
cmd/root.go
208
cmd/root.go
@@ -1,208 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
"codeberg.org/scip/tablizer/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func completion(cmd *cobra.Command, mode string) error {
|
|
||||||
switch mode {
|
|
||||||
case "bash":
|
|
||||||
return cmd.Root().GenBashCompletion(os.Stdout)
|
|
||||||
case "zsh":
|
|
||||||
return cmd.Root().GenZshCompletion(os.Stdout)
|
|
||||||
case "fish":
|
|
||||||
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
|
||||||
case "powershell":
|
|
||||||
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
|
||||||
default:
|
|
||||||
return errors.New("invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we die with exit 1 if there's an error
|
|
||||||
func wrapE(err error) {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
|
||||||
var (
|
|
||||||
conf cfg.Config
|
|
||||||
ShowManual bool
|
|
||||||
ShowVersion bool
|
|
||||||
ShowCompletion string
|
|
||||||
modeflag cfg.Modeflag
|
|
||||||
sortmode cfg.Sortmode
|
|
||||||
headers string
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "tablizer [regex] [file, ...]",
|
|
||||||
Short: "[Re-]tabularize tabular data",
|
|
||||||
Long: `Manipulate tabular output of other programs`,
|
|
||||||
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if ShowVersion {
|
|
||||||
fmt.Println(cfg.Getversion())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ShowManual {
|
|
||||||
lib.Pager("tablizer manual page", manpage)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ShowCompletion) > 0 {
|
|
||||||
wrapE(completion(cmd, ShowCompletion))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup
|
|
||||||
wrapE(conf.ParseConfigfile())
|
|
||||||
|
|
||||||
conf.CheckEnv()
|
|
||||||
conf.PrepareModeFlags(modeflag)
|
|
||||||
conf.PrepareSortFlags(sortmode)
|
|
||||||
conf.PrepareCustomHeaders(headers)
|
|
||||||
|
|
||||||
wrapE(conf.PrepareFilters())
|
|
||||||
|
|
||||||
conf.DetermineColormode()
|
|
||||||
conf.ApplyDefaults()
|
|
||||||
|
|
||||||
// actual execution starts here
|
|
||||||
wrapE(lib.ProcessFiles(&conf, args))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// options
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false,
|
|
||||||
"Enable debugging")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.Numbering, "numbering", "n", false,
|
|
||||||
"Disable header numbering")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false,
|
|
||||||
"Disable header display")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.NoColor, "no-color", "N", false,
|
|
||||||
"Disable pattern highlighting")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "V", false,
|
|
||||||
"Print program version")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false,
|
|
||||||
"select non-matching rows")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false,
|
|
||||||
"Display manual page")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false,
|
|
||||||
"Use fuzzy searching")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false,
|
|
||||||
"Use alternating background colors")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "",
|
|
||||||
"Display completion code")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.SeparatorTemplates[":default:"],
|
|
||||||
"Custom field separator")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "",
|
|
||||||
"Only show the speficied columns (separated by ,)")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&conf.YankColumns, "yank-columns", "y", "",
|
|
||||||
"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", "",
|
|
||||||
"Sort by column (default: 1)")
|
|
||||||
|
|
||||||
// sort mode, only 1 allowed
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false,
|
|
||||||
"Sort in descending order (default: ascending)")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false,
|
|
||||||
"sort according to string numerical value")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false,
|
|
||||||
"sort according to time string")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false,
|
|
||||||
"sort according to age (duration) string")
|
|
||||||
rootCmd.MarkFlagsMutuallyExclusive("sort-numeric", "sort-time",
|
|
||||||
"sort-age")
|
|
||||||
|
|
||||||
// output flags, only 1 allowed
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false,
|
|
||||||
"Enable extended output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false,
|
|
||||||
"Enable markdown table output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false,
|
|
||||||
"Enable org-mode table output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false,
|
|
||||||
"Enable shell mode output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false,
|
|
||||||
"Enable yaml output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&modeflag.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")
|
|
||||||
|
|
||||||
// config file
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile,
|
|
||||||
"config file (default: ~/.config/tablizer/config)")
|
|
||||||
|
|
||||||
// filters
|
|
||||||
rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters,
|
|
||||||
"filter", "F", nil, "Filter by field (field=regexp || field!=regexp)")
|
|
||||||
rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers,
|
|
||||||
"regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T")
|
|
||||||
|
|
||||||
// input
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&conf.InputFile, "read-file", "r", "",
|
|
||||||
"Read input data from file")
|
|
||||||
|
|
||||||
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")
|
|
||||||
|
|
||||||
if slices.Contains(os.Args, "-h") {
|
|
||||||
fmt.Println(shortusage)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := rootCmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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 -D sort descending order
|
|
||||||
-m show manual --help show detailed help -v show version
|
|
||||||
-a sort by age -i sort numerically -t sort by time`
|
|
||||||
556
cmd/tablizer.go
556
cmd/tablizer.go
@@ -1,556 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
var manpage = `
|
|
||||||
NAME
|
|
||||||
tablizer - Manipulate tabular output of other programs
|
|
||||||
|
|
||||||
SYNOPSIS
|
|
||||||
Usage:
|
|
||||||
tablizer [regex,...] [-r file] [flags]
|
|
||||||
|
|
||||||
Operational Flags:
|
|
||||||
-c, --columns string Only show the speficied columns (separated by ,)
|
|
||||||
-v, --invert-match select non-matching rows
|
|
||||||
-n, --numbering Enable header numbering
|
|
||||||
-N, --no-color Disable pattern highlighting
|
|
||||||
-H, --no-headers Disable headers display
|
|
||||||
-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
|
|
||||||
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
|
|
||||||
-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
|
|
||||||
-M, --markdown Enable markdown table output
|
|
||||||
-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
|
|
||||||
-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
|
|
||||||
|
|
||||||
Sort Mode Flags (mutually exclusive):
|
|
||||||
-a, --sort-age sort according to age (duration) string
|
|
||||||
-D, --sort-desc Sort in descending order (default: ascending)
|
|
||||||
-i, --sort-numeric sort according to string numerical value
|
|
||||||
-t, --sort-time sort according to time string
|
|
||||||
|
|
||||||
Other Flags:
|
|
||||||
-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
|
|
||||||
-h, --help help for tablizer
|
|
||||||
-m, --man Display manual page
|
|
||||||
-V, --version Print program version
|
|
||||||
|
|
||||||
DESCRIPTION
|
|
||||||
Many programs generate tabular output. But sometimes you need to
|
|
||||||
post-process these tables, you may need to remove one or more columns or
|
|
||||||
you may want to filter for some pattern (See PATTERNS) or you may need
|
|
||||||
the output in another program and need to parse it somehow. Standard
|
|
||||||
unix tools such as awk(1), grep(1) or column(1) may help, but sometimes
|
|
||||||
it's a tedious business.
|
|
||||||
|
|
||||||
Let's take the output of the tool kubectl. It contains cells with
|
|
||||||
withespace and they do not separate columns by TAB characters. This is
|
|
||||||
not easy to process.
|
|
||||||
|
|
||||||
You can use tablizer to do these and more things.
|
|
||||||
|
|
||||||
tablizer analyses the header fields of a table, registers the column
|
|
||||||
positions of each header field and separates columns by those positions.
|
|
||||||
|
|
||||||
Without any options it reads its input from "STDIN", but you can also
|
|
||||||
specify a file as a parameter. If you want to reduce the output by some
|
|
||||||
regular expression, just specify it as its first parameter. You may also
|
|
||||||
use the -v option to exclude all rows which match the pattern. Hence:
|
|
||||||
|
|
||||||
# read from STDIN
|
|
||||||
kubectl get pods | tablizer
|
|
||||||
|
|
||||||
# read a file
|
|
||||||
tablizer -r filename
|
|
||||||
|
|
||||||
# search for pattern in a file (works like grep)
|
|
||||||
tablizer regex -r filename
|
|
||||||
|
|
||||||
# search for pattern in STDIN
|
|
||||||
kubectl get pods | tablizer regex
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
You can specify the numbers in any order but output will always follow
|
|
||||||
the original order.
|
|
||||||
|
|
||||||
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
|
|
||||||
affects tabular output modes. Shell, Extended, Yaml and CSV output modes
|
|
||||||
always use the column names.
|
|
||||||
|
|
||||||
By default, if a pattern has been speficied, matches will be
|
|
||||||
highlighted. You can disable this behavior with the -N option.
|
|
||||||
|
|
||||||
Use the -k option to specify by which column to sort the tabular data
|
|
||||||
(as in GNU sort(1)). The default sort column is the first one. You can
|
|
||||||
specify column numbers or names. Column numbers start with 1, names are
|
|
||||||
case insensitive. You can specify multiple columns separated by comma to
|
|
||||||
sort, but the type must be the same. For example if you want to sort
|
|
||||||
numerically, all columns must be numbers. If you use column numbers,
|
|
||||||
then be aware, that these are the numbers before column extraction. For
|
|
||||||
example if you have a table with 4 columns and specify "-c4", then only
|
|
||||||
1 column (the fourth) will be printed, however if you want to sort by
|
|
||||||
this column, you'll have to specify "-k4".
|
|
||||||
|
|
||||||
The default sort order is ascending. You can change this to descending
|
|
||||||
order using the option -D. The default sort order is by alphanumeric
|
|
||||||
string, but there are other sort modes:
|
|
||||||
|
|
||||||
-a --sort-age
|
|
||||||
Sorts duration strings like "1d4h32m51s".
|
|
||||||
|
|
||||||
-i --sort-numeric
|
|
||||||
Sorts numeric fields.
|
|
||||||
|
|
||||||
-t --sort-time
|
|
||||||
Sorts timestamps.
|
|
||||||
|
|
||||||
Finally the -d option enables debugging output which is mostly useful
|
|
||||||
for the developer.
|
|
||||||
|
|
||||||
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
|
|
||||||
GOLANG, refer to the syntax cheat sheet here:
|
|
||||||
<https://pkg.go.dev/regexp/syntax>.
|
|
||||||
|
|
||||||
If you want to read a more comprehensive documentation about the topic
|
|
||||||
and have perl installed you can read it with:
|
|
||||||
|
|
||||||
perldoc perlre
|
|
||||||
|
|
||||||
Or read it online: <https://perldoc.perl.org/perlre>. But please note
|
|
||||||
that the GO regexp engine does NOT support all perl regex terms,
|
|
||||||
especially look-ahead and look-behind.
|
|
||||||
|
|
||||||
If you want to supply flags to a regex, then surround it with slashes
|
|
||||||
and append the flag. The following flags are supported:
|
|
||||||
|
|
||||||
i => case insensitive
|
|
||||||
! => negative match
|
|
||||||
|
|
||||||
Example for a case insensitive search:
|
|
||||||
|
|
||||||
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,
|
|
||||||
tablizer will NOT include it in the output.
|
|
||||||
|
|
||||||
For example, here we want to get all lines matching "foo" but not "bar":
|
|
||||||
|
|
||||||
cat table | tablizer foo '/bar/!'
|
|
||||||
|
|
||||||
This would match a line "foo zorro" but not "foo bar".
|
|
||||||
|
|
||||||
The flags can also be combined.
|
|
||||||
|
|
||||||
You can also use the experimental fuzzy search feature by providing the
|
|
||||||
option -z, in which case the pattern is regarded as a fuzzy search term,
|
|
||||||
not a regexp.
|
|
||||||
|
|
||||||
Sometimes you want to filter by one or more columns. You can do that
|
|
||||||
using the -F option. The option can be specified multiple times and has
|
|
||||||
the following format:
|
|
||||||
|
|
||||||
fieldname=regexp
|
|
||||||
|
|
||||||
Fieldnames (== columns headers) are case insensitive.
|
|
||||||
|
|
||||||
If you specify more than one filter, both filters have to match (AND
|
|
||||||
operation).
|
|
||||||
|
|
||||||
These field filters can also be negated:
|
|
||||||
|
|
||||||
fieldname!=regexp
|
|
||||||
|
|
||||||
If the option -v is specified, the filtering is inverted.
|
|
||||||
|
|
||||||
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
|
|
||||||
used to specify which header to display, see example above.
|
|
||||||
|
|
||||||
However, beside numbers, you can also use regular expressions with -c,
|
|
||||||
also separated by comma. And you can mix column numbers with regexps.
|
|
||||||
|
|
||||||
Lets take this table:
|
|
||||||
|
|
||||||
PID TTY TIME CMD
|
|
||||||
14001 pts/0 00:00:00 bash
|
|
||||||
42871 pts/0 00:00:00 ps
|
|
||||||
42872 pts/0 00:00:00 sed
|
|
||||||
|
|
||||||
We want to see only the CMD column and use a regex for this:
|
|
||||||
|
|
||||||
ps | tablizer -s '\s+' -c C
|
|
||||||
CMD(4)
|
|
||||||
bash
|
|
||||||
ps
|
|
||||||
tablizer
|
|
||||||
sed
|
|
||||||
|
|
||||||
where "C" is our regexp which matches CMD.
|
|
||||||
|
|
||||||
If a column specifier doesn't look like a regular expression, matching
|
|
||||||
against header fields will be case insensitive. So, if you have a field
|
|
||||||
with the name "ID" then these will all match: "-c id", "-c Id". The same
|
|
||||||
rule applies to the options "-T" and "-F".
|
|
||||||
|
|
||||||
TRANSPOSE FIELDS USING REGEXPS
|
|
||||||
You can manipulate field contents using regular expressions. You have to
|
|
||||||
tell tablizer which field[s] to operate on using the option "-T" and the
|
|
||||||
search/replace pattern using "-R". The number of columns and patterns
|
|
||||||
must match.
|
|
||||||
|
|
||||||
A search/replace pattern consists of the following elements:
|
|
||||||
|
|
||||||
/search-regexp/replace-string/
|
|
||||||
|
|
||||||
The separator can be any valid character. Especially if you want to use
|
|
||||||
a regexp containing the "/" character, eg:
|
|
||||||
|
|
||||||
|search-regexp|replace-string|
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
cat t/testtable2
|
|
||||||
NAME DURATION
|
|
||||||
x 10
|
|
||||||
a 100
|
|
||||||
z 0
|
|
||||||
u 4
|
|
||||||
k 6
|
|
||||||
|
|
||||||
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
|
|
||||||
NAME DURATION
|
|
||||||
x 40
|
|
||||||
a 400
|
|
||||||
z 4
|
|
||||||
u 4
|
|
||||||
k 4
|
|
||||||
|
|
||||||
OUTPUT MODES
|
|
||||||
There might be cases when the tabular output of a program is way too
|
|
||||||
large for your current terminal but you still need to see every column.
|
|
||||||
In such cases the -o extended or -X option can be useful which enables
|
|
||||||
*extended mode*. In this mode, each row will be printed vertically,
|
|
||||||
header left, value right, aligned by the field widths. Here's an
|
|
||||||
example:
|
|
||||||
|
|
||||||
kubectl get pods | ./tablizer -o extended
|
|
||||||
NAME: repldepl-7bcd8d5b64-7zq4l
|
|
||||||
READY: 1/1
|
|
||||||
STATUS: Running
|
|
||||||
RESTARTS: 1 (71m ago)
|
|
||||||
AGE: 5h28m
|
|
||||||
|
|
||||||
You can of course still use a regex to reduce the number of rows
|
|
||||||
displayed.
|
|
||||||
|
|
||||||
The option -o shell can be used if the output has to be processed by the
|
|
||||||
shell, it prints variable assignments for each cell, one line per row:
|
|
||||||
|
|
||||||
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
|
|
||||||
NAME="repldepl-7bcd8d5b64-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
NAME="repldepl-7bcd8d5b64-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
NAME="repldepl-7bcd8d5b64-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
|
|
||||||
You can use this in an eval loop.
|
|
||||||
|
|
||||||
Beside normal ascii mode (the default) and extended mode there are more
|
|
||||||
output modes available: orgtbl which prints an Emacs org-mode table and
|
|
||||||
markdown which prints a Markdown table, yaml, which prints yaml encoding
|
|
||||||
and CSV mode, which prints a comma separated value file.
|
|
||||||
|
|
||||||
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
|
|
||||||
just one row. For example:
|
|
||||||
|
|
||||||
cloudctl cluster ls | tablizer -yid matchbox
|
|
||||||
|
|
||||||
If "matchbox" matches one cluster, you can immediately use the id of
|
|
||||||
that cluster somewhere else and paste it. Of course, if there are
|
|
||||||
multiple matches, then all id's will be put into the clipboard separated
|
|
||||||
by one space.
|
|
||||||
|
|
||||||
ENVIRONMENT VARIABLES
|
|
||||||
tablizer supports certain environment variables which use can use to
|
|
||||||
influence program behavior. Commandline flags have always precedence
|
|
||||||
over environment variables.
|
|
||||||
|
|
||||||
<T_HEADER_NUMBERING> - enable numbering of header fields, like -n.
|
|
||||||
<T_COLUMNS> - comma separated list of columns to output, like -c
|
|
||||||
<NO_COLORS> - disable colorization of matches, like -N
|
|
||||||
|
|
||||||
COMPLETION
|
|
||||||
Shell completion for command line options can be enabled by using the
|
|
||||||
--completion flag. The required parameter is the name of your shell.
|
|
||||||
Currently supported are: bash, zsh, fish and powershell.
|
|
||||||
|
|
||||||
Detailed instructions:
|
|
||||||
|
|
||||||
Bash:
|
|
||||||
source <(tablizer --completion bash)
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
# Linux:
|
|
||||||
$ tablizer --completion bash > /etc/bash_completion.d/tablizer
|
|
||||||
|
|
||||||
# macOS:
|
|
||||||
$ tablizer --completion bash > $(brew --prefix)/etc/bash_completion.d/tablizer
|
|
||||||
|
|
||||||
Zsh:
|
|
||||||
If shell completion is not already enabled in your environment, you
|
|
||||||
will need to enable it. You can execute the following once:
|
|
||||||
|
|
||||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
$ tablizer --completion zsh > "${fpath[1]}/_tablizer"
|
|
||||||
|
|
||||||
You will need to start a new shell for this setup to take effect.
|
|
||||||
|
|
||||||
fish:
|
|
||||||
tablizer --completion fish | source
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
tablizer --completion fish > ~/.config/fish/completions/tablizer.fish
|
|
||||||
|
|
||||||
PowerShell:
|
|
||||||
tablizer --completion powershell | Out-String | Invoke-Expression
|
|
||||||
|
|
||||||
To load completions for every new session, run:
|
|
||||||
|
|
||||||
tablizer --completion powershell > tablizer.ps1
|
|
||||||
|
|
||||||
and source this file from your PowerShell profile.
|
|
||||||
|
|
||||||
CONFIGURATION AND COLORS
|
|
||||||
YOu can put certain configuration values into a configuration file in
|
|
||||||
HCL format. By default tablizer looks for
|
|
||||||
"$HOME/.config/tablizer/config", but you can provide one using the
|
|
||||||
parameter "-f".
|
|
||||||
|
|
||||||
In the configuration the following variables can be defined:
|
|
||||||
|
|
||||||
BG = "lightGreen"
|
|
||||||
FG = "white"
|
|
||||||
HighlightBG = "lightGreen"
|
|
||||||
HighlightFG = "white"
|
|
||||||
NoHighlightBG = "white"
|
|
||||||
NoHighlightFG = "lightGreen"
|
|
||||||
HighlightHdrBG = "red"
|
|
||||||
HighlightHdrFG = "white"
|
|
||||||
|
|
||||||
The following color definitions are available:
|
|
||||||
|
|
||||||
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
|
|
||||||
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta,
|
|
||||||
red, white, yellow
|
|
||||||
|
|
||||||
The Variables FG and BG are being used to highlight matches. The other
|
|
||||||
*FG and *BG variables are for colored table output (enabled with the
|
|
||||||
"-L" parameter).
|
|
||||||
|
|
||||||
Colorization can be turned off completely either by setting the
|
|
||||||
parameter "-N" or the environment variable NO_COLOR to a true value.
|
|
||||||
|
|
||||||
BUGS
|
|
||||||
In order to report a bug, unexpected behavior, feature requests or to
|
|
||||||
submit a patch, please open an issue on github:
|
|
||||||
<https://codeberg.org/scip/tablizer/issues>.
|
|
||||||
|
|
||||||
LICENSE
|
|
||||||
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
|
|
||||||
3.
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 by Thomas von Dein
|
|
||||||
|
|
||||||
This software uses the following GO modules:
|
|
||||||
|
|
||||||
repr (https://github.com/alecthomas/repr)
|
|
||||||
Released under the MIT License, Copyright (c) 2016 Alec Thomas
|
|
||||||
|
|
||||||
cobra (https://github.com/spf13/cobra)
|
|
||||||
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra
|
|
||||||
Authors
|
|
||||||
|
|
||||||
dateparse (github.com/araddon/dateparse)
|
|
||||||
Released under the MIT License, Copyright (c) 2015-2017 Aaron Raddon
|
|
||||||
|
|
||||||
color (github.com/gookit/color)
|
|
||||||
Released under the MIT License, Copyright (c) 2016 inhere
|
|
||||||
|
|
||||||
tablewriter (github.com/olekukonko/tablewriter)
|
|
||||||
Released under the MIT License, Copyright (c) 201 by Oleku Konko
|
|
||||||
|
|
||||||
yaml (gopkg.in/yaml.v3)
|
|
||||||
Released under the MIT License, Copyright (c) 2006-2011 Kirill
|
|
||||||
Simonov
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
`
|
|
||||||
var usage = `
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
tablizer [regex,...] [-r file] [flags]
|
|
||||||
|
|
||||||
Operational Flags:
|
|
||||||
-c, --columns string Only show the speficied columns (separated by ,)
|
|
||||||
-v, --invert-match select non-matching rows
|
|
||||||
-n, --numbering Enable header numbering
|
|
||||||
-N, --no-color Disable pattern highlighting
|
|
||||||
-H, --no-headers Disable headers display
|
|
||||||
-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
|
|
||||||
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
|
|
||||||
-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
|
|
||||||
-M, --markdown Enable markdown table output
|
|
||||||
-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
|
|
||||||
-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
|
|
||||||
|
|
||||||
Sort Mode Flags (mutually exclusive):
|
|
||||||
-a, --sort-age sort according to age (duration) string
|
|
||||||
-D, --sort-desc Sort in descending order (default: ascending)
|
|
||||||
-i, --sort-numeric sort according to string numerical value
|
|
||||||
-t, --sort-time sort according to time string
|
|
||||||
|
|
||||||
Other Flags:
|
|
||||||
-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
|
|
||||||
-h, --help help for tablizer
|
|
||||||
-m, --man Display manual page
|
|
||||||
-V, --version Print program version
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
12
config.hcl
12
config.hcl
@@ -1,12 +0,0 @@
|
|||||||
# supported colors:
|
|
||||||
# black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
|
|
||||||
# lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
|
|
||||||
# magenta, red, white, yellow
|
|
||||||
BG = "lightGreen"
|
|
||||||
FG = "white"
|
|
||||||
HighlightBG = "lightGreen"
|
|
||||||
HighlightFG = "white"
|
|
||||||
NoHighlightBG = "white"
|
|
||||||
NoHighlightFG = "lightGreen"
|
|
||||||
HighlightHdrBG = "red"
|
|
||||||
HighlightHdrFG = "white"
|
|
||||||
61
go.mod
61
go.mod
@@ -1,61 +0,0 @@
|
|||||||
module codeberg.org/scip/tablizer
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/alecthomas/repr v0.5.2
|
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
|
||||||
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/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.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 (
|
|
||||||
github.com/agext/levenshtein v1.2.3 // 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/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-localereader v0.0.1 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1 // 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.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.9 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
github.com/zclconf/go-cty v1.16.3 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
|
||||||
golang.org/x/mod v0.21.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
|
|
||||||
)
|
|
||||||
156
go.sum
156
go.sum
@@ -1,156 +0,0 @@
|
|||||||
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.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/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/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/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/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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
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 v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
|
||||||
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.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/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=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
||||||
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.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.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.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-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=
|
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
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=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.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=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2024 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
// contains a whole parsed table
|
|
||||||
type Tabdata struct {
|
|
||||||
maxwidthHeader int // longest header
|
|
||||||
columns int // count
|
|
||||||
headers []string // [ "ID", "NAME", ...]
|
|
||||||
entries [][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (data *Tabdata) CloneEmpty() Tabdata {
|
|
||||||
newdata := Tabdata{
|
|
||||||
maxwidthHeader: data.maxwidthHeader,
|
|
||||||
columns: data.columns,
|
|
||||||
headers: data.headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
return newdata
|
|
||||||
}
|
|
||||||
192
lib/filter.go
192
lib/filter.go
@@ -1,192 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* [!]Match a line, use fuzzy search for normal pattern strings and
|
|
||||||
* regexp otherwise.
|
|
||||||
|
|
||||||
'foo bar' foo, /bar/! => false => line contains foo and not (not bar)
|
|
||||||
'foo nix' foo, /bar/! => ture => line contains foo and (not bar)
|
|
||||||
'foo bar' foo, /bar/ => true => line contains both foo and bar
|
|
||||||
'foo nix' foo, /bar/ => false => line does not contain bar
|
|
||||||
'foo bar' foo, /nix/ => false => line does not contain nix
|
|
||||||
*/
|
|
||||||
func matchPattern(conf cfg.Config, line string) bool {
|
|
||||||
if len(conf.Patterns) == 0 {
|
|
||||||
// any line always matches ""
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.UseFuzzySearch {
|
|
||||||
// fuzzy search only considers the 1st pattern
|
|
||||||
return fuzzy.MatchFold(conf.Patterns[0].Pattern, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
var match int
|
|
||||||
|
|
||||||
//fmt.Printf("<%s>\n", line)
|
|
||||||
for _, re := range conf.Patterns {
|
|
||||||
patmatch := re.PatternRe.MatchString(line)
|
|
||||||
if re.Negate {
|
|
||||||
// toggle the meaning of match
|
|
||||||
patmatch = !patmatch
|
|
||||||
}
|
|
||||||
|
|
||||||
if patmatch {
|
|
||||||
match++
|
|
||||||
}
|
|
||||||
|
|
||||||
//fmt.Printf("patmatch: %t, match: %d, pattern: %s, negate: %t\n", patmatch, match, re.Pattern, re.Negate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fmt.Printf("result: %t\n", match == len(conf.Patterns))
|
|
||||||
//fmt.Println()
|
|
||||||
return match == len(conf.Patterns)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Filter parsed data by fields. The filter is positive, so if one or
|
|
||||||
* more filters match on a row, it will be kept, otherwise it will be
|
|
||||||
* excluded.
|
|
||||||
*/
|
|
||||||
func FilterByFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
|
|
||||||
if len(conf.Filters) == 0 {
|
|
||||||
// no filters, no checking
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
newdata := data.CloneEmpty()
|
|
||||||
|
|
||||||
for _, row := range data.entries {
|
|
||||||
keep := true
|
|
||||||
|
|
||||||
for idx, header := range data.headers {
|
|
||||||
lcheader := strings.ToLower(header)
|
|
||||||
if !Exists(conf.Filters, lcheader) {
|
|
||||||
// do not filter by unspecified field
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
match := conf.Filters[lcheader].Regex.MatchString(row[idx])
|
|
||||||
if conf.Filters[lcheader].Negate {
|
|
||||||
match = !match
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
keep = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if keep == !conf.InvertMatch {
|
|
||||||
// also apply -v
|
|
||||||
newdata.entries = append(newdata.entries, row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &newdata, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Transpose fields using search/replace regexp.
|
|
||||||
*/
|
|
||||||
func TransposeFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
|
|
||||||
if len(conf.UseTransposers) == 0 {
|
|
||||||
// nothing to be done
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
newdata := data.CloneEmpty()
|
|
||||||
transposed := false
|
|
||||||
|
|
||||||
for _, row := range data.entries {
|
|
||||||
transposedrow := false
|
|
||||||
|
|
||||||
for idx := range data.headers {
|
|
||||||
transposeidx, hasone := findindex(conf.UseTransposeColumns, idx+1)
|
|
||||||
if hasone {
|
|
||||||
row[idx] =
|
|
||||||
conf.UseTransposers[transposeidx].Search.ReplaceAllString(
|
|
||||||
row[idx],
|
|
||||||
conf.UseTransposers[transposeidx].Replace,
|
|
||||||
)
|
|
||||||
transposedrow = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if transposedrow {
|
|
||||||
// also apply -v
|
|
||||||
newdata.entries = append(newdata.entries, row)
|
|
||||||
transposed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &newdata, transposed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/* generic map.Exists(key) */
|
|
||||||
func Exists[K comparable, V any](m map[K]V, v K) bool {
|
|
||||||
if _, ok := m[v]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Filters the whole input lines, returns filtered lines
|
|
||||||
*/
|
|
||||||
func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
|
|
||||||
if len(conf.Patterns) == 0 {
|
|
||||||
return input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(input)
|
|
||||||
lines := []string{}
|
|
||||||
hadFirst := false
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if hadFirst {
|
|
||||||
// don't match 1st line, it's the header
|
|
||||||
if matchPattern(conf, line) == conf.InvertMatch {
|
|
||||||
// by default -v is false, so if a line does NOT
|
|
||||||
// match the pattern, we will ignore it. However,
|
|
||||||
// if the user specified -v, the matching is inverted,
|
|
||||||
// so we ignore all lines, which DO match.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines = append(lines, line)
|
|
||||||
|
|
||||||
hadFirst = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.NewReader(strings.Join(lines, "\n")), nil
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2024-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatchPattern(t *testing.T) {
|
|
||||||
var input = []struct {
|
|
||||||
name string
|
|
||||||
fuzzy bool
|
|
||||||
patterns []*cfg.Pattern
|
|
||||||
line string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "normal",
|
|
||||||
patterns: []*cfg.Pattern{{Pattern: "haus"}},
|
|
||||||
line: "hausparty",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fuzzy",
|
|
||||||
patterns: []*cfg.Pattern{{Pattern: "hpt"}},
|
|
||||||
line: "haus-party-termin",
|
|
||||||
fuzzy: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, inputdata := range input {
|
|
||||||
testname := fmt.Sprintf("match-pattern-%s", inputdata.name)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{}
|
|
||||||
|
|
||||||
if inputdata.fuzzy {
|
|
||||||
conf.UseFuzzySearch = true
|
|
||||||
}
|
|
||||||
|
|
||||||
err := conf.PreparePattern(inputdata.patterns)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
res := matchPattern(conf, inputdata.line)
|
|
||||||
assert.EqualValues(t, true, res)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterByFields(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", "cxxxncnc"},
|
|
||||||
{"19191", "EDD 1", "x"},
|
|
||||||
{"8d8", "AN 1", "y"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var input = []struct {
|
|
||||||
name string
|
|
||||||
filter []string
|
|
||||||
expect Tabdata
|
|
||||||
invert bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "one-field",
|
|
||||||
filter: []string{"one=19"},
|
|
||||||
expect: Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"19191", "EDD 1", "x"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "one-field-negative",
|
|
||||||
filter: []string{"one!=asd"},
|
|
||||||
expect: Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"19191", "EDD 1", "x"},
|
|
||||||
{"8d8", "AN 1", "y"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "one-field-inverted",
|
|
||||||
filter: []string{"one=19"},
|
|
||||||
invert: true,
|
|
||||||
expect: Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", "cxxxncnc"},
|
|
||||||
{"8d8", "AN 1", "y"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "many-fields",
|
|
||||||
filter: []string{"one=19", "two=DD"},
|
|
||||||
expect: Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"19191", "EDD 1", "x"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "many-fields-inverted",
|
|
||||||
filter: []string{"one=19", "two=DD"},
|
|
||||||
invert: true,
|
|
||||||
expect: Tabdata{
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", "cxxxncnc"},
|
|
||||||
{"8d8", "AN 1", "y"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, inputdata := range input {
|
|
||||||
testname := fmt.Sprintf("filter-by-fields-%s", inputdata.name)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{Rawfilters: inputdata.filter, InvertMatch: inputdata.invert}
|
|
||||||
|
|
||||||
err := conf.PrepareFilters()
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
data, _, _ := FilterByFields(conf, &data)
|
|
||||||
|
|
||||||
assert.EqualValues(t, inputdata.expect, *data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
302
lib/helpers.go
302
lib/helpers.go
@@ -1,302 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gookit/color"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func findindex(s []int, e int) (int, bool) {
|
|
||||||
for i, a := range s {
|
|
||||||
if a == e {
|
|
||||||
return i, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the consitency of parsed data
|
|
||||||
func ValidateConsistency(data *Tabdata) error {
|
|
||||||
expectedfields := len(data.headers)
|
|
||||||
|
|
||||||
for idx, row := range data.entries {
|
|
||||||
if len(row) != expectedfields {
|
|
||||||
return fmt.Errorf("row %d does not contain expected %d elements, but %d",
|
|
||||||
idx, expectedfields, len(row))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse columns list given with -c, modifies config.UseColumns based
|
|
||||||
// on eventually given regex.
|
|
||||||
// This is an output filter, because -cN,N,... is being applied AFTER
|
|
||||||
// processing of the input data.
|
|
||||||
func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
|
|
||||||
// -c columns
|
|
||||||
usecolumns, err := PrepareColumnVars(conf.Columns, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.UseColumns = usecolumns
|
|
||||||
|
|
||||||
// -y columns
|
|
||||||
useyankcolumns, err := PrepareColumnVars(conf.YankColumns, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.UseYankColumns = useyankcolumns
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same thing as above but for -T option, which is an input option,
|
|
||||||
// because transposers are being applied before output.
|
|
||||||
func PrepareTransposerColumns(conf *cfg.Config, data *Tabdata) error {
|
|
||||||
// -T columns
|
|
||||||
usetransposecolumns, err := PrepareColumnVars(conf.TransposeColumns, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.UseTransposeColumns = usetransposecolumns
|
|
||||||
|
|
||||||
// verify that columns and transposers match and prepare transposer structs
|
|
||||||
if err := conf.PrepareTransposers(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// output option, prepare -k1,2 sort fields
|
|
||||||
func PrepareSortColumns(conf *cfg.Config, data *Tabdata) error {
|
|
||||||
// -c columns
|
|
||||||
usecolumns, err := PrepareColumnVars(conf.SortByColumn, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.UseSortByColumn = usecolumns
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrepareColumnVars(columns string, data *Tabdata) ([]int, error) {
|
|
||||||
if columns == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
usecolumns := []int{}
|
|
||||||
|
|
||||||
isregex := regexp.MustCompile(`\W`)
|
|
||||||
|
|
||||||
for _, columnpattern := range strings.Split(columns, ",") {
|
|
||||||
if len(columnpattern) == 0 {
|
|
||||||
return nil, fmt.Errorf("could not parse columns list %s: empty column", columns)
|
|
||||||
}
|
|
||||||
|
|
||||||
usenum, err := strconv.Atoi(columnpattern)
|
|
||||||
if err != nil {
|
|
||||||
// not a number
|
|
||||||
|
|
||||||
if !isregex.MatchString(columnpattern) {
|
|
||||||
// is not a regexp (contains no non-word chars)
|
|
||||||
// lc() it so that word searches are case insensitive
|
|
||||||
columnpattern = strings.ToLower(columnpattern)
|
|
||||||
|
|
||||||
for i, head := range data.headers {
|
|
||||||
if columnpattern == strings.ToLower(head) {
|
|
||||||
usecolumns = append(usecolumns, i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
colPattern, err := regexp.Compile("(?i)" + columnpattern)
|
|
||||||
if err != nil {
|
|
||||||
msg := fmt.Sprintf("Could not parse columns list %s: %v", columns, err)
|
|
||||||
|
|
||||||
return nil, errors.New(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// find matching header fields, ignoring case
|
|
||||||
for i, head := range data.headers {
|
|
||||||
if colPattern.MatchString(strings.ToLower(head)) {
|
|
||||||
usecolumns = append(usecolumns, i+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we digress from go best practises here, because if
|
|
||||||
// a colum spec is not a number, we process them above
|
|
||||||
// inside the err handler for atoi(). so only add the
|
|
||||||
// number, if it's really just a number.
|
|
||||||
usecolumns = append(usecolumns, usenum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deduplicate columns, preserve order
|
|
||||||
deduped := []int{}
|
|
||||||
for _, i := range usecolumns {
|
|
||||||
if !slices.Contains(deduped, i) {
|
|
||||||
deduped = append(deduped, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return deduped, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare headers: add numbers to headers
|
|
||||||
func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
|
|
||||||
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 conf.Numbering {
|
|
||||||
newhead := fmt.Sprintf("%s(%d)", head, idx+1)
|
|
||||||
numberedHeaders[idx] = newhead
|
|
||||||
headlen = len(newhead)
|
|
||||||
} else {
|
|
||||||
headlen = len(head)
|
|
||||||
}
|
|
||||||
|
|
||||||
if headlen > maxwidth {
|
|
||||||
maxwidth = headlen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// exclude columns, if any
|
|
||||||
func reduceColumns(conf cfg.Config, data *Tabdata) {
|
|
||||||
if len(conf.Columns) > 0 {
|
|
||||||
reducedEntries := [][]string{}
|
|
||||||
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
var reducedEntry []string
|
|
||||||
|
|
||||||
for _, col := range conf.UseColumns {
|
|
||||||
col--
|
|
||||||
|
|
||||||
for idx, value := range entry {
|
|
||||||
if idx == col {
|
|
||||||
reducedEntry = append(reducedEntry, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reducedEntries = append(reducedEntries, reducedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
data.entries = reducedEntries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: refactor this beast!
|
|
||||||
func colorizeData(conf cfg.Config, output string) string {
|
|
||||||
switch {
|
|
||||||
case conf.UseHighlight && color.IsConsole(os.Stdout):
|
|
||||||
highlight := true
|
|
||||||
colorized := ""
|
|
||||||
first := true
|
|
||||||
|
|
||||||
for _, line := range strings.Split(output, "\n") {
|
|
||||||
if highlight {
|
|
||||||
if first {
|
|
||||||
// we need to add two spaces to the header line
|
|
||||||
// because tablewriter omits them for some reason
|
|
||||||
// in pprint mode. This doesn't matter as long as
|
|
||||||
// we don't use colorization. But with colors the
|
|
||||||
// missing spaces can be seen.
|
|
||||||
if conf.OutputMode == cfg.ASCII {
|
|
||||||
line += " "
|
|
||||||
}
|
|
||||||
|
|
||||||
line = conf.HighlightHdrStyle.Sprint(line)
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
line = conf.HighlightStyle.Sprint(line)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
line = conf.NoHighlightStyle.Sprint(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight = !highlight
|
|
||||||
|
|
||||||
colorized += line + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return colorized
|
|
||||||
|
|
||||||
case len(conf.Patterns) > 0 && !conf.NoColor && color.IsConsole(os.Stdout):
|
|
||||||
out := output
|
|
||||||
|
|
||||||
for _, re := range conf.Patterns {
|
|
||||||
if !re.Negate {
|
|
||||||
r := regexp.MustCompile("(" + re.Pattern + ")")
|
|
||||||
|
|
||||||
out = r.ReplaceAllStringFunc(out, func(in string) string {
|
|
||||||
return conf.ColorStyle.Sprint(in)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
default:
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContains(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
list []int
|
|
||||||
search int
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{[]int{1, 2, 3}, 2, true},
|
|
||||||
{[]int{2, 3, 4}, 5, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
answer := slices.Contains(tt.list, tt.search)
|
|
||||||
|
|
||||||
assert.EqualValues(t, tt.want, answer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepareColumns(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
maxwidthHeader: 5,
|
|
||||||
columns: 3,
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{
|
|
||||||
"2", "3", "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var tests = []struct {
|
|
||||||
input string
|
|
||||||
exp []int
|
|
||||||
wanterror bool // expect error
|
|
||||||
}{
|
|
||||||
{"1,2,3", []int{1, 2, 3}, false},
|
|
||||||
{"1,2,", []int{}, true},
|
|
||||||
{"T.", []int{2, 3}, false},
|
|
||||||
{"T.,2,3", []int{2, 3}, false},
|
|
||||||
{"[a-z,4,5", []int{4, 5}, true}, // invalid regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
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 testdata.wanterror {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, testdata.exp, conf.UseColumns)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrepareTransposerColumns(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
maxwidthHeader: 5,
|
|
||||||
columns: 3,
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{
|
|
||||||
"2", "3", "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var tests = []struct {
|
|
||||||
input string
|
|
||||||
transp []string
|
|
||||||
exp int
|
|
||||||
wanterror bool // expect error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"1",
|
|
||||||
[]string{`/\d/x/`},
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"T.", // will match [T]WO and [T]HREE
|
|
||||||
[]string{`/\d/x/`, `/.//`},
|
|
||||||
2,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"TH.,2",
|
|
||||||
[]string{`/\d/x/`, `/.//`},
|
|
||||||
2,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"1",
|
|
||||||
[]string{},
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"",
|
|
||||||
[]string{`|.|N|`},
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"1",
|
|
||||||
[]string{`|.|N|`},
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("PrepareTransposerColumns-%s-%t", testdata.input, testdata.wanterror)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{TransposeColumns: testdata.input, Transposers: testdata.transp}
|
|
||||||
err := PrepareTransposerColumns(&conf, &data)
|
|
||||||
|
|
||||||
if 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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReduceColumns(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
expect [][]string
|
|
||||||
columns []int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
expect: [][]string{{"a", "b"}},
|
|
||||||
columns: []int{1, 2},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expect: [][]string{{"a", "c"}},
|
|
||||||
columns: []int{1, 3},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expect: [][]string{{"a"}},
|
|
||||||
columns: []int{1},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expect: [][]string{nil},
|
|
||||||
columns: []int{4},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
input := [][]string{{"a", "b", "c"}}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("reduce-columns-by-%+v", testdata.columns)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
c := cfg.Config{Columns: "x", UseColumns: testdata.columns}
|
|
||||||
data := Tabdata{entries: input}
|
|
||||||
reduceColumns(c, &data)
|
|
||||||
|
|
||||||
assert.EqualValues(t, testdata.expect, data.entries)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNumberizeHeaders(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
headers: []string{"ONE", "TWO", "THREE"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var tests = []struct {
|
|
||||||
expect []string
|
|
||||||
columns []int
|
|
||||||
numberize bool
|
|
||||||
}{
|
|
||||||
{[]string{"ONE(1)", "TWO(2)", "THREE(3)"}, []int{1, 2, 3}, true},
|
|
||||||
{[]string{"ONE(1)", "TWO(2)"}, []int{1, 2}, true},
|
|
||||||
{[]string{"ONE", "TWO"}, []int{1, 2}, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t",
|
|
||||||
testdata.columns, testdata.numberize)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, Numbering: testdata.numberize}
|
|
||||||
usedata := data
|
|
||||||
numberizeAndReduceHeaders(conf, &usedata)
|
|
||||||
|
|
||||||
assert.EqualValues(t, testdata.expect, usedata.headers)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
116
lib/io.go
116
lib/io.go
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
const RWRR = 0755
|
|
||||||
|
|
||||||
func ProcessFiles(conf *cfg.Config, args []string) error {
|
|
||||||
fd, patterns, err := determineIO(conf, args)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := conf.PreparePattern(patterns); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := Parse(*conf, fd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = ValidateConsistency(&data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = PrepareSortColumns(conf, &data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = PrepareColumns(conf, &data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Interactive {
|
|
||||||
newdata, err := tableEditor(conf, &data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data = *newdata
|
|
||||||
}
|
|
||||||
|
|
||||||
printData(os.Stdout, *conf, &data)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func determineIO(conf *cfg.Config, args []string) (io.Reader, []*cfg.Pattern, error) {
|
|
||||||
var filehandle io.Reader
|
|
||||||
var patterns []*cfg.Pattern
|
|
||||||
var haveio bool
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case conf.InputFile == "-":
|
|
||||||
filehandle = os.Stdin
|
|
||||||
haveio = true
|
|
||||||
case conf.InputFile != "":
|
|
||||||
fd, err := os.OpenFile(conf.InputFile, os.O_RDONLY, RWRR)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to read input file %s: %w", conf.InputFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
filehandle = fd
|
|
||||||
haveio = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !haveio {
|
|
||||||
stat, _ := os.Stdin.Stat()
|
|
||||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
|
||||||
// we're reading from STDIN, which takes precedence over file args
|
|
||||||
filehandle = os.Stdin
|
|
||||||
haveio = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) > 0 {
|
|
||||||
patterns = make([]*cfg.Pattern, len(args))
|
|
||||||
for i, arg := range args {
|
|
||||||
patterns[i] = &cfg.Pattern{Pattern: arg}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !haveio {
|
|
||||||
return nil, nil, errors.New("no file specified and nothing to read on stdin")
|
|
||||||
}
|
|
||||||
|
|
||||||
return filehandle, patterns, nil
|
|
||||||
}
|
|
||||||
120
lib/pager.go
120
lib/pager.go
@@ -1,120 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
402
lib/parser.go
402
lib/parser.go
@@ -1,402 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Parser switch
|
|
||||||
*/
|
|
||||||
func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
|
|
||||||
var data Tabdata
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// first step, parse the data
|
|
||||||
if len(conf.Separator) == 1 {
|
|
||||||
data, err = parseCSV(conf, input)
|
|
||||||
} else if conf.InputJSON {
|
|
||||||
data, err = parseJSON(conf, input)
|
|
||||||
} else {
|
|
||||||
data, err = parseTabular(conf, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2nd step, apply filters, code or transposers, if any
|
|
||||||
postdata, changed, err := PostProcess(conf, &data)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
return *postdata, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) {
|
|
||||||
data := Tabdata{}
|
|
||||||
|
|
||||||
// apply pattern, if any
|
|
||||||
content, err := FilterByPattern(conf, input)
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
csvreader := csv.NewReader(content)
|
|
||||||
csvreader.Comma = rune(conf.Separator[0])
|
|
||||||
|
|
||||||
records, err := csvreader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return data, fmt.Errorf("could not parse CSV input: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(records) >= 1 {
|
|
||||||
data.headers = SetHeaders(conf, records[0])
|
|
||||||
data.columns = len(records)
|
|
||||||
|
|
||||||
for _, head := range data.headers {
|
|
||||||
// register widest header field
|
|
||||||
headerlen := len(head)
|
|
||||||
if headerlen > data.maxwidthHeader {
|
|
||||||
data.maxwidthHeader = headerlen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(records) >= 1 {
|
|
||||||
if conf.AutoHeaders || len(conf.CustomHeaders) > 0 {
|
|
||||||
data.entries = records
|
|
||||||
} else {
|
|
||||||
data.entries = records[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Parse tabular input.
|
|
||||||
*/
|
|
||||||
func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
|
|
||||||
data := Tabdata{}
|
|
||||||
|
|
||||||
var scanner *bufio.Scanner
|
|
||||||
|
|
||||||
hadFirst := false
|
|
||||||
separate := regexp.MustCompile(conf.Separator)
|
|
||||||
|
|
||||||
scanner = bufio.NewScanner(input)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
parts := separate.Split(line, -1)
|
|
||||||
|
|
||||||
if !hadFirst {
|
|
||||||
// header processing
|
|
||||||
data.columns = len(parts)
|
|
||||||
|
|
||||||
// process all header fields
|
|
||||||
firstrow := make([]string, len(parts))
|
|
||||||
|
|
||||||
for idx, part := range parts {
|
|
||||||
// register widest header field
|
|
||||||
headerlen := len(part)
|
|
||||||
if headerlen > data.maxwidthHeader {
|
|
||||||
data.maxwidthHeader = headerlen
|
|
||||||
}
|
|
||||||
|
|
||||||
// register fields data
|
|
||||||
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 {
|
|
||||||
// by default -v is false, so if a line does NOT
|
|
||||||
// match the pattern, we will ignore it. However,
|
|
||||||
// if the user specified -v, the matching is inverted,
|
|
||||||
// so we ignore all lines, which DO match.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := 0 // we cannot use the header index, because we could exclude columns
|
|
||||||
values := []string{}
|
|
||||||
for _, part := range parts {
|
|
||||||
// if Debug {
|
|
||||||
// fmt.Printf("<%s> ", value)
|
|
||||||
// }
|
|
||||||
values = append(values, strings.TrimSpace(part))
|
|
||||||
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
|
|
||||||
// fill up missing fields, if any
|
|
||||||
for i := len(values); i < len(data.headers); i++ {
|
|
||||||
values = append(values, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
data.entries = append(data.entries, values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if scanner.Err() != nil {
|
|
||||||
return data, fmt.Errorf("failed to read from io.Reader: %w", scanner.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// filter by field filters, if any
|
|
||||||
filtereddata, changed, err := FilterByFields(conf, data)
|
|
||||||
if err != nil {
|
|
||||||
return data, false, fmt.Errorf("failed to filter fields: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
data = filtereddata
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if transposers are valid and turn into Transposer structs
|
|
||||||
if err := PrepareTransposerColumns(&conf, data); err != nil {
|
|
||||||
return data, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// transpose if demanded
|
|
||||||
modifieddata, changed, err := TransposeFields(conf, data)
|
|
||||||
if err != nil {
|
|
||||||
return data, false, fmt.Errorf("failed to transpose fields: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
data = modifieddata
|
|
||||||
modified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Debug {
|
|
||||||
repr.Print(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, modified, nil
|
|
||||||
}
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
var input = []struct {
|
|
||||||
name string
|
|
||||||
text string
|
|
||||||
separator string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tabular-data",
|
|
||||||
separator: cfg.SeparatorTemplates[":default:"],
|
|
||||||
text: `
|
|
||||||
ONE TWO THREE
|
|
||||||
asd igig cxxxncnc
|
|
||||||
19191 EDD 1 X`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "csv-data",
|
|
||||||
separator: ",",
|
|
||||||
text: `
|
|
||||||
ONE,TWO,THREE
|
|
||||||
asd,igig,cxxxncnc
|
|
||||||
19191,"EDD 1",X`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
maxwidthHeader: 5,
|
|
||||||
columns: 3,
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", "cxxxncnc"},
|
|
||||||
{"19191", "EDD 1", "X"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range input {
|
|
||||||
testname := fmt.Sprintf("parse-%s", testdata.name)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
readFd := strings.NewReader(strings.TrimSpace(testdata.text))
|
|
||||||
conf := cfg.Config{Separator: testdata.separator}
|
|
||||||
gotdata, err := wrapValidateParser(conf, readFd)
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, data, gotdata)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParserPatternmatching(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
name string
|
|
||||||
entries [][]string
|
|
||||||
patterns []*cfg.Pattern
|
|
||||||
invert bool
|
|
||||||
wanterror bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "match",
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", "cxxxncnc"},
|
|
||||||
},
|
|
||||||
patterns: []*cfg.Pattern{{Pattern: "ig"}},
|
|
||||||
invert: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invert",
|
|
||||||
entries: [][]string{
|
|
||||||
{"19191", "EDD 1", "X"},
|
|
||||||
},
|
|
||||||
patterns: []*cfg.Pattern{{Pattern: "ig"}},
|
|
||||||
invert: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, inputdata := range input {
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
|
|
||||||
inputdata.name, testdata.name, testdata.invert)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{
|
|
||||||
InvertMatch: testdata.invert,
|
|
||||||
Patterns: testdata.patterns,
|
|
||||||
Separator: inputdata.separator,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = conf.PreparePattern(testdata.patterns)
|
|
||||||
|
|
||||||
readFd := strings.NewReader(strings.TrimSpace(inputdata.text))
|
|
||||||
data, err := wrapValidateParser(conf, readFd)
|
|
||||||
|
|
||||||
if testdata.wanterror {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, testdata.entries, data.entries)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParserIncompleteRows(t *testing.T) {
|
|
||||||
data := Tabdata{
|
|
||||||
maxwidthHeader: 5,
|
|
||||||
columns: 3,
|
|
||||||
headers: []string{
|
|
||||||
"ONE", "TWO", "THREE",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{"asd", "igig", ""},
|
|
||||||
{"19191", "EDD 1", "X"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
table := `
|
|
||||||
ONE TWO THREE
|
|
||||||
asd igig
|
|
||||||
19191 EDD 1 X`
|
|
||||||
|
|
||||||
readFd := strings.NewReader(strings.TrimSpace(table))
|
|
||||||
conf := cfg.Config{Separator: cfg.SeparatorTemplates[":default:"]}
|
|
||||||
gotdata, err := wrapValidateParser(conf, readFd)
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
390
lib/printer.go
390
lib/printer.go
@@ -1,390 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
"github.com/gookit/color"
|
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
"github.com/olekukonko/tablewriter/renderer"
|
|
||||||
"github.com/olekukonko/tablewriter/tw"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
|
|
||||||
// Sort the data first, before headers+entries are being
|
|
||||||
// reduced. That way the user can specify any valid column to sort
|
|
||||||
// by, independently if it's being used for display or not.
|
|
||||||
sortTable(conf, data)
|
|
||||||
|
|
||||||
// put one or more columns into clipboard
|
|
||||||
yankColumns(conf, data)
|
|
||||||
|
|
||||||
// add numbers to headers and remove those we're not interested in
|
|
||||||
numberizeAndReduceHeaders(conf, data)
|
|
||||||
|
|
||||||
// remove unwanted columns, if any
|
|
||||||
reduceColumns(conf, data)
|
|
||||||
|
|
||||||
switch conf.OutputMode {
|
|
||||||
case cfg.Extended:
|
|
||||||
printExtendedData(writer, conf, data)
|
|
||||||
case cfg.ASCII:
|
|
||||||
printASCIIData(writer, conf, data)
|
|
||||||
case cfg.Orgtbl:
|
|
||||||
printOrgmodeData(writer, conf, data)
|
|
||||||
case cfg.Markdown:
|
|
||||||
printMarkdownData(writer, conf, data)
|
|
||||||
case cfg.Shell:
|
|
||||||
printShellData(writer, data)
|
|
||||||
case cfg.Yaml:
|
|
||||||
printYamlData(writer, data)
|
|
||||||
case cfg.Json:
|
|
||||||
printJsonData(writer, data)
|
|
||||||
case cfg.CSV:
|
|
||||||
printCSVData(writer, conf, data)
|
|
||||||
default:
|
|
||||||
printASCIIData(writer, conf, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func output(writer io.Writer, str string) {
|
|
||||||
_, err := fmt.Fprint(writer, str)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to print output: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Emacs org-mode compatible table (also orgtbl-mode)
|
|
||||||
*/
|
|
||||||
func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
|
|
||||||
tableString := &strings.Builder{}
|
|
||||||
|
|
||||||
table := tablewriter.NewTable(tableString,
|
|
||||||
tablewriter.WithRenderer(
|
|
||||||
renderer.NewBlueprint(
|
|
||||||
tw.Rendition{
|
|
||||||
Borders: tw.Border{
|
|
||||||
Left: tw.On,
|
|
||||||
Right: tw.On,
|
|
||||||
Top: tw.On,
|
|
||||||
Bottom: tw.On,
|
|
||||||
},
|
|
||||||
Settings: tw.Settings{
|
|
||||||
Separators: tw.Separators{
|
|
||||||
ShowHeader: tw.On,
|
|
||||||
ShowFooter: tw.Off,
|
|
||||||
BetweenRows: tw.Off,
|
|
||||||
BetweenColumns: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Symbols: tw.NewSymbols(tw.StyleASCII),
|
|
||||||
})),
|
|
||||||
|
|
||||||
tablewriter.WithConfig(
|
|
||||||
tablewriter.Config{
|
|
||||||
Header: tw.CellConfig{
|
|
||||||
Formatting: tw.CellFormatting{
|
|
||||||
Alignment: tw.AlignLeft,
|
|
||||||
AutoFormat: tw.Off,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Row: tw.CellConfig{
|
|
||||||
Formatting: tw.CellFormatting{
|
|
||||||
Alignment: tw.AlignLeft,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if !conf.NoHeaders {
|
|
||||||
table.Header(data.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Bulk(data.entries); err != nil {
|
|
||||||
log.Fatalf("Failed to add data to table renderer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Render(); err != nil {
|
|
||||||
log.Fatalf("Failed to render table: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Markdown table
|
|
||||||
*/
|
|
||||||
func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
|
|
||||||
tableString := &strings.Builder{}
|
|
||||||
|
|
||||||
table := tablewriter.NewTable(tableString,
|
|
||||||
tablewriter.WithRenderer(
|
|
||||||
renderer.NewBlueprint(
|
|
||||||
tw.Rendition{
|
|
||||||
Borders: tw.Border{
|
|
||||||
Left: tw.On,
|
|
||||||
Right: tw.On,
|
|
||||||
Top: tw.Off,
|
|
||||||
Bottom: tw.Off,
|
|
||||||
},
|
|
||||||
Settings: tw.Settings{
|
|
||||||
Separators: tw.Separators{
|
|
||||||
ShowHeader: tw.On,
|
|
||||||
ShowFooter: tw.Off,
|
|
||||||
BetweenRows: tw.Off,
|
|
||||||
BetweenColumns: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Symbols: tw.NewSymbols(tw.StyleMarkdown),
|
|
||||||
})),
|
|
||||||
|
|
||||||
tablewriter.WithConfig(
|
|
||||||
tablewriter.Config{
|
|
||||||
Header: tw.CellConfig{
|
|
||||||
Formatting: tw.CellFormatting{
|
|
||||||
Alignment: tw.AlignLeft,
|
|
||||||
AutoFormat: tw.Off,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Row: tw.CellConfig{
|
|
||||||
Formatting: tw.CellFormatting{
|
|
||||||
Alignment: tw.AlignLeft,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if !conf.NoHeaders {
|
|
||||||
table.Header(data.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Bulk(data.entries); err != nil {
|
|
||||||
log.Fatalf("Failed to add data to table renderer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Render(); err != nil {
|
|
||||||
log.Fatalf("Failed to render table: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
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")
|
|
||||||
|
|
||||||
table := tablewriter.NewTable(tableString,
|
|
||||||
tablewriter.WithRenderer(
|
|
||||||
renderer.NewBlueprint(tw.Rendition{
|
|
||||||
Borders: tw.BorderNone,
|
|
||||||
Symbols: styleTSV,
|
|
||||||
Settings: tw.Settings{
|
|
||||||
Separators: tw.SeparatorsNone,
|
|
||||||
Lines: tw.LinesNone,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
tablewriter.WithConfig(tablewriter.Config{
|
|
||||||
Header: tw.CellConfig{
|
|
||||||
Formatting: tw.CellFormatting{
|
|
||||||
AutoFormat: tw.Off,
|
|
||||||
},
|
|
||||||
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{Right: OFS}},
|
|
||||||
},
|
|
||||||
|
|
||||||
Debug: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if !conf.NoHeaders {
|
|
||||||
table.Header(data.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Bulk(data.entries); err != nil {
|
|
||||||
log.Fatalf("Failed to add data to table renderer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := table.Render(); err != nil {
|
|
||||||
log.Fatalf("Failed to render table: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output(writer, color.Sprint(colorizeData(conf, tableString.String())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
We simulate the \x command of psql (the PostgreSQL client)
|
|
||||||
*/
|
|
||||||
func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) {
|
|
||||||
// needed for data output
|
|
||||||
format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader)
|
|
||||||
out := ""
|
|
||||||
|
|
||||||
if len(data.entries) > 0 {
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
for i, value := range entry {
|
|
||||||
out += color.Sprintf(format, data.headers[i], value)
|
|
||||||
}
|
|
||||||
|
|
||||||
out += "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output(writer, colorizeData(conf, out))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Shell output, ready to be eval'd. Just like FreeBSD stat(1)
|
|
||||||
*/
|
|
||||||
func printShellData(writer io.Writer, data *Tabdata) {
|
|
||||||
out := ""
|
|
||||||
|
|
||||||
if len(data.entries) > 0 {
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
shentries := []string{}
|
|
||||||
|
|
||||||
for idx, value := range entry {
|
|
||||||
shentries = append(shentries, fmt.Sprintf("%s=\"%s\"",
|
|
||||||
data.headers[idx], value))
|
|
||||||
}
|
|
||||||
|
|
||||||
out += strings.Join(shentries, " ") + "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no colorization here
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlout := Data{}
|
|
||||||
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
yamldata := map[string]interface{}{}
|
|
||||||
|
|
||||||
for idx, entry := range entry {
|
|
||||||
style := yaml.TaggedStyle
|
|
||||||
|
|
||||||
_, err := strconv.Atoi(entry)
|
|
||||||
if err != nil {
|
|
||||||
style = yaml.DoubleQuotedStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
yamldata[strings.ToLower(data.headers[idx])] =
|
|
||||||
&yaml.Node{
|
|
||||||
Kind: yaml.ScalarNode,
|
|
||||||
Style: style,
|
|
||||||
Value: entry}
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlout.Entries = append(yamlout.Entries, yamldata)
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlstr, err := yaml.Marshal(&yamlout)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output(writer, string(yamlstr))
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCSVData(writer io.Writer, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range data.entries {
|
|
||||||
if err := csvout.Write(entry); err != nil {
|
|
||||||
log.Fatalln("error writing record to csv:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
csvout.Flush()
|
|
||||||
|
|
||||||
if err := csvout.Error(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newData() Tabdata {
|
|
||||||
return Tabdata{
|
|
||||||
maxwidthHeader: 8,
|
|
||||||
columns: 4,
|
|
||||||
headers: []string{
|
|
||||||
"NAME",
|
|
||||||
"DURATION",
|
|
||||||
"COUNT",
|
|
||||||
"WHEN",
|
|
||||||
},
|
|
||||||
entries: [][]string{
|
|
||||||
{
|
|
||||||
"beta",
|
|
||||||
"1d10h5m1s",
|
|
||||||
"33",
|
|
||||||
"3/1/2014",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alpha",
|
|
||||||
"4h35m",
|
|
||||||
"170",
|
|
||||||
"2013-Feb-03",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ceta",
|
|
||||||
"33d12h",
|
|
||||||
"9",
|
|
||||||
"06/Jan/2008 15:04:05 -0700",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tests = []struct {
|
|
||||||
name string // so we can identify which one fails, can be the same
|
|
||||||
// for multiple tests, because flags will be appended to the name
|
|
||||||
sortby string // empty == default
|
|
||||||
column int // sort by this column (numbers start by 1)
|
|
||||||
desc bool // sort in descending order, default == ascending
|
|
||||||
numberize bool // add header numbering
|
|
||||||
mode int // shell, orgtbl, etc. empty == default: ascii
|
|
||||||
usecol []int // columns to display, empty == display all
|
|
||||||
usecolstr string // for testname, must match usecol
|
|
||||||
expect string // rendered output we expect
|
|
||||||
}{
|
|
||||||
// --------------------- Default settings mode tests ``
|
|
||||||
{
|
|
||||||
mode: cfg.ASCII,
|
|
||||||
numberize: true,
|
|
||||||
name: "default",
|
|
||||||
expect: `
|
|
||||||
NAME(1) DURATION(2) COUNT(3) WHEN(4)
|
|
||||||
beta 1d10h5m1s 33 3/1/2014
|
|
||||||
alpha 4h35m 170 2013-Feb-03
|
|
||||||
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: cfg.CSV,
|
|
||||||
numberize: false,
|
|
||||||
name: "csv",
|
|
||||||
expect: `
|
|
||||||
NAME,DURATION,COUNT,WHEN
|
|
||||||
beta,1d10h5m1s,33,3/1/2014
|
|
||||||
alpha,4h35m,170,2013-Feb-03
|
|
||||||
ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "orgtbl",
|
|
||||||
numberize: true,
|
|
||||||
mode: cfg.Orgtbl,
|
|
||||||
expect: `
|
|
||||||
+---------+-------------+----------+----------------------------+
|
|
||||||
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|
|
||||||
+---------+-------------+----------+----------------------------+
|
|
||||||
| beta | 1d10h5m1s | 33 | 3/1/2014 |
|
|
||||||
| alpha | 4h35m | 170 | 2013-Feb-03 |
|
|
||||||
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |
|
|
||||||
+---------+-------------+----------+----------------------------+`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "markdown",
|
|
||||||
mode: cfg.Markdown,
|
|
||||||
numberize: true,
|
|
||||||
expect: `
|
|
||||||
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|
|
||||||
|---------|-------------|----------|----------------------------|
|
|
||||||
| beta | 1d10h5m1s | 33 | 3/1/2014 |
|
|
||||||
| alpha | 4h35m | 170 | 2013-Feb-03 |
|
|
||||||
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "shell",
|
|
||||||
mode: cfg.Shell,
|
|
||||||
numberize: false,
|
|
||||||
expect: `
|
|
||||||
NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014"
|
|
||||||
NAME="alpha" DURATION="4h35m" COUNT="170" WHEN="2013-Feb-03"
|
|
||||||
NAME="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "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",
|
|
||||||
mode: cfg.Yaml,
|
|
||||||
numberize: false,
|
|
||||||
expect: `
|
|
||||||
entries:
|
|
||||||
- count: 33
|
|
||||||
duration: "1d10h5m1s"
|
|
||||||
name: "beta"
|
|
||||||
when: "3/1/2014"
|
|
||||||
- count: 170
|
|
||||||
duration: "4h35m"
|
|
||||||
name: "alpha"
|
|
||||||
when: "2013-Feb-03"
|
|
||||||
- count: 9
|
|
||||||
duration: "33d12h"
|
|
||||||
name: "ceta"
|
|
||||||
when: "06/Jan/2008 15:04:05 -0700"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "extended",
|
|
||||||
mode: cfg.Extended,
|
|
||||||
numberize: true,
|
|
||||||
expect: `
|
|
||||||
NAME(1): beta
|
|
||||||
DURATION(2): 1d10h5m1s
|
|
||||||
COUNT(3): 33
|
|
||||||
WHEN(4): 3/1/2014
|
|
||||||
|
|
||||||
NAME(1): alpha
|
|
||||||
DURATION(2): 4h35m
|
|
||||||
COUNT(3): 170
|
|
||||||
WHEN(4): 2013-Feb-03
|
|
||||||
|
|
||||||
NAME(1): ceta
|
|
||||||
DURATION(2): 33d12h
|
|
||||||
COUNT(3): 9
|
|
||||||
WHEN(4): 06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
|
|
||||||
//------------------------ SORT TESTS
|
|
||||||
{
|
|
||||||
name: "sortbycolumn3",
|
|
||||||
column: 3,
|
|
||||||
sortby: "numeric",
|
|
||||||
numberize: true,
|
|
||||||
desc: false,
|
|
||||||
expect: `
|
|
||||||
NAME(1) DURATION(2) COUNT(3) WHEN(4)
|
|
||||||
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
|
|
||||||
beta 1d10h5m1s 33 3/1/2014
|
|
||||||
alpha 4h35m 170 2013-Feb-03`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sortbycolumn4",
|
|
||||||
column: 4,
|
|
||||||
sortby: "time",
|
|
||||||
desc: false,
|
|
||||||
numberize: true,
|
|
||||||
expect: `
|
|
||||||
NAME(1) DURATION(2) COUNT(3) WHEN(4)
|
|
||||||
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
|
|
||||||
alpha 4h35m 170 2013-Feb-03
|
|
||||||
beta 1d10h5m1s 33 3/1/2014`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sortbycolumn2",
|
|
||||||
column: 2,
|
|
||||||
sortby: "duration",
|
|
||||||
numberize: true,
|
|
||||||
desc: false,
|
|
||||||
expect: `
|
|
||||||
NAME(1) DURATION(2) COUNT(3) WHEN(4)
|
|
||||||
alpha 4h35m 170 2013-Feb-03
|
|
||||||
beta 1d10h5m1s 33 3/1/2014
|
|
||||||
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------- UseColumns Tests
|
|
||||||
{
|
|
||||||
name: "usecolumns",
|
|
||||||
usecol: []int{1, 4},
|
|
||||||
numberize: true,
|
|
||||||
usecolstr: "1,4",
|
|
||||||
expect: `
|
|
||||||
NAME(1) WHEN(4)
|
|
||||||
beta 3/1/2014
|
|
||||||
alpha 2013-Feb-03
|
|
||||||
ceta 06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "usecolumns2",
|
|
||||||
usecol: []int{2},
|
|
||||||
numberize: true,
|
|
||||||
usecolstr: "2",
|
|
||||||
expect: `
|
|
||||||
DURATION(2)
|
|
||||||
1d10h5m1s
|
|
||||||
4h35m
|
|
||||||
33d12h`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "usecolumns3",
|
|
||||||
usecol: []int{3},
|
|
||||||
numberize: true,
|
|
||||||
usecolstr: "3",
|
|
||||||
expect: `
|
|
||||||
COUNT(3)
|
|
||||||
33
|
|
||||||
170
|
|
||||||
9`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "usecolumns4",
|
|
||||||
column: 0,
|
|
||||||
usecol: []int{1, 3},
|
|
||||||
numberize: true,
|
|
||||||
usecolstr: "1,3",
|
|
||||||
expect: `
|
|
||||||
NAME(1) COUNT(3)
|
|
||||||
beta 33
|
|
||||||
alpha 170
|
|
||||||
ceta 9`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "usecolumns",
|
|
||||||
usecol: []int{2, 4},
|
|
||||||
numberize: true,
|
|
||||||
usecolstr: "2,4",
|
|
||||||
expect: `
|
|
||||||
DURATION(2) WHEN(4)
|
|
||||||
1d10h5m1s 3/1/2014
|
|
||||||
4h35m 2013-Feb-03
|
|
||||||
33d12h 06/Jan/2008 15:04:05 -0700`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrinter(t *testing.T) {
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("print-%s-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s-numberize-%t",
|
|
||||||
testdata.name, testdata.column, testdata.desc, testdata.sortby,
|
|
||||||
testdata.mode, testdata.usecolstr, testdata.numberize)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
// replaces os.Stdout, but we ignore it
|
|
||||||
var writer bytes.Buffer
|
|
||||||
|
|
||||||
// cmd flags
|
|
||||||
conf := cfg.Config{
|
|
||||||
SortDescending: testdata.desc,
|
|
||||||
SortMode: testdata.sortby,
|
|
||||||
OutputMode: testdata.mode,
|
|
||||||
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!
|
|
||||||
if len(testdata.usecol) > 0 {
|
|
||||||
conf.Columns = "yes"
|
|
||||||
} else {
|
|
||||||
conf.Columns = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
data := newData()
|
|
||||||
exp := strings.TrimSpace(testdata.expect)
|
|
||||||
|
|
||||||
printData(&writer, conf, &data)
|
|
||||||
|
|
||||||
got := strings.TrimSpace(writer.String())
|
|
||||||
|
|
||||||
assert.EqualValues(t, exp, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
140
lib/sort.go
140
lib/sort.go
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/araddon/dateparse"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func sortTable(conf cfg.Config, data *Tabdata) {
|
|
||||||
if len(conf.UseSortByColumn) == 0 {
|
|
||||||
// no sorting wanted
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity checks
|
|
||||||
if len(data.entries) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// actual sorting
|
|
||||||
sort.SliceStable(data.entries, func(i, j int) bool {
|
|
||||||
// holds the result of a sort of one column
|
|
||||||
comparators := []int{}
|
|
||||||
|
|
||||||
// iterate over all columns to be sorted, conf.SortMode must be identical!
|
|
||||||
for _, column := range conf.UseSortByColumn {
|
|
||||||
comparators = append(comparators, compare(&conf, data.entries[i][column-1], data.entries[j][column-1]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the combined result
|
|
||||||
res := cmp.Or(comparators...)
|
|
||||||
|
|
||||||
switch res {
|
|
||||||
case 0:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// config is not modified here, but it would be inefficient to copy it every loop
|
|
||||||
func compare(conf *cfg.Config, left string, right string) int {
|
|
||||||
var comp bool
|
|
||||||
|
|
||||||
switch conf.SortMode {
|
|
||||||
case "numeric":
|
|
||||||
left, err := strconv.Atoi(left)
|
|
||||||
if err != nil {
|
|
||||||
left = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
right, err := strconv.Atoi(right)
|
|
||||||
if err != nil {
|
|
||||||
right = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
comp = left < right
|
|
||||||
case "duration":
|
|
||||||
left := duration2int(left)
|
|
||||||
right := duration2int(right)
|
|
||||||
|
|
||||||
comp = left < right
|
|
||||||
case "time":
|
|
||||||
left, _ := dateparse.ParseAny(left)
|
|
||||||
right, _ := dateparse.ParseAny(right)
|
|
||||||
|
|
||||||
comp = left.Unix() < right.Unix()
|
|
||||||
default:
|
|
||||||
comp = left < right
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.SortDescending {
|
|
||||||
comp = !comp
|
|
||||||
}
|
|
||||||
|
|
||||||
switch comp {
|
|
||||||
case true:
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
We could use time.ParseDuration(), but this doesn't support days.
|
|
||||||
|
|
||||||
We could also use github.com/xhit/go-str2duration/v2, which does
|
|
||||||
the job, but it's just another dependency, just for this little
|
|
||||||
gem. And we don't need a time.Time value. And int is good enough
|
|
||||||
for duration comparison.
|
|
||||||
|
|
||||||
Convert a duration into an integer. Valid time units are "s",
|
|
||||||
"m", "h" and "d".
|
|
||||||
*/
|
|
||||||
func duration2int(duration string) int {
|
|
||||||
re := regexp.MustCompile(`(\d+)([dhms])`)
|
|
||||||
seconds := 0
|
|
||||||
|
|
||||||
for _, match := range re.FindAllStringSubmatch(duration, -1) {
|
|
||||||
if len(match) == 3 {
|
|
||||||
durationvalue, _ := strconv.Atoi(match[1])
|
|
||||||
|
|
||||||
switch match[2][0] {
|
|
||||||
case 'd':
|
|
||||||
seconds += durationvalue * 86400
|
|
||||||
case 'h':
|
|
||||||
seconds += durationvalue * 3600
|
|
||||||
case 'm':
|
|
||||||
seconds += durationvalue * 60
|
|
||||||
case 's':
|
|
||||||
seconds += durationvalue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return seconds
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDuration2Seconds(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
dur string
|
|
||||||
expect int
|
|
||||||
}{
|
|
||||||
{"1d", 60 * 60 * 24},
|
|
||||||
{"1h", 60 * 60},
|
|
||||||
{"10m", 60 * 10},
|
|
||||||
{"2h4m10s", (60 * 120) + (4 * 60) + 10},
|
|
||||||
{"88u", 0},
|
|
||||||
{"19t77X what?4s", 4},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("duration-%s", testdata.dur)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
seconds := duration2int(testdata.dur)
|
|
||||||
assert.EqualValues(t, testdata.expect, seconds)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompare(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
mode string
|
|
||||||
a string
|
|
||||||
b string
|
|
||||||
want int
|
|
||||||
desc bool
|
|
||||||
}{
|
|
||||||
// ascending
|
|
||||||
{"numeric", "10", "20", 0, false},
|
|
||||||
{"duration", "2d4h5m", "45m", 1, false},
|
|
||||||
{"time", "12/24/2022", "1/1/1970", 1, false},
|
|
||||||
|
|
||||||
// descending
|
|
||||||
{"numeric", "10", "20", 1, true},
|
|
||||||
{"duration", "2d4h5m", "45m", 0, true},
|
|
||||||
{"time", "12/24/2022", "1/1/1970", 0, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testdata := range tests {
|
|
||||||
testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t",
|
|
||||||
testdata.mode, testdata.a, testdata.b, testdata.desc)
|
|
||||||
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc}
|
|
||||||
got := compare(&c, testdata.a, testdata.b)
|
|
||||||
assert.EqualValues(t, testdata.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
/*
|
|
||||||
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
|
|
||||||
}
|
|
||||||
51
lib/yank.go
51
lib/yank.go
@@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022-2025 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tiagomelo/go-clipboard/clipboard"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
func yankColumns(conf cfg.Config, data *Tabdata) {
|
|
||||||
var yank []string
|
|
||||||
|
|
||||||
if len(data.entries) == 0 || len(conf.UseYankColumns) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range data.entries {
|
|
||||||
for i, field := range row {
|
|
||||||
for _, idx := range conf.UseYankColumns {
|
|
||||||
if i == idx-1 {
|
|
||||||
yank = append(yank, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(yank) > 0 {
|
|
||||||
cb := clipboard.New(clipboard.ClipboardOptions{Primary: true})
|
|
||||||
if err := cb.CopyText(strings.Join(yank, " ")); err != nil {
|
|
||||||
log.Fatalln("error writing string to clipboard:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
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 (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tiagomelo/go-clipboard/clipboard"
|
|
||||||
"codeberg.org/scip/tablizer/cfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
var yanktests = []struct {
|
|
||||||
name string
|
|
||||||
yank []int // -y$colum,$column... after processing
|
|
||||||
filter string
|
|
||||||
expect string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "one",
|
|
||||||
yank: []int{1},
|
|
||||||
filter: "beta",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func DISABLED_TestYankColumns(t *testing.T) {
|
|
||||||
cb := clipboard.New()
|
|
||||||
|
|
||||||
for _, testdata := range yanktests {
|
|
||||||
testname := fmt.Sprintf("yank-%s-filter-%s",
|
|
||||||
testdata.name, testdata.filter)
|
|
||||||
t.Run(testname, func(t *testing.T) {
|
|
||||||
conf := cfg.Config{
|
|
||||||
OutputMode: cfg.ASCII,
|
|
||||||
UseYankColumns: testdata.yank,
|
|
||||||
NoColor: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
conf.ApplyDefaults()
|
|
||||||
data := newData() // defined in printer_test.go, reused here
|
|
||||||
|
|
||||||
var writer bytes.Buffer
|
|
||||||
printData(&writer, conf, &data)
|
|
||||||
|
|
||||||
got, err := cb.PasteText()
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.EqualValues(t, testdata.expect, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
main.go
34
main.go
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"codeberg.org/scip/tablizer/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Exit(Main())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Main() int {
|
|
||||||
cmd.Execute()
|
|
||||||
|
|
||||||
return 0 // cmd takes care of exit 1 itself
|
|
||||||
}
|
|
||||||
19
main_test.go
19
main_test.go
@@ -1,19 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/rogpeppe/go-internal/testscript"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testscript.Main(m, map[string]func(){
|
|
||||||
"tablizer": main,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTablizer(t *testing.T) {
|
|
||||||
testscript.Run(t, testscript.Params{
|
|
||||||
Dir: "t",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
72
mkrel.sh
72
mkrel.sh
@@ -1,72 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Copyright © 2022 Thomas von Dein
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
# get list with: go tool dist list
|
|
||||||
DIST="darwin/amd64
|
|
||||||
freebsd/amd64
|
|
||||||
linux/amd64
|
|
||||||
netbsd/amd64
|
|
||||||
openbsd/amd64
|
|
||||||
windows/amd64"
|
|
||||||
|
|
||||||
tool="$1"
|
|
||||||
version="$2"
|
|
||||||
|
|
||||||
if test -z "$version"; then
|
|
||||||
echo "Usage: $0 <tool name> <release version>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf releases
|
|
||||||
mkdir -p releases
|
|
||||||
|
|
||||||
|
|
||||||
for D in $DIST; do
|
|
||||||
os=${D/\/*/}
|
|
||||||
arch=${D/*\//}
|
|
||||||
binfile="releases/${tool}-${os}-${arch}-${version}"
|
|
||||||
tardir="${tool}-${os}-${arch}-${version}"
|
|
||||||
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
|
|
||||||
pie=""
|
|
||||||
|
|
||||||
if test "$D" = "linux/amd64"; then
|
|
||||||
pie="-buildmode=pie"
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -x
|
|
||||||
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}/
|
|
||||||
echo 'tool = tablizer
|
|
||||||
PREFIX = /usr/local
|
|
||||||
UID = root
|
|
||||||
GID = 0
|
|
||||||
|
|
||||||
install:
|
|
||||||
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
|
|
||||||
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
|
|
||||||
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
|
|
||||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
|
|
||||||
tar cpzf ${tarfile} ${tardir}
|
|
||||||
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
|
|
||||||
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
|
|
||||||
rm -rf ${tardir}
|
|
||||||
set +x
|
|
||||||
done
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
Simple filter hook function. Splits the argument by whitespace,
|
|
||||||
fetches the 2nd element, converts it to an int and returns true
|
|
||||||
if it s larger than 5, false otherwise.
|
|
||||||
*/
|
|
||||||
(defn uselarge [line]
|
|
||||||
(cond (> (atoi (second (resplit line `\s+`))) 5) true false))
|
|
||||||
|
|
||||||
/* Register the filter hook */
|
|
||||||
(addhook %filter %uselarge)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# usage
|
|
||||||
exec tablizer --help
|
|
||||||
stdout Usage
|
|
||||||
|
|
||||||
exec tablizer -h
|
|
||||||
stdout show
|
|
||||||
|
|
||||||
# version
|
|
||||||
exec tablizer -V
|
|
||||||
stdout version
|
|
||||||
|
|
||||||
# completion
|
|
||||||
exec tablizer --completion bash
|
|
||||||
stdout __tablizer_init_completion
|
|
||||||
|
|
||||||
# use config (configures colors, but these are not being used, since
|
|
||||||
# this env doesn't support it, but at least it should succeed.
|
|
||||||
exec tablizer -f config.hcl -r testtable.txt Runn
|
|
||||||
stdout Runn
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
|
|
||||||
|
|
||||||
-- config.hcl --
|
|
||||||
BG = "lightGreen"
|
|
||||||
FG = "white"
|
|
||||||
HighlightBG = "lightGreen"
|
|
||||||
HighlightFG = "white"
|
|
||||||
NoHighlightBG = "white"
|
|
||||||
NoHighlightFG = "lightGreen"
|
|
||||||
HighlightHdrBG = "red"
|
|
||||||
HighlightHdrFG = "white"
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# reading from file and matching with lowercase words
|
|
||||||
exec tablizer -c name,status -r testtable.csv -s,
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching mixed case
|
|
||||||
exec tablizer -c NAME,staTUS -r testtable.csv -s,
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching using numbers
|
|
||||||
exec tablizer -c 1,3 -r testtable.csv -s,
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching using regex
|
|
||||||
exec tablizer -c 'na.*,stat.' -r testtable.csv -s,
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.csv --
|
|
||||||
NAME,READY,STATUS,RESTARTS,AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0,2/2,Running,35 (45m ago),11d
|
|
||||||
grafana-fcc54cbc9-bk7s8,1/1,Running,17 (45m ago),1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7,1/1,Running,17 (45m ago),1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f,1/1,Running,20 (45m ago),45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl,1/1,Running,17 (45m ago),54s
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# filtering
|
|
||||||
exec tablizer -r testtable.txt -F name=grafana
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# filtering two columns
|
|
||||||
exec tablizer -r testtable.txt -F name=prometh -F age=1h
|
|
||||||
stdout blackbox.*Runn
|
|
||||||
|
|
||||||
# filtering two same columns
|
|
||||||
exec tablizer -r testtable.txt -F name=prometh -F name=alert
|
|
||||||
stdout prometheus-alertmanager.*Runn
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# reading from file and matching with lowercase words
|
|
||||||
exec tablizer -c name,status -r testtable.txt
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching mixed case
|
|
||||||
exec tablizer -c NAME,staTUS -r testtable.txt
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching using numbers
|
|
||||||
exec tablizer -c 1,3 -r testtable.txt
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# matching using regex
|
|
||||||
exec tablizer -c 'na.*,stat.' -r testtable.txt
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# filtering
|
|
||||||
|
|
||||||
# a AND b
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies invasive imperium
|
|
||||||
stdout 'namak'
|
|
||||||
! stdout human
|
|
||||||
|
|
||||||
# a AND !b
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies invasive '/imperium/!'
|
|
||||||
stdout 'human'
|
|
||||||
! stdout namak
|
|
||||||
|
|
||||||
# a AND !b AND c
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies peaceful '/imperium/!' planetary
|
|
||||||
stdout 'kenaha'
|
|
||||||
! stdout 'namak|heduu|riedl'
|
|
||||||
|
|
||||||
# case insensitive
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies '/REGIONAL/i'
|
|
||||||
stdout namak
|
|
||||||
! stdout 'human|riedl|heduu|kenaa'
|
|
||||||
|
|
||||||
# case insensitive negated
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies '/REGIONAL/!i'
|
|
||||||
stdout 'human|riedl|heduu|kenaa'
|
|
||||||
! stdout namak
|
|
||||||
|
|
||||||
# !a AND !b
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies '/galactic/!' '/planetary/!'
|
|
||||||
stdout namak
|
|
||||||
! stdout 'human|riedl|heduu|kenaa'
|
|
||||||
|
|
||||||
# same case insensitive
|
|
||||||
exec tablizer -r testtable.txt -H -cspecies '/GALACTIC/i!' '/PLANETARY/!i'
|
|
||||||
stdout namak
|
|
||||||
! stdout 'human|riedl|heduu|kenaa'
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
SPECIES TYPE HOME STAGE SPREAD
|
|
||||||
human invasive earth brink planetary
|
|
||||||
riedl peaceful keauna civilized pangalactic
|
|
||||||
namak invasive namak imperium regional
|
|
||||||
heduu peaceful iu imperium galactic
|
|
||||||
kenaha peaceful kohi hunter-gatherer planetary
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# sort by name
|
|
||||||
exec tablizer -r testtable.txt -k 1
|
|
||||||
stdout '^alert.*\n^grafana.*\n^kube'
|
|
||||||
|
|
||||||
# sort by name reversed
|
|
||||||
exec tablizer -r testtable.txt -k 1 -D
|
|
||||||
stdout 'kube.*\n^grafana.*\n^alert'
|
|
||||||
|
|
||||||
# sort by starts numerically
|
|
||||||
exec tablizer -r testtable.txt -k 4 -i -c4
|
|
||||||
stdout '17\s*\n^20\s*\n^35'
|
|
||||||
|
|
||||||
# sort by starts numerically reversed
|
|
||||||
exec tablizer -r testtable.txt -k 4 -i -c4 -D
|
|
||||||
stdout '35\s*\n^20\s*\n^17'
|
|
||||||
|
|
||||||
# sort by age
|
|
||||||
exec tablizer -r testtable.txt -k 5 -a
|
|
||||||
stdout '45m\s*\n.*1h44m'
|
|
||||||
|
|
||||||
# sort by age reverse
|
|
||||||
exec tablizer -r testtable.txt -k 5 -a -D
|
|
||||||
stdout '1h44m\s*\n.*45m'
|
|
||||||
|
|
||||||
# sort by time
|
|
||||||
exec tablizer -r timetable.txt -k 2 -t
|
|
||||||
stdout '^sel.*\n^foo.*\nbar'
|
|
||||||
|
|
||||||
# sort by time reverse
|
|
||||||
exec tablizer -r timetable.txt -k 2 -t -D
|
|
||||||
stdout '^bar.*\n^foo.*\nsel'
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS STARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 11d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 1h44m
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 1d
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 54s
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- timetable.txt --
|
|
||||||
NAME TIME
|
|
||||||
foo 2024-11-18T12:00:00+01:00
|
|
||||||
bar 2024-11-18T12:45:00+01:00
|
|
||||||
sel 2024-07-18T12:00:00+01:00
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# reading from stdin and matching with lowercase words
|
|
||||||
stdin testtable.txt
|
|
||||||
exec tablizer -c name,status
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# reading from -r stdin and matching with lowercase words
|
|
||||||
stdin testtable.txt
|
|
||||||
exec tablizer -c name,status -r -
|
|
||||||
stdout grafana.*Runn
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# transpose one field
|
|
||||||
exec tablizer -r testtable.txt -T status -R '/Running/OK/'
|
|
||||||
stdout grafana.*OK
|
|
||||||
|
|
||||||
# transpose two fields
|
|
||||||
exec tablizer -r testtable.txt -T name,status -R '/alertmanager-//' -R '/Running/OK/'
|
|
||||||
stdout prometheus-0.*OK
|
|
||||||
|
|
||||||
# transpose one field and show one column
|
|
||||||
exec tablizer -r testtable.txt -T status -R '/Running/OK/' -c name
|
|
||||||
! stdout grafana.*OK
|
|
||||||
|
|
||||||
|
|
||||||
# will be automatically created in work dir
|
|
||||||
-- testtable.txt --
|
|
||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
NAME READY STATUS RESTARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 (45m ago) 11d
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 (45m ago) 1d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 (45m ago) 1h44m
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 (45m ago) 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 (45m ago) 54s
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
NAME,DURATION
|
|
||||||
x,10
|
|
||||||
a,100
|
|
||||||
z,0
|
|
||||||
u,4
|
|
||||||
k,6
|
|
||||||
|
@@ -1,6 +0,0 @@
|
|||||||
NAME DURATION
|
|
||||||
x 10
|
|
||||||
a 100
|
|
||||||
z 0
|
|
||||||
u 4
|
|
||||||
k 6
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
NAME READY STATUS STARTS AGE
|
|
||||||
alertmanager-kube-prometheus-alertmanager-0 2/2 Running 35 11d
|
|
||||||
kube-prometheus-blackbox-exporter-5d85b5d8f4-tskh7 1/1 Running 17 1h44m
|
|
||||||
grafana-fcc54cbc9-bk7s8 1/1 Running 17 1d
|
|
||||||
kube-prometheus-kube-state-metrics-b4cd9487-75p7f 1/1 Running 20 45m
|
|
||||||
kube-prometheus-node-exporter-bfzpl 1/1 Running 17 54s
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
ONE TWO
|
|
||||||
1 4
|
|
||||||
3 1
|
|
||||||
5 2
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
SPECIES TYPE HOME STAGE
|
|
||||||
human invasive earth brink
|
|
||||||
riedl peaceful keauna civilized
|
|
||||||
namak invasive namak imperium
|
|
||||||
heduu peaceful iu imperium
|
|
||||||
kenaha peaceful kohi hunter-gatherer
|
|
||||||
711
tablizer.1
711
tablizer.1
@@ -1,711 +0,0 @@
|
|||||||
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
|
|
||||||
.\"
|
|
||||||
.\" Standard preamble:
|
|
||||||
.\" ========================================================================
|
|
||||||
.de Sp \" Vertical space (when we can't use .PP)
|
|
||||||
.if t .sp .5v
|
|
||||||
.if n .sp
|
|
||||||
..
|
|
||||||
.de Vb \" Begin verbatim text
|
|
||||||
.ft CW
|
|
||||||
.nf
|
|
||||||
.ne \\$1
|
|
||||||
..
|
|
||||||
.de Ve \" End verbatim text
|
|
||||||
.ft R
|
|
||||||
.fi
|
|
||||||
..
|
|
||||||
.\" Set up some character translations and predefined strings. \*(-- will
|
|
||||||
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
|
|
||||||
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
|
|
||||||
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
|
|
||||||
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
|
|
||||||
.\" nothing in troff, for use with C<>.
|
|
||||||
.tr \(*W-
|
|
||||||
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
|
|
||||||
.ie n \{\
|
|
||||||
. ds -- \(*W-
|
|
||||||
. ds PI pi
|
|
||||||
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
|
|
||||||
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
|
|
||||||
. ds L" ""
|
|
||||||
. ds R" ""
|
|
||||||
. ds C` ""
|
|
||||||
. ds C' ""
|
|
||||||
'br\}
|
|
||||||
.el\{\
|
|
||||||
. ds -- \|\(em\|
|
|
||||||
. ds PI \(*p
|
|
||||||
. ds L" ``
|
|
||||||
. ds R" ''
|
|
||||||
. ds C`
|
|
||||||
. ds C'
|
|
||||||
'br\}
|
|
||||||
.\"
|
|
||||||
.\" Escape single quotes in literal strings from groff's Unicode transform.
|
|
||||||
.ie \n(.g .ds Aq \(aq
|
|
||||||
.el .ds Aq '
|
|
||||||
.\"
|
|
||||||
.\" If the F register is >0, we'll generate index entries on stderr for
|
|
||||||
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
|
|
||||||
.\" entries marked with X<> in POD. Of course, you'll have to process the
|
|
||||||
.\" output yourself in some meaningful fashion.
|
|
||||||
.\"
|
|
||||||
.\" Avoid warning from groff about undefined register 'F'.
|
|
||||||
.de IX
|
|
||||||
..
|
|
||||||
.nr rF 0
|
|
||||||
.if \n(.g .if rF .nr rF 1
|
|
||||||
.if (\n(rF:(\n(.g==0)) \{\
|
|
||||||
. if \nF \{\
|
|
||||||
. de IX
|
|
||||||
. tm Index:\\$1\t\\n%\t"\\$2"
|
|
||||||
..
|
|
||||||
. if !\nF==2 \{\
|
|
||||||
. nr % 0
|
|
||||||
. nr F 2
|
|
||||||
. \}
|
|
||||||
. \}
|
|
||||||
.\}
|
|
||||||
.rr rF
|
|
||||||
.\"
|
|
||||||
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
|
|
||||||
.\" Fear. Run. Save yourself. No user-serviceable parts.
|
|
||||||
. \" fudge factors for nroff and troff
|
|
||||||
.if n \{\
|
|
||||||
. ds #H 0
|
|
||||||
. ds #V .8m
|
|
||||||
. ds #F .3m
|
|
||||||
. ds #[ \f1
|
|
||||||
. ds #] \fP
|
|
||||||
.\}
|
|
||||||
.if t \{\
|
|
||||||
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
|
|
||||||
. ds #V .6m
|
|
||||||
. ds #F 0
|
|
||||||
. ds #[ \&
|
|
||||||
. ds #] \&
|
|
||||||
.\}
|
|
||||||
. \" simple accents for nroff and troff
|
|
||||||
.if n \{\
|
|
||||||
. ds ' \&
|
|
||||||
. ds ` \&
|
|
||||||
. ds ^ \&
|
|
||||||
. ds , \&
|
|
||||||
. ds ~ ~
|
|
||||||
. ds /
|
|
||||||
.\}
|
|
||||||
.if t \{\
|
|
||||||
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
|
|
||||||
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
|
|
||||||
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
|
|
||||||
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
|
|
||||||
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
|
|
||||||
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
|
|
||||||
.\}
|
|
||||||
. \" troff and (daisy-wheel) nroff accents
|
|
||||||
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
|
|
||||||
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
|
|
||||||
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
|
|
||||||
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
|
|
||||||
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
|
|
||||||
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
|
|
||||||
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
|
|
||||||
.ds ae a\h'-(\w'a'u*4/10)'e
|
|
||||||
.ds Ae A\h'-(\w'A'u*4/10)'E
|
|
||||||
. \" corrections for vroff
|
|
||||||
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
|
|
||||||
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
|
|
||||||
. \" for low resolution devices (crt and lpr)
|
|
||||||
.if \n(.H>23 .if \n(.V>19 \
|
|
||||||
\{\
|
|
||||||
. ds : e
|
|
||||||
. ds 8 ss
|
|
||||||
. ds o a
|
|
||||||
. ds d- d\h'-1'\(ga
|
|
||||||
. ds D- D\h'-1'\(hy
|
|
||||||
. ds th \o'bp'
|
|
||||||
. ds Th \o'LP'
|
|
||||||
. ds ae ae
|
|
||||||
. ds Ae AE
|
|
||||||
.\}
|
|
||||||
.rm #[ #] #H #V #F C
|
|
||||||
.\" ========================================================================
|
|
||||||
.\"
|
|
||||||
.IX Title "TABLIZER 1"
|
|
||||||
.TH TABLIZER 1 "2025-10-13" "1" "User Commands"
|
|
||||||
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
|
|
||||||
.\" way too many mistakes in technical documents.
|
|
||||||
.if n .ad l
|
|
||||||
.nh
|
|
||||||
.SH "NAME"
|
|
||||||
tablizer \- Manipulate tabular output of other programs
|
|
||||||
.SH "SYNOPSIS"
|
|
||||||
.IX Header "SYNOPSIS"
|
|
||||||
.Vb 2
|
|
||||||
\& Usage:
|
|
||||||
\& tablizer [regex,...] [\-r file] [flags]
|
|
||||||
\&
|
|
||||||
\& Operational Flags:
|
|
||||||
\& \-c, \-\-columns string Only show the speficied columns (separated by ,)
|
|
||||||
\& \-v, \-\-invert\-match select non\-matching rows
|
|
||||||
\& \-n, \-\-numbering Enable header numbering
|
|
||||||
\& \-N, \-\-no\-color Disable pattern highlighting
|
|
||||||
\& \-H, \-\-no\-headers Disable headers display
|
|
||||||
\& \-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
|
|
||||||
\& \-T, \-\-transpose\-columns string Transpose the speficied columns (separated by ,)
|
|
||||||
\& \-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
|
|
||||||
\& \-M, \-\-markdown Enable markdown table output
|
|
||||||
\& \-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
|
|
||||||
\& \-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
|
|
||||||
\&
|
|
||||||
\& Sort Mode Flags (mutually exclusive):
|
|
||||||
\& \-a, \-\-sort\-age sort according to age (duration) string
|
|
||||||
\& \-D, \-\-sort\-desc Sort in descending order (default: ascending)
|
|
||||||
\& \-i, \-\-sort\-numeric sort according to string numerical value
|
|
||||||
\& \-t, \-\-sort\-time sort according to time string
|
|
||||||
\&
|
|
||||||
\& Other Flags:
|
|
||||||
\& \-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
|
|
||||||
\& \-h, \-\-help help for tablizer
|
|
||||||
\& \-m, \-\-man Display manual page
|
|
||||||
\& \-V, \-\-version Print program version
|
|
||||||
.Ve
|
|
||||||
.SH "DESCRIPTION"
|
|
||||||
.IX Header "DESCRIPTION"
|
|
||||||
Many programs generate tabular output. But sometimes you need to
|
|
||||||
post-process these tables, you may need to remove one or more columns
|
|
||||||
or you may want to filter for some pattern (See \s-1PATTERNS\s0) or you
|
|
||||||
may need the output in another program and need to parse it somehow.
|
|
||||||
Standard unix tools such as \fBawk\fR\|(1), \fBgrep\fR\|(1) or \fBcolumn\fR\|(1) may help, but
|
|
||||||
sometimes it's a tedious business.
|
|
||||||
.PP
|
|
||||||
Let's take the output of the tool kubectl. It contains cells with
|
|
||||||
withespace and they do not separate columns by \s-1TAB\s0 characters. This is
|
|
||||||
not easy to process.
|
|
||||||
.PP
|
|
||||||
You can use \fBtablizer\fR to do these and more things.
|
|
||||||
.PP
|
|
||||||
\&\fBtablizer\fR analyses the header fields of a table, registers the
|
|
||||||
column positions of each header field and separates columns by those
|
|
||||||
positions.
|
|
||||||
.PP
|
|
||||||
Without any options it reads its input from \f(CW\*(C`STDIN\*(C'\fR, but you can also
|
|
||||||
specify a file as a parameter. If you want to reduce the output by
|
|
||||||
some regular expression, just specify it as its first parameter. You
|
|
||||||
may also use the \fB\-v\fR option to exclude all rows which match the
|
|
||||||
pattern. Hence:
|
|
||||||
.PP
|
|
||||||
.Vb 2
|
|
||||||
\& # read from STDIN
|
|
||||||
\& kubectl get pods | tablizer
|
|
||||||
\&
|
|
||||||
\& # read a file
|
|
||||||
\& tablizer \-r filename
|
|
||||||
\&
|
|
||||||
\& # search for pattern in a file (works like grep)
|
|
||||||
\& tablizer regex \-r filename
|
|
||||||
\&
|
|
||||||
\& # search for pattern in STDIN
|
|
||||||
\& kubectl get pods | tablizer regex
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
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)
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
These numbers denote the column and you can use them to specify which
|
|
||||||
columns you want to have in your output (see \s-1COLUMNS\s0:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& kubectl get pods | tablizer \-c1,3
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
You can specify the numbers in any order but output will always follow
|
|
||||||
the original order.
|
|
||||||
.PP
|
|
||||||
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
|
|
||||||
this only affects tabular output modes. Shell, Extended, Yaml and \s-1CSV\s0
|
|
||||||
output modes always use the column names.
|
|
||||||
.PP
|
|
||||||
By default, if a \fBpattern\fR has been speficied, matches will be
|
|
||||||
highlighted. You can disable this behavior with the \fB\-N\fR option.
|
|
||||||
.PP
|
|
||||||
Use the \fB\-k\fR option to specify by which column to sort the tabular
|
|
||||||
data (as in \s-1GNU\s0 \fBsort\fR\|(1)). The default sort column is the first
|
|
||||||
one. You can specify column numbers or names. Column numbers start
|
|
||||||
with 1, names are case insensitive. You can specify multiple columns
|
|
||||||
separated by comma to sort, but the type must be the same. For example
|
|
||||||
if you want to sort numerically, all columns must be numbers. If you
|
|
||||||
use column numbers, then be aware, that these are the numbers before
|
|
||||||
column extraction. For example if you have a table with 4 columns and
|
|
||||||
specify \f(CW\*(C`\-c4\*(C'\fR, then only 1 column (the fourth) will be printed,
|
|
||||||
however if you want to sort by this column, you'll have to specify
|
|
||||||
\&\f(CW\*(C`\-k4\*(C'\fR.
|
|
||||||
.PP
|
|
||||||
The default sort order is ascending. You can change this to
|
|
||||||
descending order using the option \fB\-D\fR. The default sort order is by
|
|
||||||
alphanumeric string, but there are other sort modes:
|
|
||||||
.IP "\fB\-a \-\-sort\-age\fR" 4
|
|
||||||
.IX Item "-a --sort-age"
|
|
||||||
Sorts duration strings like \*(L"1d4h32m51s\*(R".
|
|
||||||
.IP "\fB\-i \-\-sort\-numeric\fR" 4
|
|
||||||
.IX Item "-i --sort-numeric"
|
|
||||||
Sorts numeric fields.
|
|
||||||
.IP "\fB\-t \-\-sort\-time\fR" 4
|
|
||||||
.IX Item "-t --sort-time"
|
|
||||||
Sorts timestamps.
|
|
||||||
.PP
|
|
||||||
Finally the \fB\-d\fR option enables debugging output which is mostly
|
|
||||||
useful for the developer.
|
|
||||||
.SS "\s-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
|
|
||||||
expression patterns. The regexp language being used is the one of
|
|
||||||
\&\s-1GOLANG,\s0 refer to the syntax cheat sheet here:
|
|
||||||
<https://pkg.go.dev/regexp/syntax>.
|
|
||||||
.PP
|
|
||||||
If you want to read a more comprehensive documentation about the
|
|
||||||
topic and have perl installed you can read it with:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& perldoc perlre
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
Or read it online: <https://perldoc.perl.org/perlre>. But please note
|
|
||||||
that the \s-1GO\s0 regexp engine does \s-1NOT\s0 support all perl regex terms,
|
|
||||||
especially look-ahead and look-behind.
|
|
||||||
.PP
|
|
||||||
If you want to supply flags to a regex, then surround it with slashes
|
|
||||||
and append the flag. The following flags are supported:
|
|
||||||
.PP
|
|
||||||
.Vb 2
|
|
||||||
\& i => case insensitive
|
|
||||||
\& ! => negative match
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
Example for a case insensitive search:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& 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
|
|
||||||
is, if a line in the input matches the given regex, but \f(CW\*(C`!\*(C'\fR is
|
|
||||||
supplied, tablizer will \s-1NOT\s0 include it in the output.
|
|
||||||
.PP
|
|
||||||
For example, here we want to get all lines matching \*(L"foo\*(R" but not
|
|
||||||
\&\*(L"bar\*(R":
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& cat table | tablizer foo \*(Aq/bar/!\*(Aq
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
This would match a line \*(L"foo zorro\*(R" but not \*(L"foo bar\*(R".
|
|
||||||
.PP
|
|
||||||
The flags can also be combined.
|
|
||||||
.PP
|
|
||||||
You can also use the experimental fuzzy search feature by providing the
|
|
||||||
option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search
|
|
||||||
term, not a regexp.
|
|
||||||
.PP
|
|
||||||
Sometimes you want to filter by one or more columns. You can do that
|
|
||||||
using the \fB\-F\fR option. The option can be specified multiple times and
|
|
||||||
has the following format:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& fieldname=regexp
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
Fieldnames (== columns headers) are case insensitive.
|
|
||||||
.PP
|
|
||||||
If you specify more than one filter, both filters have to match (\s-1AND\s0
|
|
||||||
operation).
|
|
||||||
.PP
|
|
||||||
These field filters can also be negated:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& fieldname!=regexp
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
If the option \fB\-v\fR is specified, the filtering is inverted.
|
|
||||||
.SS "\s-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
|
|
||||||
display. By default tablizer numerizes the header names and these
|
|
||||||
numbers can be used to specify which header to display, see example
|
|
||||||
above.
|
|
||||||
.PP
|
|
||||||
However, beside numbers, you can also use regular expressions with
|
|
||||||
\&\fB\-c\fR, also separated by comma. And you can mix column numbers with
|
|
||||||
regexps.
|
|
||||||
.PP
|
|
||||||
Lets take this table:
|
|
||||||
.PP
|
|
||||||
.Vb 4
|
|
||||||
\& PID TTY TIME CMD
|
|
||||||
\& 14001 pts/0 00:00:00 bash
|
|
||||||
\& 42871 pts/0 00:00:00 ps
|
|
||||||
\& 42872 pts/0 00:00:00 sed
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
We want to see only the \s-1CMD\s0 column and use a regex for this:
|
|
||||||
.PP
|
|
||||||
.Vb 6
|
|
||||||
\& ps | tablizer \-s \*(Aq\es+\*(Aq \-c C
|
|
||||||
\& CMD(4)
|
|
||||||
\& bash
|
|
||||||
\& ps
|
|
||||||
\& tablizer
|
|
||||||
\& sed
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
where \*(L"C\*(R" is our regexp which matches \s-1CMD.\s0
|
|
||||||
.PP
|
|
||||||
If a column specifier doesn't look like a regular expression, matching
|
|
||||||
against header fields will be case insensitive. So, if you have a
|
|
||||||
field with the name \f(CW\*(C`ID\*(C'\fR then these will all match: \f(CW\*(C`\-c id\*(C'\fR, \f(CW\*(C`\-c
|
|
||||||
Id\*(C'\fR. The same rule applies to the options \f(CW\*(C`\-T\*(C'\fR and \f(CW\*(C`\-F\*(C'\fR.
|
|
||||||
.SS "\s-1TRANSPOSE FIELDS USING REGEXPS\s0"
|
|
||||||
.IX Subsection "TRANSPOSE FIELDS USING REGEXPS"
|
|
||||||
You can manipulate field contents using regular expressions. You have
|
|
||||||
to tell tablizer which field[s] to operate on using the option \f(CW\*(C`\-T\*(C'\fR
|
|
||||||
and the search/replace pattern using \f(CW\*(C`\-R\*(C'\fR. The number of columns and
|
|
||||||
patterns must match.
|
|
||||||
.PP
|
|
||||||
A search/replace pattern consists of the following elements:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& /search\-regexp/replace\-string/
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
The separator can be any valid character. Especially if you want to
|
|
||||||
use a regexp containing the \f(CW\*(C`/\*(C'\fR character, eg:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& |search\-regexp|replace\-string|
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
Example:
|
|
||||||
.PP
|
|
||||||
.Vb 7
|
|
||||||
\& cat t/testtable2
|
|
||||||
\& NAME DURATION
|
|
||||||
\& x 10
|
|
||||||
\& a 100
|
|
||||||
\& z 0
|
|
||||||
\& u 4
|
|
||||||
\& k 6
|
|
||||||
\&
|
|
||||||
\& cat t/testtable2 | tablizer \-T2 \-R \*(Aq/^\ed/4/\*(Aq \-n
|
|
||||||
\& NAME DURATION
|
|
||||||
\& x 40
|
|
||||||
\& a 400
|
|
||||||
\& z 4
|
|
||||||
\& u 4
|
|
||||||
\& k 4
|
|
||||||
.Ve
|
|
||||||
.SS "\s-1OUTPUT MODES\s0"
|
|
||||||
.IX Subsection "OUTPUT MODES"
|
|
||||||
There might be cases when the tabular output of a program is way too
|
|
||||||
large for your current terminal but you still need to see every
|
|
||||||
column. In such cases the \fB\-o extended\fR or \fB\-X\fR option can be
|
|
||||||
useful which enables \fIextended mode\fR. In this mode, each row will be
|
|
||||||
printed vertically, header left, value right, aligned by the field
|
|
||||||
widths. Here's an example:
|
|
||||||
.PP
|
|
||||||
.Vb 6
|
|
||||||
\& kubectl get pods | ./tablizer \-o extended
|
|
||||||
\& NAME: repldepl\-7bcd8d5b64\-7zq4l
|
|
||||||
\& READY: 1/1
|
|
||||||
\& STATUS: Running
|
|
||||||
\& RESTARTS: 1 (71m ago)
|
|
||||||
\& AGE: 5h28m
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
You can of course still use a regex to reduce the number of rows
|
|
||||||
displayed.
|
|
||||||
.PP
|
|
||||||
The option \fB\-o shell\fR can be used if the output has to be processed
|
|
||||||
by the shell, it prints variable assignments for each cell, one line
|
|
||||||
per row:
|
|
||||||
.PP
|
|
||||||
.Vb 4
|
|
||||||
\& kubectl get pods | ./tablizer \-o extended ./tablizer \-o shell
|
|
||||||
\& NAME="repldepl\-7bcd8d5b64\-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
\& NAME="repldepl\-7bcd8d5b64\-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
\& NAME="repldepl\-7bcd8d5b64\-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
You can use this in an eval loop.
|
|
||||||
.PP
|
|
||||||
Beside normal ascii mode (the default) and extended mode there are
|
|
||||||
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
|
|
||||||
table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
|
|
||||||
prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
|
|
||||||
value file.
|
|
||||||
.SS "\s-1PUT FIELDS TO CLIPBOARD\s0"
|
|
||||||
.IX Subsection "PUT FIELDS TO CLIPBOARD"
|
|
||||||
You can let tablizer put fields to the clipboard using the option
|
|
||||||
\&\f(CW\*(C`\-y\*(C'\fR. This best fits the use-case when the result of your filtering
|
|
||||||
yields just one row. For example:
|
|
||||||
.PP
|
|
||||||
.Vb 1
|
|
||||||
\& cloudctl cluster ls | tablizer \-yid matchbox
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
If \*(L"matchbox\*(R" matches one cluster, you can immediately use the id of
|
|
||||||
that cluster somewhere else and paste it. Of course, if there are
|
|
||||||
multiple matches, then all id's will be put into the clipboard
|
|
||||||
separated by one space.
|
|
||||||
.SS "\s-1ENVIRONMENT VARIABLES\s0"
|
|
||||||
.IX Subsection "ENVIRONMENT VARIABLES"
|
|
||||||
\&\fBtablizer\fR supports certain environment variables which use can use
|
|
||||||
to influence program behavior. Commandline flags have always
|
|
||||||
precedence over environment variables.
|
|
||||||
.IP "<T_HEADER_NUMBERING> \- enable numbering of header fields, like \fB\-n\fR." 4
|
|
||||||
.IX Item "<T_HEADER_NUMBERING> - enable numbering of header fields, like -n."
|
|
||||||
.PD 0
|
|
||||||
.IP "<T_COLUMNS> \- comma separated list of columns to output, like \fB\-c\fR" 4
|
|
||||||
.IX Item "<T_COLUMNS> - comma separated list of columns to output, like -c"
|
|
||||||
.IP "<\s-1NO_COLORS\s0> \- disable colorization of matches, like \fB\-N\fR" 4
|
|
||||||
.IX Item "<NO_COLORS> - disable colorization of matches, like -N"
|
|
||||||
.PD
|
|
||||||
.SS "\s-1COMPLETION\s0"
|
|
||||||
.IX Subsection "COMPLETION"
|
|
||||||
Shell completion for command line options can be enabled by using the
|
|
||||||
\&\fB\-\-completion\fR flag. The required parameter is the name of your
|
|
||||||
shell. Currently supported are: bash, zsh, fish and powershell.
|
|
||||||
.PP
|
|
||||||
Detailed instructions:
|
|
||||||
.IP "Bash:" 4
|
|
||||||
.IX Item "Bash:"
|
|
||||||
.Vb 1
|
|
||||||
\& source <(tablizer \-\-completion bash)
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
.Sp
|
|
||||||
.Vb 2
|
|
||||||
\& # Linux:
|
|
||||||
\& $ tablizer \-\-completion bash > /etc/bash_completion.d/tablizer
|
|
||||||
\&
|
|
||||||
\& # macOS:
|
|
||||||
\& $ tablizer \-\-completion bash > $(brew \-\-prefix)/etc/bash_completion.d/tablizer
|
|
||||||
.Ve
|
|
||||||
.IP "Zsh:" 4
|
|
||||||
.IX Item "Zsh:"
|
|
||||||
If shell completion is not already enabled in your environment,
|
|
||||||
you will need to enable it. You can execute the following once:
|
|
||||||
.Sp
|
|
||||||
.Vb 1
|
|
||||||
\& echo "autoload \-U compinit; compinit" >> ~/.zshrc
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
.Sp
|
|
||||||
.Vb 1
|
|
||||||
\& $ tablizer \-\-completion zsh > "${fpath[1]}/_tablizer"
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
You will need to start a new shell for this setup to take effect.
|
|
||||||
.IP "fish:" 4
|
|
||||||
.IX Item "fish:"
|
|
||||||
.Vb 1
|
|
||||||
\& tablizer \-\-completion fish | source
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
.Sp
|
|
||||||
.Vb 1
|
|
||||||
\& tablizer \-\-completion fish > ~/.config/fish/completions/tablizer.fish
|
|
||||||
.Ve
|
|
||||||
.IP "PowerShell:" 4
|
|
||||||
.IX Item "PowerShell:"
|
|
||||||
.Vb 1
|
|
||||||
\& tablizer \-\-completion powershell | Out\-String | Invoke\-Expression
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
To load completions for every new session, run:
|
|
||||||
.Sp
|
|
||||||
.Vb 1
|
|
||||||
\& tablizer \-\-completion powershell > tablizer.ps1
|
|
||||||
.Ve
|
|
||||||
.Sp
|
|
||||||
and source this file from your PowerShell profile.
|
|
||||||
.SH "CONFIGURATION AND COLORS"
|
|
||||||
.IX Header "CONFIGURATION AND COLORS"
|
|
||||||
YOu can put certain configuration values into a configuration file in
|
|
||||||
\&\s-1HCL\s0 format. By default tablizer looks for
|
|
||||||
\&\f(CW\*(C`$HOME/.config/tablizer/config\*(C'\fR, but you can provide one using the
|
|
||||||
parameter \f(CW\*(C`\-f\*(C'\fR.
|
|
||||||
.PP
|
|
||||||
In the configuration the following variables can be defined:
|
|
||||||
.PP
|
|
||||||
.Vb 8
|
|
||||||
\& BG = "lightGreen"
|
|
||||||
\& FG = "white"
|
|
||||||
\& HighlightBG = "lightGreen"
|
|
||||||
\& HighlightFG = "white"
|
|
||||||
\& NoHighlightBG = "white"
|
|
||||||
\& NoHighlightFG = "lightGreen"
|
|
||||||
\& HighlightHdrBG = "red"
|
|
||||||
\& HighlightHdrFG = "white"
|
|
||||||
.Ve
|
|
||||||
.PP
|
|
||||||
The following color definitions are available:
|
|
||||||
.PP
|
|
||||||
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
|
|
||||||
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
|
|
||||||
magenta, red, white, yellow
|
|
||||||
.PP
|
|
||||||
The Variables \fB\s-1FG\s0\fR and \fB\s-1BG\s0\fR are being used to highlight matches. The
|
|
||||||
other *FG and *BG variables are for colored table output (enabled with
|
|
||||||
the \f(CW\*(C`\-L\*(C'\fR parameter).
|
|
||||||
.PP
|
|
||||||
Colorization can be turned off completely either by setting the
|
|
||||||
parameter \f(CW\*(C`\-N\*(C'\fR or the environment variable \fB\s-1NO_COLOR\s0\fR to a true value.
|
|
||||||
.SH "BUGS"
|
|
||||||
.IX Header "BUGS"
|
|
||||||
In order to report a bug, unexpected behavior, feature requests
|
|
||||||
or to submit a patch, please open an issue on github:
|
|
||||||
<https://codeberg.org/scip/tablizer/issues>.
|
|
||||||
.SH "LICENSE"
|
|
||||||
.IX Header "LICENSE"
|
|
||||||
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
|
|
||||||
.PP
|
|
||||||
Copyright (c) 2022\-2024 by Thomas von Dein
|
|
||||||
.PP
|
|
||||||
This software uses the following \s-1GO\s0 modules:
|
|
||||||
.IP "repr (https://github.com/alecthomas/repr)" 4
|
|
||||||
.IX Item "repr (https://github.com/alecthomas/repr)"
|
|
||||||
Released under the \s-1MIT\s0 License, Copyright (c) 2016 Alec Thomas
|
|
||||||
.IP "cobra (https://github.com/spf13/cobra)" 4
|
|
||||||
.IX Item "cobra (https://github.com/spf13/cobra)"
|
|
||||||
Released under the Apache 2.0 license, Copyright 2013\-2022 The Cobra Authors
|
|
||||||
.IP "dateparse (github.com/araddon/dateparse)" 4
|
|
||||||
.IX Item "dateparse (github.com/araddon/dateparse)"
|
|
||||||
Released under the \s-1MIT\s0 License, Copyright (c) 2015\-2017 Aaron Raddon
|
|
||||||
.IP "color (github.com/gookit/color)" 4
|
|
||||||
.IX Item "color (github.com/gookit/color)"
|
|
||||||
Released under the \s-1MIT\s0 License, Copyright (c) 2016 inhere
|
|
||||||
.IP "tablewriter (github.com/olekukonko/tablewriter)" 4
|
|
||||||
.IX Item "tablewriter (github.com/olekukonko/tablewriter)"
|
|
||||||
Released under the \s-1MIT\s0 License, Copyright (c) 201 by Oleku Konko
|
|
||||||
.IP "yaml (gopkg.in/yaml.v3)" 4
|
|
||||||
.IX Item "yaml (gopkg.in/yaml.v3)"
|
|
||||||
Released under the \s-1MIT\s0 License, Copyright (c) 2006\-2011 Kirill Simonov
|
|
||||||
.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
|
|
||||||
563
tablizer.pod
563
tablizer.pod
@@ -1,563 +0,0 @@
|
|||||||
=head1 NAME
|
|
||||||
|
|
||||||
tablizer - Manipulate tabular output of other programs
|
|
||||||
|
|
||||||
=head1 SYNOPSIS
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
tablizer [regex,...] [-r file] [flags]
|
|
||||||
|
|
||||||
Operational Flags:
|
|
||||||
-c, --columns string Only show the speficied columns (separated by ,)
|
|
||||||
-v, --invert-match select non-matching rows
|
|
||||||
-n, --numbering Enable header numbering
|
|
||||||
-N, --no-color Disable pattern highlighting
|
|
||||||
-H, --no-headers Disable headers display
|
|
||||||
-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
|
|
||||||
-T, --transpose-columns string Transpose the speficied columns (separated by ,)
|
|
||||||
-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
|
|
||||||
-M, --markdown Enable markdown table output
|
|
||||||
-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
|
|
||||||
-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
|
|
||||||
|
|
||||||
Sort Mode Flags (mutually exclusive):
|
|
||||||
-a, --sort-age sort according to age (duration) string
|
|
||||||
-D, --sort-desc Sort in descending order (default: ascending)
|
|
||||||
-i, --sort-numeric sort according to string numerical value
|
|
||||||
-t, --sort-time sort according to time string
|
|
||||||
|
|
||||||
Other Flags:
|
|
||||||
-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
|
|
||||||
-h, --help help for tablizer
|
|
||||||
-m, --man Display manual page
|
|
||||||
-V, --version Print program version
|
|
||||||
|
|
||||||
|
|
||||||
=head1 DESCRIPTION
|
|
||||||
|
|
||||||
Many programs generate tabular output. But sometimes you need to
|
|
||||||
post-process these tables, you may need to remove one or more columns
|
|
||||||
or you may want to filter for some pattern (See L<PATTERNS>) or you
|
|
||||||
may need the output in another program and need to parse it somehow.
|
|
||||||
Standard unix tools such as awk(1), grep(1) or column(1) may help, but
|
|
||||||
sometimes it's a tedious business.
|
|
||||||
|
|
||||||
Let's take the output of the tool kubectl. It contains cells with
|
|
||||||
withespace and they do not separate columns by TAB characters. This is
|
|
||||||
not easy to process.
|
|
||||||
|
|
||||||
You can use B<tablizer> to do these and more things.
|
|
||||||
|
|
||||||
B<tablizer> analyses the header fields of a table, registers the
|
|
||||||
column positions of each header field and separates columns by those
|
|
||||||
positions.
|
|
||||||
|
|
||||||
Without any options it reads its input from C<STDIN>, but you can also
|
|
||||||
specify a file as a parameter. If you want to reduce the output by
|
|
||||||
some regular expression, just specify it as its first parameter. You
|
|
||||||
may also use the B<-v> option to exclude all rows which match the
|
|
||||||
pattern. Hence:
|
|
||||||
|
|
||||||
# read from STDIN
|
|
||||||
kubectl get pods | tablizer
|
|
||||||
|
|
||||||
# read a file
|
|
||||||
tablizer -r filename
|
|
||||||
|
|
||||||
# search for pattern in a file (works like grep)
|
|
||||||
tablizer regex -r filename
|
|
||||||
|
|
||||||
# search for pattern in STDIN
|
|
||||||
kubectl get pods | tablizer regex
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
You can specify the numbers in any order but output will always follow
|
|
||||||
the original order.
|
|
||||||
|
|
||||||
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
|
|
||||||
this only affects tabular output modes. Shell, Extended, Yaml and CSV
|
|
||||||
output modes always use the column names.
|
|
||||||
|
|
||||||
By default, if a B<pattern> has been speficied, matches will be
|
|
||||||
highlighted. You can disable this behavior with the B<-N> option.
|
|
||||||
|
|
||||||
Use the B<-k> option to specify by which column to sort the tabular
|
|
||||||
data (as in GNU sort(1)). The default sort column is the first
|
|
||||||
one. You can specify column numbers or names. Column numbers start
|
|
||||||
with 1, names are case insensitive. You can specify multiple columns
|
|
||||||
separated by comma to sort, but the type must be the same. For example
|
|
||||||
if you want to sort numerically, all columns must be numbers. If you
|
|
||||||
use column numbers, then be aware, that these are the numbers before
|
|
||||||
column extraction. For example if you have a table with 4 columns and
|
|
||||||
specify C<-c4>, then only 1 column (the fourth) will be printed,
|
|
||||||
however if you want to sort by this column, you'll have to specify
|
|
||||||
C<-k4>.
|
|
||||||
|
|
||||||
The default sort order is ascending. You can change this to
|
|
||||||
descending order using the option B<-D>. The default sort order is by
|
|
||||||
alphanumeric string, but there are other sort modes:
|
|
||||||
|
|
||||||
=over
|
|
||||||
|
|
||||||
=item B<-a --sort-age>
|
|
||||||
|
|
||||||
Sorts duration strings like "1d4h32m51s".
|
|
||||||
|
|
||||||
=item B<-i --sort-numeric>
|
|
||||||
|
|
||||||
Sorts numeric fields.
|
|
||||||
|
|
||||||
=item B<-t --sort-time>
|
|
||||||
|
|
||||||
Sorts timestamps.
|
|
||||||
|
|
||||||
=back
|
|
||||||
|
|
||||||
Finally the B<-d> option enables debugging output which is mostly
|
|
||||||
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
|
|
||||||
expression patterns. The regexp language being used is the one of
|
|
||||||
GOLANG, refer to the syntax cheat sheet here:
|
|
||||||
L<https://pkg.go.dev/regexp/syntax>.
|
|
||||||
|
|
||||||
If you want to read a more comprehensive documentation about the
|
|
||||||
topic and have perl installed you can read it with:
|
|
||||||
|
|
||||||
perldoc perlre
|
|
||||||
|
|
||||||
Or read it online: L<https://perldoc.perl.org/perlre>. But please note
|
|
||||||
that the GO regexp engine does NOT support all perl regex terms,
|
|
||||||
especially look-ahead and look-behind.
|
|
||||||
|
|
||||||
If you want to supply flags to a regex, then surround it with slashes
|
|
||||||
and append the flag. The following flags are supported:
|
|
||||||
|
|
||||||
i => case insensitive
|
|
||||||
! => negative match
|
|
||||||
|
|
||||||
Example for a case insensitive search:
|
|
||||||
|
|
||||||
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
|
|
||||||
supplied, tablizer will NOT include it in the output.
|
|
||||||
|
|
||||||
For example, here we want to get all lines matching "foo" but not
|
|
||||||
"bar":
|
|
||||||
|
|
||||||
cat table | tablizer foo '/bar/!'
|
|
||||||
|
|
||||||
This would match a line "foo zorro" but not "foo bar".
|
|
||||||
|
|
||||||
The flags can also be combined.
|
|
||||||
|
|
||||||
You can also use the experimental fuzzy search feature by providing the
|
|
||||||
option B<-z>, in which case the pattern is regarded as a fuzzy search
|
|
||||||
term, not a regexp.
|
|
||||||
|
|
||||||
Sometimes you want to filter by one or more columns. You can do that
|
|
||||||
using the B<-F> option. The option can be specified multiple times and
|
|
||||||
has the following format:
|
|
||||||
|
|
||||||
fieldname=regexp
|
|
||||||
|
|
||||||
Fieldnames (== columns headers) are case insensitive.
|
|
||||||
|
|
||||||
If you specify more than one filter, both filters have to match (AND
|
|
||||||
operation).
|
|
||||||
|
|
||||||
These field filters can also be negated:
|
|
||||||
|
|
||||||
fieldname!=regexp
|
|
||||||
|
|
||||||
If the option B<-v> is specified, the filtering is inverted.
|
|
||||||
|
|
||||||
=head2 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
|
|
||||||
|
|
||||||
The parameter B<-c> can be used to specify, which columns to
|
|
||||||
display. By default tablizer numerizes the header names and these
|
|
||||||
numbers can be used to specify which header to display, see example
|
|
||||||
above.
|
|
||||||
|
|
||||||
However, beside numbers, you can also use regular expressions with
|
|
||||||
B<-c>, also separated by comma. And you can mix column numbers with
|
|
||||||
regexps.
|
|
||||||
|
|
||||||
Lets take this table:
|
|
||||||
|
|
||||||
PID TTY TIME CMD
|
|
||||||
14001 pts/0 00:00:00 bash
|
|
||||||
42871 pts/0 00:00:00 ps
|
|
||||||
42872 pts/0 00:00:00 sed
|
|
||||||
|
|
||||||
We want to see only the CMD column and use a regex for this:
|
|
||||||
|
|
||||||
ps | tablizer -s '\s+' -c C
|
|
||||||
CMD(4)
|
|
||||||
bash
|
|
||||||
ps
|
|
||||||
tablizer
|
|
||||||
sed
|
|
||||||
|
|
||||||
where "C" is our regexp which matches CMD.
|
|
||||||
|
|
||||||
If a column specifier doesn't look like a regular expression, matching
|
|
||||||
against header fields will be case insensitive. So, if you have a
|
|
||||||
field with the name C<ID> then these will all match: C<-c id>, C<-c
|
|
||||||
Id>. The same rule applies to the options C<-T> and C<-F>.
|
|
||||||
|
|
||||||
|
|
||||||
=head2 TRANSPOSE FIELDS USING REGEXPS
|
|
||||||
|
|
||||||
You can manipulate field contents using regular expressions. You have
|
|
||||||
to tell tablizer which field[s] to operate on using the option C<-T>
|
|
||||||
and the search/replace pattern using C<-R>. The number of columns and
|
|
||||||
patterns must match.
|
|
||||||
|
|
||||||
A search/replace pattern consists of the following elements:
|
|
||||||
|
|
||||||
/search-regexp/replace-string/
|
|
||||||
|
|
||||||
The separator can be any valid character. Especially if you want to
|
|
||||||
use a regexp containing the C</> character, eg:
|
|
||||||
|
|
||||||
|search-regexp|replace-string|
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
cat t/testtable2
|
|
||||||
NAME DURATION
|
|
||||||
x 10
|
|
||||||
a 100
|
|
||||||
z 0
|
|
||||||
u 4
|
|
||||||
k 6
|
|
||||||
|
|
||||||
cat t/testtable2 | tablizer -T2 -R '/^\d/4/' -n
|
|
||||||
NAME DURATION
|
|
||||||
x 40
|
|
||||||
a 400
|
|
||||||
z 4
|
|
||||||
u 4
|
|
||||||
k 4
|
|
||||||
|
|
||||||
=head2 OUTPUT MODES
|
|
||||||
|
|
||||||
There might be cases when the tabular output of a program is way too
|
|
||||||
large for your current terminal but you still need to see every
|
|
||||||
column. In such cases the B<-o extended> or B<-X> option can be
|
|
||||||
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
|
|
||||||
NAME: repldepl-7bcd8d5b64-7zq4l
|
|
||||||
READY: 1/1
|
|
||||||
STATUS: Running
|
|
||||||
RESTARTS: 1 (71m ago)
|
|
||||||
AGE: 5h28m
|
|
||||||
|
|
||||||
You can of course still use a regex to reduce the number of rows
|
|
||||||
displayed.
|
|
||||||
|
|
||||||
The option B<-o shell> can be used if the output has to be processed
|
|
||||||
by the shell, it prints variable assignments for each cell, one line
|
|
||||||
per row:
|
|
||||||
|
|
||||||
kubectl get pods | ./tablizer -o extended ./tablizer -o shell
|
|
||||||
NAME="repldepl-7bcd8d5b64-7zq4l" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
NAME="repldepl-7bcd8d5b64-m48n8" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
NAME="repldepl-7bcd8d5b64-q2bf4" READY="1/1" STATUS="Running" RESTARTS="9 (47m ago)" AGE="4d23h"
|
|
||||||
|
|
||||||
You can use this in an eval loop.
|
|
||||||
|
|
||||||
Beside normal ascii mode (the default) and extended mode there are
|
|
||||||
more output modes available: B<orgtbl> which prints an Emacs org-mode
|
|
||||||
table and B<markdown> which prints a Markdown table, B<yaml>, which
|
|
||||||
prints yaml encoding and CSV mode, which prints a comma separated
|
|
||||||
value file.
|
|
||||||
|
|
||||||
=head2 PUT FIELDS TO CLIPBOARD
|
|
||||||
|
|
||||||
You can let tablizer put fields to the clipboard using the option
|
|
||||||
C<-y>. This best fits the use-case when the result of your filtering
|
|
||||||
yields just one row. For example:
|
|
||||||
|
|
||||||
cloudctl cluster ls | tablizer -yid matchbox
|
|
||||||
|
|
||||||
If "matchbox" matches one cluster, you can immediately use the id of
|
|
||||||
that cluster somewhere else and paste it. Of course, if there are
|
|
||||||
multiple matches, then all id's will be put into the clipboard
|
|
||||||
separated by one space.
|
|
||||||
|
|
||||||
=head2 ENVIRONMENT VARIABLES
|
|
||||||
|
|
||||||
B<tablizer> supports certain environment variables which use can use
|
|
||||||
to influence program behavior. Commandline flags have always
|
|
||||||
precedence over environment variables.
|
|
||||||
|
|
||||||
=over
|
|
||||||
|
|
||||||
=item <T_HEADER_NUMBERING> - enable numbering of header fields, like B<-n>.
|
|
||||||
|
|
||||||
=item <T_COLUMNS> - comma separated list of columns to output, like B<-c>
|
|
||||||
|
|
||||||
=item <NO_COLORS> - disable colorization of matches, like B<-N>
|
|
||||||
|
|
||||||
=back
|
|
||||||
|
|
||||||
=head2 COMPLETION
|
|
||||||
|
|
||||||
Shell completion for command line options can be enabled by using the
|
|
||||||
B<--completion> flag. The required parameter is the name of your
|
|
||||||
shell. Currently supported are: bash, zsh, fish and powershell.
|
|
||||||
|
|
||||||
Detailed instructions:
|
|
||||||
|
|
||||||
=over
|
|
||||||
|
|
||||||
=item Bash:
|
|
||||||
|
|
||||||
source <(tablizer --completion bash)
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
# Linux:
|
|
||||||
$ tablizer --completion bash > /etc/bash_completion.d/tablizer
|
|
||||||
|
|
||||||
# macOS:
|
|
||||||
$ tablizer --completion bash > $(brew --prefix)/etc/bash_completion.d/tablizer
|
|
||||||
|
|
||||||
=item Zsh:
|
|
||||||
|
|
||||||
If shell completion is not already enabled in your environment,
|
|
||||||
you will need to enable it. You can execute the following once:
|
|
||||||
|
|
||||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
$ tablizer --completion zsh > "${fpath[1]}/_tablizer"
|
|
||||||
|
|
||||||
You will need to start a new shell for this setup to take effect.
|
|
||||||
|
|
||||||
=item fish:
|
|
||||||
|
|
||||||
tablizer --completion fish | source
|
|
||||||
|
|
||||||
To load completions for each session, execute once:
|
|
||||||
|
|
||||||
tablizer --completion fish > ~/.config/fish/completions/tablizer.fish
|
|
||||||
|
|
||||||
=item PowerShell:
|
|
||||||
|
|
||||||
tablizer --completion powershell | Out-String | Invoke-Expression
|
|
||||||
|
|
||||||
To load completions for every new session, run:
|
|
||||||
|
|
||||||
tablizer --completion powershell > tablizer.ps1
|
|
||||||
|
|
||||||
and source this file from your PowerShell profile.
|
|
||||||
|
|
||||||
=back
|
|
||||||
|
|
||||||
=head1 CONFIGURATION AND COLORS
|
|
||||||
|
|
||||||
YOu can put certain configuration values into a configuration file in
|
|
||||||
HCL format. By default tablizer looks for
|
|
||||||
C<$HOME/.config/tablizer/config>, but you can provide one using the
|
|
||||||
parameter C<-f>.
|
|
||||||
|
|
||||||
In the configuration the following variables can be defined:
|
|
||||||
|
|
||||||
BG = "lightGreen"
|
|
||||||
FG = "white"
|
|
||||||
HighlightBG = "lightGreen"
|
|
||||||
HighlightFG = "white"
|
|
||||||
NoHighlightBG = "white"
|
|
||||||
NoHighlightFG = "lightGreen"
|
|
||||||
HighlightHdrBG = "red"
|
|
||||||
HighlightHdrFG = "white"
|
|
||||||
|
|
||||||
The following color definitions are available:
|
|
||||||
|
|
||||||
black, blue, cyan, darkGray, default, green, lightBlue, lightCyan,
|
|
||||||
lightGreen, lightMagenta, lightRed, lightWhite, lightYellow,
|
|
||||||
magenta, red, white, yellow
|
|
||||||
|
|
||||||
The Variables B<FG> and B<BG> are being used to highlight matches. The
|
|
||||||
other *FG and *BG variables are for colored table output (enabled with
|
|
||||||
the C<-L> parameter).
|
|
||||||
|
|
||||||
Colorization can be turned off completely either by setting the
|
|
||||||
parameter C<-N> or the environment variable B<NO_COLOR> to a true value.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
=head1 BUGS
|
|
||||||
|
|
||||||
In order to report a bug, unexpected behavior, feature requests
|
|
||||||
or to submit a patch, please open an issue on github:
|
|
||||||
L<https://codeberg.org/scip/tablizer/issues>.
|
|
||||||
|
|
||||||
=head1 LICENSE
|
|
||||||
|
|
||||||
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
|
|
||||||
|
|
||||||
Copyright (c) 2022-2024 by Thomas von Dein
|
|
||||||
|
|
||||||
This software uses the following GO modules:
|
|
||||||
|
|
||||||
=over 4
|
|
||||||
|
|
||||||
=item repr (https://github.com/alecthomas/repr)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 2016 Alec Thomas
|
|
||||||
|
|
||||||
=item cobra (https://github.com/spf13/cobra)
|
|
||||||
|
|
||||||
Released under the Apache 2.0 license, Copyright 2013-2022 The Cobra Authors
|
|
||||||
|
|
||||||
=item dateparse (github.com/araddon/dateparse)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 2015-2017 Aaron Raddon
|
|
||||||
|
|
||||||
=item color (github.com/gookit/color)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 2016 inhere
|
|
||||||
|
|
||||||
=item tablewriter (github.com/olekukonko/tablewriter)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 201 by Oleku Konko
|
|
||||||
|
|
||||||
=item yaml (gopkg.in/yaml.v3)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov
|
|
||||||
|
|
||||||
=item bubble-table (https://github.com/Evertras/bubble-table)
|
|
||||||
|
|
||||||
Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
|
|
||||||
|
|
||||||
=back
|
|
||||||
|
|
||||||
=head1 AUTHORS
|
|
||||||
|
|
||||||
Thomas von Dein B<tom AT vondein DOT org>
|
|
||||||
|
|
||||||
=cut
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.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
BIN
vhsdemo/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 MiB |
@@ -1,157 +0,0 @@
|
|||||||
# -*-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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
while read LINE; do
|
|
||||||
eval "$LINE"; echo "$Customer ordered $Count ${Units}s"
|
|
||||||
done
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
Reference in New Issue
Block a user