Compare commits

..

1 Commits
v1.5.7 ... sort

45 changed files with 872 additions and 2650 deletions

View File

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

View File

@@ -1,22 +1,24 @@
name: build-and-test-tablizer name: build-and-test-tablizer
on: [push] on: [push, pull_request]
jobs: jobs:
build: build:
strategy: strategy:
matrix: matrix:
version: ['1.24'] version: ['1.22']
os: [ubuntu-latest, macos-latest, windows-latest] # windows-latest removed, see:
# https://github.com/rogpeppe/go-internal/issues/284
os: [ubuntu-latest, macos-latest]
name: Build name: Build
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Set up Go ${{ matrix.version }} - name: Set up Go ${{ matrix.version }}
uses: actions/setup-go@v6 uses: actions/setup-go@v5
with: with:
go-version: '${{ matrix.version }}' go-version: '${{ matrix.version }}'
id: go id: go
- name: checkout - name: checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: build - name: build
run: make run: make
@@ -28,10 +30,10 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v6 - uses: actions/setup-go@v5
with: with:
go-version: 1.24 go-version: 1.22
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v6
with: with:

View File

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

View File

@@ -53,7 +53,8 @@ buildlocal:
go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'" go build -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=$(VERSION)'"
release: release:
gh release create $(version) --generate-notes ./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
@@ -64,12 +65,12 @@ install: buildlocal
clean: clean:
rm -rf $(tool) releases coverage.out rm -rf $(tool) releases coverage.out
test: clean test:
go test -cover ./... $(OPTS) go test ./... $(OPTS)
singletest: singletest:
@echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'" @echo "Call like this: 'make singletest TEST=TestPrepareColumns MOD=lib'"
go test -run $(TEST) github.com/tlinden/tablizer/$(MOD) $(OPTS) go test -run $(TEST) github.com/tlinden/tablizer/$(MOD)
cover-report: cover-report:
go test ./... -cover -coverprofile=coverage.out go test ./... -cover -coverprofile=coverage.out

View File

@@ -6,64 +6,49 @@
Tablizer can be used to re-format tabular output of other Tablizer can be used to re-format tabular output of other
programs. While you could do this using standard unix tools, in some programs. While you could do this using standard unix tools, in some
cases it's a hard job. With tablizer you can filter by column[s], cases it's a hard job.
ignore certain column[s] by regex, name or number. It can output the
tabular data in a range of formats (see below). There's even an
interactive filter/selection tool available.
## Demo
![demo cast](vhsdemo/demo.gif)
## Usage
Usage:
```default ```default
Usage: Usage:
tablizer [regex,...] [file, ...] [flags] tablizer [regex] [file, ...] [flags]
Operational Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-n, --numbering Enable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display -H, --no-headers Disable headers display
-s, --separator <string> Custom field separator -s, --separator string Custom field separator
-k, --sort-by <int|name> Sort by column (default: 1) -k, --sort-by int Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times -F, --filter field=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,) -T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T -R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-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, --shell Enable shell evaluable output -S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output -C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables -L, --hightlight-lines Use alternating background colors for tables
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
--ofs <char> Output field separator, used by -A and -C.
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags: Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN --completion <shell> Generate the autocompletion script for <shell>
--completion <shell> Generate the autocompletion script for <shell> -f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-f, --config <file> Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging
-d, --debug Enable debugging -h, --help help for tablizer
-h, --help help for tablizer -m, --man Display manual page
-m, --man Display manual page -V, --version Print program version
-V, --version Print program version
``` ```
Let's take this output: Let's take this output:
@@ -80,13 +65,13 @@ to do this with tablizer:
``` ```
% kubectl get pods | tablizer % kubectl get pods | tablizer
NAME READY STATUS RESTARTS AGE 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 STATUS NAME(1) STATUS(3)
repldepl-7bcd8d5b64-7zq4l Running repldepl-7bcd8d5b64-7zq4l Running
repldepl-7bcd8d5b64-m48n8 Running repldepl-7bcd8d5b64-m48n8 Running
repldepl-7bcd8d5b64-q2bf4 Running repldepl-7bcd8d5b64-q2bf4 Running
@@ -124,14 +109,14 @@ You can also specify a regex pattern to reduce the output:
``` ```
% kubectl get pods | tablizer q2bf4 % kubectl get pods | tablizer q2bf4
NAME READY STATUS RESTARTS AGE NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
``` ```
Sometimes a filter regex is to broad and you wish to filter only on a Sometimes a filter regex is to broad and you wish to filter only on a
particular column. This is possible using `-F`: particular column. This is possible using `-F`:
``` ```
% kubectl get pods | tablizer -Fname=2 % kubectl get pods | tablizer -n -Fname=2
NAME READY STATUS RESTARTS AGE NAME READY STATUS RESTARTS AGE
repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m
``` ```
@@ -145,7 +130,7 @@ You can also use it to modify certain cells using regular expression
matching. For example: matching. For example:
```shell ```shell
kubectl get pods | tablizer -T4 -R '/ /-/' kubectl get pods | tablizer -n -T4 -R '/ /-/'
NAME READY STATUS RESTARTS AGE NAME READY STATUS RESTARTS AGE
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
@@ -156,12 +141,11 @@ Here, we modified the 4th column (`-T4`) by replacing every space with
a dash. If you need to work with `/` characters, you can also use any a dash. If you need to work with `/` characters, you can also use any
other separator, for instance: `-R '| |-|'`. other separator, for instance: `-R '| |-|'`.
There's also an interactive mode, invoked with the option B<-I>, where
you can interactively filter and select rows:
<img width="937" height="293" alt="interactive" src="https://github.com/user-attachments/assets/0d4d65e2-d156-43ed-8021-39047c7939ed" />
## Demo
[![asciicast](demo/tablizer-demo.gif)](https://asciinema.org/a/9FKc3HPnlg8D2X8otheleEa9t)
## Installation ## Installation

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@ import (
) )
const DefaultSeparator string = `(\s\s+|\t)` const DefaultSeparator string = `(\s\s+|\t)`
const Version string = "v1.5.7" const Version string = "v1.3.1"
const MAXPARTS = 2 const MAXPARTS = 2
var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config" var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config"
@@ -52,34 +52,20 @@ type Transposer struct {
Replace string Replace string
} }
type Pattern struct {
Pattern string
PatternRe *regexp.Regexp
Negate bool
}
type Filter struct {
Regex *regexp.Regexp
Negate bool
}
// internal config // internal config
type Config struct { type Config struct {
Debug bool Debug bool
Numbering bool NoNumbering bool
NoHeaders bool NoHeaders bool
Columns string Columns string
UseColumns []int UseColumns []int
YankColumns string
UseYankColumns []int
Separator string Separator string
OutputMode int OutputMode int
InvertMatch bool InvertMatch bool
Patterns []*Pattern Pattern string
PatternR *regexp.Regexp
UseFuzzySearch bool UseFuzzySearch bool
UseHighlight bool UseHighlight bool
Interactive bool
InputJSON bool
SortMode string SortMode string
SortDescending bool SortDescending bool
@@ -109,12 +95,10 @@ type Config struct {
// used for field filtering // used for field filtering
Rawfilters []string Rawfilters []string
Filters map[string]Filter //map[string]*regexp.Regexp Filters map[string]*regexp.Regexp
// -r <file> // -r <file>
InputFile string InputFile string
OFS string
} }
// maps outputmode short flags to output mode, ie. -O => -o orgtbl // maps outputmode short flags to output mode, ie. -O => -o orgtbl
@@ -281,20 +265,12 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) {
} }
func (conf *Config) PrepareFilters() error { func (conf *Config) PrepareFilters() error {
conf.Filters = make(map[string]Filter, len(conf.Rawfilters)) conf.Filters = make(map[string]*regexp.Regexp, len(conf.Rawfilters))
for _, rawfilter := range conf.Rawfilters { for _, filter := range conf.Rawfilters {
filter := Filter{} parts := strings.Split(filter, "=")
parts := strings.Split(rawfilter, "!=")
if len(parts) != MAXPARTS { if len(parts) != MAXPARTS {
parts = strings.Split(rawfilter, "=") return errors.New("filter field and value must be separated by =")
if len(parts) != MAXPARTS {
return errors.New("filter field and value must be separated by '=' or '!='")
}
} else {
filter.Negate = true
} }
reg, err := regexp.Compile(parts[1]) reg, err := regexp.Compile(parts[1])
@@ -303,8 +279,7 @@ func (conf *Config) PrepareFilters() error {
parts[0], err) parts[0], err)
} }
filter.Regex = reg conf.Filters[strings.ToLower(strings.ToLower(parts[0]))] = reg
conf.Filters[strings.ToLower(parts[0])] = filter
} }
return nil return nil
@@ -336,10 +311,10 @@ func (conf *Config) PrepareTransposers() error {
func (conf *Config) CheckEnv() { func (conf *Config) CheckEnv() {
// check for environment vars, command line flags have precedence, // check for environment vars, command line flags have precedence,
// NO_COLOR is being checked by the color module itself. // NO_COLOR is being checked by the color module itself.
if !conf.Numbering { if !conf.NoNumbering {
_, set := os.LookupEnv("T_HEADER_NUMBERING") _, set := os.LookupEnv("T_NO_HEADER_NUMBERING")
if set { if set {
conf.Numbering = true conf.NoNumbering = true
} }
} }
@@ -354,41 +329,19 @@ func (conf *Config) CheckEnv() {
func (conf *Config) ApplyDefaults() { func (conf *Config) ApplyDefaults() {
// mode specific defaults // mode specific defaults
if conf.OutputMode == Yaml || conf.OutputMode == CSV { if conf.OutputMode == Yaml || conf.OutputMode == CSV {
conf.Numbering = false conf.NoNumbering = true
} }
} }
func (conf *Config) PreparePattern(patterns []*Pattern) error { func (conf *Config) PreparePattern(pattern string) error {
// regex checks if a pattern looks like /$pattern/[i!] PatternR, err := regexp.Compile(pattern)
flagre := regexp.MustCompile(`^/(.*)/([i!]*)$`)
for _, pattern := range patterns { if err != nil {
matches := flagre.FindAllStringSubmatch(pattern.Pattern, -1) return fmt.Errorf("regexp pattern %s is invalid: %w", conf.Pattern, err)
// we have a regex with flags
for _, match := range matches {
pattern.Pattern = match[1] // the inner part is our actual pattern
flags := match[2] // the flags
for _, flag := range flags {
switch flag {
case 'i':
pattern.Pattern = `(?i)` + pattern.Pattern
case '!':
pattern.Negate = true
}
}
}
PatternRe, err := regexp.Compile(pattern.Pattern)
if err != nil {
return fmt.Errorf("regexp pattern %s is invalid: %w", pattern.Pattern, err)
}
pattern.PatternRe = PatternRe
} }
conf.Patterns = patterns conf.PatternR = PatternR
conf.Pattern = pattern
return nil return nil
} }

View File

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

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -17,10 +17,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd package cmd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"slices" "os/exec"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -28,6 +30,24 @@ import (
"github.com/tlinden/tablizer/lib" "github.com/tlinden/tablizer/lib"
) )
func man() {
man := exec.Command("less", "-")
var buffer bytes.Buffer
buffer.Write([]byte(manpage))
man.Stdout = os.Stdout
man.Stdin = &buffer
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
log.Fatal(err)
}
}
func completion(cmd *cobra.Command, mode string) error { func completion(cmd *cobra.Command, mode string) error {
switch mode { switch mode {
case "bash": case "bash":
@@ -74,7 +94,7 @@ func Execute() {
} }
if ShowManual { if ShowManual {
lib.Pager("tablizer manual page", manpage) man()
return return
} }
@@ -105,7 +125,7 @@ func Execute() {
// options // options
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false,
"Enable debugging") "Enable debugging")
rootCmd.PersistentFlags().BoolVarP(&conf.Numbering, "numbering", "n", false, rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false,
"Disable header numbering") "Disable header numbering")
rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false, rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false,
"Disable header display") "Disable header display")
@@ -127,16 +147,8 @@ func Execute() {
"Custom field separator") "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "",
"Only show the speficied columns (separated by ,)") "Only show the speficied columns (separated by ,)")
rootCmd.PersistentFlags().StringVarP(&conf.YankColumns, "yank-columns", "y", "",
"Yank the speficied columns (separated by ,) to the clipboard")
rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "", rootCmd.PersistentFlags().StringVarP(&conf.TransposeColumns, "transpose-columns", "T", "",
"Transpose the speficied columns (separated by ,)") "Transpose the speficied columns (separated by ,)")
rootCmd.PersistentFlags().BoolVarP(&conf.Interactive, "interactive", "I", false,
"interactive mode")
rootCmd.PersistentFlags().StringVarP(&conf.OFS, "ofs", "", "",
"Output field separator (' ' for ascii table, ',' for CSV)")
rootCmd.PersistentFlags().BoolVarP(&conf.InputJSON, "json", "j", false,
"JSON input mode")
// sort options // sort options
rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "", rootCmd.PersistentFlags().StringVarP(&conf.SortByColumn, "sort-by", "k", "",
@@ -178,7 +190,7 @@ func Execute() {
// filters // filters
rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters, rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters,
"filter", "F", nil, "Filter by field (field=regexp || field!=regexp)") "filter", "F", nil, "Filter by field (field=regexp)")
rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers, rootCmd.PersistentFlags().StringArrayVarP(&conf.Transposers,
"regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T") "regex-transposer", "R", nil, "apply /search/replace/ regexp to fields given in -T")
@@ -188,11 +200,6 @@ func Execute() {
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")
if slices.Contains(os.Args, "-h") {
fmt.Println(shortusage)
os.Exit(0)
}
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)

View File

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

View File

@@ -6,50 +6,44 @@ NAME
SYNOPSIS SYNOPSIS
Usage: Usage:
tablizer [regex,...] [-r file] [flags] tablizer [regex] [file, ...] [flags]
Operational Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-n, --numbering Enable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display -H, --no-headers Disable headers display
-s, --separator <string> Custom field separator -s, --separator string Custom field separator
-k, --sort-by <int|name> Sort by column (default: 1) -k, --sort-by int|name Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times -F, --filter field=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,) -T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T -R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-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, --shell Enable shell evaluable output -S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output -C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables -L, --hightlight-lines Use alternating background colors for tables
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
--ofs <char> Output field separator, used by -A and -C.
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags: Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN --completion <shell> Generate the autocompletion script for <shell>
--completion <shell> Generate the autocompletion script for <shell> -f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-f, --config <file> Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging
-d, --debug Enable debugging -h, --help help for tablizer
-h, --help help for tablizer -m, --man Display manual page
-m, --man Display manual page -V, --version Print program version
-V, --version Print program version
DESCRIPTION DESCRIPTION
Many programs generate tabular output. But sometimes you need to Many programs generate tabular output. But sometimes you need to
@@ -77,16 +71,16 @@ DESCRIPTION
kubectl get pods | tablizer kubectl get pods | tablizer
# read a file # read a file
tablizer -r filename tablizer filename
# search for pattern in a file (works like grep) # search for pattern in a file (works like grep)
tablizer regex -r filename tablizer regex filename
# search for pattern in STDIN # search for pattern in STDIN
kubectl get pods | tablizer regex kubectl get pods | tablizer regex
The output looks like the original one. You can add the option -n, then The output looks like the original one but every header field will have
every header field will have a numer associated with it, e.g.: 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)
@@ -98,13 +92,7 @@ DESCRIPTION
You can specify the numbers in any order but output will always follow You can specify the numbers in any order but output will always follow
the original order. the original order.
However, you may also just use the header names instead of numbers, eg: The numbering can be suppressed by using the -n option.
kubectl get pods | tablizer -cname,status
You can also use regular expressions with -c, eg:
kubectl get pods | tablizer -c '[ae]'
By default tablizer shows a header containing the names of each column. By default tablizer shows a header containing the names of each column.
This can be disabled using the -H option. Be aware that this only This can be disabled using the -H option. Be aware that this only
@@ -142,43 +130,30 @@ DESCRIPTION
for the developer. for the developer.
PATTERNS AND FILTERING PATTERNS AND FILTERING
You can reduce the rows being displayed by using one or more regular You can reduce the rows being displayed by using a regular expression
expression patterns. The regexp language being used is the one of pattern. The regexp is PCRE compatible, refer to the syntax cheat sheet
GOLANG, refer to the syntax cheat sheet here: here: <https://github.com/google/re2/wiki/Syntax>. If you want to read a
<https://pkg.go.dev/regexp/syntax>. more comprehensive documentation about the topic and have perl installed
you can read it with:
If you want to read a more comprehensive documentation about the topic
and have perl installed you can read it with:
perldoc perlre perldoc perlre
Or read it online: <https://perldoc.perl.org/perlre>. But please note Or read it online: <https://perldoc.perl.org/perlre>.
that the GO regexp engine does NOT support all perl regex terms,
especially look-ahead and look-behind.
If you want to supply flags to a regex, then surround it with slashes A note on modifiers: the regexp engine used in tablizer uses another
and append the flag. The following flags are supported: modifier syntax:
i => case insensitive (?MODIFIER)
! => negative match
The most important modifiers are:
"i" ignore case "m" multiline mode "s" single line mode
Example for a case insensitive search: Example for a case insensitive search:
kubectl get pods -A | tablizer "/account/i" kubectl get pods -A | tablizer "(?i)account"
If you use the "!" flag, then the regex match will be negated, that is, You can use the experimental fuzzy search feature by providing the
if a line in the input matches the given regex, but "!" is supplied,
tablizer will NOT include it in the output.
For example, here we want to get all lines matching "foo" but not "bar":
cat table | tablizer foo '/bar/!'
This would match a line "foo zorro" but not "foo bar".
The flags can also be combined.
You can also use the experimental fuzzy search feature by providing the
option -z, in which case the pattern is regarded as a fuzzy search term, option -z, in which case the pattern is regarded as a fuzzy search term,
not a regexp. not a regexp.
@@ -193,26 +168,8 @@ DESCRIPTION
If you specify more than one filter, both filters have to match (AND If you specify more than one filter, both filters have to match (AND
operation). operation).
These field filters can also be negated:
fieldname!=regexp
If the option -v is specified, the filtering is inverted. If the option -v is specified, the filtering is inverted.
INTERACTIVE FILTERING
You can also use the interactive mode, enabled with "-I" to filter and
select rows. This mode is complementary, that is, other filter options
are still being respected.
To enter e filter, hit "/", enter a filter string and finish with
"ENTER". Use "SPACE" to select/deselect rows, use "a" to select all
(visible) rows.
Commit your selection with "q". The selected rows are being fed to the
requested output mode as usual. Abort with "CTRL-c", in which case the
results of the interactive mode are being ignored and all rows are being
fed to output.
COLUMNS COLUMNS
The parameter -c can be used to specify, which columns to display. By The parameter -c can be used to specify, which columns to display. By
default tablizer numerizes the header names and these numbers can be default tablizer numerizes the header names and these numbers can be
@@ -310,24 +267,12 @@ DESCRIPTION
markdown which prints a Markdown table, yaml, which prints yaml encoding markdown which prints a Markdown table, yaml, which prints yaml encoding
and CSV mode, which prints a comma separated value file. and CSV mode, which prints a comma separated value file.
PUT FIELDS TO CLIPBOARD
You can let tablizer put fields to the clipboard using the option "-y".
This best fits the use-case when the result of your filtering yields
just one row. For example:
cloudctl cluster ls | tablizer -yid matchbox
If "matchbox" matches one cluster, you can immediately use the id of
that cluster somewhere else and paste it. Of course, if there are
multiple matches, then all id's will be put into the clipboard separated
by one space.
ENVIRONMENT VARIABLES ENVIRONMENT VARIABLES
tablizer supports certain environment variables which use can use to tablizer supports certain environment variables which use can use to
influence program behavior. Commandline flags have always precedence influence program behavior. Commandline flags have always precedence
over environment variables. over environment variables.
<T_HEADER_NUMBERING> - enable numbering of header fields, like -n. <T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n.
<T_COLUMNS> - comma separated list of columns to output, like -c <T_COLUMNS> - comma separated list of columns to output, like -c
<NO_COLORS> - disable colorization of matches, like -N <NO_COLORS> - disable colorization of matches, like -N
@@ -440,9 +385,6 @@ LICENSE
Released under the MIT License, Copyright (c) 2006-2011 Kirill Released under the MIT License, Copyright (c) 2006-2011 Kirill
Simonov Simonov
bubble-table (https://github.com/Evertras/bubble-table)
Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
AUTHORS AUTHORS
Thomas von Dein tom AT vondein DOT org Thomas von Dein tom AT vondein DOT org
@@ -450,50 +392,44 @@ AUTHORS
var usage = ` var usage = `
Usage: Usage:
tablizer [regex,...] [-r file] [flags] tablizer [regex] [file, ...] [flags]
Operational Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-n, --numbering Enable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display -H, --no-headers Disable headers display
-s, --separator <string> Custom field separator -s, --separator string Custom field separator
-k, --sort-by <int|name> Sort by column (default: 1) -k, --sort-by int|name Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times -F, --filter field=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,) -T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T -R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-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, --shell Enable shell evaluable output -S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output -C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables -L, --hightlight-lines Use alternating background colors for tables
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
--ofs <char> Output field separator, used by -A and -C.
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags: Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN --completion <shell> Generate the autocompletion script for <shell>
--completion <shell> Generate the autocompletion script for <shell> -f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-f, --config <file> Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging
-d, --debug Enable debugging -h, --help help for tablizer
-h, --help help for tablizer -m, --man Display manual page
-m, --man Display manual page -V, --version Print program version
-V, --version Print program version
` `

3
demo/Makefile Normal file
View File

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

31
demo/demo.sh Executable file
View File

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

4
demo/table.demo1 Normal file
View File

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

3
demo/table.demo2 Normal file
View File

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

54
demo/tables.yaml Normal file
View File

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

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

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

BIN
demo/tablizer-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

61
go.mod
View File

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

125
go.sum
View File

@@ -1,118 +1,69 @@
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw=
github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/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/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.9/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/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tiagomelo/go-clipboard v0.1.2 h1:Ph2icR0vZRIj3v5ExvsGweBwsbbDUTlS6HoF40MkQD8=
github.com/tiagomelo/go-clipboard v0.1.2/go.mod h1:kXtjJBIMimZaGbxmcKZ8+JqK+acSNf5tAJiChlZBOr8=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= github.com/zclconf/go-cty v1.13.3 h1:m+b9q3YDbg6Bec5rr+KGy1MzEVzY/jC2X+YX4yqKtHI=
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty v1.13.3/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -120,18 +71,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -140,14 +89,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 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=

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -27,46 +27,15 @@ import (
) )
/* /*
* [!]Match a line, use fuzzy search for normal pattern strings and * [!]Match a line, use fuzzy search for normal pattern strings and
* regexp otherwise. * regexp otherwise.
*/
'foo bar' foo, /bar/! => false => line contains foo and not (not bar)
'foo nix' foo, /bar/! => ture => line contains foo and (not bar)
'foo bar' foo, /bar/ => true => line contains both foo and bar
'foo nix' foo, /bar/ => false => line does not contain bar
'foo bar' foo, /nix/ => false => line does not contain nix
*/
func matchPattern(conf cfg.Config, line string) bool { func matchPattern(conf cfg.Config, line string) bool {
if len(conf.Patterns) == 0 {
// any line always matches ""
return true
}
if conf.UseFuzzySearch { if conf.UseFuzzySearch {
// fuzzy search only considers the 1st pattern return fuzzy.MatchFold(conf.Pattern, line)
return fuzzy.MatchFold(conf.Patterns[0].Pattern, line)
} }
var match int return conf.PatternR.MatchString(line)
//fmt.Printf("<%s>\n", line)
for _, re := range conf.Patterns {
patmatch := re.PatternRe.MatchString(line)
if re.Negate {
// toggle the meaning of match
patmatch = !patmatch
}
if patmatch {
match++
}
//fmt.Printf("patmatch: %t, match: %d, pattern: %s, negate: %t\n", patmatch, match, re.Pattern, re.Negate)
}
// fmt.Printf("result: %t\n", match == len(conf.Patterns))
//fmt.Println()
return match == len(conf.Patterns)
} }
/* /*
@@ -86,19 +55,15 @@ func FilterByFields(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
keep := true keep := true
for idx, header := range data.headers { for idx, header := range data.headers {
lcheader := strings.ToLower(header) if !Exists(conf.Filters, strings.ToLower(header)) {
if !Exists(conf.Filters, lcheader) {
// do not filter by unspecified field // do not filter by unspecified field
continue continue
} }
match := conf.Filters[lcheader].Regex.MatchString(row[idx]) if !conf.Filters[strings.ToLower(header)].MatchString(row[idx]) {
if conf.Filters[lcheader].Negate { // there IS a filter, but it doesn't match
match = !match
}
if !match {
keep = false keep = false
break break
} }
} }
@@ -158,11 +123,8 @@ func Exists[K comparable, V any](m map[K]V, v K) bool {
return false return false
} }
/*
* Filters the whole input lines, returns filtered lines
*/
func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) { func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
if len(conf.Patterns) == 0 { if conf.Pattern == "" {
return input, nil return input, nil
} }
@@ -174,7 +136,7 @@ func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
if hadFirst { if hadFirst {
// don't match 1st line, it's the header // don't match 1st line, it's the header
if matchPattern(conf, line) == conf.InvertMatch { if conf.Pattern != "" && matchPattern(conf, line) == conf.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,

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2024-2025 Thomas von Dein Copyright © 2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -19,29 +19,29 @@ package lib
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
) )
func TestMatchPattern(t *testing.T) { func TestMatchPattern(t *testing.T) {
var input = []struct { var input = []struct {
name string name string
fuzzy bool fuzzy bool
patterns []*cfg.Pattern pattern string
line string line string
}{ }{
{ {
name: "normal", name: "normal",
patterns: []*cfg.Pattern{{Pattern: "haus"}}, pattern: "haus",
line: "hausparty", line: "hausparty",
}, },
{ {
name: "fuzzy", name: "fuzzy",
patterns: []*cfg.Pattern{{Pattern: "hpt"}}, pattern: "hpt",
line: "haus-party-termin", line: "haus-party-termin",
fuzzy: true, fuzzy: true,
}, },
} }
@@ -55,12 +55,14 @@ func TestMatchPattern(t *testing.T) {
conf.UseFuzzySearch = true conf.UseFuzzySearch = true
} }
err := conf.PreparePattern(inputdata.patterns) err := conf.PreparePattern(inputdata.pattern)
if err != nil {
t.Errorf("PreparePattern returned error: %s", err)
}
assert.NoError(t, err) if !matchPattern(conf, inputdata.line) {
t.Errorf("matchPattern() did not match\nExp: true\nGot: false\n")
res := matchPattern(conf, inputdata.line) }
assert.EqualValues(t, true, res)
}) })
} }
} }
@@ -96,20 +98,6 @@ func TestFilterByFields(t *testing.T) {
}, },
}, },
{
name: "one-field-negative",
filter: []string{"one!=asd"},
expect: Tabdata{
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{"19191", "EDD 1", "x"},
{"8d8", "AN 1", "y"},
},
},
},
{ {
name: "one-field-inverted", name: "one-field-inverted",
filter: []string{"one=19"}, filter: []string{"one=19"},
@@ -161,12 +149,14 @@ func TestFilterByFields(t *testing.T) {
conf := cfg.Config{Rawfilters: inputdata.filter, InvertMatch: inputdata.invert} conf := cfg.Config{Rawfilters: inputdata.filter, InvertMatch: inputdata.invert}
err := conf.PrepareFilters() err := conf.PrepareFilters()
if err != nil {
assert.NoError(t, err) t.Errorf("PrepareFilters returned error: %s", err)
}
data, _, _ := FilterByFields(conf, &data) data, _, _ := FilterByFields(conf, &data)
if !reflect.DeepEqual(*data, inputdata.expect) {
assert.EqualValues(t, inputdata.expect, *data) t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect)
}
}) })
} }
} }

View File

@@ -77,14 +77,6 @@ func PrepareColumns(conf *cfg.Config, data *Tabdata) error {
conf.UseColumns = usecolumns conf.UseColumns = usecolumns
// -y columns
useyankcolumns, err := PrepareColumnVars(conf.YankColumns, data)
if err != nil {
return err
}
conf.UseYankColumns = useyankcolumns
return nil return nil
} }
@@ -208,13 +200,13 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) {
} }
} }
if conf.Numbering { if conf.NoNumbering {
numberedHeaders = append(numberedHeaders, head)
headlen = len(head)
} else {
numhead := fmt.Sprintf("%s(%d)", head, idx+1) numhead := fmt.Sprintf("%s(%d)", head, idx+1)
headlen = len(numhead) headlen = len(numhead)
numberedHeaders = append(numberedHeaders, numhead) numberedHeaders = append(numberedHeaders, numhead)
} else {
numberedHeaders = append(numberedHeaders, head)
headlen = len(head)
} }
if headlen > maxwidth { if headlen > maxwidth {
@@ -254,6 +246,17 @@ func reduceColumns(conf cfg.Config, data *Tabdata) {
} }
} }
// FIXME: remove this when we only use Tablewriter and strip in ParseFile()!
func trimRow(row []string) []string {
var fixedrow = make([]string, len(row))
for idx, cell := range row {
fixedrow[idx] = strings.TrimSpace(cell)
}
return fixedrow
}
// FIXME: refactor this beast! // FIXME: refactor this beast!
func colorizeData(conf cfg.Config, output string) string { func colorizeData(conf cfg.Config, output string) string {
switch { switch {
@@ -290,20 +293,12 @@ func colorizeData(conf cfg.Config, output string) string {
return colorized return colorized
case len(conf.Patterns) > 0 && !conf.NoColor && color.IsConsole(os.Stdout): case len(conf.Pattern) > 0 && !conf.NoColor && color.IsConsole(os.Stdout):
out := output r := regexp.MustCompile("(" + conf.Pattern + ")")
for _, re := range conf.Patterns { return r.ReplaceAllStringFunc(output, func(in string) string {
if !re.Negate { return conf.ColorStyle.Sprint(in)
r := regexp.MustCompile("(" + re.Pattern + ")") })
out = r.ReplaceAllStringFunc(out, func(in string) string {
return conf.ColorStyle.Sprint(in)
})
}
}
return out
default: default:
return output return output

View File

@@ -19,9 +19,9 @@ package lib
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
) )
@@ -39,8 +39,9 @@ func TestContains(t *testing.T) {
testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want) testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
answer := contains(tt.list, tt.search) answer := contains(tt.list, tt.search)
if answer != tt.want {
assert.EqualValues(t, tt.want, answer) t.Errorf("got %t, want %t", answer, tt.want)
}
}) })
} }
} }
@@ -76,12 +77,14 @@ func TestPrepareColumns(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
conf := cfg.Config{Columns: testdata.input} conf := cfg.Config{Columns: testdata.input}
err := PrepareColumns(&conf, &data) err := PrepareColumns(&conf, &data)
if err != nil {
if testdata.wanterror { if !testdata.wanterror {
assert.Error(t, err) t.Errorf("got error: %v", err)
}
} else { } else {
assert.NoError(t, err) if !reflect.DeepEqual(conf.UseColumns, testdata.exp) {
assert.EqualValues(t, testdata.exp, conf.UseColumns) t.Errorf("got: %v, expected: %v", conf.UseColumns, testdata.exp)
}
} }
}) })
} }
@@ -150,13 +153,18 @@ func TestPrepareTransposerColumns(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
conf := cfg.Config{TransposeColumns: testdata.input, Transposers: testdata.transp} conf := cfg.Config{TransposeColumns: testdata.input, Transposers: testdata.transp}
err := PrepareTransposerColumns(&conf, &data) err := PrepareTransposerColumns(&conf, &data)
if err != nil {
if testdata.wanterror { if !testdata.wanterror {
assert.Error(t, err) t.Errorf("got error: %v", err)
}
} else { } else {
assert.NoError(t, err) if len(conf.UseTransposeColumns) != testdata.exp {
assert.EqualValues(t, testdata.exp, len(conf.UseTransposeColumns)) t.Errorf("got %d, want %d", conf.UseTransposeColumns, testdata.exp)
assert.EqualValues(t, len(conf.UseTransposeColumns), len(conf.Transposers)) }
if len(conf.Transposers) != len(conf.UseTransposeColumns) {
t.Errorf("got %d, want %d", conf.UseTransposeColumns, testdata.exp)
}
} }
}) })
} }
@@ -194,8 +202,10 @@ func TestReduceColumns(t *testing.T) {
c := cfg.Config{Columns: "x", UseColumns: testdata.columns} c := cfg.Config{Columns: "x", UseColumns: testdata.columns}
data := Tabdata{entries: input} data := Tabdata{entries: input}
reduceColumns(c, &data) reduceColumns(c, &data)
if !reflect.DeepEqual(data.entries, testdata.expect) {
assert.EqualValues(t, testdata.expect, data.entries) t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v",
data.entries, testdata.expect)
}
}) })
} }
} }
@@ -206,25 +216,27 @@ func TestNumberizeHeaders(t *testing.T) {
} }
var tests = []struct { var tests = []struct {
expect []string expect []string
columns []int columns []int
numberize bool nonum bool
}{ }{
{[]string{"ONE(1)", "TWO(2)", "THREE(3)"}, []int{1, 2, 3}, true}, {[]string{"ONE(1)", "TWO(2)", "THREE(3)"}, []int{1, 2, 3}, false},
{[]string{"ONE(1)", "TWO(2)"}, []int{1, 2}, true}, {[]string{"ONE(1)", "TWO(2)"}, []int{1, 2}, false},
{[]string{"ONE", "TWO"}, []int{1, 2}, false}, {[]string{"ONE", "TWO"}, []int{1, 2}, true},
} }
for _, testdata := range tests { for _, testdata := range tests {
testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t",
testdata.columns, testdata.numberize) testdata.columns, testdata.nonum)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, Numbering: testdata.numberize} conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, NoNumbering: testdata.nonum}
usedata := data usedata := data
numberizeAndReduceHeaders(conf, &usedata) numberizeAndReduceHeaders(conf, &usedata)
if !reflect.DeepEqual(usedata.headers, testdata.expect) {
assert.EqualValues(t, testdata.expect, usedata.headers) t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v",
usedata.headers, testdata.expect)
}
}) })
} }
} }

View File

@@ -29,13 +29,13 @@ import (
const RWRR = 0755 const RWRR = 0755
func ProcessFiles(conf *cfg.Config, args []string) error { func ProcessFiles(conf *cfg.Config, args []string) error {
fd, patterns, err := determineIO(conf, args) fd, pattern, err := determineIO(conf, args)
if err != nil { if err != nil {
return err return err
} }
if err := conf.PreparePattern(patterns); err != nil { if err := conf.PreparePattern(pattern); err != nil {
return err return err
} }
@@ -58,23 +58,14 @@ func ProcessFiles(conf *cfg.Config, args []string) error {
return err return err
} }
if conf.Interactive {
newdata, err := tableEditor(conf, &data)
if err != nil {
return err
}
data = *newdata
}
printData(os.Stdout, *conf, &data) printData(os.Stdout, *conf, &data)
return nil return nil
} }
func determineIO(conf *cfg.Config, args []string) (io.Reader, []*cfg.Pattern, error) { func determineIO(conf *cfg.Config, args []string) (io.Reader, string, error) {
var filehandle io.Reader var filehandle io.Reader
var patterns []*cfg.Pattern var pattern string
var haveio bool var haveio bool
switch { switch {
@@ -85,7 +76,7 @@ func determineIO(conf *cfg.Config, args []string) (io.Reader, []*cfg.Pattern, er
fd, err := os.OpenFile(conf.InputFile, os.O_RDONLY, RWRR) fd, err := os.OpenFile(conf.InputFile, os.O_RDONLY, RWRR)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to read input file %s: %w", conf.InputFile, err) return nil, "", fmt.Errorf("failed to read input file %s: %w", conf.InputFile, err)
} }
filehandle = fd filehandle = fd
@@ -102,15 +93,13 @@ func determineIO(conf *cfg.Config, args []string) (io.Reader, []*cfg.Pattern, er
} }
if len(args) > 0 { if len(args) > 0 {
patterns = make([]*cfg.Pattern, len(args)) pattern = args[0]
for i, arg := range args { conf.Pattern = args[0]
patterns[i] = &cfg.Pattern{Pattern: arg}
}
} }
if !haveio { if !haveio {
return nil, nil, errors.New("no file specified and nothing to read on stdin") return nil, "", errors.New("no file specified and nothing to read on stdin")
} }
return filehandle, patterns, nil return filehandle, pattern, nil
} }

View File

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

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022-2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -20,11 +20,8 @@ package lib
import ( import (
"bufio" "bufio"
"encoding/csv" "encoding/csv"
"encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"log"
"regexp" "regexp"
"strings" "strings"
@@ -42,8 +39,6 @@ func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) {
// first step, parse the data // first step, parse the data
if len(conf.Separator) == 1 { if len(conf.Separator) == 1 {
data, err = parseCSV(conf, input) data, err = parseCSV(conf, input)
} else if conf.InputJSON {
data, err = parseJSON(conf, input)
} else { } else {
data, err = parseTabular(conf, input) data, err = parseTabular(conf, input)
} }
@@ -142,7 +137,7 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
} }
} else { } else {
// data processing // data processing
if matchPattern(conf, line) == conf.InvertMatch { if conf.Pattern != "" && matchPattern(conf, line) == conf.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,
@@ -177,109 +172,6 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) {
return data, nil return data, nil
} }
/*
Parse JSON input. We only support an array of maps.
*/
func parseRawJSON(conf cfg.Config, input io.Reader) (Tabdata, error) {
dec := json.NewDecoder(input)
headers := []string{}
idxmap := map[string]int{}
data := [][]string{}
row := []string{}
iskey := true
haveheaders := false
var currentfield string
var idx int
var isjson bool
for {
t, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
switch val := t.(type) {
case string:
if iskey {
if !haveheaders {
// consider only the keys of the first item as headers
headers = append(headers, val)
}
currentfield = val
} else {
if !haveheaders {
// the first row uses the order as it comes in
row = append(row, val)
} else {
// use the pre-determined order, that way items
// can be in any order as long as they contain all
// neccessary fields. They may also contain less
// fields than the first item, these will contain
// the empty string
row[idxmap[currentfield]] = val
}
}
case json.Delim:
if val.String() == "}" {
data = append(data, row)
row = make([]string, len(headers))
idx++
if !haveheaders {
// remember the array position of header fields,
// which we use to assign elements to the correct
// row index
for i, header := range headers {
idxmap[header] = i
}
}
haveheaders = true
}
isjson = true
}
iskey = !iskey
}
if isjson && (len(headers) == 0 || len(data) == 0) {
return Tabdata{}, errors.New("failed to parse JSON, input did not contain array of hashes")
}
return Tabdata{headers: headers, entries: data, columns: len(headers)}, nil
}
func parseJSON(conf cfg.Config, input io.Reader) (Tabdata, error) {
// parse raw json
data, err := parseRawJSON(conf, input)
if err != nil {
return data, err
}
// apply filter, if any
filtered := [][]string{}
var line string
for _, row := range data.entries {
line = strings.Join(row, " ")
if matchPattern(conf, line) == conf.InvertMatch {
continue
}
filtered = append(filtered, row)
}
if len(filtered) != len(data.entries) {
data.entries = filtered
}
return data, nil
}
func PostProcess(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) { func PostProcess(conf cfg.Config, data *Tabdata) (*Tabdata, bool, error) {
var modified bool var modified bool

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -19,11 +19,10 @@ package lib
import ( import (
"fmt" "fmt"
"io" "reflect"
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
) )
@@ -68,61 +67,66 @@ func TestParser(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
readFd := strings.NewReader(strings.TrimSpace(testdata.text)) readFd := strings.NewReader(strings.TrimSpace(testdata.text))
conf := cfg.Config{Separator: testdata.separator} conf := cfg.Config{Separator: testdata.separator}
gotdata, err := wrapValidateParser(conf, readFd) gotdata, err := Parse(conf, readFd)
assert.NoError(t, err) if err != nil {
assert.EqualValues(t, data, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
}
if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n",
data, gotdata)
}
}) })
} }
} }
func TestParserPatternmatching(t *testing.T) { func TestParserPatternmatching(t *testing.T) {
var tests = []struct { var tests = []struct {
name string entries [][]string
entries [][]string pattern string
patterns []*cfg.Pattern invert bool
invert bool want bool
wanterror bool
}{ }{
{ {
name: "match",
entries: [][]string{ entries: [][]string{
{"asd", "igig", "cxxxncnc"}, {"asd", "igig", "cxxxncnc"},
}, },
patterns: []*cfg.Pattern{{Pattern: "ig"}}, pattern: "ig",
invert: false, invert: false,
}, },
{ {
name: "invert",
entries: [][]string{ entries: [][]string{
{"19191", "EDD 1", "X"}, {"19191", "EDD 1", "X"},
}, },
patterns: []*cfg.Pattern{{Pattern: "ig"}}, pattern: "ig",
invert: true, invert: true,
}, },
} }
for _, inputdata := range input { for _, inputdata := range input {
for _, testdata := range tests { for _, testdata := range tests {
testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t", testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
inputdata.name, testdata.name, testdata.invert) inputdata.name, testdata.pattern, testdata.invert)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
conf := cfg.Config{ conf := cfg.Config{InvertMatch: testdata.invert, Pattern: testdata.pattern,
InvertMatch: testdata.invert, Separator: inputdata.separator}
Patterns: testdata.patterns,
Separator: inputdata.separator,
}
_ = conf.PreparePattern(testdata.patterns) _ = conf.PreparePattern(testdata.pattern)
readFd := strings.NewReader(strings.TrimSpace(inputdata.text)) readFd := strings.NewReader(strings.TrimSpace(inputdata.text))
data, err := wrapValidateParser(conf, readFd) gotdata, err := Parse(conf, readFd)
if testdata.wanterror { if err != nil {
assert.Error(t, err) if !testdata.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
}
} else { } else {
assert.NoError(t, err) if !reflect.DeepEqual(testdata.entries, gotdata.entries) {
assert.EqualValues(t, testdata.entries, data.entries) t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n",
testdata.pattern, testdata.invert, testdata.entries, gotdata.entries)
}
} }
}) })
} }
@@ -149,147 +153,14 @@ asd igig
readFd := strings.NewReader(strings.TrimSpace(table)) readFd := strings.NewReader(strings.TrimSpace(table))
conf := cfg.Config{Separator: cfg.DefaultSeparator} conf := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := wrapValidateParser(conf, readFd) gotdata, err := Parse(conf, readFd)
assert.NoError(t, err)
assert.EqualValues(t, data, gotdata)
}
func TestParserJSONInput(t *testing.T) {
var tests = []struct {
name string
input string
expect Tabdata
wanterror bool // true: expect fail, false: expect success
}{
{
// too deep nesting
name: "invalidjson",
wanterror: true,
input: `[
{
"item": {
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
}
}
`,
expect: Tabdata{},
},
{
// one field missing + different order
// but shall not fail
name: "kgpfail",
wanterror: false,
input: `[
{
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
},
{
"NAME": "wal-g-exporter-778dcd95f5-wcjzn",
"RESTARTS": "0",
"READY": "1/1",
"AGE": "24h"
}
]`,
expect: Tabdata{
columns: 5,
headers: []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"},
entries: [][]string{
[]string{
"postgres-operator-7f4c7c8485-ntlns",
"1/1",
"Running",
"0",
"24h",
},
[]string{
"wal-g-exporter-778dcd95f5-wcjzn",
"1/1",
"",
"0",
"24h",
},
},
},
},
{
name: "kgp",
wanterror: false,
input: `[
{
"NAME": "postgres-operator-7f4c7c8485-ntlns",
"READY": "1/1",
"STATUS": "Running",
"RESTARTS": "0",
"AGE": "24h"
},
{
"NAME": "wal-g-exporter-778dcd95f5-wcjzn",
"STATUS": "Running",
"READY": "1/1",
"RESTARTS": "0",
"AGE": "24h"
}
]`,
expect: Tabdata{
columns: 5,
headers: []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"},
entries: [][]string{
[]string{
"postgres-operator-7f4c7c8485-ntlns",
"1/1",
"Running",
"0",
"24h",
},
[]string{
"wal-g-exporter-778dcd95f5-wcjzn",
"1/1",
"Running",
"0",
"24h",
},
},
},
},
}
for _, testdata := range tests {
testname := fmt.Sprintf("parse-json-%s", testdata.name)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{InputJSON: true}
readFd := strings.NewReader(strings.TrimSpace(testdata.input))
data, err := wrapValidateParser(conf, readFd)
if testdata.wanterror {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.EqualValues(t, testdata.expect, data)
}
})
}
}
func wrapValidateParser(conf cfg.Config, input io.Reader) (Tabdata, error) {
data, err := Parse(conf, input)
if err != nil { if err != nil {
return data, err t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata)
} }
err = ValidateConsistency(&data) if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n",
return data, err conf.Separator, data, gotdata)
}
} }

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2022-2025 Thomas von Dein Copyright © 2022 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -22,13 +22,12 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -39,9 +38,6 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
// by, independently if it's being used for display or not. // by, independently if it's being used for display or not.
sortTable(conf, data) sortTable(conf, data)
// put one or more columns into clipboard
yankColumns(conf, data)
// add numbers to headers and remove those we're not interested in // add numbers to headers and remove those we're not interested in
numberizeAndReduceHeaders(conf, data) numberizeAndReduceHeaders(conf, data)
@@ -62,7 +58,7 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) {
case cfg.Yaml: case cfg.Yaml:
printYamlData(writer, data) printYamlData(writer, data)
case cfg.CSV: case cfg.CSV:
printCSVData(writer, conf, data) printCSVData(writer, data)
default: default:
printASCIIData(writer, conf, data) printASCIIData(writer, conf, data)
} }
@@ -77,58 +73,36 @@ Emacs org-mode compatible table (also orgtbl-mode)
*/ */
func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) { func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(
tw.Rendition{
Borders: tw.Border{
Left: tw.On,
Right: tw.On,
Top: tw.On,
Bottom: tw.On,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.Off,
BetweenRows: tw.Off,
BetweenColumns: 0,
},
},
Symbols: tw.NewSymbols(tw.StyleASCII),
})),
tablewriter.WithConfig(
tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
AutoFormat: tw.Off,
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
},
},
},
),
)
if !conf.NoHeaders { if !conf.NoHeaders {
table.Header(data.headers) table.SetHeader(data.headers)
} }
if err := table.Bulk(data.entries); err != nil { for _, row := range data.entries {
log.Fatalf("Failed to add data to table renderer: %s", err) table.Append(trimRow(row))
} }
if err := table.Render(); err != nil { table.Render()
log.Fatalf("Failed to render table: %s", err)
}
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) /* fix output for org-mode (orgtbl)
tableWriter output:
+------+------+
| cell | cell |
+------+------+
Needed for org-mode compatibility:
|------+------|
| cell | cell |
|------+------|
*/
leftR := regexp.MustCompile(`(?m)^\\+`)
rightR := regexp.MustCompile(`\\+(?m)$`)
output(writer, color.Sprint(
colorizeData(conf,
rightR.ReplaceAllString(
leftR.ReplaceAllString(tableString.String(), "|"), "|"))))
} }
/* /*
@@ -136,57 +110,20 @@ Markdown table
*/ */
func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) { func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(
tw.Rendition{
Borders: tw.Border{
Left: tw.On,
Right: tw.On,
Top: tw.Off,
Bottom: tw.Off,
},
Settings: tw.Settings{
Separators: tw.Separators{
ShowHeader: tw.On,
ShowFooter: tw.Off,
BetweenRows: tw.Off,
BetweenColumns: 0,
},
},
Symbols: tw.NewSymbols(tw.StyleMarkdown),
})),
tablewriter.WithConfig(
tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
AutoFormat: tw.Off,
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
Alignment: tw.AlignLeft,
},
},
},
),
)
if !conf.NoHeaders { if !conf.NoHeaders {
table.Header(data.headers) table.SetHeader(data.headers)
} }
if err := table.Bulk(data.entries); err != nil { for _, row := range data.entries {
log.Fatalf("Failed to add data to table renderer: %s", err) table.Append(trimRow(row))
} }
if err := table.Render(); err != nil { table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
log.Fatalf("Failed to render table: %s", err) table.SetCenterSeparator("|")
}
table.Render()
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) output(writer, color.Sprint(colorizeData(conf, tableString.String())))
} }
@@ -194,56 +131,34 @@ func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) {
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(writer io.Writer, conf cfg.Config, data *Tabdata) { func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) {
OFS := " "
if conf.OFS != "" {
OFS = conf.OFS
}
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
styleTSV := tw.NewSymbolCustom("space").WithColumn("\t")
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Symbols: styleTSV,
Settings: tw.Settings{
Separators: tw.SeparatorsNone,
Lines: tw.LinesNone,
},
})),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
Padding: tw.CellPadding{Global: tw.Padding{Left: "", Right: OFS}},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
Alignment: tw.AlignLeft,
},
Padding: tw.CellPadding{Global: tw.Padding{Right: OFS}},
},
Debug: true,
}),
)
if !conf.NoHeaders { if !conf.NoHeaders {
table.Header(data.headers) table.SetHeader(data.headers)
} }
if err := table.Bulk(data.entries); err != nil { table.AppendBulk(data.entries)
log.Fatalf("Failed to add data to table renderer: %s", err)
} table.SetAutoWrapText(false)
table.SetAutoFormatHeaders(true)
if err := table.Render(); err != nil { table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
log.Fatalf("Failed to render table: %s", err) table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetNoWhiteSpace(true)
if !conf.UseHighlight {
// the tabs destroy the highlighting
table.SetTablePadding("\t") // pad with tabs
} else {
table.SetTablePadding(" ")
} }
table.Render()
output(writer, color.Sprint(colorizeData(conf, tableString.String()))) output(writer, color.Sprint(colorizeData(conf, tableString.String())))
} }
@@ -328,14 +243,8 @@ func printYamlData(writer io.Writer, data *Tabdata) {
output(writer, string(yamlstr)) output(writer, string(yamlstr))
} }
func printCSVData(writer io.Writer, conf cfg.Config, data *Tabdata) { func printCSVData(writer io.Writer, data *Tabdata) {
OFS := ","
if conf.OFS != "" {
OFS = conf.OFS
}
csvout := csv.NewWriter(writer) csvout := csv.NewWriter(writer)
csvout.Comma = []rune(OFS)[0]
if err := csvout.Write(data.headers); err != nil { if err := csvout.Write(data.headers); err != nil {
log.Fatalln("error writing record to csv:", err) log.Fatalln("error writing record to csv:", err)

View File

@@ -23,7 +23,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
) )
@@ -66,7 +65,7 @@ var tests = []struct {
sortby string // empty == default sortby string // empty == default
column int // sort by this column (numbers start by 1) column int // sort by this column (numbers start by 1)
desc bool // sort in descending order, default == ascending desc bool // sort in descending order, default == ascending
numberize bool // add header numbering nonum bool // hide numbering
mode int // shell, orgtbl, etc. empty == default: ascii mode int // shell, orgtbl, etc. empty == default: ascii
usecol []int // columns to display, empty == display all usecol []int // columns to display, empty == display all
usecolstr string // for testname, must match usecol usecolstr string // for testname, must match usecol
@@ -74,19 +73,17 @@ var tests = []struct {
}{ }{
// --------------------- Default settings mode tests `` // --------------------- Default settings mode tests ``
{ {
mode: cfg.ASCII, mode: cfg.ASCII,
numberize: true, name: "default",
name: "default",
expect: ` expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4) NAME(1) DURATION(2) COUNT(3) WHEN(4)
beta 1d10h5m1s 33 3/1/2014 beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03 alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
}, },
{ {
mode: cfg.CSV, mode: cfg.CSV,
numberize: false, name: "csv",
name: "csv",
expect: ` expect: `
NAME,DURATION,COUNT,WHEN NAME,DURATION,COUNT,WHEN
beta,1d10h5m1s,33,3/1/2014 beta,1d10h5m1s,33,3/1/2014
@@ -94,42 +91,40 @@ alpha,4h35m,170,2013-Feb-03
ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`, ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "orgtbl", name: "default",
numberize: true, mode: cfg.Orgtbl,
mode: cfg.Orgtbl,
expect: ` expect: `
+---------+-------------+----------+----------------------------+ +---------+-------------+----------+----------------------------+
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | | NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
+---------+-------------+----------+----------------------------+ +---------+-------------+----------+----------------------------+
| beta | 1d10h5m1s | 33 | 3/1/2014 | | beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 | | alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 | | ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |
+---------+-------------+----------+----------------------------+`, +---------+-------------+----------+----------------------------+`,
}, },
{ {
name: "markdown", name: "default",
mode: cfg.Markdown, mode: cfg.Markdown,
numberize: true,
expect: ` expect: `
| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | | NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) |
|---------|-------------|----------|----------------------------| |---------|-------------|----------|----------------------------|
| beta | 1d10h5m1s | 33 | 3/1/2014 | | beta | 1d10h5m1s | 33 | 3/1/2014 |
| alpha | 4h35m | 170 | 2013-Feb-03 | | alpha | 4h35m | 170 | 2013-Feb-03 |
| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`, | ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`,
}, },
{ {
name: "shell", name: "default",
mode: cfg.Shell, mode: cfg.Shell,
numberize: false, nonum: true,
expect: ` expect: `
NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014" NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014"
NAME="alpha" DURATION="4h35m" COUNT="170" WHEN="2013-Feb-03" 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="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`,
}, },
{ {
name: "yaml", name: "default",
mode: cfg.Yaml, mode: cfg.Yaml,
numberize: false, nonum: true,
expect: ` expect: `
entries: entries:
- count: 33 - count: 33
@@ -146,9 +141,8 @@ entries:
when: "06/Jan/2008 15:04:05 -0700"`, when: "06/Jan/2008 15:04:05 -0700"`,
}, },
{ {
name: "extended", name: "default",
mode: cfg.Extended, mode: cfg.Extended,
numberize: true,
expect: ` expect: `
NAME(1): beta NAME(1): beta
DURATION(2): 1d10h5m1s DURATION(2): 1d10h5m1s
@@ -168,58 +162,53 @@ DURATION(2): 33d12h
//------------------------ SORT TESTS //------------------------ SORT TESTS
{ {
name: "sortbycolumn3", name: "sortbycolumn3",
column: 3, column: 3,
sortby: "numeric", sortby: "numeric",
numberize: true, desc: false,
desc: false,
expect: ` expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4) NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700 ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
beta 1d10h5m1s 33 3/1/2014 beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03`, alpha 4h35m 170 2013-Feb-03`,
}, },
{ {
name: "sortbycolumn4", name: "sortbycolumn4",
column: 4, column: 4,
sortby: "time", sortby: "time",
desc: false, desc: false,
numberize: true,
expect: ` expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4) NAME(1) DURATION(2) COUNT(3) WHEN(4)
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700 ceta 33d12h 9 06/Jan/2008 15:04:05 -0700
alpha 4h35m 170 2013-Feb-03 alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014`, beta 1d10h5m1s 33 3/1/2014`,
}, },
{ {
name: "sortbycolumn2", name: "sortbycolumn2",
column: 2, column: 2,
sortby: "duration", sortby: "duration",
numberize: true, desc: false,
desc: false,
expect: ` expect: `
NAME(1) DURATION(2) COUNT(3) WHEN(4) NAME(1) DURATION(2) COUNT(3) WHEN(4)
alpha 4h35m 170 2013-Feb-03 alpha 4h35m 170 2013-Feb-03
beta 1d10h5m1s 33 3/1/2014 beta 1d10h5m1s 33 3/1/2014
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
}, },
// ----------------------- UseColumns Tests // ----------------------- UseColumns Tests
{ {
name: "usecolumns", name: "usecolumns",
usecol: []int{1, 4}, usecol: []int{1, 4},
numberize: true,
usecolstr: "1,4", usecolstr: "1,4",
expect: ` expect: `
NAME(1) WHEN(4) NAME(1) WHEN(4)
beta 3/1/2014 beta 3/1/2014
alpha 2013-Feb-03 alpha 2013-Feb-03
ceta 06/Jan/2008 15:04:05 -0700`, ceta 06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "usecolumns2", name: "usecolumns",
usecol: []int{2}, usecol: []int{2},
numberize: true,
usecolstr: "2", usecolstr: "2",
expect: ` expect: `
DURATION(2) DURATION(2)
@@ -228,9 +217,8 @@ DURATION(2)
33d12h`, 33d12h`,
}, },
{ {
name: "usecolumns3", name: "usecolumns",
usecol: []int{3}, usecol: []int{3},
numberize: true,
usecolstr: "3", usecolstr: "3",
expect: ` expect: `
COUNT(3) COUNT(3)
@@ -239,36 +227,32 @@ COUNT(3)
9`, 9`,
}, },
{ {
name: "usecolumns4", name: "usecolumns",
column: 0, column: 0,
usecol: []int{1, 3}, usecol: []int{1, 3},
numberize: true,
usecolstr: "1,3", usecolstr: "1,3",
expect: ` expect: `
NAME(1) COUNT(3) NAME(1) COUNT(3)
beta 33 beta 33
alpha 170 alpha 170
ceta 9`, ceta 9`,
}, },
{ {
name: "usecolumns", name: "usecolumns",
usecol: []int{2, 4}, usecol: []int{2, 4},
numberize: true,
usecolstr: "2,4", usecolstr: "2,4",
expect: ` expect: `
DURATION(2) WHEN(4) DURATION(2) WHEN(4)
1d10h5m1s 3/1/2014 1d10h5m1s 3/1/2014
4h35m 2013-Feb-03 4h35m 2013-Feb-03
33d12h 06/Jan/2008 15:04:05 -0700`, 33d12h 06/Jan/2008 15:04:05 -0700`,
}, },
} }
func TestPrinter(t *testing.T) { func TestPrinter(t *testing.T) {
for _, testdata := range tests { for _, testdata := range tests {
testname := fmt.Sprintf("print-%s-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s-numberize-%t", testname := fmt.Sprintf("print-%s-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s",
testdata.name, testdata.column, testdata.desc, testdata.sortby, testdata.name, testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr)
testdata.mode, testdata.usecolstr, testdata.numberize)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
// replaces os.Stdout, but we ignore it // replaces os.Stdout, but we ignore it
var writer bytes.Buffer var writer bytes.Buffer
@@ -278,14 +262,9 @@ func TestPrinter(t *testing.T) {
SortDescending: testdata.desc, SortDescending: testdata.desc,
SortMode: testdata.sortby, SortMode: testdata.sortby,
OutputMode: testdata.mode, OutputMode: testdata.mode,
Numbering: testdata.numberize, NoNumbering: testdata.nonum,
UseColumns: testdata.usecol, UseColumns: testdata.usecol,
NoColor: true, NoColor: true,
OFS: " ",
}
if conf.OutputMode == cfg.CSV {
conf.OFS = ","
} }
if testdata.column > 0 { if testdata.column > 0 {
@@ -308,7 +287,10 @@ func TestPrinter(t *testing.T) {
got := strings.TrimSpace(writer.String()) got := strings.TrimSpace(writer.String())
assert.EqualValues(t, exp, got) if got != exp {
t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s",
got, exp)
}
}) })
} }
} }

View File

@@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/cfg"
) )
@@ -42,7 +41,9 @@ func TestDuration2Seconds(t *testing.T) {
testname := fmt.Sprintf("duration-%s", testdata.dur) testname := fmt.Sprintf("duration-%s", testdata.dur)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
seconds := duration2int(testdata.dur) seconds := duration2int(testdata.dur)
assert.EqualValues(t, testdata.expect, seconds) if seconds != testdata.expect {
t.Errorf("got %d, want %d", seconds, testdata.expect)
}
}) })
} }
} }
@@ -73,7 +74,9 @@ func TestCompare(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc} c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc}
got := compare(&c, testdata.a, testdata.b) got := compare(&c, testdata.a, testdata.b)
assert.EqualValues(t, testdata.want, got) if got != testdata.want {
t.Errorf("got %d, want %d", got, testdata.want)
}
}) })
} }
} }

View File

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

View File

@@ -1,51 +0,0 @@
/*
Copyright © 2022-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"log"
"strings"
"github.com/tiagomelo/go-clipboard/clipboard"
"github.com/tlinden/tablizer/cfg"
)
func yankColumns(conf cfg.Config, data *Tabdata) {
var yank []string
if len(data.entries) == 0 || len(conf.UseYankColumns) == 0 {
return
}
for _, row := range data.entries {
for i, field := range row {
for _, idx := range conf.UseYankColumns {
if i == idx-1 {
yank = append(yank, field)
}
}
}
}
if len(yank) > 0 {
cb := clipboard.New(clipboard.ClipboardOptions{Primary: true})
if err := cb.CopyText(strings.Join(yank, " ")); err != nil {
log.Fatalln("error writing string to clipboard:", err)
}
}
}

View File

@@ -1,68 +0,0 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package lib
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tiagomelo/go-clipboard/clipboard"
"github.com/tlinden/tablizer/cfg"
)
var yanktests = []struct {
name string
yank []int // -y$colum,$column... after processing
filter string
expect string
}{
{
name: "one",
yank: []int{1},
filter: "beta",
},
}
func DISABLED_TestYankColumns(t *testing.T) {
cb := clipboard.New()
for _, testdata := range yanktests {
testname := fmt.Sprintf("yank-%s-filter-%s",
testdata.name, testdata.filter)
t.Run(testname, func(t *testing.T) {
conf := cfg.Config{
OutputMode: cfg.ASCII,
UseYankColumns: testdata.yank,
NoColor: true,
}
conf.ApplyDefaults()
data := newData() // defined in printer_test.go, reused here
var writer bytes.Buffer
printData(&writer, conf, &data)
got, err := cb.PasteText()
assert.NoError(t, err)
assert.EqualValues(t, testdata.expect, got)
})
}
}

View File

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

View File

@@ -42,15 +42,8 @@ for D in $DIST; do
binfile="releases/${tool}-${os}-${arch}-${version}" binfile="releases/${tool}-${os}-${arch}-${version}"
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"
pie=""
if test "$D" = "linux/amd64"; then
pie="-buildmode=pie"
fi
set -x set -x
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static -w -X 'github.com/tlinden/tablizer/cfg.VERSION=${version}'" --trimpath $pie -o ${binfile} GOOS=${os} GOARCH=${arch} go build -o ${binfile} -ldflags "-X 'github.com/tlinden/tablizer/cfg.VERSION=${version}'"
strip --strip-all ${binfile}
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

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

View File

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

View File

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

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "TABLIZER 1" .IX Title "TABLIZER 1"
.TH TABLIZER 1 "2025-10-01" "1" "User Commands" .TH TABLIZER 1 "2025-01-15" "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
@@ -144,50 +144,44 @@ tablizer \- Manipulate tabular output of other programs
.IX Header "SYNOPSIS" .IX Header "SYNOPSIS"
.Vb 2 .Vb 2
\& Usage: \& Usage:
\& tablizer [regex,...] [\-r file] [flags] \& tablizer [regex] [file, ...] [flags]
\& \&
\& Operational Flags: \& Operational Flags:
\& \-c, \-\-columns string Only show the speficied columns (separated by ,) \& \-c, \-\-columns string Only show the speficied columns (separated by ,)
\& \-v, \-\-invert\-match select non\-matching rows \& \-v, \-\-invert\-match select non\-matching rows
\& \-n, \-\-numbering Enable header numbering \& \-n, \-\-no\-numbering Disable header numbering
\& \-N, \-\-no\-color Disable pattern highlighting \& \-N, \-\-no\-color Disable pattern highlighting
\& \-H, \-\-no\-headers Disable headers display \& \-H, \-\-no\-headers Disable headers display
\& \-s, \-\-separator <string> Custom field separator \& \-s, \-\-separator string Custom field separator
\& \-k, \-\-sort\-by <int|name> Sort by column (default: 1) \& \-k, \-\-sort\-by int|name Sort by column (default: 1)
\& \-z, \-\-fuzzy Use fuzzy search [experimental] \& \-z, \-\-fuzzy Use fuzzy search [experimental]
\& \-F, \-\-filter <field[!]=reg> Filter given field with regex, can be used multiple times \& \-F, \-\-filter field=reg Filter given field with regex, can be used multiple times
\& \-T, \-\-transpose\-columns string Transpose the speficied columns (separated by ,) \& \-T, \-\-transpose\-columns string Transpose the speficied columns (separated by ,)
\& \-R, \-\-regex\-transposer </from/to/> Apply /search/replace/ regexp to fields given in \-T \& \-R, \-\-regex\-transposer /from/to/ Apply /search/replace/ regexp to fields given in \-T
\& \-j, \-\-json Read JSON input (must be array of hashes)
\& \-I, \-\-interactive Interactively filter and select rows
\& \&
\& Output Flags (mutually exclusive): \& Output Flags (mutually exclusive):
\& \-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, \-\-shell Enable shell evaluable output \& \-S, \-\-shell Enable shell evaluable output
\& \-Y, \-\-yaml Enable yaml output \& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output \& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular \& \-A, \-\-ascii Default output mode, ascii tabular
\& \-L, \-\-hightlight\-lines Use alternating background colors for tables \& \-L, \-\-hightlight\-lines Use alternating background colors for tables
\& \-y, \-\-yank\-columns Yank specified columns (separated by ,) to clipboard,
\& space separated
\& \-\-ofs <char> Output field separator, used by \-A and \-C.
\& \&
\& Sort Mode Flags (mutually exclusive): \& Sort Mode Flags (mutually exclusive):
\& \-a, \-\-sort\-age sort according to age (duration) string \& \-a, \-\-sort\-age sort according to age (duration) string
\& \-D, \-\-sort\-desc Sort in descending order (default: ascending) \& \-D, \-\-sort\-desc Sort in descending order (default: ascending)
\& \-i, \-\-sort\-numeric sort according to string numerical value \& \-i, \-\-sort\-numeric sort according to string numerical value
\& \-t, \-\-sort\-time sort according to time string \& \-t, \-\-sort\-time sort according to time string
\& \&
\& Other Flags: \& Other Flags:
\& \-r \-\-read\-file <file> Use <file> as input instead of STDIN \& \-\-completion <shell> Generate the autocompletion script for <shell>
\& \-\-completion <shell> Generate the autocompletion script for <shell> \& \-f, \-\-config <file> Configuration file (default: ~/.config/tablizer/config)
\& \-f, \-\-config <file> Configuration file (default: ~/.config/tablizer/config) \& \-d, \-\-debug Enable debugging
\& \-d, \-\-debug Enable debugging \& \-h, \-\-help help for tablizer
\& \-h, \-\-help help for tablizer \& \-m, \-\-man Display manual page
\& \-m, \-\-man Display manual page \& \-V, \-\-version Print program version
\& \-V, \-\-version Print program version
.Ve .Ve
.SH "DESCRIPTION" .SH "DESCRIPTION"
.IX Header "DESCRIPTION" .IX Header "DESCRIPTION"
@@ -219,17 +213,17 @@ pattern. Hence:
\& kubectl get pods | tablizer \& kubectl get pods | tablizer
\& \&
\& # read a file \& # read a file
\& tablizer \-r filename \& tablizer filename
\& \&
\& # search for pattern in a file (works like grep) \& # search for pattern in a file (works like grep)
\& tablizer regex \-r filename \& tablizer regex filename
\& \&
\& # search for pattern in STDIN \& # search for pattern in STDIN
\& kubectl get pods | tablizer regex \& kubectl get pods | tablizer regex
.Ve .Ve
.PP .PP
The output looks like the original one. You can add the option \fB\-n\fR, The output looks like the original one but every header field will
then every header field will have a numer associated with it, e.g.: have a numer associated with it, e.g.:
.PP .PP
.Vb 1 .Vb 1
\& NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) \& NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5)
@@ -245,18 +239,7 @@ columns you want to have in your output (see \s-1COLUMNS\s0:
You can specify the numbers in any order but output will always follow You can specify the numbers in any order but output will always follow
the original order. the original order.
.PP .PP
However, you may also just use the header names instead of numbers, The numbering can be suppressed by using the \fB\-n\fR option.
eg:
.PP
.Vb 1
\& kubectl get pods | tablizer \-cname,status
.Ve
.PP
You can also use regular expressions with \fB\-c\fR, eg:
.PP
.Vb 1
\& kubectl get pods | tablizer \-c \*(Aq[ae]\*(Aq
.Ve
.PP .PP
By default tablizer shows a header containing the names of each By default tablizer shows a header containing the names of each
column. This can be disabled using the \fB\-H\fR option. Be aware that column. This can be disabled using the \fB\-H\fR option. Be aware that
@@ -295,52 +278,38 @@ Finally the \fB\-d\fR option enables debugging output which is mostly
useful for the developer. useful for the developer.
.SS "\s-1PATTERNS AND FILTERING\s0" .SS "\s-1PATTERNS AND FILTERING\s0"
.IX Subsection "PATTERNS AND FILTERING" .IX Subsection "PATTERNS AND FILTERING"
You can reduce the rows being displayed by using one or more regular You can reduce the rows being displayed by using a regular expression
expression patterns. The regexp language being used is the one of pattern. The regexp is \s-1PCRE\s0 compatible, refer to the syntax cheat
\&\s-1GOLANG,\s0 refer to the syntax cheat sheet here: sheet here: <https://github.com/google/re2/wiki/Syntax>. If you want
<https://pkg.go.dev/regexp/syntax>. to read a more comprehensive documentation about the topic and have
.PP perl installed you can read it with:
If you want to read a more comprehensive documentation about the
topic and have perl installed you can read it with:
.PP .PP
.Vb 1 .Vb 1
\& perldoc perlre \& perldoc perlre
.Ve .Ve
.PP .PP
Or read it online: <https://perldoc.perl.org/perlre>. But please note Or read it online: <https://perldoc.perl.org/perlre>.
that the \s-1GO\s0 regexp engine does \s-1NOT\s0 support all perl regex terms,
especially look-ahead and look-behind.
.PP .PP
If you want to supply flags to a regex, then surround it with slashes A note on modifiers: the regexp engine used in tablizer uses another
and append the flag. The following flags are supported: modifier syntax:
.PP .PP
.Vb 2 .Vb 1
\& i => case insensitive \& (?MODIFIER)
\& ! => negative match
.Ve .Ve
.PP .PP
The most important modifiers are:
.PP
\&\f(CW\*(C`i\*(C'\fR ignore case
\&\f(CW\*(C`m\*(C'\fR multiline mode
\&\f(CW\*(C`s\*(C'\fR single line mode
.PP
Example for a case insensitive search: Example for a case insensitive search:
.PP .PP
.Vb 1 .Vb 1
\& kubectl get pods \-A | tablizer "/account/i" \& kubectl get pods \-A | tablizer "(?i)account"
.Ve .Ve
.PP .PP
If you use the \f(CW\*(C`!\*(C'\fR flag, then the regex match will be negated, that You can use the experimental fuzzy search feature by providing the
is, if a line in the input matches the given regex, but \f(CW\*(C`!\*(C'\fR is
supplied, tablizer will \s-1NOT\s0 include it in the output.
.PP
For example, here we want to get all lines matching \*(L"foo\*(R" but not
\&\*(L"bar\*(R":
.PP
.Vb 1
\& cat table | tablizer foo \*(Aq/bar/!\*(Aq
.Ve
.PP
This would match a line \*(L"foo zorro\*(R" but not \*(L"foo bar\*(R".
.PP
The flags can also be combined.
.PP
You can also use the experimental fuzzy search feature by providing the
option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search
term, not a regexp. term, not a regexp.
.PP .PP
@@ -357,27 +326,7 @@ Fieldnames (== columns headers) are case insensitive.
If you specify more than one filter, both filters have to match (\s-1AND\s0 If you specify more than one filter, both filters have to match (\s-1AND\s0
operation). operation).
.PP .PP
These field filters can also be negated:
.PP
.Vb 1
\& fieldname!=regexp
.Ve
.PP
If the option \fB\-v\fR is specified, the filtering is inverted. If the option \fB\-v\fR is specified, the filtering is inverted.
.SS "\s-1INTERACTIVE FILTERING\s0"
.IX Subsection "INTERACTIVE FILTERING"
You can also use the interactive mode, enabled with \f(CW\*(C`\-I\*(C'\fR to filter
and select rows. This mode is complementary, that is, other filter
options are still being respected.
.PP
To enter e filter, hit \f(CW\*(C`/\*(C'\fR, enter a filter string and finish with
\&\f(CW\*(C`ENTER\*(C'\fR. Use \f(CW\*(C`SPACE\*(C'\fR to select/deselect rows, use \f(CW\*(C`a\*(C'\fR to select all
(visible) rows.
.PP
Commit your selection with \f(CW\*(C`q\*(C'\fR. The selected rows are being fed to
the requested output mode as usual. Abort with \f(CW\*(C`CTRL\-c\*(C'\fR, in which
case the results of the interactive mode are being ignored and all
rows are being fed to output.
.SS "\s-1COLUMNS\s0" .SS "\s-1COLUMNS\s0"
.IX Subsection "COLUMNS" .IX Subsection "COLUMNS"
The parameter \fB\-c\fR can be used to specify, which columns to The parameter \fB\-c\fR can be used to specify, which columns to
@@ -493,27 +442,13 @@ more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
value file. value file.
.SS "\s-1PUT FIELDS TO CLIPBOARD\s0"
.IX Subsection "PUT FIELDS TO CLIPBOARD"
You can let tablizer put fields to the clipboard using the option
\&\f(CW\*(C`\-y\*(C'\fR. This best fits the use-case when the result of your filtering
yields just one row. For example:
.PP
.Vb 1
\& cloudctl cluster ls | tablizer \-yid matchbox
.Ve
.PP
If \*(L"matchbox\*(R" matches one cluster, you can immediately use the id of
that cluster somewhere else and paste it. Of course, if there are
multiple matches, then all id's will be put into the clipboard
separated by one space.
.SS "\s-1ENVIRONMENT VARIABLES\s0" .SS "\s-1ENVIRONMENT VARIABLES\s0"
.IX Subsection "ENVIRONMENT VARIABLES" .IX Subsection "ENVIRONMENT VARIABLES"
\&\fBtablizer\fR supports certain environment variables which use can use \&\fBtablizer\fR supports certain environment variables which use can use
to influence program behavior. Commandline flags have always to influence program behavior. Commandline flags have always
precedence over environment variables. precedence over environment variables.
.IP "<T_HEADER_NUMBERING> \- enable numbering of header fields, like \fB\-n\fR." 4 .IP "<T_NO_HEADER_NUMBERING> \- disable numbering of header fields, like \fB\-n\fR." 4
.IX Item "<T_HEADER_NUMBERING> - enable numbering of header fields, like -n." .IX Item "<T_NO_HEADER_NUMBERING> - disable numbering of header fields, like -n."
.PD 0 .PD 0
.IP "<T_COLUMNS> \- comma separated list of columns to output, like \fB\-c\fR" 4 .IP "<T_COLUMNS> \- comma separated list of columns to output, like \fB\-c\fR" 4
.IX Item "<T_COLUMNS> - comma separated list of columns to output, like -c" .IX Item "<T_COLUMNS> - comma separated list of columns to output, like -c"
@@ -644,9 +579,6 @@ Released under the \s-1MIT\s0 License, Copyright (c) 201 by Oleku Konko
.IP "yaml (gopkg.in/yaml.v3)" 4 .IP "yaml (gopkg.in/yaml.v3)" 4
.IX Item "yaml (gopkg.in/yaml.v3)" .IX Item "yaml (gopkg.in/yaml.v3)"
Released under the \s-1MIT\s0 License, Copyright (c) 2006\-2011 Kirill Simonov Released under the \s-1MIT\s0 License, Copyright (c) 2006\-2011 Kirill Simonov
.IP "bubble-table (https://github.com/Evertras/bubble\-table)" 4
.IX Item "bubble-table (https://github.com/Evertras/bubble-table)"
Released under the \s-1MIT\s0 License, Copyright (c) 2022 Brandon Fulljames
.SH "AUTHORS" .SH "AUTHORS"
.IX Header "AUTHORS" .IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR

View File

@@ -5,50 +5,44 @@ tablizer - Manipulate tabular output of other programs
=head1 SYNOPSIS =head1 SYNOPSIS
Usage: Usage:
tablizer [regex,...] [-r file] [flags] tablizer [regex] [file, ...] [flags]
Operational Flags: Operational Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-v, --invert-match select non-matching rows -v, --invert-match select non-matching rows
-n, --numbering Enable header numbering -n, --no-numbering Disable header numbering
-N, --no-color Disable pattern highlighting -N, --no-color Disable pattern highlighting
-H, --no-headers Disable headers display -H, --no-headers Disable headers display
-s, --separator <string> Custom field separator -s, --separator string Custom field separator
-k, --sort-by <int|name> Sort by column (default: 1) -k, --sort-by int|name Sort by column (default: 1)
-z, --fuzzy Use fuzzy search [experimental] -z, --fuzzy Use fuzzy search [experimental]
-F, --filter <field[!]=reg> Filter given field with regex, can be used multiple times -F, --filter field=reg Filter given field with regex, can be used multiple times
-T, --transpose-columns string Transpose the speficied columns (separated by ,) -T, --transpose-columns string Transpose the speficied columns (separated by ,)
-R, --regex-transposer </from/to/> Apply /search/replace/ regexp to fields given in -T -R, --regex-transposer /from/to/ Apply /search/replace/ regexp to fields given in -T
-j, --json Read JSON input (must be array of hashes)
-I, --interactive Interactively filter and select rows
Output Flags (mutually exclusive): Output Flags (mutually exclusive):
-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, --shell Enable shell evaluable output -S, --shell Enable shell evaluable output
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output -C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
-L, --hightlight-lines Use alternating background colors for tables -L, --hightlight-lines Use alternating background colors for tables
-y, --yank-columns Yank specified columns (separated by ,) to clipboard,
space separated
--ofs <char> Output field separator, used by -A and -C.
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
-a, --sort-age sort according to age (duration) string -a, --sort-age sort according to age (duration) string
-D, --sort-desc Sort in descending order (default: ascending) -D, --sort-desc Sort in descending order (default: ascending)
-i, --sort-numeric sort according to string numerical value -i, --sort-numeric sort according to string numerical value
-t, --sort-time sort according to time string -t, --sort-time sort according to time string
Other Flags: Other Flags:
-r --read-file <file> Use <file> as input instead of STDIN --completion <shell> Generate the autocompletion script for <shell>
--completion <shell> Generate the autocompletion script for <shell> -f, --config <file> Configuration file (default: ~/.config/tablizer/config)
-f, --config <file> Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging
-d, --debug Enable debugging -h, --help help for tablizer
-h, --help help for tablizer -m, --man Display manual page
-m, --man Display manual page -V, --version Print program version
-V, --version Print program version
=head1 DESCRIPTION =head1 DESCRIPTION
@@ -80,16 +74,16 @@ pattern. Hence:
kubectl get pods | tablizer kubectl get pods | tablizer
# read a file # read a file
tablizer -r filename tablizer filename
# search for pattern in a file (works like grep) # search for pattern in a file (works like grep)
tablizer regex -r filename tablizer regex filename
# search for pattern in STDIN # search for pattern in STDIN
kubectl get pods | tablizer regex kubectl get pods | tablizer regex
The output looks like the original one. You can add the option B<-n>, The output looks like the original one but every header field will
then every header field will have a numer associated with it, e.g.: 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)
@@ -101,14 +95,7 @@ columns you want to have in your output (see L<COLUMNS>:
You can specify the numbers in any order but output will always follow You can specify the numbers in any order but output will always follow
the original order. the original order.
However, you may also just use the header names instead of numbers, The numbering can be suppressed by using the B<-n> option.
eg:
kubectl get pods | tablizer -cname,status
You can also use regular expressions with B<-c>, eg:
kubectl get pods | tablizer -c '[ae]'
By default tablizer shows a header containing the names of each By default tablizer shows a header containing the names of each
column. This can be disabled using the B<-H> option. Be aware that column. This can be disabled using the B<-H> option. Be aware that
@@ -155,44 +142,32 @@ useful for the developer.
=head2 PATTERNS AND FILTERING =head2 PATTERNS AND FILTERING
You can reduce the rows being displayed by using one or more regular You can reduce the rows being displayed by using a regular expression
expression patterns. The regexp language being used is the one of pattern. The regexp is PCRE compatible, refer to the syntax cheat
GOLANG, refer to the syntax cheat sheet here: sheet here: L<https://github.com/google/re2/wiki/Syntax>. If you want
L<https://pkg.go.dev/regexp/syntax>. to read a more comprehensive documentation about the topic and have
perl installed you can read it with:
If you want to read a more comprehensive documentation about the
topic and have perl installed you can read it with:
perldoc perlre perldoc perlre
Or read it online: L<https://perldoc.perl.org/perlre>. But please note Or read it online: L<https://perldoc.perl.org/perlre>.
that the GO regexp engine does NOT support all perl regex terms,
especially look-ahead and look-behind.
If you want to supply flags to a regex, then surround it with slashes A note on modifiers: the regexp engine used in tablizer uses another
and append the flag. The following flags are supported: modifier syntax:
i => case insensitive (?MODIFIER)
! => negative match
The most important modifiers are:
C<i> ignore case
C<m> multiline mode
C<s> single line mode
Example for a case insensitive search: Example for a case insensitive search:
kubectl get pods -A | tablizer "/account/i" kubectl get pods -A | tablizer "(?i)account"
If you use the C<!> flag, then the regex match will be negated, that You can use the experimental fuzzy search feature by providing the
is, if a line in the input matches the given regex, but C<!> is
supplied, tablizer will NOT include it in the output.
For example, here we want to get all lines matching "foo" but not
"bar":
cat table | tablizer foo '/bar/!'
This would match a line "foo zorro" but not "foo bar".
The flags can also be combined.
You can also use the experimental fuzzy search feature by providing the
option B<-z>, in which case the pattern is regarded as a fuzzy search option B<-z>, in which case the pattern is regarded as a fuzzy search
term, not a regexp. term, not a regexp.
@@ -207,26 +182,8 @@ Fieldnames (== columns headers) are case insensitive.
If you specify more than one filter, both filters have to match (AND If you specify more than one filter, both filters have to match (AND
operation). operation).
These field filters can also be negated:
fieldname!=regexp
If the option B<-v> is specified, the filtering is inverted. If the option B<-v> is specified, the filtering is inverted.
=head2 INTERACTIVE FILTERING
You can also use the interactive mode, enabled with C<-I> to filter
and select rows. This mode is complementary, that is, other filter
options are still being respected.
To enter e filter, hit C</>, enter a filter string and finish with
C<ENTER>. Use C<SPACE> to select/deselect rows, use C<a> to select all
(visible) rows.
Commit your selection with C<q>. The selected rows are being fed to
the requested output mode as usual. Abort with C<CTRL-c>, in which
case the results of the interactive mode are being ignored and all
rows are being fed to output.
=head2 COLUMNS =head2 COLUMNS
@@ -333,19 +290,6 @@ table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding and CSV mode, which prints a comma separated prints yaml encoding and CSV mode, which prints a comma separated
value file. value file.
=head2 PUT FIELDS TO CLIPBOARD
You can let tablizer put fields to the clipboard using the option
C<-y>. This best fits the use-case when the result of your filtering
yields just one row. For example:
cloudctl cluster ls | tablizer -yid matchbox
If "matchbox" matches one cluster, you can immediately use the id of
that cluster somewhere else and paste it. Of course, if there are
multiple matches, then all id's will be put into the clipboard
separated by one space.
=head2 ENVIRONMENT VARIABLES =head2 ENVIRONMENT VARIABLES
B<tablizer> supports certain environment variables which use can use B<tablizer> supports certain environment variables which use can use
@@ -354,7 +298,7 @@ precedence over environment variables.
=over =over
=item <T_HEADER_NUMBERING> - enable numbering of header fields, like B<-n>. =item <T_NO_HEADER_NUMBERING> - disable numbering of header fields, like B<-n>.
=item <T_COLUMNS> - comma separated list of columns to output, like B<-c> =item <T_COLUMNS> - comma separated list of columns to output, like B<-c>
@@ -490,10 +434,6 @@ Released under the MIT License, Copyright (c) 201 by Oleku Konko
Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov Released under the MIT License, Copyright (c) 2006-2011 Kirill Simonov
=item bubble-table (https://github.com/Evertras/bubble-table)
Released under the MIT License, Copyright (c) 2022 Brandon Fulljames
=back =back
=head1 AUTHORS =head1 AUTHORS

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

View File

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

View File

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

View File

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