Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:ojkastl_buildservice:Branch_devel_kubic
tabloid
tabloid-0.0.3.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor