diff --git a/Makefile b/Makefile index 719e4ae..2f9079a 100644 --- a/Makefile +++ b/Makefile @@ -23,11 +23,24 @@ UID = root GID = 0 HAVE_POD := $(shell pod2text -h 2>/dev/null) +BRANCH = $(shell git branch --show-current) +COMMIT = $(shell git rev-parse --short=8 HEAD) +BUILD = $(shell date +%Y.%m.%d.%H%M%S) +GOVERSION = $(shell grep -E "^go" go.mod | cut -d' ' -f2) +APIVERSION = $(shell grep github.com/elastic/go-elasticsearch go.mod | cut -d' ' -f2) + + all: buildlocal buildlocal: - CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o $(tool) + CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" \ + -ldflags "-X codeberg.org/scip/esctl/pkg/cfg.BRANCH=$(BRANCH) \ + -X codeberg.org/scip/esctl/pkg/cfg.COMMIT=$(COMMIT) \ + -X codeberg.org/scip/esctl/pkg/cfg.GOVERSION=$(GOVERSION) \ + -X codeberg.org/scip/esctl/pkg/cfg.APIVERSION=$(APIVERSION) \ + -X codeberg.org/scip/esctl/pkg/cfg.BUILD=$(BUILD)" \ + -o $(tool) strip $(tool) install: buildlocal diff --git a/cmd/root.go b/cmd/root.go index da8de03..6916dec 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,9 +40,9 @@ func Main() int { conf := cfg.NewConfig() cmd := &cli.Command{ - Name: "esctl", - Usage: "manage elasticsearch from cli", - Version: cfg.Version, + Name: "esctl", + Usage: "manage elasticsearch from cli", + //Version: cfg.Version, EnableShellCompletion: true, Flags: []cli.Flag{ @@ -69,6 +69,13 @@ func Main() int { Usage: "cluster alias to work with", Destination: &conf.CurrentCluster, }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Value: "", + Usage: "output mode (tsv, markdown, json, yaml) default: tsv", + Destination: &conf.Output, + }, }, Commands: []*cli.Command{ @@ -81,6 +88,7 @@ func Main() int { Node(conf), Doc(conf), Repl(conf), + Version(conf), }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { @@ -101,3 +109,16 @@ func Main() int { return Finish(cmd.Run(context.Background(), os.Args)) } + +func Version(conf *cfg.Config) *cli.Command { + return &cli.Command{ + Name: "version", + Usage: "show esctl version information", + + Action: func(ctx context.Context, cmd *cli.Command) error { + _, err := fmt.Printf("esctl version: %s\n build: %s\n branch: %s\n commit: %s\n go version: %s\n API Version: %s\n", + cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION) + return err + }, + } +} diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index ebe44d2..cc240e3 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -31,7 +31,11 @@ import ( ) const ( - Version string = `v0.0.11` + Version string = `v0.0.12` +) + +var ( + APIVERSION, GOVERSION, BUILD, COMMIT, BRANCH string ) type Cluster struct { @@ -43,6 +47,7 @@ type Config struct { ConfigFile string // -c CurrentCluster string // -C Debug bool // -d + Output string // -o Clusters map[string]*Cluster DefaultCluster *Cluster Index string // index: -i diff --git a/pkg/es/ccr.go b/pkg/es/ccr.go index a28e03d..b419896 100644 --- a/pkg/es/ccr.go +++ b/pkg/es/ccr.go @@ -23,6 +23,7 @@ import ( "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/elastic/go-elasticsearch/v9/typedapi/types" ) @@ -115,10 +116,10 @@ func CcrRemoteInfo(conf *cfg.Config, index string) error { mode = "leader" } - table := NewTable(2, 5) + table := printer.NewTable(conf, 2, 5) table.Addheaders("ccr remote property", "value") - table.entries = [][]string{ + table.Entries = [][]string{ {"Remote Cluster", remote}, {"CCR Mode", mode}, {"Connected", fmt.Sprintf("%t", info.Connected)}, @@ -126,7 +127,7 @@ func CcrRemoteInfo(conf *cfg.Config, index string) error { {"Proxy Address", info.ProxyAddress}, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/ccr_follower.go b/pkg/es/ccr_follower.go index 20ac745..c27886a 100644 --- a/pkg/es/ccr_follower.go +++ b/pkg/es/ccr_follower.go @@ -22,6 +22,7 @@ import ( "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" ) func getRemoteName(conf *cfg.Config) (string, error) { @@ -175,10 +176,10 @@ func CcrFollowerShow(conf *cfg.Config, index string) error { follower := res.Indices[0].Shards[0] - table := NewTable(2, 9) + table := printer.NewTable(conf, 2, 9) table.Addheaders("ccr follower property", "value") - table.entries = [][]string{ + table.Entries = [][]string{ {"name", index}, {"remote_cluster", follower.RemoteCluster}, {"leader_checkpoint", fmt.Sprintf("%d", follower.LeaderGlobalCheckpoint)}, @@ -190,7 +191,7 @@ func CcrFollowerShow(conf *cfg.Config, index string) error { {"successful_write_requests", fmt.Sprintf("%d", follower.SuccessfulWriteRequests)}, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/cluster.go b/pkg/es/cluster.go index 08ae3b5..bff768b 100644 --- a/pkg/es/cluster.go +++ b/pkg/es/cluster.go @@ -25,6 +25,7 @@ import ( "sync" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/dustin/go-humanize" "github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats" "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health" @@ -52,7 +53,7 @@ type apiResponse struct { } func ClusterList(conf *cfg.Config) error { - table := NewTable(3, len(conf.Clusters)) + table := printer.NewTable(conf, 3, len(conf.Clusters)) table.Addheaders("cluster", "uri", "default") @@ -76,13 +77,13 @@ func ClusterList(conf *cfg.Config) error { Do(context.Background()) if err == nil { - name = Colorize("green", name) + name = printer.Colorize(conf, "green", name) } - table.entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)} + table.Entries[idx] = []string{name, cluster.Uri, fmt.Sprintf("%t", current)} } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } @@ -162,11 +163,11 @@ func ClusterStatus(conf *cfg.Config) error { ) } - table := NewTable(2, 5) + table := printer.NewTable(conf, 2, 5) table.Addheaders(cluster, "status") - table.entries = [][]string{ - {"Cluster Name", Colorize(clusterhealth.Status.Name, clusterhealth.ClusterName)}, + table.Entries = [][]string{ + {"Cluster Name", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.ClusterName)}, {"ES Version", info.Version.Int}, {"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)}, {"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)}, @@ -178,7 +179,7 @@ func ClusterStatus(conf *cfg.Config) error { table = gatherClusterStats(conf, clusterstats, table) } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } } @@ -186,7 +187,7 @@ func ClusterStatus(conf *cfg.Config) error { return nil } -func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, table *Table) *Table { +func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, table *printer.Table) *printer.Table { var querycount int64 var vmversion string @@ -202,7 +203,7 @@ func gatherClusterStats(conf *cfg.Config, clusterstats *clusterstats.Response, t isleader := checkClusterIsLeader(conf, conf.CurrentCluster) - table.entries = append(table.entries, [][]string{ + table.Entries = append(table.Entries, [][]string{ {"Indicies", fmt.Sprintf("%d", clusterstats.Indices.Count)}, {"Is Leader", fmt.Sprintf("%t", isleader)}, {"Docs", fmt.Sprintf("%d", clusterstats.Indices.Docs.Count)}, diff --git a/pkg/es/cluster_settings.go b/pkg/es/cluster_settings.go index 84fa050..48af363 100644 --- a/pkg/es/cluster_settings.go +++ b/pkg/es/cluster_settings.go @@ -23,6 +23,7 @@ import ( "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/urfave/cli/v3" ) @@ -65,7 +66,7 @@ func ClusterSettingsList(conf *cfg.Config) error { return fmt.Errorf("failed to get cluster settings: %s", err) } - table := NewTable(2, 0) + table := printer.NewTable(conf, 2, 0) table.Addheaders("setting", "value") entries := [][]string{} @@ -94,10 +95,10 @@ func ClusterSettingsList(conf *cfg.Config) error { } } - table.entries = entries + table.Entries = entries table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/cluster_util.go b/pkg/es/cluster_util.go index f0064ae..f14fbde 100644 --- a/pkg/es/cluster_util.go +++ b/pkg/es/cluster_util.go @@ -25,6 +25,7 @@ import ( "sync" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/elastic/go-elasticsearch/v9" "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health" "github.com/elastic/go-elasticsearch/v9/typedapi/types" @@ -74,14 +75,14 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool { status[cluster] = st } - table := NewTable(3, 5) + table := printer.NewTable(conf, 3, 5) table.Addheaders("setting", "leader:"+leader, "follower:"+follower) - table.entries = [][]string{ + table.Entries = [][]string{ {"Cluster Name", - Colorize(status[leader].Status.Name, status[leader].ClusterName), - Colorize(status[follower].Status.Name, status[follower].ClusterName), + printer.Colorize(conf, status[leader].Status.Name, status[leader].ClusterName), + printer.Colorize(conf, status[follower].Status.Name, status[follower].ClusterName), }, {"Active Shards", fmt.Sprintf("%d", status[leader].ActiveShards), @@ -101,7 +102,7 @@ func checkClusterStatus(conf *cfg.Config, leader, follower string) bool { }, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { fmt.Println(err) return false } @@ -151,17 +152,17 @@ func findIlmErrors(conf *cfg.Config, leader, follower string) bool { if len(failed[cluster]) > 0 { idx := 0 - table := NewTable(2, len(failed[cluster])) + table := printer.NewTable(conf, 2, len(failed[cluster])) table.Addheaders("ilm errors on "+which, "errors") for name, count := range failed[cluster] { - table.entries[idx] = []string{name, count} + table.Entries[idx] = []string{name, count} idx++ } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { fmt.Println(err) return false } @@ -223,18 +224,18 @@ func findIndicesOnlyOnLeader(conf *cfg.Config, indices ClusterIndices, leader, f } idx := 0 - table := NewTable(3, len(indexOnlyOnLeader)) + table := printer.NewTable(conf, 3, len(indexOnlyOnLeader)) table.Addheaders("index only on leader", "size", "docscount") for name, index := range indexOnlyOnLeader { - name := Colorize("red", name) + name := printer.Colorize(conf, "red", name) - table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} + table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} idx++ } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { fmt.Println(err) return false } @@ -273,18 +274,18 @@ func findOrphanedIndices(conf *cfg.Config, indices ClusterIndices, leader, follo } idx := 0 - table := NewTable(3, len(orphaned)) + table := printer.NewTable(conf, 3, len(orphaned)) table.Addheaders("orphaned index on follower", "size", "docscount") for name, index := range orphaned { - name := Colorize("red", name) + name := printer.Colorize(conf, "red", name) - table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} + table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} idx++ } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { fmt.Println(err) return false } @@ -307,18 +308,18 @@ func findFailedFollowerIndices(conf *cfg.Config, indices ClusterIndices, followe } idx := 0 - table := NewTable(3, len(red)) + table := printer.NewTable(conf, 3, len(red)) table.Addheaders("red index on follower", "size", "docscount") for name, index := range red { - name := Colorize("red", name) + name := printer.Colorize(conf, "red", name) - table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} + table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} idx++ } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { fmt.Println(err) return false } diff --git a/pkg/es/index.go b/pkg/es/index.go index 52fb4fc..ee4588f 100644 --- a/pkg/es/index.go +++ b/pkg/es/index.go @@ -26,6 +26,7 @@ import ( "time" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices" "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl" "github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/healthstatus" @@ -95,13 +96,13 @@ func IndexList(conf *cfg.Config) error { } } - table := NewTable(3, size) + table := printer.NewTable(conf, 3, size) table.Addheaders("name", "size", "docscount") for idx, index := range list { - name := Colorize(*index.Health, *index.Index) + name := printer.Colorize(conf, *index.Health, *index.Index) - table.entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} + table.Entries[idx] = []string{name, *index.DatasetSize, *index.DocsCount} if idx == size-1 { break @@ -109,7 +110,7 @@ func IndexList(conf *cfg.Config) error { } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } @@ -128,7 +129,7 @@ func IndexShow(conf *cfg.Config, index string) error { slog.Debug("ES result", "index", res) - table := NewTable(2, 5) + table := printer.NewTable(conf, 2, 5) table.Addheaders("index property", "value") ts, err := strconv.ParseInt(res[index].Settings.Index.CreationDate.(string), 10, 64) @@ -138,7 +139,7 @@ func IndexShow(conf *cfg.Config, index string) error { created := time.Unix(ts/1000, 0) - table.entries = [][]string{ + table.Entries = [][]string{ {"name", index}, {"replicas", *res[index].Settings.Index.NumberOfReplicas}, {"shards", *res[index].Settings.Index.NumberOfShards}, @@ -146,7 +147,7 @@ func IndexShow(conf *cfg.Config, index string) error { {"uuid", *res[index].Settings.Index.Uuid}, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } @@ -247,7 +248,7 @@ func IndexAllocation(conf *cfg.Config, index string) error { currentNode := res.CurrentNode - table := NewTable(2, 10) + table := printer.NewTable(conf, 2, 10) table.Addheaders("index allocation setting", "value") roles := make([]string, len(currentNode.Roles)) @@ -255,7 +256,7 @@ func IndexAllocation(conf *cfg.Config, index string) error { roles[idx] = role.Name } - table.entries = [][]string{ + table.Entries = [][]string{ {"Index", index}, {"Current node", currentNode.Name}, {"Current k8s node", currentNode.Attributes["k8s_node_name"]}, @@ -268,7 +269,7 @@ func IndexAllocation(conf *cfg.Config, index string) error { {"Can remain on current node", res.CanRemainOnCurrentNode.Name}, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/index_alias.go b/pkg/es/index_alias.go index 51c538c..f59a7a7 100644 --- a/pkg/es/index_alias.go +++ b/pkg/es/index_alias.go @@ -24,6 +24,7 @@ import ( "strings" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" ) // FIXME: add filter support, see IndexCreate mapping @@ -78,12 +79,12 @@ func IndexAliasList(conf *cfg.Config) error { } } - table := NewTable(2, len(aliaslist)) + table := printer.NewTable(conf, 2, len(aliaslist)) table.Addheaders("index", "alias") idx := 0 for index, aliases := range aliaslist { - table.entries[idx] = []string{ + table.Entries[idx] = []string{ index, strings.Join(aliases, ","), } @@ -93,7 +94,7 @@ func IndexAliasList(conf *cfg.Config) error { table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/node.go b/pkg/es/node.go index bc54679..d5c9ff4 100644 --- a/pkg/es/node.go +++ b/pkg/es/node.go @@ -22,6 +22,7 @@ import ( "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" ) func NodeList(conf *cfg.Config) error { @@ -33,11 +34,11 @@ func NodeList(conf *cfg.Config) error { slog.Debug("ES result", "nodes", nodes) - table := NewTable(7, len(nodes)) + table := printer.NewTable(conf, 7, len(nodes)) table.Addheaders("name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %") for idx, node := range nodes { - table.entries[idx] = []string{ + table.Entries[idx] = []string{ *node.Name, *node.Ip, *node.Load1M, @@ -49,7 +50,7 @@ func NodeList(conf *cfg.Config) error { } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/shard.go b/pkg/es/shard.go index c6e3286..897e292 100644 --- a/pkg/es/shard.go +++ b/pkg/es/shard.go @@ -22,10 +22,11 @@ import ( "log/slog" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" "github.com/elastic/go-elasticsearch/v9/typedapi/cat/shards" ) -func colorzizeShard(state, name string) string { +func colorzizeShard(conf *cfg.Config, state, name string) string { color := "red" // in case of RELOCATING and UNASSIGNED switch state { @@ -35,7 +36,7 @@ func colorzizeShard(state, name string) string { color = "yellow" } - return Colorize(color, name) + return printer.Colorize(conf, color, name) } func resolvePrirep(state string) string { @@ -102,13 +103,13 @@ func printShards(conf *cfg.Config, shardlist shards.Response) error { headers = append(headers, "node", "ip") } - table := NewTable(len(headers), len(shardlist)) + table := printer.NewTable(conf, len(headers), len(shardlist)) table.Addheaders(headers...) for idx, shard := range shardlist { - name := colorzizeShard(*shard.State, *shard.Index) + name := colorzizeShard(conf, *shard.State, *shard.Index) - table.entries[idx] = []string{ + table.Entries[idx] = []string{ name, *shard.Shard, resolvePrirep(*shard.Prirep), @@ -118,7 +119,7 @@ func printShards(conf *cfg.Config, shardlist shards.Response) error { } if conf.Verbose { - table.entries[idx] = append(table.entries[idx], + table.Entries[idx] = append(table.Entries[idx], *shard.Node, *shard.Ip, ) @@ -126,7 +127,7 @@ func printShards(conf *cfg.Config, shardlist shards.Response) error { } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/snapshot.go b/pkg/es/snapshot.go index 4149e2d..658b9fa 100644 --- a/pkg/es/snapshot.go +++ b/pkg/es/snapshot.go @@ -25,6 +25,7 @@ import ( "strings" "codeberg.org/scip/esctl/pkg/cfg" + "codeberg.org/scip/esctl/pkg/printer" ) var ( @@ -83,11 +84,11 @@ func SnapshotList(conf *cfg.Config) error { } } - table := NewTable(5, len(snapshots)) + table := printer.NewTable(conf, 5, len(snapshots)) table.Addheaders("name", "index", "start", "orphaned", "status") for idx, snap := range snapshots { - table.entries[idx] = []string{ + table.Entries[idx] = []string{ snap.Name, snap.Forindex, snap.Start, @@ -97,7 +98,7 @@ func SnapshotList(conf *cfg.Config) error { } table.Sort() - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } @@ -116,12 +117,12 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error { return errors.New("no snapshot retrieved") } - table := NewTable(2, 17) + table := printer.NewTable(conf, 2, 17) table.Addheaders("snapshot property", "value") snap := res.Snapshots[0] - table.entries = [][]string{ + table.Entries = [][]string{ {"snapshot", snapshot}, {"uuid", snap.Uuid}, {"repository", *snap.Repository}, @@ -137,7 +138,7 @@ func SnapshotShow(conf *cfg.Config, snapshot string) error { {"shards-successful", fmt.Sprintf("%d", snap.Shards.Successful)}, } - if err := table.PrintMarkdown(); err != nil { + if err := table.Print(); err != nil { return err } diff --git a/pkg/es/table.go b/pkg/es/table.go deleted file mode 100644 index 6a6d552..0000000 --- a/pkg/es/table.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright © 2026 Thomas von Dein - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ -package es - -import ( - "fmt" - "sort" - "strings" - - "github.com/olekukonko/tablewriter" - "github.com/olekukonko/tablewriter/renderer" - "github.com/olekukonko/tablewriter/tw" -) - -type Table struct { - headers []string - entries [][]string -} - -func NewTable(columns, rows int) *Table { - table := Table{} - - table.headers = make([]string, columns) - table.entries = make([][]string, rows) - - return &table -} - -func (data *Table) PrintMarkdown() error { - tableString := &strings.Builder{} - - table := tablewriter.NewTable(tableString, - tablewriter.WithRenderer( - renderer.NewBlueprint( - tw.Rendition{ - Borders: tw.Border{ - Left: tw.On, - Right: tw.On, - Top: tw.Off, - Bottom: tw.Off, - }, - Settings: tw.Settings{ - Separators: tw.Separators{ - ShowHeader: tw.On, - ShowFooter: tw.Off, - BetweenRows: tw.Off, - BetweenColumns: 0, - }, - }, - Symbols: tw.NewSymbols(tw.StyleMarkdown), - })), - - tablewriter.WithConfig( - tablewriter.Config{ - Header: tw.CellConfig{ - Formatting: tw.CellFormatting{ - Alignment: tw.AlignLeft, - AutoFormat: tw.Off, - }, - }, - Row: tw.CellConfig{ - Formatting: tw.CellFormatting{ - Alignment: tw.AlignLeft, - }, - }, - }, - ), - ) - - table.Header(data.headers) - - if err := table.Bulk(data.entries); err != nil { - return fmt.Errorf("failed to add data to table renderer: %s", err) - } - - if err := table.Render(); err != nil { - return fmt.Errorf("failed to render table: %s", err) - } - - fmt.Println(tableString.String()) - - return nil -} - -func (data *Table) Sort() { - // sanity checks - if len(data.entries) == 0 { - return - } - - sort.Slice(data.entries, func(i, j int) bool { - return data.entries[i][0] < data.entries[j][0] - }) -} - -func (data *Table) Addheaders(headers ...string) { - for idx, header := range headers { - data.headers[idx] = bold(strings.ToUpper(header)) - } -} diff --git a/pkg/es/color.go b/pkg/printer/color.go similarity index 83% rename from pkg/es/color.go rename to pkg/printer/color.go index 930b208..2c328c6 100644 --- a/pkg/es/color.go +++ b/pkg/printer/color.go @@ -15,9 +15,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -package es +package printer -import "github.com/fatih/color" +import ( + "codeberg.org/scip/esctl/pkg/cfg" + "github.com/fatih/color" +) var ( green = color.New(color.BgGreen, color.FgHiWhite).SprintFunc() @@ -26,7 +29,12 @@ var ( bold = color.New(color.Bold).SprintFunc() ) -func Colorize(col, what string) string { +func Colorize(conf *cfg.Config, col, what string) string { + switch conf.Output { + case "json", "yaml": + return what + } + switch col { case "green": what = green(what) diff --git a/pkg/printer/table.go b/pkg/printer/table.go new file mode 100644 index 0000000..7ee17d9 --- /dev/null +++ b/pkg/printer/table.go @@ -0,0 +1,306 @@ +/* +Copyright © 2026 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +package printer + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "codeberg.org/scip/esctl/pkg/cfg" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/renderer" + "github.com/olekukonko/tablewriter/tw" + "gopkg.in/yaml.v3" +) + +type Table struct { + Mode string // table, markdown, json + Headers []string + Entries [][]string + + lenHeaders []int +} + +func NewTable(conf *cfg.Config, columns, rows int) *Table { + table := Table{Mode: conf.Output} + + table.Headers = make([]string, columns) + table.Entries = make([][]string, rows) + table.lenHeaders = make([]int, columns) + + return &table +} + +func (data *Table) Print() error { + switch data.Mode { + case "markdown", "md": + return data.PrintMarkdown() + case "json": + return data.PrintJSON() + case "yaml": + return data.PrintYAML() + default: + return data.PrintTSV() + } +} + +var ( + ansiCtrlSeq = regexp.MustCompile(`\033.[0-9;]+m`) +) + +// needed for json and yaml output +func (data *Table) toMap() []map[string]string { + raw := make([]map[string]string, len(data.Entries)) + + for idx, entries := range data.Entries { + raw[idx] = make(map[string]string, len(data.Headers)) + for eidx, entry := range entries { + raw[idx][data.Headers[eidx]] = entry + } + } + + return raw +} + +func (data *Table) PrintYAML() error { + raw := data.toMap() + + body, err := yaml.Marshal(raw) + if err != nil { + return fmt.Errorf("failed to produce YAML output: %s", err) + } + + fmt.Println(string(body)) + + return nil +} + +func (data *Table) PrintJSON() error { + raw := data.toMap() + + body, err := json.MarshalIndent(raw, "", " ") + if err != nil { + return fmt.Errorf("failed to produce JSON output: %s", err) + } + + fmt.Println(string(body)) + + return nil +} + +// Well. tablewriter should be able to print in TSV mode, but it +// doesn't work because of the various ansic color escapes we're +// using. Therefore we do it the hard way and use our own TSV printer. +func (data *Table) PrintTSV() error { + // determine lenght's + for idx, head := range data.Headers { + data.lenHeaders[idx] = visibleLen(head) + } + + for _, entries := range data.Entries { + for idx, entry := range entries { + length := visibleLen(entry) + if data.lenHeaders[idx] < length { + data.lenHeaders[idx] = length + } + } + } + + // output + for idx, header := range data.Headers { + fmt.Print(header, strings.Repeat(" ", data.lenHeaders[idx]-visibleLen(header))) + if idx < len(data.Headers)-1 { + fmt.Print(" ") + } + } + fmt.Println() + + for _, entries := range data.Entries { + for idx, entry := range entries { + length := visibleLen(entry) + + if isInt(entry) { + // align right + fmt.Print(strings.Repeat(" ", data.lenHeaders[idx]-length), entry) + } else { + fmt.Print(entry, strings.Repeat(" ", data.lenHeaders[idx]-length)) + } + + if idx < len(data.Headers)-1 { + fmt.Print(" ") + } + } + fmt.Println() + } + + return nil +} + +func (data *Table) PrintTabularFail() error { + OFS := " " + + tableString := &strings.Builder{} + + styleTSV := tw.NewSymbolCustom("space").WithColumn("\t") + + table := tablewriter.NewTable(tableString, + tablewriter.WithRenderer( + renderer.NewBlueprint(tw.Rendition{ + Borders: tw.BorderNone, + Symbols: styleTSV, + Settings: tw.Settings{ + Separators: tw.SeparatorsNone, + Lines: tw.LinesNone, + }, + })), + tablewriter.WithConfig(tablewriter.Config{ + Header: tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoFormat: tw.Off, + }, + Padding: tw.CellPadding{Global: tw.Padding{Left: "", Right: OFS}}, + }, + Row: tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoWrap: tw.WrapNone, + Alignment: tw.AlignLeft, + }, + Padding: tw.CellPadding{Global: tw.Padding{Right: OFS}}, + }, + + Debug: true, + }), + ) + + table.Header(data.Headers) + + if err := table.Bulk(data.Entries); err != nil { + return fmt.Errorf("failed to add data to table renderer: %s", err) + } + + if err := table.Render(); err != nil { + return fmt.Errorf("failed to render table: %s", err) + } + + // we need to trim our output here, because tablewriter appends + // excess whitespace to our rows. + cleanedString := &strings.Builder{} + + for _, row := range strings.Split(strings.TrimSpace(tableString.String()), "\n") { + cleanedString.WriteString(strings.TrimSpace(row)) + cleanedString.WriteString("\n") + } + + fmt.Println(cleanedString.String()) + + return nil +} + +func (data *Table) PrintMarkdown() error { + tableString := &strings.Builder{} + + table := tablewriter.NewTable(tableString, + tablewriter.WithRenderer( + renderer.NewBlueprint( + tw.Rendition{ + Borders: tw.Border{ + Left: tw.On, + Right: tw.On, + Top: tw.Off, + Bottom: tw.Off, + }, + Settings: tw.Settings{ + Separators: tw.Separators{ + ShowHeader: tw.On, + ShowFooter: tw.Off, + BetweenRows: tw.Off, + BetweenColumns: 0, + }, + }, + Symbols: tw.NewSymbols(tw.StyleMarkdown), + })), + + tablewriter.WithConfig( + tablewriter.Config{ + Header: tw.CellConfig{ + Formatting: tw.CellFormatting{ + Alignment: tw.AlignLeft, + AutoFormat: tw.Off, + }, + }, + Row: tw.CellConfig{ + Formatting: tw.CellFormatting{ + Alignment: tw.AlignLeft, + }, + }, + }, + ), + ) + + table.Header(data.Headers) + + if err := table.Bulk(data.Entries); err != nil { + return fmt.Errorf("failed to add data to table renderer: %s", err) + } + + if err := table.Render(); err != nil { + return fmt.Errorf("failed to render table: %s", err) + } + + fmt.Println(tableString.String()) + + return nil +} + +func (data *Table) Sort() { + // sanity checks + if len(data.Entries) == 0 { + return + } + + sort.Slice(data.Entries, func(i, j int) bool { + return data.Entries[i][0] < data.Entries[j][0] + }) +} + +func (data *Table) Addheaders(headers ...string) { + for idx, header := range headers { + switch data.Mode { + case "json", "yaml": + data.Headers[idx] = strings.ToLower(header) + default: + data.Headers[idx] = bold(strings.ToUpper(header)) + } + } +} + +// return the length of a string but only visible chars, w/o ansi color escapes +func visibleLen(word string) int { + return len(ansiCtrlSeq.ReplaceAllLiteralString(word, "")) +} + +func isInt(num string) bool { + if _, err := strconv.Atoi(num); err == nil { + return true + } + + return false +}