File tabloid-0.0.3.obscpio of Package tabloid
07070100000000000041ED00000000000000000000000263FB035300000000000000000000000000000000000000000000001600000000tabloid-0.0.3/.github07070100000001000041ED00000000000000000000000263FB035300000000000000000000000000000000000000000000002000000000tabloid-0.0.3/.github/workflows07070100000002000081A400000000000000000000000163FB035300000275000000000000000000000000000000000000002C00000000tabloid-0.0.3/.github/workflows/release.ymlname: Release
on:
push:
tags:
- "*"
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Test application
run: go test ./...
- name: Release application to Github
uses: goreleaser/goreleaser-action@v3
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
07070100000003000081A400000000000000000000000163FB0353000001A5000000000000000000000000000000000000002C00000000tabloid-0.0.3/.github/workflows/testing.ymlname: Testing
on:
push:
pull_request:
jobs:
test-app:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod
- name: Test application
run: go test ./...
- name: Compile application
run: go build -o tabloid-tmp && rm -rf tabloid-tmp
07070100000004000081A400000000000000000000000163FB035300000006000000000000000000000000000000000000001900000000tabloid-0.0.3/.gitignore*.txt
07070100000005000081A400000000000000000000000163FB0353000002AB000000000000000000000000000000000000001E00000000tabloid-0.0.3/.goreleaser.ymlbuilds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm
- arm64
tags:
- netgo
flags:
- -trimpath
ldflags:
- -s -w -X main.version={{.Version}} -extldflags "-static"
archives:
- name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- tolower .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
07070100000006000081A400000000000000000000000163FB035300001E09000000000000000000000000000000000000001800000000tabloid-0.0.3/README.md# `tabloid` -- your tabulated data's best friend
[](https://github.com/patrickdappollonio/tabloid/releases)
`tabloid` is a weekend project. The goal is to be able to **parse inputs from several command line applications like `kubectl` and `docker` that use a `tabwriter` to format their output**: this is, they write column-based outputs where the first line is the column title -- often uppercased -- and the values come below, and they're often perfectly aligned.
Here's an example: more often than not though, you want one field from that output instead of a tens-of-lines long output. So your first attempt is to resort to `grep`:
```bash
$ kubectl get pods --all-namespaces | grep frontend
team-a-apps frontend-5c6c94684f-5kzbk 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-k2d7d 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-ppgkx 1/1 Running 0 8d
```
You have a couple of issues here:
* The first column disappeared, which holds the titles. I'm often forgetful and won't remember what each column is supposed to be. Maybe for some outputs, but not all (looking at you, `kubectl api-resources`!)
* There's some awkward space between the columns now, since the columns keep the original formatting.
We could fix the first issue by using `awk` instead of `grep`:
```bash
$ kubectl get pods --all-namespaces | awk 'NR == 1 || /frontend/'
NAMESPACE NAME READY STATUS RESTARTS AGE
team-a-apps frontend-5c6c94684f-5kzbk 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-k2d7d 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-ppgkx 1/1 Running 0 8d
```
Much better! Now if this works for you, you can stop reading here. Chances are, you won't need `tabloid`. But if you want:
* Some more human-readable filters than `awk`
* The ability to customize the columns' order
* The ability to filter with `AND` and `OR` rules
* Or filter using regular expressions
Then `tabloid` is the right tool for you. Here's an example:
```bash
# show only pods whose name starts with `frontend` or `redis`
$ kubectl get pods --all-namespaces | tabloid --expr 'name =~ "^frontend" || name =~ "^redis"'
NAMESPACE NAME READY STATUS RESTARTS AGE
team-a-apps frontend-5c6c94684f-5kzbk 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-k2d7d 1/1 Running 0 8d
team-a-apps frontend-5c6c94684f-ppgkx 1/1 Running 0 8d
team-a-apps redis-follower-dddfbdcc9-9xd8l 1/1 Running 0 8d
team-a-apps redis-follower-dddfbdcc9-l9ngl 1/1 Running 0 8d
team-a-apps redis-leader-fb76b4755-6t5bk 1/1 Running 0 8d
```
Or better even...
```bash
# show only pods whose name starts with `frontend` or `redis`
# and only display the columns `namespace` and `name`
$ kubectl get pods --all-namespaces | tabloid \
> --expr '(name =~ "^frontend" || name =~ "^redis") && namespace == "team-a-apps"' \
> --column namespace,name
NAMESPACE NAME
team-a-apps frontend-5c6c94684f-5kzbk
team-a-apps frontend-5c6c94684f-k2d7d
team-a-apps frontend-5c6c94684f-ppgkx
team-a-apps redis-follower-dddfbdcc9-9xd8l
team-a-apps redis-follower-dddfbdcc9-l9ngl
team-a-apps redis-leader-fb76b4755-6t5bk
```
Or we can also reorder the output:
```bash
# show only pods whose name starts with `frontend` or `redis`
# and only display the columns `namespace` and `name`, but reverse
$ kubectl get pods --all-namespaces | tabloid \
> --expr '(name =~ "^frontend" || name =~ "^redis") && namespace == "team-a-apps"' \
> --column name, namespace
NAME NAMESPACE
frontend-5c6c94684f-5kzbk team-a-apps
frontend-5c6c94684f-k2d7d team-a-apps
frontend-5c6c94684f-ppgkx team-a-apps
redis-follower-dddfbdcc9-9xd8l team-a-apps
redis-follower-dddfbdcc9-l9ngl team-a-apps
redis-leader-fb76b4755-6t5bk team-a-apps
```
## Features
The following features are available:
* [Column titles are always on by default](docs/column-titles.md#column-titles-always-on-by-default) and their titles are [normalized for querying with the expression language](docs/column-titles.md#column-title-normalization). Additionally, [columns can be reordered](docs/column-titles.md#column-selection-and-reordering).
* There's a [powerful expression filtering](docs/expressions.md#powerful-expression-evaluator) with [several additional built-in functions](docs/expressions.md#expression-functions) to handle specific filtering (like `kubectl` durations or pod restart count).
* Extra whitespaces (like the one that `awk` or `grep` could produce) [is removed automatically, and space count is recalculated](docs/qol-improvements.md#cleaning-up-extra-whitespace).
## Why creating this app? Isn't `enter-tool-here` enough?
The answer is "maybe". In short, I wanted to create a tool that serves my own purpose, with a quick and easy to use interface where I don't have to remember either cryptic languages or need to hack my way through to get the outputs I want.
While it's possible for `kubectl`, for example, to output JSON or YAML and have that parsed instead, I want this tool to be a one-size-fits-most in terms of column parsing. I build my own tools around the same premise of the 3+ tab padding and using Go's amazing `tabwriter`, so why not make this tool work with future versions of my own apps and potentially other 3rd-party apps?
## You have a bug, can I fix it?
Absolutely! This was a weekend project and really doesn't have much testing. Parsing columns might sound like a simple task, but you see, given the following input to the best tool out there to parse columns, `awk`, you'll see how quickly it goes wrong:
```
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 8d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 8d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 8d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 8d
```
```
$ awk '{ print $2 }' pods-wrong-title.txt
NAME
argocd-application-controller-0
argocd-dex-server-6dcf645b6b-nf2xb
argocd-redis-5b6967fdfc-48z9d
argocd-repo-server-7598bf5999-jfqlt
argocd-server-79f9bc9b44-5fdsp
```
The name of the 2nd column is `NAME (PROVIDED)`, yet `awk` parsed it as just `NAME`. `awk` is suitable for more generic approaches, while this tool works in harmony with `tabwriter` outputs, and as such, we can totally parse the column well:
```bash
$ cat pods-wrong-title.txt | tabloid --column name_provided
# or --column "NAME (PROVIDED)"
# or --column "name (provided)"
NAME (PROVIDED)
argocd-application-controller-0
argocd-dex-server-6dcf645b6b-nf2xb
argocd-redis-5b6967fdfc-48z9d
argocd-repo-server-7598bf5999-jfqlt
argocd-server-79f9bc9b44-5fdsp
```
Back to the point at hand though... Absolutely! Feel free to send any PRs you might want to see fixed/improved.
07070100000007000041ED00000000000000000000000263FB035300000000000000000000000000000000000000000000001300000000tabloid-0.0.3/docs07070100000008000081A400000000000000000000000163FB035300000EBA000000000000000000000000000000000000002400000000tabloid-0.0.3/docs/column-titles.md# Column Handling
- [Column Handling](#column-handling)
- [Column titles always on by default](#column-titles-always-on-by-default)
- [Column title normalization](#column-title-normalization)
- [Column selection and reordering](#column-selection-and-reordering)
- [Limitations](#limitations)
## Column titles always on by default
The column titles are always on by default, so you don't have to worry about manually selecting them. Want them off? Use `--no-titles`.
## Column title normalization
In order to allow query expressions, titles are normalized: any non alphanumeric characters are removed, with the exception of `-` (dash) which is converted to underscore, and spaces are also replaced with underscores. This convention can be used both for the query expressions as well as the column selector.
In the column selector, you can also use the original column name as well in both uppercase and lowercase format.
An example conversion will be:
```diff
- NAME (PROVIDED)
+ name_provided
```
Moreover, if you prefer to see the columns before working with them, you can use `--titles-only` to print a list of titles and exit. For example, consider the following fictitional input file called `pods.txt`:
```
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
You can use `--titles-only` to print the titles and exit:
```bash
$ cat pods.txt | tabloid --titles-only
NAMESPACE
NAME (PROVIDED)
READY
STATUS
RESTARTS
AGE
```
You can also combine `--titles-only` with `--titles-normalized` to print the titles post-normalization for expressions:
```bash
$ cat pods.txt | tabloid --titles-only --titles-normalized
namespace
name_provided
ready
status
restarts
age
```
## Column selection and reordering
By default, all columns are shown exactly as shown by the original. However, if one or more columns are provided -- either via the `--column` parameter using comma-separated values, or by repeating `--column` as many times as needed -- then only those columns are shown, in the order they are received.
## Limitations
* Column names must be unique.
* Column values are always strings [unless processed by a built-in function](expressions.md#expression-functions) -- this means it's not possible to perform math comparisons yet.
* The `--expr` parameter must be quoted depending on your terminal.
* The input must adhere to Go's `tabwriter` using 2 or more spaces between columns minimum (this is true for both `docker` and `kubectl`).
* Due to the previous item, column names must not contain 2+ consecutive spaces, otherwise they are treated as multiple columns, potentially breaking parsing.
07070100000009000081A400000000000000000000000163FB035300002807000000000000000000000000000000000000002200000000tabloid-0.0.3/docs/expressions.md# Expressions
- [Expressions](#expressions)
- [Powerful expression evaluator](#powerful-expression-evaluator)
- [Expression functions](#expression-functions)
- [`isready`, `isnotready`](#isready-isnotready)
- [`hasrestarts`, `hasnorestarts`](#hasrestarts-hasnorestarts)
- [`olderthan`, `olderthaneq`, `newerthan`, `newerthaneq`, `eqduration`](#olderthan-olderthaneq-newerthan-newerthaneq-eqduration)
## Powerful expression evaluator
The `--expr` parameter allows you to specify any boolean expression. `tabloid` uses [`govaluate`](https://github.com/Knetic/govaluate) for its expression evaluator and multiple options are supported, such as:
* Grouping with parenthesis
* `&&` and `||` operators
* `!=`, `==`, `>`, `<`, `>=`, `<=` operators
* And regexp-based operators such as `=~` and `!~`, based on Go's own `regexp` package
The only requirement, evaluated after parsing your expression, is that the expression must evaluate to a boolean output.
Mathematical operators do not work due to how the table is parsed: all values are strings.
## Expression functions
The following functions are available. Their parameters are the column names and potential additional values you want to pass to them. See their examples for more details.
**Note:** Expressions always use the normalized column name as an input parameter or matching value. For example, if you have a column named `NAME (PROVIDED)`, then you would use `name_provided` as the parameter name.
The following file is used for the examples below:
```bash
$ cat pods.txt
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
### `isready`, `isnotready`
Returns true for any value that matches the format `<current>/<total>`. If `<current>` is equal to `<total>`, then `isready` returns true, otherwise `isnotready` returns true.
These functions will work with columns that contains values such as `1/1` or `0/1`. A row is considered "not ready" if the `<current>` value is not equal to the `<total>` value.
**Examples:**
```bash
# Print all pods that have an amount of pods matching the expected amount
$ cat pods.txt | tabloid --expr 'isready(ready)'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
```bash
# Print all pods that have an amount of pods NOT matching the expected amount
$ cat pods.txt | tabloid --expr 'isnotready(ready)'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
```
### `hasrestarts`, `hasnorestarts`
Returns true for any value that matches the format `<number>` where `<number>` is a positive integer. Optionally, it also supports values whith the format `<number> (<duration> ago)`, where `<number>` is a positive integer, and `<duration>` is a Go-parseable `time.Duration` (with additional support up to days, like `kubectl`).
If `<number>` is greater than 0, then `hasrestarts` returns true, otherwise `hasnorestarts` returns true.
Column values could have formats like `5` or `5 (5d ago)`. The value within the parenthesis is ignored.
**Examples:**
```bash
# Print all pods that have had at least one restart
$ cat pods.txt | tabloid --expr 'hasrestarts(restarts)'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
```
```bash
# Print all pods that have had no restarts
$ cat pods.txt | tabloid --expr 'hasnorestarts(restarts)'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
### `olderthan`, `olderthaneq`, `newerthan`, `newerthaneq`, `eqduration`
Utility functions to manage durations, as seen in the `kubectl` output. These functions are useful to compare durations, such as the age of a pod. You can use it to formulate queries like "all pods older or equal than 1 day".
These functions will work with columns that contains values parseable by `time.ParseDuration` (with additional support up to days, like `kubectl`).
**Examples:**
```bash
# Print all pods that are older than 8 days
$ cat pods.txt | tabloid --expr 'olderthan(age, "8d")'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
```
```bash
# Print all pods that are older or equal than 8 days
$ cat pods.txt | tabloid --expr 'olderthaneq(age, "8d")'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
argocd argocd-dex-server-6dcf645b6b-nf2xb 1/1 Running 0 12d
argocd argocd-redis-5b6967fdfc-48z9d 1/1 Running 0 14d
argocd argocd-repo-server-7598bf5999-jfqlt 1/1 Running 0 12d
argocd argocd-server-79f9bc9b44-5fdsp 1/1 Running 0 12d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-5qzdd 1/1 Running 0 200d
kube-system gke-metrics-agent-95vkn 1/1 Running 0 200d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
```bash
# Print all pods that are newer than 8 days
$ cat pods.txt | tabloid --expr 'newerthan(age, "8d")'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
```
```bash
# Print all pods that are newer or equal than 8 days
$ cat pods.txt | tabloid --expr 'newerthaneq(age, "8d")'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-s2f82 0/1 CrashLoopBackOff 592 (3m33s ago) 1h
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
```bash
# Print all pods that are exactly 8 days old
$ cat pods.txt | tabloid --expr 'eqduration(age, "8d")'
NAMESPACE NAME (PROVIDED) READY STATUS RESTARTS AGE
argocd argocd-application-controller-0 1/1 Running 0 8d
kube-system fluentbit-gke-qx76z 2/2 Running 3 (2d ago) 8d
kube-system fluentbit-gke-wm55d 2/2 Running 0 8d
kube-system gke-metrics-agent-blbbm 1/1 Running 0 8d
```
0707010000000A000081A400000000000000000000000163FB035300000228000000000000000000000000000000000000002700000000tabloid-0.0.3/docs/qol-improvements.md# Quality of Life Improvements
- [Quality of Life Improvements](#quality-of-life-improvements)
- [Cleaning up extra whitespace](#cleaning-up-extra-whitespace)
## Cleaning up extra whitespace
By default, `tabloid` will remove extra whitespace from the original output. The goal here is to provide human-readable outputs and, as seen above, `grep` or `awk` might work, but the additional whitespaces between columns are kept from the original. `tabloid` will reorganize the columns to maintain the 3-space padding between columns based on its data.
0707010000000B000081A400000000000000000000000163FB03530000012F000000000000000000000000000000000000001500000000tabloid-0.0.3/go.modmodule github.com/patrickdappollonio/tabloid
go 1.19
require (
github.com/Knetic/govaluate v3.0.0+incompatible
github.com/spf13/cobra v1.6.1
github.com/xhit/go-str2duration/v2 v2.1.0
)
require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
0707010000000C000081A400000000000000000000000163FB035300000502000000000000000000000000000000000000001500000000tabloid-0.0.3/go.sumgithub.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
0707010000000D000081A400000000000000000000000163FB0353000000FC000000000000000000000000000000000000001600000000tabloid-0.0.3/main.gopackage main
import (
"fmt"
"os"
)
func main() {
if err := rootCommand(os.Stdin).Execute(); err != nil {
errfn("Error: %s", err)
os.Exit(1)
}
}
func errfn(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
}
0707010000000E000081A400000000000000000000000163FB035300000029000000000000000000000000000000000000001C00000000tabloid-0.0.3/renovate.json{
"extends": [
"config:base"
]
}
0707010000000F000081A400000000000000000000000163FB035300000D3C000000000000000000000000000000000000001600000000tabloid-0.0.3/root.gopackage main
import (
"bytes"
"fmt"
"io"
"os"
"text/tabwriter"
"github.com/patrickdappollonio/tabloid/tabloid"
"github.com/spf13/cobra"
)
var version = "development"
const (
helpShort = "tabloid is a simple command line tool to parse and filter column-based CLI outputs from commands like kubectl or docker"
helpLong = `tabloid is a simple command line tool to parse and filter column-based CLI outputs from commands like kubectl or docker.
For documentation, see https://github.com/patrickdappollonio/tabloid`
)
var examples = []string{
`kubectl api-resources | tabloid --expr 'kind == "Namespace"'`,
`kubectl api-resources | tabloid --expr 'apiversion =~ "networking"'`,
`kubectl api-resources | tabloid --expr 'shortnames == "sa"' --column name,shortnames`,
`kubectl get pods --all-namespaces | tabloid --expr 'name =~ "^frontend" || name =~ "redis$"'`,
}
type settings struct {
expr string
columns []string
debug bool
noTitles bool
titlesOnly bool
titlesNormalized bool
}
func rootCommand(r io.Reader) *cobra.Command {
var opts settings
cmd := &cobra.Command{
Use: "tabloid",
Short: helpShort,
Long: helpLong,
SilenceUsage: true,
SilenceErrors: true,
Version: version,
Example: sliceToTabulated(examples),
RunE: func(cmd *cobra.Command, args []string) error {
return run(r, os.Stdout, opts)
},
}
cmd.Flags().StringVarP(&opts.expr, "expr", "e", "", "expression to filter the output")
cmd.Flags().StringSliceVarP(&opts.columns, "column", "c", []string{}, "columns to display")
cmd.Flags().BoolVar(&opts.debug, "debug", false, "enable debug mode")
cmd.Flags().BoolVar(&opts.noTitles, "no-titles", false, "remove column titles from the output")
cmd.Flags().BoolVar(&opts.titlesOnly, "titles-only", false, "only display column titles")
cmd.Flags().BoolVar(&opts.titlesNormalized, "titles-normalized", false, "normalize column titles")
return cmd
}
func run(r io.Reader, w io.Writer, opts settings) error {
var b bytes.Buffer
if _, err := io.Copy(&b, r); err != nil {
return err
}
tab := tabloid.New(&b)
tab.EnableDebug(opts.debug)
cols, err := tab.ParseColumns()
if err != nil {
return err
}
if opts.titlesOnly {
if opts.expr != "" {
return fmt.Errorf("cannot use --expr with --titles-only")
}
if len(opts.columns) > 0 {
return fmt.Errorf("cannot use --column with --titles-only")
}
for _, v := range cols {
if opts.titlesNormalized {
fmt.Fprintln(w, v.ExprTitle)
continue
}
fmt.Fprintln(w, v.Title)
}
return nil
}
filtered, err := tab.Filter(cols, opts.expr)
if err != nil {
return err
}
output, err := tab.Select(filtered, opts.columns)
if err != nil {
return err
}
if len(output) == 0 {
return fmt.Errorf("input had no columns to handle")
}
t := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
if !opts.noTitles {
for _, v := range output {
if opts.titlesNormalized {
fmt.Fprintf(t, "%s\t", v.ExprTitle)
continue
}
fmt.Fprintf(t, "%s\t", v.Title)
}
fmt.Fprintln(t, "")
}
for i := 0; i < len(output[0].Values); i++ {
for _, v := range output {
fmt.Fprintf(t, "%s\t", v.Values[i])
}
fmt.Fprintln(t, "")
}
if err := t.Flush(); err != nil {
return fmt.Errorf("unable to flush table contents to screen: %w", err)
}
return nil
}
07070100000010000041ED00000000000000000000000263FB035300000000000000000000000000000000000000000000001600000000tabloid-0.0.3/tabloid07070100000011000081A400000000000000000000000163FB035300000FD0000000000000000000000000000000000000002300000000tabloid-0.0.3/tabloid/exprfuncs.gopackage tabloid
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/Knetic/govaluate"
str2duration "github.com/xhit/go-str2duration/v2"
)
// isready checks if a string is in the form of <current>/<total> and if the
// current value is equal to the total value, false otherwise.
func isready(args ...interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, fmt.Errorf("isready function only accepts one argument")
}
str, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("isready function only accepts string arguments")
}
pieces := strings.FieldsFunc(str, func(r rune) bool {
return r == '/'
})
if len(pieces) != 2 {
return nil, fmt.Errorf("isready function only accepts string arguments in the form of <current>/<total>")
}
if pieces[0] != pieces[1] {
return false, nil
}
return true, nil
}
var reRestart = regexp.MustCompile(`[1-9]\d*( \([^\)]+\))?`)
// hasrestarts checks if a string contains a restart count, or if it's zero.
func hasrestarts(args ...interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, fmt.Errorf("hasrestarts function only accepts one argument")
}
str, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("hasrestarts function only accepts string arguments")
}
return reRestart.MatchString(str), nil
}
// parseDurations parses two string arguments into time.Duration values.
func parseDurations(args ...interface{}) (time.Duration, time.Duration, error) {
if len(args) != 2 {
return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts two arguments")
}
str, ok := args[0].(string)
if !ok {
return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments")
}
age, ok := args[1].(string)
if !ok {
return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments")
}
t1, err := str2duration.ParseDuration(str)
if err != nil {
return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %w", err)
}
t2, err := str2duration.ParseDuration(age)
if err != nil {
return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %w", err)
}
return t1, t2, nil
}
// olderthan checks if the first argument is older than the second argument,
// using Go's time.Duration parsing.
func olderThan(args ...interface{}) (interface{}, error) {
t1, t2, err := parseDurations(args...)
return t1 > t2, err
}
// olderthaneq checks if the first argument is older than or equal to the second
// argument, using Go's time.Duration parsing.
func olderThanEq(args ...interface{}) (interface{}, error) {
t1, t2, err := parseDurations(args...)
return t1 >= t2, err
}
// newerthan checks if the first argument is newer than the second argument,
// using Go's time.Duration parsing.
func newerThan(args ...interface{}) (interface{}, error) {
t1, t2, err := parseDurations(args...)
return t1 < t2, err
}
// newerthaneq checks if the first argument is newer than or equal to the
// second argument, using Go's time.Duration parsing.
func newerThanEq(args ...interface{}) (interface{}, error) {
t1, t2, err := parseDurations(args...)
return t1 <= t2, err
}
// eqduration checks if the first argument is equal to the second argument,
// using Go's time.Duration parsing.
func eqduration(args ...interface{}) (interface{}, error) {
t1, t2, err := parseDurations(args...)
return t1 == t2, err
}
// funcs is a map of functions that can be used in the filter expression.
var funcs = map[string]govaluate.ExpressionFunction{
"isready": isready,
"isnotready": func(args ...interface{}) (interface{}, error) {
ready, err := isready(args...)
return !ready.(bool), err
},
"hasrestarts": hasrestarts,
"hasnorestarts": func(args ...interface{}) (interface{}, error) {
restarts, err := hasrestarts(args...)
return !restarts.(bool), err
},
"olderthan": olderThan,
"olderthaneq": olderThanEq,
"newerthan": newerThan,
"newerthaneq": newerThanEq,
"eqduration": eqduration,
}
07070100000012000081A400000000000000000000000163FB035300001987000000000000000000000000000000000000002800000000tabloid-0.0.3/tabloid/exprfuncs_test.gopackage tabloid
import (
"testing"
"time"
)
func Test_isready(t *testing.T) {
type args struct {
args []interface{}
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "basic",
args: args{
args: []interface{}{
"1/1",
},
},
want: true,
wantErr: false,
},
{
name: "more than 1 argument",
args: args{
args: []interface{}{
"1/1",
"2/2",
},
},
want: nil,
wantErr: true,
},
{
name: "not a string",
args: args{
args: []interface{}{
1,
},
},
want: nil,
wantErr: true,
},
{
name: "not in the form of <current>/<total>",
args: args{
args: []interface{}{
"1",
},
},
want: nil,
wantErr: true,
},
{
name: "not ready",
args: args{
args: []interface{}{
"0/1",
},
},
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := isready(tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("isready() error = %v, wantErr %v", err, tt.wantErr)
return
}
assertEqual(t, got, tt.want, "isready() = %v, want %v", got, tt.want)
})
}
}
func Test_hasrestarts(t *testing.T) {
type args struct {
args []interface{}
}
tests := []struct {
name string
args args
want interface{}
wantErr bool
}{
{
name: "basic no restarts",
args: args{
args: []interface{}{
"0",
},
},
want: false,
wantErr: false,
},
{
name: "basic with restarts",
args: args{
args: []interface{}{
"1",
},
},
want: true,
wantErr: false,
},
{
name: "basic with restarts and time",
args: args{
args: []interface{}{
"1 (5s ago)",
},
},
want: true,
wantErr: false,
},
{
name: "more than 1 argument",
args: args{
args: []interface{}{
"1",
"2",
},
},
want: nil,
wantErr: true,
},
{
name: "not a string",
args: args{
args: []interface{}{
1,
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := hasrestarts(tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("hasrestarts() error = %v, wantErr %v", err, tt.wantErr)
return
}
assertEqual(t, got, tt.want, "hasrestarts() = %v, want %v", got, tt.want)
})
}
}
func Test_parseDurations(t *testing.T) {
type args struct {
args []interface{}
}
tests := []struct {
name string
args args
ret1 time.Duration
ret2 time.Duration
wantErr bool
}{
{
name: "basic",
args: args{
args: []interface{}{
"1h",
"2h",
},
},
ret1: time.Hour,
ret2: 2 * time.Hour,
wantErr: false,
},
{
name: "using days",
args: args{
args: []interface{}{
"1d",
"2d",
},
},
ret1: 24 * time.Hour,
ret2: 2 * 24 * time.Hour,
wantErr: false,
},
{
name: "using weeks",
args: args{
args: []interface{}{
"1w",
"2w",
},
},
ret1: 7 * 24 * time.Hour,
ret2: 2 * 7 * 24 * time.Hour,
wantErr: false,
},
{
name: "single argument",
args: args{
args: []interface{}{
"1h",
},
},
wantErr: true,
},
{
name: "more than 2 arguments",
args: args{
args: []interface{}{
"1h",
"2h",
"3h",
},
},
wantErr: true,
},
{
name: "not a string",
args: args{
args: []interface{}{
1,
"2h",
},
},
wantErr: true,
},
{
name: "not a string 2nd place",
args: args{
args: []interface{}{
"1h",
1,
},
},
wantErr: true,
},
{
name: "not a valid duration",
args: args{
args: []interface{}{
"1",
"1h",
},
},
wantErr: true,
},
{
name: "not a valid duration 2nd place",
args: args{
args: []interface{}{
"1h",
"1",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := parseDurations(tt.args.args...)
if (err != nil) != tt.wantErr {
t.Errorf("parseDurations() error = %v, wantErr %v", err, tt.wantErr)
return
}
assertEqual(t, got, tt.ret1, "parseDurations() got = %v, want %v", got, tt.ret1)
assertEqual(t, got1, tt.ret2, "parseDurations() got1 = %v, want %v", got1, tt.ret2)
})
}
}
func TestDurations(t *testing.T) {
cases := []struct {
name string
d1 string
d2 string
isOlder bool
isOlderEqual bool
isNewer bool
isNewerEqual bool
isEqualDur bool
}{
{
name: "d1 is older",
d1: "3h",
d2: "1h",
isOlder: true,
isOlderEqual: true,
isNewer: false,
isNewerEqual: false,
isEqualDur: false,
},
{
name: "d1 is newer",
d1: "1h",
d2: "3h",
isOlder: false,
isOlderEqual: false,
isNewer: true,
isNewerEqual: true,
isEqualDur: false,
},
{
name: "d1 is equal to d2",
d1: "1h",
d2: "1h",
isOlder: false,
isOlderEqual: true,
isNewer: false,
isNewerEqual: true,
isEqualDur: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotIsOlder, err := olderThan(c.d1, c.d2)
if err != nil {
t.Errorf("olderThan() error = %v", err)
return
}
gotIsOlderEqual, err := olderThanEq(c.d1, c.d2)
if err != nil {
t.Errorf("olderThanEq() error = %v", err)
return
}
gotIsNewer, err := newerThan(c.d1, c.d2)
if err != nil {
t.Errorf("newerThan() error = %v", err)
return
}
gotIsNewerEqual, err := newerThanEq(c.d1, c.d2)
if err != nil {
t.Errorf("newerThanEq() error = %v", err)
return
}
gotIsEqDuration, err := eqduration(c.d1, c.d2)
if err != nil {
t.Errorf("eqduration() error = %v", err)
return
}
assertEqual(t, gotIsOlder, c.isOlder, "olderThan() = %t, want %t", gotIsOlder, c.isOlder)
assertEqual(t, gotIsOlderEqual, c.isOlderEqual, "olderThanEq() = %t, want %t", gotIsOlderEqual, c.isOlderEqual)
assertEqual(t, gotIsNewer, c.isNewer, "newerThan() = %t, want %t", gotIsNewer, c.isNewer)
assertEqual(t, gotIsNewerEqual, c.isNewerEqual, "newerThanEq() = %t, want %t", gotIsNewerEqual, c.isNewerEqual)
assertEqual(t, gotIsEqDuration, c.isEqualDur, "eqduration() = %t, want %t", gotIsEqDuration, c.isEqualDur)
})
}
}
07070100000013000081A400000000000000000000000163FB035300000694000000000000000000000000000000000000002000000000tabloid-0.0.3/tabloid/filter.gopackage tabloid
import (
"fmt"
"strings"
"github.com/Knetic/govaluate"
)
func (t *Tabloid) Filter(columns []Column, expression string) ([]Column, error) {
expression = strings.TrimSpace(expression)
if expression == "" {
t.logger.Printf("no filter expression provided, returning all rows")
return columns, nil
}
expr, err := govaluate.NewEvaluableExpressionWithFunctions(expression, funcs)
if err != nil {
return nil, fmt.Errorf("unable to process expression %q: %w", expression, err)
}
newColumns := make([]Column, 0, len(columns))
for _, column := range columns {
for pos := range column.Values {
row := make(map[string]interface{})
for _, column := range columns {
row[column.ExprTitle] = column.Values[pos]
}
result, err := expr.Evaluate(row)
if err != nil {
t.logger.Printf("error type: %T", err)
return nil, fmt.Errorf("unable to evaluate expression for row %d: %w", pos+1, err)
}
chosen, ok := result.(bool)
if !ok {
return nil, fmt.Errorf("expression %q must return a boolean value", expression)
}
if chosen {
newColumns = upsertColumn(newColumns, column, column.Values[pos])
}
}
}
return newColumns, nil
}
func upsertColumn(columns []Column, column Column, data string) []Column {
for pos, v := range columns {
if v.ExprTitle == column.ExprTitle {
columns[pos].Values = append(columns[pos].Values, data)
return columns
}
}
return append(columns, Column{
VisualPosition: column.VisualPosition,
ExprTitle: column.ExprTitle,
Title: column.Title,
StartIndex: column.StartIndex,
EndIndex: column.EndIndex,
Values: []string{data},
})
}
07070100000014000081A400000000000000000000000163FB035300000C08000000000000000000000000000000000000002000000000tabloid-0.0.3/tabloid/parser.gopackage tabloid
import (
"bufio"
"fmt"
"strings"
)
const endOfLine = -1
// ParseHeading parses the heading of a tabloid table and returns a list of
// columns with their respective start and end indexes. If it's the last column,
// the end index is -1. It also returns an error if there are duplicate column
// titles.
func (t *Tabloid) ParseHeading(heading string) ([]Column, error) {
var columns []Column
uniques := make(map[string]struct{})
prevIndex := 0
spaceCount := 0
for i := 0; i < len(heading); i++ {
if heading[i] == ' ' {
spaceCount++
continue
}
if spaceCount > 1 {
titleTrimmed := strings.TrimSpace(heading[prevIndex:i])
if _, ok := uniques[titleTrimmed]; ok {
return nil, &DuplicateColumnTitleError{Title: titleTrimmed}
}
uniques[titleTrimmed] = struct{}{}
columns = append(columns, Column{
VisualPosition: len(columns) + 1,
Title: titleTrimmed,
ExprTitle: fnKey(titleTrimmed),
StartIndex: prevIndex,
EndIndex: i,
})
prevIndex = i
spaceCount = 0
}
if len(heading)-1 == i {
titleTrimmed := strings.TrimSpace(heading[prevIndex:])
if _, ok := uniques[titleTrimmed]; ok {
return nil, &DuplicateColumnTitleError{Title: titleTrimmed}
}
uniques[titleTrimmed] = struct{}{}
columns = append(columns, Column{
VisualPosition: len(columns) + 1,
Title: titleTrimmed,
ExprTitle: fnKey(titleTrimmed),
StartIndex: prevIndex,
EndIndex: endOfLine,
})
}
}
return columns, nil
}
func (t *Tabloid) ParseColumns() ([]Column, error) {
scanner := bufio.NewScanner(t.input)
var columns []Column
for rowNumber := 1; scanner.Scan(); rowNumber++ {
line := scanner.Text()
// The first line is the header, so we use it to find the column titles
// the assumption here is that both target apps, kubectl and docker use
// a Go tabwriter with a padding of 3 spaces.
if rowNumber == 1 {
// Find the column titles
local, err := t.ParseHeading(line)
if err != nil {
return nil, err
}
// The first line is the header, so it doesn't need any processing
t.logger.Printf("finished parsing columns, found: %d", len(local))
columns = local
continue
}
// Skip empty lines
if strings.TrimSpace(line) == "" {
t.logger.Printf("omitting empty row found in line %d", rowNumber)
continue
}
// Parse each column's content
for pos := 0; pos < len(columns); pos++ {
// Calculate end index if it's the last column
endIdx := columns[pos].EndIndex
if endIdx == endOfLine {
endIdx = len(line)
}
value := strings.TrimSpace(line[columns[pos].StartIndex:endIdx])
// Store the value in the local copy of the metadata
columns[pos].Values = append(columns[pos].Values, value)
}
}
t.logger.Printf("finished parsing contents, found: %#v", columns)
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error while scanning input: %w", err)
}
if len(columns) == 0 {
return nil, fmt.Errorf("no data found in input")
}
return columns, nil
}
07070100000015000081A400000000000000000000000163FB03530000077B000000000000000000000000000000000000002500000000tabloid-0.0.3/tabloid/parser_test.gopackage tabloid
import (
"reflect"
"testing"
)
func TestTabloid_ParseHeading(t *testing.T) {
tests := []struct {
name string
heading string
want []Column
wantErr bool
}{
{
name: "basic",
heading: "NAME READY STATUS %RESTART AGE GAP",
want: []Column{
{
Title: "NAME",
StartIndex: 0,
EndIndex: 7,
},
{
Title: "READY",
StartIndex: 7,
EndIndex: 15,
},
{
Title: "STATUS",
StartIndex: 15,
EndIndex: 25,
},
{
Title: "%RESTART",
StartIndex: 25,
EndIndex: 36,
},
{
Title: "AGE GAP",
StartIndex: 36,
EndIndex: -1,
},
},
},
{
name: "duplicate column title",
heading: "NAME READY STATUS %RESTART AGE GAP AGE GAP",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr := &Tabloid{}
got, err := tr.ParseHeading(tt.heading)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(got) != len(tt.want) {
t.Errorf("mismatched number of returned values, got %v, want %v", got, tt.want)
}
for i, c := range got {
assertEqual(t, c.Title, tt.want[i].Title, "item %d title = %q, want %q", i+1, c.Title, tt.want[i].Title)
assertEqual(t, c.StartIndex, tt.want[i].StartIndex, "item %d start index = %d, want %d", i+1, c.StartIndex, tt.want[i].StartIndex)
assertEqual(t, c.EndIndex, tt.want[i].EndIndex, "item %d end index = %d, want %d", i+1, c.EndIndex, tt.want[i].EndIndex)
}
})
}
}
func assertEqual(t *testing.T, got, want interface{}, msg string, args ...interface{}) {
if !reflect.DeepEqual(got, want) {
if len(args) == 0 && msg != "" {
t.Errorf(msg)
}
if len(args) > 0 && msg != "" {
t.Errorf(msg, args...)
}
t.Errorf("got: %v, want: %v", got, want)
}
}
07070100000016000081A400000000000000000000000163FB0353000006AB000000000000000000000000000000000000002000000000tabloid-0.0.3/tabloid/select.gopackage tabloid
import (
"fmt"
"strings"
)
func (t *Tabloid) Select(columns []Column, requestedColumnNames []string) ([]Column, error) {
// If there are no requested columns, we return them all
if len(requestedColumnNames) == 0 {
return columns, nil
}
returnedColumns := make([]Column, 0, len(requestedColumnNames))
for _, v := range requestedColumnNames {
var column Column
for _, c := range columns {
if c.Title == v || strings.ToLower(c.Title) == v || c.ExprTitle == v {
column = c
break
}
}
if column.ExprTitle == "" {
return nil, fmt.Errorf("column %q does not exist in the input dataset", v)
}
returnedColumns = append(returnedColumns, column)
}
return returnedColumns, nil
}
// func (t *Tabloid) Select(columns []Column, data []map[string]interface{}, requestedColumns []string) ([]map[string]interface{}, error) {
// foundColumnNames := make([]string, 0, len(requestedColumns))
// // If there are no requested columns, we return them all
// for _, v := range requestedColumns {
// var columnExpr string
// for _, c := range columns {
// if c.Title == v || strings.ToLower(c.Title) == v || c.ExprTitle == v {
// columnExpr = c.ExprTitle
// break
// }
// }
// if columnExpr == "" {
// return nil, fmt.Errorf("column %q does not exist in the input dataset", v)
// }
// }
// for pos, column := range selectedColumns {
// for _, row := range data {
// value, ok := row[column.ExprTitle]
// if ok {
// selectedColumns[pos].Values = append(selectedColumns[pos].Values, value.(string))
// }
// }
// }
// t.logger.Printf("columns after select: %#v", selectedColumns)
// return selectedColumns, nil
// }
07070100000017000081A400000000000000000000000163FB03530000029F000000000000000000000000000000000000002100000000tabloid-0.0.3/tabloid/tabloid.gopackage tabloid
import (
"bytes"
"io"
"log"
"os"
)
type Logger interface {
Println(v ...interface{})
Printf(format string, v ...interface{})
SetOutput(w io.Writer)
}
type Tabloid struct {
input *bytes.Buffer
logger Logger
}
type Column struct {
VisualPosition int
Title string
ExprTitle string
StartIndex int
EndIndex int
Values []string
}
func New(input *bytes.Buffer) *Tabloid {
return &Tabloid{
input: input,
logger: log.New(io.Discard, "🚨 --> ", log.Lshortfile),
}
}
func (t *Tabloid) EnableDebug(debug bool) {
if debug {
t.logger.SetOutput(os.Stderr)
} else {
t.logger.SetOutput(io.Discard)
}
}
07070100000018000081A400000000000000000000000163FB035300000257000000000000000000000000000000000000001F00000000tabloid-0.0.3/tabloid/utils.gopackage tabloid
import (
"fmt"
"strings"
"unicode"
)
type DuplicateColumnTitleError struct {
Title string
}
func (e *DuplicateColumnTitleError) Error() string {
return fmt.Sprintf("duplicate column title found: %q -- unable to work with non-unique column titles", e.Title)
}
func fnKey(s string) string {
s = strings.ToLower(s)
out := make([]rune, 0, len(s))
for _, v := range s {
if unicode.IsLetter(v) || unicode.IsDigit(v) || v == ' ' || v == '-' {
switch v {
case ' ', '-':
out = append(out, '_')
default:
out = append(out, v)
}
}
}
return string(out)
}
07070100000019000081A400000000000000000000000163FB035300000108000000000000000000000000000000000000001700000000tabloid-0.0.3/utils.gopackage main
import (
"bytes"
"fmt"
)
func sliceToTabulated(slice []string) string {
var s bytes.Buffer
for pos, v := range examples {
s.WriteString(fmt.Sprintf(" %s", v))
if pos != len(examples)-1 {
s.WriteString("\n")
}
}
return s.String()
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!105 blocks