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

[![Downloads](https://img.shields.io/github/downloads/patrickdappollonio/tabloid/total?color=blue&logo=github&style=flat-square)](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
openSUSE Build Service is sponsored by