Compare commits

...

34 Commits

Author SHA1 Message Date
9dd2a49d9b adapted version generation to cfg module, added and fixed unit tests 2022-10-19 19:32:41 +02:00
90872e0c60 fix linter errors 2022-10-19 12:57:50 +02:00
baac74eb47 yaml fix 2022-10-19 12:51:54 +02:00
360dd28e20 add linter 2022-10-19 12:50:20 +02:00
1e36c148ff get rid of global variables, makes testing way easier 2022-10-19 12:44:19 +02:00
399620de98 Added so we can use struct holding all configuration 2022-10-19 12:43:54 +02:00
5d10875a3f fix pointer bug, mockdata have been overwritten by go test everytime,
now use a const struct via func.
2022-10-18 19:45:09 +02:00
4481f59eda no need to return string when using io.Writer anyway 2022-10-17 23:40:53 +02:00
1b2f51dcaf Changed print funcs to use an io.Writer, reimplemented print tests 2022-10-17 20:04:05 +02:00
0d6de3fe5b add-fixes 2022-10-16 19:48:12 +02:00
ec23ae2e76 fix todo 2022-10-16 19:45:46 +02:00
76930ab45a added yaml output mode support (-o yaml or -Y) 2022-10-16 19:44:26 +02:00
a77e4dbc5a added NumberizeHeaders() unit test 2022-10-16 16:37:47 +02:00
9305f48639 added target to execute a single unit test manually 2022-10-16 16:37:26 +02:00
da276a1b50 replaced github.com/xhit/go-str2duration with my own func + tests 2022-10-16 15:30:34 +02:00
dfd3ab9b77 fixed version generation 2022-10-15 19:46:03 +02:00
d53b32b95e updated Changelog 2022-10-15 19:37:30 +02:00
3edbd53ef8 bump version 2022-10-15 19:33:07 +02:00
9c49b78593 added unit test + docs for the various sort modes. 2022-10-15 19:31:42 +02:00
ca87c339b0 added support to sort by time, duration, numerical 2022-10-15 17:05:15 +02:00
fd74628259 added unit test for descending order, fixed deduplication bug 2022-10-15 16:27:04 +02:00
839f33a7fc unit test missing 2022-10-15 14:25:38 +02:00
ebd391df63 added -D to alter sort order to descending order (default: ascending) 2022-10-15 14:24:43 +02:00
752406815c catch incomplete rows and fill them with empty string[s] 2022-10-15 14:15:36 +02:00
4ec6ccd0fd added support for regexp in -c parameter, added deduplication as well 2022-10-15 14:03:30 +02:00
aef545d51e added open todo items I still had on the list 2022-10-15 14:02:46 +02:00
3249e1719f some rewording of the NCOC and turn TODO into a md file 2022-10-15 14:02:09 +02:00
f830cc6256 updated 2022-10-14 19:56:55 +02:00
7e01d54b08 added. 2022-10-14 19:52:02 +02:00
487ba6253d Introduced changelog. 2022-10-14 19:51:45 +02:00
745d15b459 Made corrections to satisfy linter. 2022-10-14 19:51:19 +02:00
8e2ba58ddb added -k parameter to sort by columns 2022-10-13 18:56:34 +02:00
6eedb60a6a minor update to current state 2022-10-11 18:50:58 +02:00
81fac864f1 using gh for release generation, fixed mkrel.sh to add version 2022-10-11 18:42:10 +02:00
27 changed files with 1774 additions and 380 deletions

View File

@@ -23,3 +23,36 @@ jobs:
- name: test - name: test
run: make test run: make test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.17
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
#with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
# version: v1.29
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: golangci-lint command line arguments.
# args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
# Optional: if set to true then the all caching functionality will be complete disabled,
# takes precedence over all other caching options.
# skip-cache: true
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
# skip-pkg-cache: true
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
# skip-build-cache: true

205
CHANGELOG.md Normal file
View File

@@ -0,0 +1,205 @@
# 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.11](https://github.com/TLINDEN/tablizer/tree/v1.0.11) - 2022-10-19
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.10...v1.0.11)
### Added
- Added CI job golinter to regularly check for common mistakes.
- Added YAML output mode.
- Added more unit tests, we're over 95% in the lib module.
### Changed
- do not use any global variables anymore, makes the code easier to
maintain, understand and test
- using io.Writer in print* functions, which is easier to test, also
re-implemented the print tests.
- replaced go-str2duration with my own implementation `duration2int()`.
## [v1.0.10](https://github.com/TLINDEN/tablizer/tree/v1.0.10) - 2022-10-15
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.9...v1.0.10)
### Added
- Added various sort modes: sort by time, by duration, numerical (-a -t -i)
- Added possibility to modify sort order to descending (-D)
- Added support to specify a regexp in column selector -c, which can
also be mixed with numerical column spec
- More unit tests
### Fixed
- Column specification allowed to specify duplicate columns like `-c
1,2,1,2` unchecked. Now this list will be deduplicated before use.
## [v1.0.9](https://github.com/TLINDEN/tablizer/tree/v1.0.9) - 2022-10-14
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.8...v1.0.9)
### Added
- Added Changelog, Contribution guidelines and no COC.
### Changed
- some minor changes to satisfy linter.
## [v1.0.8](https://github.com/TLINDEN/tablizer/tree/v1.0.8) - 2022-10-13
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.7...v1.0.8)
### Added
- Added sort support with the new parameter -k (like sort(1)).
## [v1.0.7](https://github.com/TLINDEN/tablizer/tree/v1.0.7) - 2022-10-11
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.6...v1.0.7)
### Added
- Added pattern highlighting support.
- Added more unit tests.
### Fixed
- Fixed extended more output in combination with -c.
- Fixed issue #4, the version string was missing.
## [v1.0.6](https://github.com/TLINDEN/tablizer/tree/v1.0.6) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.5...v1.0.6)
### Added
- Added documentation about regexp syntax in the manpage.
- Added more unit tests.
### Changed
- Rewrote the input parser.
- Some more refactoring work has been done.
## [v1.0.5](https://github.com/TLINDEN/tablizer/tree/v1.0.5) - 2022-10-05
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.4...v1.0.5)
### Added
- A new option has been added: --invert-match -v which behaves like
the same option in grep(1): it inverts the pattern match.
- A few more unit tests have been added.
### Fixed
- Pattern matching did not work, because the (new) help subcommand
lead to cobra taking care of the first arg to the program
(argv[1]). So now there's a new parameter -m which displays the
manpage and no more subcommands.
## [v1.0.4](https://github.com/TLINDEN/tablizer/tree/v1.0.4) - 2022-10-04
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.3...v1.0.4)
### Added
- Development version of the compiled binary now uses git vars
in addition to program version.
- Added an option to display the manual page (compiled in) as text:
--help, for cases where a user just installed the binary.
### Changed
- Fixed go module namespace.
## [v1.0.3](https://github.com/TLINDEN/tablizer/tree/v1.0.3) - 2022-10-03
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.2...v1.0.3)
### Added
- Added a new output mode: shell mode, which allows the user
to use the output in a shell eval loop to further process
the data.
### Changed
- More refactoring work has been done.
## [v1.0.2](https://github.com/TLINDEN/tablizer/tree/v1.0.2) - 2022-10-02
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.1...v1.0.2)
### Added
- Added some basic unit tests.
### Changed
- Code has been refactored to be more efficient.
- Replaced table generation code with Tablewriter.
## [v1.0.1](https://github.com/TLINDEN/tablizer/tree/v1.0.1) - 2022-09-30
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/v1.0.0...v1.0.1)
### Added
- Added a unix manual page.
- Added release builder to Makefile
### Changed
- Various minor fixes.
## [v1.0.0](https://github.com/TLINDEN/tablizer/tree/v1.0.0) - 2022-09-28
[Full Changelog](https://github.com/TLINDEN/tablizer/compare/02a64a5c3fe4220df2c791ff1421d16ebd428c19...v1.0.0)
Initial release.

113
CODE_OF_CONDUCT.md Normal file
View File

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

94
CONTRIBUTING.md Normal file
View File

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

View File

@@ -18,12 +18,12 @@
# #
# no need to modify anything below # no need to modify anything below
tool = tablizer tool = tablizer
version = $(shell egrep "= .v" lib/common.go | cut -d'=' -f2 | cut -d'"' -f 2) version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local PREFIX = /usr/local
UID = root UID = root
GID = 0 GID = 0
BRANCH = $(shell git describe --all | cut -d/ -f2) BRANCH = $(shell git branch --show-current)
COMMIT = $(shell git rev-parse --short=8 HEAD) COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S) BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION:= $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version)) VERSION:= $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
@@ -36,15 +36,17 @@ all: $(tool).1 cmd/$(tool).go buildlocal
cmd/%.go: %.pod cmd/%.go: %.pod
echo "package cmd" > cmd/$*.go echo "package cmd" > cmd/$*.go
echo >> cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go echo "var manpage = \`" >> cmd/$*.go
pod2text $*.pod >> cmd/$*.go pod2text $*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go echo "\`" >> cmd/$*.go
buildlocal: buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/lib.VERSION=$(VERSION)'" go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"
release: release:
./mkrel.sh $(tool) $(version) ./mkrel.sh $(tool) $(version)
gh release create $(version) --generate-notes releases/*
install: buildlocal install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin install -d -o $(UID) -g $(GID) $(PREFIX)/bin
@@ -53,7 +55,15 @@ install: buildlocal
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean: clean:
rm -rf $(tool) releases rm -rf $(tool) releases coverage.out
test: test:
go test -v ./... go test -v ./...
singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib"
go test -run $(TEST) github.com/tlinden/tablizer/$(MOD)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out

View File

@@ -1,5 +1,6 @@
[![Actions](https://github.com/tlinden/tablizer/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/tablizer/actions) [![Actions](https://github.com/tlinden/tablizer/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/tablizer/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/tablizer/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/tablizer/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/tablizer)](https://goreportcard.com/report/github.com/tlinden/tablizer)
## tablizer - Manipulate tabular output of other programs ## tablizer - Manipulate tabular output of other programs
@@ -20,13 +21,13 @@ But you're only interested in the NAME and STATUS columns. Here's how
to do this with tablizer: to do this with tablizer:
``` ```
% kubectl get pods | ./tablizer % kubectl get pods | tablizer
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-7zq4l 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-m48n8 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-m48n8 1/1 Running 1 (69m ago) 5h26m
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
% kubectl get pods | ./tablizer -c 1,3 % kubectl get pods | tablizer -c 1,3
NAME(1) STATUS(3) NAME(1) STATUS(3)
repldepl-7bcd8d5b64-7zq4l Running repldepl-7bcd8d5b64-7zq4l Running
repldepl-7bcd8d5b64-m48n8 Running repldepl-7bcd8d5b64-m48n8 Running
@@ -34,27 +35,28 @@ repldepl-7bcd8d5b64-q2bf4 Running
``` ```
Another use case is when the tabular output is so wide that lines are Another use case is when the tabular output is so wide that lines are
being broken and the whole output is completely distorted. In such a being broken and the whole output is completely distorted. In such a
case you can use the `-x` flag to get an output similar to `\x` in `psql`: case you can use the `-o extended | -X` flag to get an output similar
to `\x` in `psql`:
``` ```
% kubectl get pods | ./tablizer -x % kubectl get pods | tablizer -X
NAME: repldepl-7bcd8d5b64-7zq4l NAME: repldepl-7bcd8d5b64-7zq4l
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
NAME: repldepl-7bcd8d5b64-m48n8 NAME: repldepl-7bcd8d5b64-m48n8
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
NAME: repldepl-7bcd8d5b64-q2bf4 NAME: repldepl-7bcd8d5b64-q2bf4
READY: 1/1 READY: 1/1
STATUS: Running STATUS: Running
RESTARTS: 1 (71m ago) RESTARTS: 1 (71m ago)
AGE: 5h28m AGE: 5h28m
``` ```
@@ -63,11 +65,12 @@ Tablize can read one or more files or - if none specified - from STDIN.
You can also specify a regex pattern to reduce the output: You can also specify a regex pattern to reduce the output:
``` ```
% kubectl get pods | ./tablizer q2bf4 % kubectl get pods | tablizer q2bf4
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
``` ```
There are more output modes like org-mode (orgtbl) and markdown.
## Installation ## Installation
@@ -106,6 +109,10 @@ https://github.com/TLINDEN/tablizer/blob/main/tablizer.pod
Or if you cloned the repository you can read it this way (perl needs Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc tablizer.pod`. to be installed though): `perldoc tablizer.pod`.
If you have the binary installed, you can also read the man page with
this command:
tablizer --man
## Getting help ## Getting help

1
TODO
View File

@@ -1 +0,0 @@

15
TODO.md Normal file
View File

@@ -0,0 +1,15 @@
## Fixes to be implemented
- rm printYamlData() log.Fatal(), maybe return error on all printers?
- printShellData() checks Columns unnecessarily (compare to yaml or extended)
## Features to be implemented
- add output mode csv
- add --no-headers option
- add input parsing support for CSV including unquoting of stuff
like: `"xxx","1919 b"` etc, maybe an extra option for unquoting

144
cfg/config.go Normal file
View File

@@ -0,0 +1,144 @@
/*
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 (
"errors"
"fmt"
"github.com/gookit/color"
"regexp"
)
const DefaultSeparator string = `(\s\s+|\t)`
const ValidOutputModes string = "(orgtbl|markdown|extended|ascii|yaml|shell)"
const Version string = "v1.0.11"
var VERSION string // maintained by -x
type Config struct {
Debug bool
NoNumbering bool
Columns string
UseColumns []int
Separator string
OutputMode string
InvertMatch bool
Pattern string
SortMode string
SortDescending bool
SortByColumn int
/*
FIXME: make configurable somehow, config file or ENV
see https://github.com/gookit/color will be set by
io.ProcessFiles() according to currently supported
color mode.
*/
MatchFG string
MatchBG string
NoColor bool
}
// 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
}
// various sort types
type Sortmode struct {
Numeric bool
Time bool
Age bool
}
func Colors() map[color.Level]map[string]string {
// default color schemes
return map[color.Level]map[string]string{
color.Level16: {
"bg": "green", "fg": "black",
},
color.Level256: {
"bg": "lightGreen", "fg": "black",
},
color.LevelRgb: {
// FIXME: maybe use something nicer
"bg": "lightGreen", "fg": "black",
},
}
}
func Getversion() string {
// main program version
// generated version string, used by -v contains lib.Version on
// main branch, and lib.Version-$branch-$lastcommit-$date on
// development branch
return fmt.Sprintf("This is tablizer version %s", VERSION)
}
func (conf *Config) PrepareModeFlags(flag Modeflag, mode string) error {
if len(mode) == 0 {
// associate short flags like -X with mode selector
switch {
case flag.X:
conf.OutputMode = "extended"
case flag.M:
conf.OutputMode = "markdown"
case flag.O:
conf.OutputMode = "orgtbl"
case flag.S:
conf.OutputMode = "shell"
conf.NoNumbering = true
case flag.Y:
conf.OutputMode = "yaml"
conf.NoNumbering = true
default:
conf.OutputMode = "ascii"
}
} else {
r, _ := regexp.Compile(ValidOutputModes) // hardcoded, no fail expected
match := r.MatchString(mode)
if !match {
return errors.New("Invalid output mode!")
}
conf.OutputMode = mode
}
return nil
}
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"
}
}

98
cfg/config_test.go Normal file
View File

@@ -0,0 +1,98 @@
/*
Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cfg
import (
"fmt"
// "reflect"
"testing"
)
func TestPrepareModeFlags(t *testing.T) {
var tests = []struct {
flag Modeflag
mode string // input, if any
expect string // output
want bool
}{
// short commandline flags like -M
{Modeflag{X: true}, "", "extended", false},
{Modeflag{S: true}, "", "shell", false},
{Modeflag{O: true}, "", "orgtbl", false},
{Modeflag{Y: true}, "", "yaml", false},
{Modeflag{M: true}, "", "markdown", false},
{Modeflag{}, "", "ascii", false},
// long flags like -o yaml
{Modeflag{}, "extended", "extended", false},
{Modeflag{}, "shell", "shell", false},
{Modeflag{}, "orgtbl", "orgtbl", false},
{Modeflag{}, "yaml", "yaml", false},
{Modeflag{}, "markdown", "markdown", false},
// failing
{Modeflag{}, "blah", "", true},
}
for _, tt := range tests {
testname := fmt.Sprintf("PrepareModeFlags-flags-mode-%s-expect-%s-want-%t",
tt.mode, tt.expect, tt.want)
t.Run(testname, func(t *testing.T) {
c := Config{OutputMode: tt.mode}
// check either flag or pre filled mode, whatever is defined in tt
err := c.PrepareModeFlags(tt.flag, tt.mode)
if err != nil {
if !tt.want {
// expect to fail
t.Fatalf("PrepareModeFlags returned unexpected error: %s", err)
}
} else {
if c.OutputMode != tt.expect {
t.Errorf("got: %s, expect: %s", c.OutputMode, tt.expect)
}
}
})
}
}
func TestPrepareSortFlags(t *testing.T) {
var tests = []struct {
flag Sortmode
expect string // output
}{
// short commandline flags like -M
{Sortmode{Numeric: true}, "numeric"},
{Sortmode{Age: true}, "duration"},
{Sortmode{Time: true}, "time"},
{Sortmode{}, "string"},
}
for _, tt := range tests {
testname := fmt.Sprintf("PrepareSortFlags-expect-%s", tt.expect)
t.Run(testname, func(t *testing.T) {
c := Config{}
c.PrepareSortFlags(tt.flag)
if c.SortMode != tt.expect {
t.Errorf("got: %s, expect: %s", c.SortMode, tt.expect)
}
})
}
}

View File

@@ -20,14 +20,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/tablizer/cfg"
"github.com/tlinden/tablizer/lib" "github.com/tlinden/tablizer/lib"
"log" "log"
"os" "os"
"os/exec" "os/exec"
) )
var ShowManual = false
func man() { func man() {
man := exec.Command("less", "-") man := exec.Command("less", "-")
@@ -45,63 +44,79 @@ func man() {
} }
} }
var rootCmd = &cobra.Command{
Use: "tablizer [regex] [file, ...]",
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
RunE: func(cmd *cobra.Command, args []string) error {
if lib.ShowVersion {
fmt.Printf("This is tablizer version %s\n", lib.VERSION)
return nil
}
if ShowManual {
man()
return nil
}
err := lib.PrepareColumns()
if err != nil {
return err
}
err = lib.PrepareModeFlags()
if err != nil {
return err
}
return lib.ProcessFiles(args)
},
}
func Execute() { func Execute() {
var (
conf cfg.Config
ShowManual bool
Outputmode string
ShowVersion bool
modeflag cfg.Modeflag
sortmode cfg.Sortmode
)
var rootCmd = &cobra.Command{
Use: "tablizer [regex] [file, ...]",
Short: "[Re-]tabularize tabular data",
Long: `Manipulate tabular output of other programs`,
RunE: func(cmd *cobra.Command, args []string) error {
if ShowVersion {
fmt.Println(cfg.Getversion())
return nil
}
if ShowManual {
man()
return nil
}
// prepare flags
err := conf.PrepareModeFlags(modeflag, Outputmode)
if err != nil {
return err
}
conf.PrepareSortFlags(sortmode)
// actual execution starts here
return lib.ProcessFiles(conf, args)
},
}
// options
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&conf.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().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator, "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
// sort options
rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)")
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")
// output flags, only 1 allowed, hidden, since just short cuts
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.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml")
_ = rootCmd.Flags().MarkHidden("extended")
_ = rootCmd.Flags().MarkHidden("orgtbl")
_ = rootCmd.Flags().MarkHidden("markdown")
_ = rootCmd.Flags().MarkHidden("shell")
_ = rootCmd.Flags().MarkHidden("yaml")
// same thing but more common, takes precedence over above group
rootCmd.PersistentFlags().StringVarP(&Outputmode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)")
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
} }
func init() {
rootCmd.PersistentFlags().BoolVarP(&lib.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&lib.NoNumbering, "no-numbering", "n", false, "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&lib.NoColor, "no-color", "N", false, "Disable pattern highlighting")
rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "V", false, "Print program version")
rootCmd.PersistentFlags().BoolVarP(&lib.InvertMatch, "invert-match", "v", false, "select non-matching rows")
rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page")
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", lib.DefaultSeparator, "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)")
// output flags, only 1 allowed, hidden, since just short cuts
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagShell, "shell", "S", false, "Enable shell mode output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell")
rootCmd.Flags().MarkHidden("extended")
rootCmd.Flags().MarkHidden("orgtbl")
rootCmd.Flags().MarkHidden("markdown")
rootCmd.Flags().MarkHidden("shell")
// same thing but more common, takes precedence over above group
rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)")
}

View File

@@ -1,4 +1,5 @@
package cmd package cmd
var manpage = ` var manpage = `
NAME NAME
tablizer - Manipulate tabular output of other programs tablizer - Manipulate tabular output of other programs
@@ -15,11 +16,16 @@ SYNOPSIS
-m, --man Display manual page -m, --man Display manual page
-n, --no-numbering Disable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default) -o, --output string Output mode - one of: orgtbl, markdown, extended, yaml, ascii(default)
-X, --extended Enable extended output -X, --extended Enable extended output
-M, --markdown Enable markdown table output -M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator -s, --separator string Custom field separator
-a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-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
-v, --version Print program version -v, --version Print program version
DESCRIPTION DESCRIPTION
@@ -36,7 +42,7 @@ DESCRIPTION
You can use tablizer to do these and more things. You can use tablizer to do these and more things.
tablizer analyses the header fiels of a table, registers the column tablizer analyses the header fields of a table, registers the column
positions of each header field and separates columns by those positions. positions of each header field and separates columns by those positions.
Without any options it reads its input from "STDIN", but you can also Without any options it reads its input from "STDIN", but you can also
@@ -62,7 +68,7 @@ DESCRIPTION
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which These numbers denote the column and you can use them to specify which
columns you want to have in your output: columns you want to have in your output (see COLUMNS:
kubectl get pods | tablizer -c1,3 kubectl get pods | tablizer -c1,3
@@ -74,7 +80,22 @@ DESCRIPTION
By default, if a pattern has been speficied, matches will be By default, if a pattern has been speficied, matches will be
highlighted. You can disable this behavior with the -N option. highlighted. You can disable this behavior with the -N option.
Finally the -d option enables debugging output which is mostly usefull Use the -k option to specify by which column to sort the tabular data
(as in GNU sort(1)). The default sort column is the first one. To
disable sorting at all, supply 0 (Zero) to -k. The default sort order is
ascending. You can change this to descending order using the option -D.
The default sort order is by string, but there are other sort modes:
-a --sort-age
Sorts duration strings like "1d4h32m51s".
-i --sort-numeric
Sorts numeric fields.
-t --sort-time
Sorts timestamps.
Finally the -d option enables debugging output which is mostly useful
for the developer. for the developer.
PATTERNS PATTERNS
@@ -97,14 +118,40 @@ DESCRIPTION
"i" ignore case "m" multiline mode "s" single line mode "i" ignore case "m" multiline mode "s" single line mode
Example for a case insensitve search: Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account" kubectl get pods -A | tablizer "(?i)account"
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.
OUTPUT MODES OUTPUT MODES
There might be cases when the tabular output of a program is way too There might be cases when the tabular output of a program is way too
large for your current terminal but you still need to see every column. large for your current terminal but you still need to see every column.
In such cases the -o extended or -X option can be usefull which enables 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, *extended mode*. In this mode, each row will be printed vertically,
header left, value right, aligned by the field widths. Here's an header left, value right, aligned by the field widths. Here's an
example: example:
@@ -131,7 +178,8 @@ DESCRIPTION
Beside normal ascii mode (the default) and extended mode there are more Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table. markdown which prints a Markdown table and yaml, which prints yaml
encoding.
BUGS BUGS
In order to report a bug, unexpected behavior, feature requests or to In order to report a bug, unexpected behavior, feature requests or to

7
go.mod
View File

@@ -4,15 +4,18 @@ go 1.18
require ( require (
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/gookit/color v1.5.2 github.com/gookit/color v1.5.2
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
) )

10
go.sum
View File

@@ -1,5 +1,7 @@
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -8,19 +10,24 @@ github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -28,6 +35,7 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -17,61 +17,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( // contains a whole parsed table
"github.com/gookit/color" type Tabdata struct {
//"github.com/xo/terminfo" maxwidthHeader int // longest header
) maxwidthPerCol []int // max width per column
columns int // count
var ( headers []string // [ "ID", "NAME", ...]
// command line flags entries [][]string
Debug bool }
XtendedOut bool
NoNumbering bool
ShowVersion bool
Columns string
UseColumns []int
DefaultSeparator string = `(\s\s+|\t)`
Separator string = `(\s\s+|\t)`
OutflagExtended bool
OutflagMarkdown bool
OutflagOrgtable bool
OutflagShell bool
OutputMode string
InvertMatch bool
Pattern string
/*
FIXME: make configurable somehow, config file or ENV
see https://github.com/gookit/color will be set by
io.ProcessFiles() according to currently supported
color mode.
*/
MatchFG string
MatchBG string
NoColor bool
// colors to be used per supported color mode
Colors = map[color.Level]map[string]string{
color.Level16: map[string]string{
"bg": "green", "fg": "black",
},
color.Level256: map[string]string{
"bg": "lightGreen", "fg": "black",
},
color.LevelRgb: map[string]string{
// FIXME: maybe use something nicer
"bg": "lightGreen", "fg": "black",
},
}
// used for validation
validOutputmodes = "(orgtbl|markdown|extended|ascii)"
// main program version
Version = "v1.0.7"
// generated version string, used by -v contains lib.Version on
// main branch, and lib.Version-$branch-$lastcommit-$date on
// development branch
VERSION string
)

View File

@@ -21,8 +21,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/tlinden/tablizer/cfg"
"os" "os"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
) )
@@ -36,35 +38,72 @@ func contains(s []int, e int) bool {
return false return false
} }
func PrepareColumns() error { // parse columns list given with -c, modifies config.UseColumns based
if len(Columns) > 0 { // on eventually given regex
for _, use := range strings.Split(Columns, ",") { func PrepareColumns(c *cfg.Config, data *Tabdata) error {
usenum, err := strconv.Atoi(use) if len(c.Columns) > 0 {
if err != nil { for _, use := range strings.Split(c.Columns, ",") {
msg := fmt.Sprintf("Could not parse columns list %s: %v", Columns, err) if len(use) == 0 {
msg := fmt.Sprintf("Could not parse columns list %s: empty column", c.Columns)
return errors.New(msg) return errors.New(msg)
} }
UseColumns = append(UseColumns, usenum)
usenum, err := strconv.Atoi(use)
if err != nil {
// might be a regexp
colPattern, err := regexp.Compile(use)
if err != nil {
msg := fmt.Sprintf("Could not parse columns list %s: %v", c.Columns, err)
return errors.New(msg)
}
// find matching header fields
for i, head := range data.headers {
if colPattern.MatchString(head) {
c.UseColumns = append(c.UseColumns, i+1)
}
}
} else {
// we digress from go best practises here, because if
// a colum spec is not a number, we process them above
// inside the err handler for atoi(). so only add the
// number, if it's really just a number.
c.UseColumns = append(c.UseColumns, usenum)
}
} }
// deduplicate: put all values into a map (value gets map key)
// thereby removing duplicates, extract keys into new slice
// and sort it
imap := make(map[int]int, len(c.UseColumns))
for _, i := range c.UseColumns {
imap[i] = 0
}
c.UseColumns = nil
for k := range imap {
c.UseColumns = append(c.UseColumns, k)
}
sort.Ints(c.UseColumns)
} }
return nil return nil
} }
func numberizeHeaders(data *Tabdata) { // prepare headers: add numbers to headers
// prepare headers: add numbers to headers func numberizeAndReduceHeaders(c cfg.Config, data *Tabdata) {
numberedHeaders := []string{} numberedHeaders := []string{}
maxwidth := 0 // start from scratch, so we only look at displayed column widths maxwidth := 0 // start from scratch, so we only look at displayed column widths
for i, head := range data.headers { for i, head := range data.headers {
headlen := 0 headlen := 0
if len(Columns) > 0 { if len(c.Columns) > 0 {
// -c specified // -c specified
if !contains(UseColumns, i+1) { if !contains(c.UseColumns, i+1) {
// ignore this one // ignore this one
continue continue
} }
} }
if NoNumbering { if c.NoNumbering {
numberedHeaders = append(numberedHeaders, head) numberedHeaders = append(numberedHeaders, head)
headlen = len(head) headlen = len(head)
} else { } else {
@@ -83,15 +122,15 @@ func numberizeHeaders(data *Tabdata) {
} }
} }
func reduceColumns(data *Tabdata) { // exclude columns, if any
// exclude columns, if any func reduceColumns(c cfg.Config, data *Tabdata) {
if len(Columns) > 0 { if len(c.Columns) > 0 {
reducedEntries := [][]string{} reducedEntries := [][]string{}
reducedEntry := []string{} var reducedEntry []string
for _, entry := range data.entries { for _, entry := range data.entries {
reducedEntry = nil reducedEntry = nil
for i, value := range entry { for i, value := range entry {
if !contains(UseColumns, i+1) { if !contains(c.UseColumns, i+1) {
continue continue
} }
@@ -103,39 +142,6 @@ func reduceColumns(data *Tabdata) {
} }
} }
func PrepareModeFlags() error {
if len(OutputMode) == 0 {
// associate short flags like -X with mode selector
switch {
case OutflagExtended:
OutputMode = "extended"
case OutflagMarkdown:
OutputMode = "markdown"
case OutflagOrgtable:
OutputMode = "orgtbl"
case OutflagShell:
OutputMode = "shell"
NoNumbering = true
default:
OutputMode = "ascii"
}
} else {
r, err := regexp.Compile(validOutputmodes)
if err != nil {
return errors.New("Failed to validate output mode spec!")
}
match := r.MatchString(OutputMode)
if !match {
return errors.New("Invalid output mode!")
}
}
return nil
}
func trimRow(row []string) []string { func trimRow(row []string) []string {
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()! // FIXME: remove this when we only use Tablewriter and strip in ParseFile()!
var fixedrow []string var fixedrow []string
@@ -146,10 +152,10 @@ func trimRow(row []string) []string {
return fixedrow return fixedrow
} }
func colorizeData(output string) string { func colorizeData(c cfg.Config, output string) string {
if len(Pattern) > 0 && !NoColor && color.IsConsole(os.Stdout) { if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) {
r := regexp.MustCompile("(" + Pattern + ")") r := regexp.MustCompile("(" + c.Pattern + ")")
return r.ReplaceAllString(output, "<bg="+MatchBG+";fg="+MatchFG+">$1</>") return r.ReplaceAllString(output, "<bg="+c.MatchBG+";fg="+c.MatchFG+">$1</>")
} else { } else {
return output return output
} }

View File

@@ -19,11 +19,12 @@ package lib
import ( import (
"fmt" "fmt"
"github.com/tlinden/tablizer/cfg"
"reflect" "reflect"
"testing" "testing"
) )
func Testcontains(t *testing.T) { func TestContains(t *testing.T) {
var tests = []struct { var tests = []struct {
list []int list []int
search int search int
@@ -45,6 +46,24 @@ func Testcontains(t *testing.T) {
} }
func TestPrepareColumns(t *testing.T) { func TestPrepareColumns(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"2", "3", "4",
},
},
}
var tests = []struct { var tests = []struct {
input string input string
exp []int exp []int
@@ -52,21 +71,23 @@ func TestPrepareColumns(t *testing.T) {
}{ }{
{"1,2,3", []int{1, 2, 3}, false}, {"1,2,3", []int{1, 2, 3}, false},
{"1,2,", []int{}, true}, {"1,2,", []int{}, true},
{"a,b", []int{}, true}, {"T", []int{2, 3}, false},
{"T,2,3", []int{2, 3}, false},
{"[a-z,4,5", []int{4, 5}, true}, // invalid regexp
} }
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror) testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
Columns = tt.input c := cfg.Config{Columns: tt.input}
err := PrepareColumns() err := PrepareColumns(&c, &data)
if err != nil { if err != nil {
if !tt.wanterror { if !tt.wanterror {
t.Errorf("got error: %v", err) t.Errorf("got error: %v", err)
} }
} else { } else {
if !reflect.DeepEqual(UseColumns, tt.exp) { if !reflect.DeepEqual(c.UseColumns, tt.exp) {
t.Errorf("got: %v, expected: %v", UseColumns, tt.exp) t.Errorf("got: %v, expected: %v", c.UseColumns, tt.exp)
} }
} }
}) })
@@ -79,15 +100,15 @@ func TestReduceColumns(t *testing.T) {
columns []int columns []int
}{ }{
{ {
expect: [][]string{[]string{"a", "b"}}, expect: [][]string{{"a", "b"}},
columns: []int{1, 2}, columns: []int{1, 2},
}, },
{ {
expect: [][]string{[]string{"a", "c"}}, expect: [][]string{{"a", "c"}},
columns: []int{1, 3}, columns: []int{1, 3},
}, },
{ {
expect: [][]string{[]string{"a"}}, expect: [][]string{{"a"}},
columns: []int{1}, columns: []int{1},
}, },
{ {
@@ -96,22 +117,46 @@ func TestReduceColumns(t *testing.T) {
}, },
} }
input := [][]string{[]string{"a", "b", "c"}} input := [][]string{{"a", "b", "c"}}
Columns = "y" // used as a flag with len(Columns)...
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns) testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
UseColumns = tt.columns c := cfg.Config{Columns: "x", UseColumns: tt.columns}
data := Tabdata{entries: input} data := Tabdata{entries: input}
reduceColumns(&data) reduceColumns(c, &data)
if !reflect.DeepEqual(data.entries, tt.expect) { if !reflect.DeepEqual(data.entries, tt.expect) {
t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect) t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect)
} }
}) })
} }
}
Columns = "" // reset for other tests
UseColumns = nil func TestNumberizeHeaders(t *testing.T) {
data := Tabdata{
headers: []string{"ONE", "TWO", "THREE"},
}
var tests = []struct {
expect []string
columns []int
nonum bool
}{
{[]string{"ONE(1)", "TWO(2)", "THREE(3)"}, []int{1, 2, 3}, false},
{[]string{"ONE(1)", "TWO(2)"}, []int{1, 2}, false},
{[]string{"ONE", "TWO"}, []int{1, 2}, true},
}
for _, tt := range tests {
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", tt.columns, tt.nonum)
t.Run(testname, func(t *testing.T) {
c := cfg.Config{Columns: "x", UseColumns: tt.columns, NoNumbering: tt.nonum}
usedata := data
numberizeAndReduceHeaders(c, &usedata)
if !reflect.DeepEqual(usedata.headers, tt.expect) {
t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v",
usedata.headers, tt.expect)
}
})
}
} }

View File

@@ -20,37 +20,50 @@ package lib
import ( import (
"errors" "errors"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/tlinden/tablizer/cfg"
"io" "io"
"os" "os"
) )
func ProcessFiles(args []string) error { func ProcessFiles(c cfg.Config, args []string) error {
fds, pattern, err := determineIO(args) fds, pattern, err := determineIO(&c, args)
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
MatchFG = Colors[level]["fg"]
MatchBG = Colors[level]["bg"]
}
if err != nil { if err != nil {
return err return err
} }
determineColormode(&c)
for _, fd := range fds { for _, fd := range fds {
data, err := parseFile(fd, pattern) data, err := parseFile(c, fd, pattern)
if err != nil { if err != nil {
return err return err
} }
printData(&data)
err = PrepareColumns(&c, &data)
if err != nil {
return err
}
printData(os.Stdout, c, &data)
} }
return nil return nil
} }
func determineIO(args []string) ([]io.Reader, string, error) { // find supported color mode, modifies config based on constants
func determineColormode(c *cfg.Config) {
if !isTerminal(os.Stdout) {
color.Disable()
} else {
level := color.TermColorLevel()
colors := cfg.Colors()
c.MatchFG = colors[level]["fg"]
c.MatchBG = colors[level]["bg"]
}
}
func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) {
var pattern string var pattern string
var fds []io.Reader var fds []io.Reader
var havefiles bool var havefiles bool
@@ -61,7 +74,7 @@ func determineIO(args []string) ([]io.Reader, string, error) {
// first one is not a file, consider it as regexp and // first one is not a file, consider it as regexp and
// shift arg list // shift arg list
pattern = args[0] pattern = args[0]
Pattern = args[0] // FIXME c.Pattern = args[0] // used for colorization by printData()
args = args[1:] args = args[1:]
} }

View File

@@ -22,30 +22,22 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/alecthomas/repr" "github.com/alecthomas/repr"
"github.com/tlinden/tablizer/cfg"
"io" "io"
"regexp" "regexp"
"strings" "strings"
) )
// contains a whole parsed table
type Tabdata struct {
maxwidthHeader int // longest header
maxwidthPerCol []int // max width per column
columns int // count
headers []string // [ "ID", "NAME", ...]
entries [][]string
}
/* /*
Parse tabular input. Parse tabular input.
*/ */
func parseFile(input io.Reader, pattern string) (Tabdata, error) { func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
data := Tabdata{} data := Tabdata{}
var scanner *bufio.Scanner var scanner *bufio.Scanner
hadFirst := false hadFirst := false
separate := regexp.MustCompile(Separator) separate := regexp.MustCompile(c.Separator)
patternR, err := regexp.Compile(pattern) patternR, err := regexp.Compile(pattern)
if err != nil { if err != nil {
return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err)) return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err))
@@ -85,7 +77,7 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) {
} else { } else {
// data processing // data processing
if len(pattern) > 0 { if len(pattern) > 0 {
if patternR.MatchString(line) == InvertMatch { if patternR.MatchString(line) == c.InvertMatch {
// by default -v is false, so if a line does NOT // by default -v is false, so if a line does NOT
// match the pattern, we will ignore it. However, // match the pattern, we will ignore it. However,
// if the user specified -v, the matching is inverted, // if the user specified -v, the matching is inverted,
@@ -114,6 +106,12 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) {
idx++ idx++
} }
// fill up missing fields, if any
for i := len(values); i < len(data.headers); i++ {
values = append(values, "")
}
data.entries = append(data.entries, values) data.entries = append(data.entries, values)
} }
} }
@@ -122,7 +120,7 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) {
return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err())) return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err()))
} }
if Debug { if c.Debug {
repr.Print(data) repr.Print(data)
} }

View File

@@ -19,6 +19,7 @@ package lib
import ( import (
"fmt" "fmt"
"github.com/tlinden/tablizer/cfg"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -35,10 +36,10 @@ func TestParser(t *testing.T) {
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",
}, },
entries: [][]string{ entries: [][]string{
[]string{ {
"asd", "igig", "cxxxncnc", "asd", "igig", "cxxxncnc",
}, },
[]string{ {
"19191", "EDD 1", "X", "19191", "EDD 1", "X",
}, },
}, },
@@ -49,15 +50,15 @@ asd igig cxxxncnc
19191 EDD 1 X` 19191 EDD 1 X`
readFd := strings.NewReader(table) readFd := strings.NewReader(table)
gotdata, err := parseFile(readFd, "") c := cfg.Config{Separator: cfg.DefaultSeparator}
Separator = DefaultSeparator gotdata, err := parseFile(c, readFd, "")
if err != nil { if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
} }
if !reflect.DeepEqual(data, gotdata) { if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", Separator, data, gotdata) t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", c.Separator, data, gotdata)
} }
} }
@@ -66,10 +67,11 @@ func TestParserPatternmatching(t *testing.T) {
entries [][]string entries [][]string
pattern string pattern string
invert bool invert bool
want bool
}{ }{
{ {
entries: [][]string{ entries: [][]string{
[]string{ {
"asd", "igig", "cxxxncnc", "asd", "igig", "cxxxncnc",
}, },
}, },
@@ -78,13 +80,22 @@ func TestParserPatternmatching(t *testing.T) {
}, },
{ {
entries: [][]string{ entries: [][]string{
[]string{ {
"19191", "EDD 1", "X", "19191", "EDD 1", "X",
}, },
}, },
pattern: "ig", pattern: "ig",
invert: true, invert: true,
}, },
{
entries: [][]string{
{
"asd", "igig", "cxxxncnc",
},
},
pattern: "[a-z",
want: true,
},
} }
table := `ONE TWO THREE table := `ONE TWO THREE
@@ -92,21 +103,61 @@ asd igig cxxxncnc
19191 EDD 1 X` 19191 EDD 1 X`
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("parse-with-inverted-pattern-%t", tt.invert) testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
InvertMatch = tt.invert c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: cfg.DefaultSeparator}
readFd := strings.NewReader(table) readFd := strings.NewReader(table)
gotdata, err := parseFile(readFd, tt.pattern) gotdata, err := parseFile(c, readFd, tt.pattern)
if err != nil { if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) if !tt.want {
} t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
}
if !reflect.DeepEqual(tt.entries, gotdata.entries) { } else {
t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n", if !reflect.DeepEqual(tt.entries, gotdata.entries) {
tt.pattern, tt.invert, tt.entries, gotdata.entries) t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n",
tt.pattern, tt.invert, tt.entries, gotdata.entries)
}
} }
}) })
} }
} }
func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 1,
},
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(table)
c := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := parseFile(c, readFd, "")
if err != nil {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
}
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
c.Separator, data, gotdata)
}
}

View File

@@ -21,36 +21,53 @@ import (
"fmt" "fmt"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/tlinden/tablizer/cfg"
"gopkg.in/yaml.v3"
"io"
"log"
"regexp" "regexp"
"strconv"
"strings" "strings"
) )
func printData(data *Tabdata) { func printData(w io.Writer, c cfg.Config, data *Tabdata) {
if OutputMode != "shell" { // some output preparations:
numberizeHeaders(data)
}
reduceColumns(data)
switch OutputMode { // add numbers to headers and remove this we're not interested in
numberizeAndReduceHeaders(c, data)
// remove unwanted columns, if any
reduceColumns(c, data)
// sort the data
sortTable(c, data)
switch c.OutputMode {
case "extended": case "extended":
printExtendedData(data) printExtendedData(w, c, data)
case "ascii": case "ascii":
printAsciiData(data) printAsciiData(w, c, data)
case "orgtbl": case "orgtbl":
printOrgmodeData(data) printOrgmodeData(w, c, data)
case "markdown": case "markdown":
printMarkdownData(data) printMarkdownData(w, c, data)
case "shell": case "shell":
printShellData(data) printShellData(w, c, data)
case "yaml":
printYamlData(w, c, data)
default: default:
printAsciiData(data) printAsciiData(w, c, data)
} }
} }
func output(w io.Writer, str string) {
fmt.Fprint(w, str)
}
/* /*
Emacs org-mode compatible table (also orgtbl-mode) Emacs org-mode compatible table (also orgtbl-mode)
*/ */
func printOrgmodeData(data *Tabdata) { func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString) table := tablewriter.NewWriter(tableString)
@@ -73,19 +90,19 @@ func printOrgmodeData(data *Tabdata) {
| cell | cell | | cell | cell |
|------+------| |------+------|
*/ */
leftR := regexp.MustCompile("(?m)^\\+") leftR := regexp.MustCompile(`(?m)^\\+`)
rightR := regexp.MustCompile("\\+(?m)$") rightR := regexp.MustCompile(`\\+(?m)$`)
color.Print( output(w, color.Sprint(
colorizeData( colorizeData(c,
rightR.ReplaceAllString( rightR.ReplaceAllString(
leftR.ReplaceAllString(tableString.String(), "|"), "|"))) leftR.ReplaceAllString(tableString.String(), "|"), "|"))))
} }
/* /*
Markdown table Markdown table
*/ */
func printMarkdownData(data *Tabdata) { func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString) table := tablewriter.NewWriter(tableString)
@@ -99,13 +116,13 @@ func printMarkdownData(data *Tabdata) {
table.SetCenterSeparator("|") table.SetCenterSeparator("|")
table.Render() table.Render()
color.Print(colorizeData(tableString.String())) output(w, color.Sprint(colorizeData(c, tableString.String())))
} }
/* /*
Simple ASCII table without any borders etc, just like the input we expect Simple ASCII table without any borders etc, just like the input we expect
*/ */
func printAsciiData(data *Tabdata) { func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString) table := tablewriter.NewWriter(tableString)
@@ -129,47 +146,81 @@ func printAsciiData(data *Tabdata) {
table.SetNoWhiteSpace(true) table.SetNoWhiteSpace(true)
table.Render() table.Render()
color.Print(colorizeData(tableString.String())) output(w, color.Sprint(colorizeData(c, tableString.String())))
} }
/* /*
We simulate the \x command of psql (the PostgreSQL client) We simulate the \x command of psql (the PostgreSQL client)
*/ */
func printExtendedData(data *Tabdata) { func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) {
// needed for data output // needed for data output
format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader)
out := ""
if len(data.entries) > 0 { if len(data.entries) > 0 {
for _, entry := range data.entries { for _, entry := range data.entries {
for i, value := range entry { for i, value := range entry {
color.Printf(format, data.headers[i], value) out += color.Sprintf(format, data.headers[i], value)
} }
fmt.Println() out += "\n"
} }
} }
output(w, colorizeData(c, out))
} }
/* /*
Shell output, ready to be eval'd. Just like FreeBSD stat(1) Shell output, ready to be eval'd. Just like FreeBSD stat(1)
*/ */
func printShellData(data *Tabdata) { func printShellData(w io.Writer, c cfg.Config, data *Tabdata) {
out := ""
if len(data.entries) > 0 { if len(data.entries) > 0 {
var idx int
for _, entry := range data.entries { for _, entry := range data.entries {
idx = 0
shentries := []string{} shentries := []string{}
for i, value := range entry { for i, value := range entry {
if len(Columns) > 0 { shentries = append(shentries, fmt.Sprintf("%s=\"%s\"",
if !contains(UseColumns, i+1) { data.headers[i], value))
continue
}
}
shentries = append(shentries, fmt.Sprintf("%s=\"%s\"", data.headers[idx], value))
idx++
} }
fmt.Println(strings.Join(shentries, " ")) out += fmt.Sprint(strings.Join(shentries, " ")) + "\n"
} }
} }
// no colrization here
output(w, out)
}
func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) {
type D struct {
Entries []map[string]interface{} `yaml:"entries"`
}
d := D{}
for _, entry := range data.entries {
ml := map[string]interface{}{}
for i, entry := range entry {
style := yaml.TaggedStyle
_, err := strconv.Atoi(entry)
if err != nil {
style = yaml.DoubleQuotedStyle
}
ml[strings.ToLower(data.headers[i])] =
&yaml.Node{
Kind: yaml.ScalarNode,
Style: style,
Value: entry}
}
d.Entries = append(d.Entries, ml)
}
yamlstr, err := yaml.Marshal(&d)
if err != nil {
log.Fatal(err)
}
output(w, string(yamlstr))
} }

View File

@@ -18,95 +18,271 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"bytes"
"fmt" "fmt"
"github.com/gookit/color" //"github.com/alecthomas/repr"
"os" "github.com/tlinden/tablizer/cfg"
"strings" "strings"
"testing" "testing"
) )
func TestPrinter(t *testing.T) { func newData() Tabdata {
startdata := Tabdata{ return Tabdata{
maxwidthHeader: 5, maxwidthHeader: 8,
maxwidthPerCol: []int{ maxwidthPerCol: []int{
5, 5,
5, 9,
8, 3,
26,
}, },
columns: 3, columns: 4,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "NAME",
"DURATION",
"COUNT",
"WHEN",
}, },
entries: [][]string{ entries: [][]string{
[]string{ {
"asd", "igig", "cxxxncnc", "beta",
"1d10h5m1s",
"33",
"3/1/2014",
}, },
[]string{ {
"19191", "EDD 1", "X", "alpha",
"4h35m",
"170",
"2013-Feb-03",
},
{
"ceta",
"33d12h",
"9",
"06/Jan/2008 15:04:05 -0700",
}, },
}, },
} }
}
expects := map[string]string{ var tests = []struct {
"ascii": `ONE(1) TWO(2) THREE(3) name string // so we can identify which one fails, can be the same
asd igig cxxxncnc // for multiple tests, because flags will be appended to the name
19191 EDD 1 X`, sortby string // empty == default
"orgtbl": `|--------+--------+----------| column int // sort by this column, 0 == default first or NO Sort
| ONE(1) | TWO(2) | THREE(3) | desc bool // sort in descending order, default == ascending
|--------+--------+----------| nonum bool // hide numbering
| asd | igig | cxxxncnc | mode string // shell, orgtbl, etc. empty == default: ascii
| 19191 | EDD 1 | X | usecol []int // columns to display, empty == display all
|--------+--------+----------|`, usecolstr string // for testname, must match usecol
"markdown": `| ONE(1) | TWO(2) | THREE(3) | expect string // rendered output we expect
|--------|--------|----------| }{
| asd | igig | cxxxncnc | // --------------------- Default settings mode tests ``
| 19191 | EDD 1 | X |`, {
"shell": `ONE="asd" TWO="igig" THREE="cxxxncnc" mode: "ascii",
ONE="19191" TWO="EDD 1" THREE="X"`, name: "default",
"extended": `ONE(1): asd expect: `
TWO(2): igig NAME(1) DURATION(2) COUNT(3) WHEN(4)
THREE(3): cxxxncnc beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
{
name: "default",
mode: "orgtbl",
expect: `
+---------+-------------+----------+----------------------------+
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
+---------+-------------+----------+----------------------------+
| beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |
+---------+-------------+----------+----------------------------+`,
},
{
name: "default",
mode: "markdown",
expect: `
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|---------|-------------|----------|----------------------------|
| beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`,
},
{
name: "default",
mode: "shell",
nonum: true,
expect: `
NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014"
NAME="alpha" DURATION="4h35m" COUNT="170" WHEN="2013-Feb-03"
NAME="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`,
},
{
name: "default",
mode: "yaml",
nonum: true,
expect: `
entries:
- count: 33
duration: "1d10h5m1s"
name: "beta"
when: "3/1/2014"
- count: 170
duration: "4h35m"
name: "alpha"
when: "2013-Feb-03"
- count: 9
duration: "33d12h"
name: "ceta"
when: "06/Jan/2008 15:04:05 -0700"`,
},
{
name: "default",
mode: "extended",
expect: `
NAME(1): beta
DURATION(2): 1d10h5m1s
COUNT(3): 33
WHEN(4): 3/1/2014
ONE(1): 19191 NAME(1): alpha
TWO(2): EDD 1 DURATION(2): 4h35m
THREE(3): X`, COUNT(3): 170
} WHEN(4): 2013-Feb-03
NoColor = true NAME(1): ceta
DURATION(2): 33d12h
COUNT(3): 9
WHEN(4): 06/Jan/2008 15:04:05 -0700`,
},
r, w, err := os.Pipe() //------------------------ SORT TESTS
if err != nil { {
t.Fatal(err) name: "sortbycolumn",
} column: 3,
origStdout := os.Stdout sortby: "numeric",
os.Stdout = w desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03`,
},
{
name: "sortbycolumn",
column: 4,
sortby: "time",
desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014`,
},
{
name: "sortbycolumn",
column: 2,
sortby: "duration",
desc: false,
expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4)
alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
// we need to tell the color mode the io.Writer, even if we don't usw colorization // ----------------------- UseColumns Tests
color.SetOutput(w) {
name: "usecolumns",
usecol: []int{1, 4},
usecolstr: "1,4",
expect: `
NAME(1) WHEN(4)
beta 3/1/2014
alpha 2013-Feb-03
ceta 06/Jan/2008 15:04:05 -0700`,
},
{
name: "usecolumns",
usecol: []int{2},
usecolstr: "2",
expect: `
DURATION(2)
1d10h5m1s
4h35m
33d12h`,
},
{
name: "usecolumns",
usecol: []int{3},
usecolstr: "3",
expect: `
COUNT(3)
33
170
9`,
},
{
name: "usecolumns",
column: 0,
usecol: []int{1, 3},
usecolstr: "1,3",
expect: `
NAME(1) COUNT(3)
beta 33
alpha 170
ceta 9`,
},
{
name: "usecolumns",
usecol: []int{2, 4},
usecolstr: "2,4",
expect: `
DURATION(2) WHEN(4)
1d10h5m1s 3/1/2014
4h35m 2013-Feb-03
33d12h 06/Jan/2008 15:04:05 -0700`,
},
}
for mode, expect := range expects { func TestPrinter(t *testing.T) {
testname := fmt.Sprintf("print-%s", mode) for _, tt := range tests {
testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%s-usecolumns-%s",
tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
// replaces os.Stdout, but we ignore it
var w bytes.Buffer
OutputMode = mode // cmd flags
data := startdata // we need to reset our mock data, since it's being modified in printData() c := cfg.Config{
printData(&data) SortByColumn: tt.column,
SortDescending: tt.desc,
buf := make([]byte, 1024) SortMode: tt.sortby,
n, err := r.Read(buf) OutputMode: tt.mode,
if err != nil { NoNumbering: tt.nonum,
t.Fatal(err) UseColumns: tt.usecol,
NoColor: true,
} }
buf = buf[:n]
output := strings.TrimSpace(string(buf))
if output != expect { // the test checks the len!
t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect)) if len(tt.usecol) > 0 {
c.Columns = "yes"
} else {
c.Columns = ""
}
testdata := newData()
exp := strings.TrimSpace(tt.expect)
printData(&w, c, &testdata)
got := strings.TrimSpace(w.String())
if got != exp {
t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s",
got, exp)
} }
}) })
} }
// Restore
os.Stdout = origStdout
} }

121
lib/sort.go Normal file
View File

@@ -0,0 +1,121 @@
/*
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 (
"github.com/araddon/dateparse"
"github.com/tlinden/tablizer/cfg"
"regexp"
"sort"
"strconv"
)
func sortTable(c cfg.Config, data *Tabdata) {
if c.SortByColumn <= 0 {
// no sorting wanted
return
}
// slightly modified here to match internal array indicies
col := c.SortByColumn
col-- // ui starts counting by 1, but use 0 internally
// sanity checks
if len(data.entries) == 0 {
return
}
if col >= len(data.headers) {
// fall back to default column
col = 0
}
// actual sorting
sort.SliceStable(data.entries, func(i, j int) bool {
return compare(&c, data.entries[i][col], data.entries[j][col])
})
}
// config is not modified here, but it would be inefficient to copy it every loop
func compare(c *cfg.Config, a string, b string) bool {
var comp bool
switch c.SortMode {
case "numeric":
left, err := strconv.Atoi(a)
if err != nil {
left = 0
}
right, err := strconv.Atoi(b)
if err != nil {
right = 0
}
comp = left < right
case "duration":
left := duration2int(a)
right := duration2int(b)
comp = left < right
case "time":
left, _ := dateparse.ParseAny(a)
right, _ := dateparse.ParseAny(b)
comp = left.Unix() < right.Unix()
default:
comp = a < b
}
if c.SortDescending {
comp = !comp
}
return comp
}
/*
We could use time.ParseDuration(), but this doesn't support days.
We could also use github.com/xhit/go-str2duration/v2, which does
the job, but it's just another dependency, just for this little
gem. And we don't need a time.Time value. And int is good enough
for duration comparision.
Convert a durartion 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 {
v, _ := strconv.Atoi(match[1])
switch match[2][0] {
case 'd':
seconds += v * 86400
case 'h':
seconds += v * 3600
case 'm':
seconds += v * 60
case 's':
seconds += v
}
}
}
return seconds
}

79
lib/sort_test.go Normal file
View File

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

View File

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

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "TABLIZER 1" .IX Title "TABLIZER 1"
.TH TABLIZER 1 "2022-10-10" "1" "User Commands" .TH TABLIZER 1 "2022-10-16" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .if n .ad l
@@ -154,11 +154,16 @@ tablizer \- Manipulate tabular output of other programs
\& \-m, \-\-man Display manual page \& \-m, \-\-man Display manual page
\& \-n, \-\-no\-numbering Disable header numbering \& \-n, \-\-no\-numbering Disable header numbering
\& \-N, \-\-no\-color Disable pattern highlighting \& \-N, \-\-no\-color Disable pattern highlighting
\& \-o, \-\-output string Output mode \- one of: orgtbl, markdown, extended, ascii(default) \& \-o, \-\-output string Output mode \- one of: orgtbl, markdown, extended, yaml, ascii(default)
\& \-X, \-\-extended Enable extended output \& \-X, \-\-extended Enable extended output
\& \-M, \-\-markdown Enable markdown table output \& \-M, \-\-markdown Enable markdown table output
\& \-O, \-\-orgtbl Enable org\-mode table output \& \-O, \-\-orgtbl Enable org\-mode table output
\& \-s, \-\-separator string Custom field separator \& \-s, \-\-separator string Custom field separator
\& \-a, \-\-sort\-age sort according to age (duration) string
\& \-k, \-\-sort\-by int Sort by column (default: 1)
\& \-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
\& \-v, \-\-version Print program version \& \-v, \-\-version Print program version
.Ve .Ve
.SH "DESCRIPTION" .SH "DESCRIPTION"
@@ -176,8 +181,8 @@ not easy to process.
.PP .PP
You can use \fBtablizer\fR to do these and more things. You can use \fBtablizer\fR to do these and more things.
.PP .PP
\&\fBtablizer\fR analyses the header fiels of a table, registers the column \&\fBtablizer\fR analyses the header fields of a table, registers the
positions of each header field and separates columns by those column positions of each header field and separates columns by those
positions. positions.
.PP .PP
Without any options it reads its input from \f(CW\*(C`STDIN\*(C'\fR, but you can also Without any options it reads its input from \f(CW\*(C`STDIN\*(C'\fR, but you can also
@@ -208,7 +213,7 @@ have a numer associated with it, e.g.:
.Ve .Ve
.PP .PP
These numbers denote the column and you can use them to specify which These numbers denote the column and you can use them to specify which
columns you want to have in your output: columns you want to have in your output (see \s-1COLUMNS\s0:
.PP .PP
.Vb 1 .Vb 1
\& kubectl get pods | tablizer \-c1,3 \& kubectl get pods | tablizer \-c1,3
@@ -222,8 +227,24 @@ The numbering can be suppressed by using the \fB\-n\fR option.
By default, if a \fBpattern\fR has been speficied, matches will be By default, if a \fBpattern\fR has been speficied, matches will be
highlighted. You can disable this behavior with the \fB\-N\fR option. highlighted. You can disable this behavior with the \fB\-N\fR option.
.PP .PP
Use the \fB\-k\fR option to specify by which column to sort the tabular
data (as in \s-1GNU\s0 \fBsort\fR\|(1)). The default sort column is the first one. To
disable sorting at all, supply 0 (Zero) to \-k. The default sort order
is ascending. You can change this to descending order using the option
\&\fB\-D\fR. The default sort order is by string, but there are other sort
modes:
.IP "\fB\-a \-\-sort\-age\fR" 4
.IX Item "-a --sort-age"
Sorts duration strings like \*(L"1d4h32m51s\*(R".
.IP "\fB\-i \-\-sort\-numeric\fR" 4
.IX Item "-i --sort-numeric"
Sorts numeric fields.
.IP "\fB\-t \-\-sort\-time\fR" 4
.IX Item "-t --sort-time"
Sorts timestamps.
.PP
Finally the \fB\-d\fR option enables debugging output which is mostly Finally the \fB\-d\fR option enables debugging output which is mostly
usefull for the developer. useful for the developer.
.SS "\s-1PATTERNS\s0" .SS "\s-1PATTERNS\s0"
.IX Subsection "PATTERNS" .IX Subsection "PATTERNS"
You can reduce the rows being displayed by using a regular expression You can reduce the rows being displayed by using a regular expression
@@ -251,17 +272,49 @@ The most important modifiers are:
\&\f(CW\*(C`m\*(C'\fR multiline mode \&\f(CW\*(C`m\*(C'\fR multiline mode
\&\f(CW\*(C`s\*(C'\fR single line mode \&\f(CW\*(C`s\*(C'\fR single line mode
.PP .PP
Example for a case insensitve search: Example for a case insensitive search:
.PP .PP
.Vb 1 .Vb 1
\& kubectl get pods \-A | tablizer "(?i)account" \& kubectl get pods \-A | tablizer "(?i)account"
.Ve .Ve
.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
.SS "\s-1OUTPUT MODES\s0" .SS "\s-1OUTPUT MODES\s0"
.IX Subsection "OUTPUT MODES" .IX Subsection "OUTPUT MODES"
There might be cases when the tabular output of a program is way too There might be cases when the tabular output of a program is way too
large for your current terminal but you still need to see every large for your current terminal but you still need to see every
column. In such cases the \fB\-o extended\fR or \fB\-X\fR option can be column. In such cases the \fB\-o extended\fR or \fB\-X\fR option can be
usefull which enables \fIextended mode\fR. In this mode, each row will be useful which enables \fIextended mode\fR. In this mode, each row will be
printed vertically, header left, value right, aligned by the field printed vertically, header left, value right, aligned by the field
widths. Here's an example: widths. Here's an example:
.PP .PP
@@ -292,7 +345,8 @@ You can use this in an eval loop.
.PP .PP
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table. table and \fBmarkdown\fR which prints a Markdown table and \fByaml\fR, which
prints yaml encoding.
.SH "BUGS" .SH "BUGS"
.IX Header "BUGS" .IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests In order to report a bug, unexpected behavior, feature requests

View File

@@ -15,11 +15,16 @@ tablizer - Manipulate tabular output of other programs
-m, --man Display manual page -m, --man Display manual page
-n, --no-numbering Disable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default) -o, --output string Output mode - one of: orgtbl, markdown, extended, yaml, ascii(default)
-X, --extended Enable extended output -X, --extended Enable extended output
-M, --markdown Enable markdown table output -M, --markdown Enable markdown table output
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-s, --separator string Custom field separator -s, --separator string Custom field separator
-a, --sort-age sort according to age (duration) string
-k, --sort-by int Sort by column (default: 1)
-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
-v, --version Print program version -v, --version Print program version
@@ -38,8 +43,8 @@ not easy to process.
You can use B<tablizer> to do these and more things. You can use B<tablizer> to do these and more things.
B<tablizer> analyses the header fiels of a table, registers the column B<tablizer> analyses the header fields of a table, registers the
positions of each header field and separates columns by those column positions of each header field and separates columns by those
positions. positions.
Without any options it reads its input from C<STDIN>, but you can also Without any options it reads its input from C<STDIN>, but you can also
@@ -66,7 +71,7 @@ have a numer associated with it, e.g.:
NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
These numbers denote the column and you can use them to specify which These numbers denote the column and you can use them to specify which
columns you want to have in your output: columns you want to have in your output (see L<COLUMNS>:
kubectl get pods | tablizer -c1,3 kubectl get pods | tablizer -c1,3
@@ -78,8 +83,31 @@ The numbering can be suppressed by using the B<-n> option.
By default, if a B<pattern> has been speficied, matches will be By default, if a B<pattern> has been speficied, matches will be
highlighted. You can disable this behavior with the B<-N> option. highlighted. You can disable this behavior with the B<-N> option.
Use the B<-k> option to specify by which column to sort the tabular
data (as in GNU sort(1)). The default sort column is the first one. To
disable sorting at all, supply 0 (Zero) to -k. The default sort order
is ascending. You can change this to descending order using the option
B<-D>. The default sort order is by string, but there are other sort
modes:
=over
=item B<-a --sort-age>
Sorts duration strings like "1d4h32m51s".
=item B<-i --sort-numeric>
Sorts numeric fields.
=item B<-t --sort-time>
Sorts timestamps.
=back
Finally the B<-d> option enables debugging output which is mostly Finally the B<-d> option enables debugging output which is mostly
usefull for the developer. useful for the developer.
=head2 PATTERNS =head2 PATTERNS
@@ -104,17 +132,46 @@ C<i> ignore case
C<m> multiline mode C<m> multiline mode
C<s> single line mode C<s> single line mode
Example for a case insensitve search: Example for a case insensitive search:
kubectl get pods -A | tablizer "(?i)account" kubectl get pods -A | tablizer "(?i)account"
=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.
=head2 OUTPUT MODES =head2 OUTPUT MODES
There might be cases when the tabular output of a program is way too There might be cases when the tabular output of a program is way too
large for your current terminal but you still need to see every large for your current terminal but you still need to see every
column. In such cases the B<-o extended> or B<-X> option can be column. In such cases the B<-o extended> or B<-X> option can be
usefull which enables I<extended mode>. In this mode, each row will be useful which enables I<extended mode>. In this mode, each row will be
printed vertically, header left, value right, aligned by the field printed vertically, header left, value right, aligned by the field
widths. Here's an example: widths. Here's an example:
@@ -141,7 +198,8 @@ You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table. table and B<markdown> which prints a Markdown table and B<yaml>, which
prints yaml encoding.
=head1 BUGS =head1 BUGS