diff --git a/go.mod b/go.mod
index c268fb9..e8116bb 100644
--- a/go.mod
+++ b/go.mod
@@ -85,6 +85,7 @@ require (
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
diff --git a/go.sum b/go.sum
index ba50cb2..2918b3b 100644
--- a/go.sum
+++ b/go.sum
@@ -171,6 +171,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550 h1:C3CfUXH/qmWuQFRqnPm3Sx8PFxa+pqACjhV5CaNO8pw=
+github.com/seeruk/go-wordwrap v0.0.0-20191208221741-14ec4aac9550/go.mod h1:Sl541M2Em6rRG3V9WObycR7MYFZiERVkd/TJg0Gt0U4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/pkg/cfg/term.go b/pkg/cfg/term.go
new file mode 100644
index 0000000..c05069f
--- /dev/null
+++ b/pkg/cfg/term.go
@@ -0,0 +1,38 @@
+/*
+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 cfg
+
+import (
+ "os"
+
+ "golang.org/x/term"
+)
+
+const (
+ DefaultMargin = 4
+)
+
+func GetTermWidth() int {
+ if term.IsTerminal(int(os.Stdout.Fd())) {
+ width, _, err := term.GetSize(int(os.Stdout.Fd()))
+ if err == nil {
+ return width - DefaultMargin
+ }
+ }
+
+ return 80
+}
diff --git a/pkg/es/api.go b/pkg/es/api.go
index bf6bc4e..ab06457 100644
--- a/pkg/es/api.go
+++ b/pkg/es/api.go
@@ -41,12 +41,10 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/chzyer/readline"
"github.com/go-openapi/spec"
- "golang.org/x/term"
)
const (
- DefaultMargin = 4
- intro = `Input format: verb path [data]"
+ intro = `Input format: verb path [data]"
Example:
@@ -328,19 +326,19 @@ func ApiShow(conf *cfg.Config, showpath, verb string) error {
cleanMarkup := regexp.MustCompile(`<[^<>]+>`)
- width := getTermWidth()
+ width := cfg.GetTermWidth()
params := getApiParameters(op, showpath, width)
sample := getApiExample(op)
- description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, DefaultMargin)
+ description := markdown.Render(cleanMarkup.ReplaceAllString(op.Op.Description, ""), width, cfg.DefaultMargin)
var bold = lipgloss.NewStyle().
Bold(true)
var paragraph = lipgloss.NewStyle().
MarginBottom(1).
- MarginLeft(DefaultMargin)
+ MarginLeft(cfg.DefaultMargin)
var boldparagraph = lipgloss.NewStyle().
MarginBottom(1).
- MarginLeft(DefaultMargin).
+ MarginLeft(cfg.DefaultMargin).
Bold(true)
var indentparagraph = lipgloss.NewStyle().
MarginBottom(1).
@@ -423,7 +421,7 @@ func getApiParameters(op *Op, path string, width int) Params {
}
} else {
par := Param{
- Description: string(markdown.Render(param.Description, width, DefaultMargin)),
+ Description: string(markdown.Render(param.Description, width, cfg.DefaultMargin)),
Param: param.Name,
}
@@ -527,14 +525,3 @@ func findOperation(item spec.PathItemProps) []*Op {
return ops
}
-
-func getTermWidth() int {
- if term.IsTerminal(int(os.Stdout.Fd())) {
- width, _, err := term.GetSize(int(os.Stdout.Fd()))
- if err == nil {
- return width - DefaultMargin
- }
- }
-
- return 80
-}
diff --git a/pkg/es/cluser_health_report.go b/pkg/es/cluser_health_report.go
new file mode 100644
index 0000000..e751e51
--- /dev/null
+++ b/pkg/es/cluser_health_report.go
@@ -0,0 +1,59 @@
+/*
+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 (
+ "encoding/json"
+ "fmt"
+
+ "codeberg.org/scip/esctl/pkg/cfg"
+)
+
+type HealthReport struct {
+ Indicators map[string]HealthReportIndicator
+}
+
+type HealthReportIndicator struct {
+ Status, Symptom string
+ Diagnosis []HealthReportDiagnosis
+}
+
+type HealthReportDiagnosis struct {
+ Id, Cause, Action string
+ AffectedResources map[string][]string `json:"affected_resources"`
+}
+
+// we do not use the go-elasticsearch client API here but call the ES
+// API directly, because the returned structure (a
+// healthreport.Response) is not iterable, you'd have to explicitly
+// call every indicator type and every cause etc which also have
+// different types each. To check which is !green would result in a
+// gigantic function.
+func getHealthReport(conf *cfg.Config) (*HealthReport, error) {
+ raw, err := CallAPI(conf, "GET", "/_health_report", "")
+ if err != nil {
+ return nil, err
+ }
+
+ report := HealthReport{}
+
+ if err := json.Unmarshal(raw, &report); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
+ }
+
+ return &report, nil
+}
diff --git a/pkg/es/cluster.go b/pkg/es/cluster.go
index 571e0f9..fe17035 100644
--- a/pkg/es/cluster.go
+++ b/pkg/es/cluster.go
@@ -25,12 +25,7 @@ import (
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
"github.com/dustin/go-humanize"
- "github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
- "github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
- "github.com/elastic/go-elasticsearch/v9/typedapi/ccr/stats"
- "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/health"
clusterstats "github.com/elastic/go-elasticsearch/v9/typedapi/cluster/stats"
- "github.com/elastic/go-elasticsearch/v9/typedapi/core/info"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
)
@@ -78,8 +73,8 @@ func ClusterList(conf *cfg.Config) error {
// We're using goroutines here to parallelize API requests, since we
// have to do 3 of'em for each cluster. This speeds things up.
-func ClusterStatus(conf *cfg.Config) error {
- gocount := 5
+func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
+ gocount := 6
if conf.Verbose {
gocount++
}
@@ -90,65 +85,72 @@ func ClusterStatus(conf *cfg.Config) error {
wg := &sync.WaitGroup{}
wg.Add(gocount)
- go getApiData(es, wg, responses, "health")
- go getApiData(es, wg, responses, "info")
- go getApiData(es, wg, responses, "ccrstats")
- go getApiData(es, wg, responses, "indices")
- go getApiData(es, wg, responses, "tasks")
+ go getApiData(conf, es, wg, responses, "health")
+ go getApiData(conf, es, wg, responses, "healthreport")
+ go getApiData(conf, es, wg, responses, "info")
+ go getApiData(conf, es, wg, responses, "ccr")
+ go getApiData(conf, es, wg, responses, "indices")
+ go getApiData(conf, es, wg, responses, "tasks")
if conf.Verbose {
- go getApiData(es, wg, responses, "stats")
+ go getApiData(conf, es, wg, responses, "stats")
}
wg.Wait()
- var clusterhealth *health.Response
- var info *info.Response
- var ccrstats *stats.Response
- var clusterstats *clusterstats.Response
- var indexstats *indices.Response
- var taskstatus *tasks.Response
+ all := apiResponse{}
for i := 0; i < gocount; i++ {
r := <-responses
if r.error != nil {
- return r.error
+ return nil, r.error
}
switch r.which {
case ResponseHealth:
- clusterhealth = r.health
+ all.health = r.health
case ResponseCcr:
- ccrstats = r.ccr
+ all.ccr = r.ccr
case ResponseInfo:
- info = r.info
+ all.info = r.info
case ResponseStats:
- clusterstats = r.stats
+ all.stats = r.stats
case ResponseIndices:
- indexstats = r.indices
+ all.indices = r.indices
case ResponseTasks:
- taskstatus = r.tasks
+ all.tasks = r.tasks
+ case ResponseHealthReport:
+ all.healthreport = r.healthreport
}
}
- slog.Debug("ES result", "cluster health", clusterhealth)
+ return &all, nil
+}
- isleader := len(ccrstats.AutoFollowStats.AutoFollowedClusters) == 0
+func ClusterStatus(conf *cfg.Config) error {
+ res, err := getClusterStatus(conf)
+ if err != nil {
+ return err
+ }
+
+ slog.Debug("ES result", "cluster health", res.health)
+
+ isleader := len(res.ccr.AutoFollowStats.AutoFollowedClusters) == 0
ccrfollowing := ""
- if len(ccrstats.AutoFollowStats.AutoFollowedClusters) > 0 {
+ if len(res.ccr.AutoFollowStats.AutoFollowedClusters) > 0 {
// is following another cluster
ccrfollowing = fmt.Sprintf("%s (%d/%d)",
- ccrstats.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
- ccrstats.AutoFollowStats.NumberOfSuccessfulFollowIndices,
- ccrstats.AutoFollowStats.NumberOfFailedFollowIndices,
+ res.ccr.AutoFollowStats.AutoFollowedClusters[0].ClusterName,
+ res.ccr.AutoFollowStats.NumberOfSuccessfulFollowIndices,
+ res.ccr.AutoFollowStats.NumberOfFailedFollowIndices,
)
}
// look for red indices, if any
redindices := 0
- for _, index := range *indexstats {
+ for _, index := range *res.indices {
if *index.Health == "red" {
redindices++
}
@@ -156,7 +158,7 @@ func ClusterStatus(conf *cfg.Config) error {
// look for long running tasks
longtasks := 0
- for _, task := range *taskstatus {
+ for _, task := range *res.tasks {
if strings.Contains(*task.RunningTime, "d") {
longtasks++
}
@@ -166,16 +168,16 @@ func ClusterStatus(conf *cfg.Config) error {
table.Addheaders(conf.DefaultCluster.Name, "status")
table.Entries = [][]string{
- {"Cluster Name", clusterhealth.ClusterName},
- {"ES Status", printer.Colorize(conf, clusterhealth.Status.Name, clusterhealth.Status.Name)},
- {"ES Version", info.Version.Int},
+ {"Cluster Name", res.health.ClusterName},
+ {"ES Status", printer.Colorize(conf, res.health.Status.Name, res.health.Status.Name)},
+ {"ES Version", res.info.Version.Int},
{"Is Leader", fmt.Sprintf("%t", isleader)},
- {"Active Shards", fmt.Sprintf("%d", clusterhealth.ActiveShards)},
- {"Active Primary Shards", fmt.Sprintf("%d", clusterhealth.ActivePrimaryShards)},
- {"Unassigned Shards", fmt.Sprintf("%d", clusterhealth.UnassignedShards)},
- {"Unassigned Primary Shards", fmt.Sprintf("%d", clusterhealth.UnassignedPrimaryShards)},
- {"Pending Tasks", fmt.Sprintf("%d", clusterhealth.NumberOfPendingTasks)},
- {"Nodes", fmt.Sprintf("%d", clusterhealth.NumberOfNodes)},
+ {"Active Shards", fmt.Sprintf("%d", res.health.ActiveShards)},
+ {"Active Primary Shards", fmt.Sprintf("%d", res.health.ActivePrimaryShards)},
+ {"Unassigned Shards", fmt.Sprintf("%d", res.health.UnassignedShards)},
+ {"Unassigned Primary Shards", fmt.Sprintf("%d", res.health.UnassignedPrimaryShards)},
+ {"Pending Tasks", fmt.Sprintf("%d", res.health.NumberOfPendingTasks)},
+ {"Nodes", fmt.Sprintf("%d", res.health.NumberOfNodes)},
{"Red Indices", fmt.Sprintf("%d", redindices)},
{"Long Running Tasks", fmt.Sprintf("%d", longtasks)},
}
@@ -183,12 +185,30 @@ func ClusterStatus(conf *cfg.Config) error {
if !isleader {
table.Entries = append(table.Entries, [][]string{
{"AutoFollow (success/failed indices)", ccrfollowing},
- {"Followed Indices", fmt.Sprintf("%d", len(ccrstats.FollowStats.Indices))},
+ {"Followed Indices", fmt.Sprintf("%d", len(res.ccr.FollowStats.Indices))},
}...)
}
if conf.Verbose {
- table = gatherClusterStats(conf, clusterstats, table)
+ table = gatherClusterStats(conf, res.stats, table)
+ }
+
+ if res.health.Status.Name != "green" {
+ for name, indicator := range res.healthreport.Indicators {
+ if indicator.Status != "green" {
+ table.Entries = append(table.Entries, []string{
+ printer.Colorize(conf, indicator.Status, "Bad health "+name), indicator.Symptom,
+ })
+
+ for _, diag := range indicator.Diagnosis {
+ table.Entries = append(table.Entries, []string{" -> cause", diag.Cause})
+
+ for resource, items := range diag.AffectedResources {
+ table.Entries = append(table.Entries, []string{" -> affected " + resource, strings.Join(items, ",")})
+ }
+ }
+ }
+ }
}
if err := table.Print(); err != nil {
diff --git a/pkg/es/debug.go b/pkg/es/debug.go
new file mode 100644
index 0000000..9045bba
--- /dev/null
+++ b/pkg/es/debug.go
@@ -0,0 +1,48 @@
+/*
+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 (
+ "codeberg.org/scip/esctl/pkg/cfg"
+ "github.com/alecthomas/repr"
+)
+
+func Debug(conf *cfg.Config) error {
+ report, err := getHealthReport(conf)
+ if err != nil {
+ return err
+ }
+
+ repr.Println(report)
+ /*
+ res, err := conf.DefaultCluster.ES().Search().
+ Index(conf.Index).
+ Size(0).
+ Aggregations(map[string]types.Aggregations{
+ "min_ts": *esdsl.NewMinAggregation().Field("@timestamp").AggregationsCaster(),
+ "max_ts": *esdsl.NewMaxAggregation().Field("@timestamp").AggregationsCaster(),
+ }).
+ Do(context.Background())
+
+ if err != nil {
+ return err
+ }
+
+ repr.Println(res)
+ */
+ return nil
+}
diff --git a/pkg/es/ilm_forecast.go b/pkg/es/ilm_forecast.go
index c9db433..569bf2d 100644
--- a/pkg/es/ilm_forecast.go
+++ b/pkg/es/ilm_forecast.go
@@ -190,9 +190,9 @@ func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) {
wg := &sync.WaitGroup{}
wg.Add(3)
- go getApiData(conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
- go getApiData(conf.DefaultCluster.ES(), wg, responses, "explain")
- go getApiData(conf.DefaultCluster.ES(), wg, responses, "policies")
+ go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
+ go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "explain")
+ go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "policies")
wg.Wait()
diff --git a/pkg/es/parallel.go b/pkg/es/parallel.go
index 9097c18..25a47be 100644
--- a/pkg/es/parallel.go
+++ b/pkg/es/parallel.go
@@ -18,10 +18,10 @@ package es
import (
"context"
- "errors"
"fmt"
"sync"
+ "codeberg.org/scip/esctl/pkg/cfg"
"github.com/elastic/go-elasticsearch/v9"
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/indices"
"github.com/elastic/go-elasticsearch/v9/typedapi/cat/tasks"
@@ -43,12 +43,14 @@ const (
ResponseTasks
ResponseExplain
ResponseLifecycle
+ ResponseHealthReport
)
type apiResponse struct {
error error
info *info.Response
health *health.Response
+ healthreport *HealthReport
ccr *stats.Response
stats *clusterstats.Response
indices *indices.Response
@@ -59,12 +61,16 @@ type apiResponse struct {
which int
}
-func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
- reschan chan apiResponse, which string) {
+func getApiData(
+ conf *cfg.Config,
+ es *elasticsearch.TypedClient,
+ wg *sync.WaitGroup,
+ reschan chan apiResponse,
+ which string) {
defer wg.Done()
ar := apiResponse{}
- arerr := errors.New("")
+ var arerr error
switch which {
case "health":
@@ -75,6 +81,13 @@ func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
ar.which = ResponseHealth
arerr = err
+ case "healthreport":
+ report, err := getHealthReport(conf)
+
+ ar.healthreport = report
+ ar.which = ResponseHealthReport
+ arerr = err
+
case "info":
res, err := es.Info().
Do(context.Background())
@@ -83,7 +96,7 @@ func getApiData(es *elasticsearch.TypedClient, wg *sync.WaitGroup,
ar.which = ResponseInfo
arerr = err
- case "ccrstats":
+ case "ccr":
res, err := es.Ccr.Stats().
Do(context.Background())
diff --git a/pkg/es/search.go b/pkg/es/search.go
index f93c5f1..19a2204 100644
--- a/pkg/es/search.go
+++ b/pkg/es/search.go
@@ -26,7 +26,6 @@ import (
"codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer"
- "github.com/alecthomas/repr"
"github.com/elastic/go-elasticsearch/v9/typedapi/core/search"
"github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
"github.com/elastic/go-elasticsearch/v9/typedapi/indices/validatequery"
@@ -149,25 +148,6 @@ func validateSearch(conf *cfg.Config, queries []string) error {
return nil
}
-func Debug(conf *cfg.Config) error {
- res, err := conf.DefaultCluster.ES().Search().
- Index(conf.Index).
- Size(0).
- Aggregations(map[string]types.Aggregations{
- "min_ts": *esdsl.NewMinAggregation().Field("@timestamp").AggregationsCaster(),
- "max_ts": *esdsl.NewMaxAggregation().Field("@timestamp").AggregationsCaster(),
- }).
- Do(context.Background())
-
- if err != nil {
- return err
- }
-
- repr.Println(res)
-
- return nil
-}
-
func searchOnce(conf *cfg.Config, search *search.Search) error {
res, err := search.
From(conf.From).
diff --git a/pkg/printer/table.go b/pkg/printer/table.go
index d0083da..236ffc8 100644
--- a/pkg/printer/table.go
+++ b/pkg/printer/table.go
@@ -28,6 +28,7 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
+ "github.com/seeruk/go-wordwrap"
"gopkg.in/yaml.v3"
)
@@ -38,10 +39,11 @@ type Table struct {
lenHeaders []int
alignInts bool
+ maxwidth int
}
func NewTable(conf *cfg.Config, columns, rows int) *Table {
- table := Table{Mode: conf.Output}
+ table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
table.Headers = make([]string, columns)
table.Entries = make([][]string, rows)
@@ -118,17 +120,31 @@ func (data *Table) PrintTSV() error {
}
for _, entries := range data.Entries {
+ currentWidth := 0
for idx, entry := range entries {
length := visibleLen(entry)
+
if data.lenHeaders[idx] < length {
- data.lenHeaders[idx] = length
+ if length > currentWidth+data.maxwidth {
+ data.lenHeaders[idx] = data.maxwidth - currentWidth
+ } else {
+ data.lenHeaders[idx] = length
+ }
}
+
+ currentWidth += data.lenHeaders[idx]
}
}
- // output
+ // output headers
for idx, header := range data.Headers {
- fmt.Print(header, strings.Repeat(" ", data.lenHeaders[idx]-visibleLen(header)))
+ if idx+1 != len(data.Headers) {
+ fmt.Print(header, strings.Repeat(" ", data.lenHeaders[idx]-visibleLen(header)))
+ } else {
+ // no padding for last header
+ fmt.Print(header)
+ }
+
if idx < len(data.Headers)-1 {
fmt.Print(" ")
}
@@ -136,14 +152,37 @@ func (data *Table) PrintTSV() error {
fmt.Println()
for _, entries := range data.Entries {
+ currentWidth := 0
+
for idx, entry := range entries {
length := visibleLen(entry)
+ if length+currentWidth > data.maxwidth {
+ // text is too wide to be put into one line, wrap it
+ wrapper := wordwrap.Wrapper(data.maxwidth-currentWidth, false)
+ wrapped := wrapper(entry)
+
+ // and indent it
+ for idx, line := range strings.Split(wrapped, "\n") {
+ if idx == 0 {
+ entry = line
+ } else {
+ entry += "\n " + strings.Repeat(" ", currentWidth) + line
+ }
+ }
+ }
+
+ currentWidth += data.lenHeaders[idx]
+
if isInt(entry) && data.alignInts {
// align right
fmt.Print(strings.Repeat(" ", data.lenHeaders[idx]-length), entry)
- } else {
+ } else if length < data.lenHeaders[idx] && idx+1 != len(entries) {
+ // pad right, if required
fmt.Print(entry, strings.Repeat(" ", data.lenHeaders[idx]-length))
+ } else {
+ // no padding for last entry
+ fmt.Print(entry)
}
if idx < len(data.Headers)-1 {