Compare commits

..

3 Commits

Author SHA1 Message Date
7f1018f0e1 satisfy linter 2026-07-10 15:06:17 +02:00
3486bc0370 add CSV output support 2026-07-10 15:04:55 +02:00
374ff99916 add searchql and help-esql commands 2026-07-10 15:04:41 +02:00
20 changed files with 114 additions and 184 deletions

View File

@@ -29,7 +29,6 @@ Features:
logical condition (OR, AND), use PIT, limit datetime (ES date math logical condition (OR, AND), use PIT, limit datetime (ES date math
can be used), etc. It is however not yet possible to create can be used), etc. It is however not yet possible to create
recursive searches like: `(cond1 AND cond2) OR (cond3 OR cond4)`. recursive searches like: `(cond1 AND cond2) OR (cond3 OR cond4)`.
- Search using ES|QL language: `esctl searchql`.
- Cross cluster replication (ccr): view, pause, resume, delete - Cross cluster replication (ccr): view, pause, resume, delete
replication. You can also manage follower configuration. replication. You can also manage follower configuration.
- Index management: manage aliases, create, modify, delete indices, - Index management: manage aliases, create, modify, delete indices,
@@ -353,30 +352,6 @@ $ esctl search -i foo* -F title=zeitbuchung message=pause | jq
} }
``` ```
You can also search using [ES|QL](https://www.elastic.co/docs/reference/query-languages/esql/esql-getting-started):
```console
$ esctl searchql "from hyperdrive | sort @timestamp | limit 5"
@TIMESTAMP MESSAGE TAG
2026-06-24T08:24:56.000Z arosu loop
2026-06-24T08:24:58.000Z hami loop
2026-06-24T08:24:59.000Z ishininu loop
2026-06-24T08:25:00.000Z uyomoruron loop
2026-06-24T08:25:02.000Z ishimime loop
```
There are several output modes (json, yaml, csv), to get esql output as CSV:
```console
$ esctl searchql "from hyperdrive | sort @timestamp | limit 5" -o csv
@timestamp,message,tag
2026-06-24T08:24:56.000Z,arosu,loop
2026-06-24T08:24:58.000Z,hami,loop
2026-06-24T08:24:59.000Z,ishininu,loop
2026-06-24T08:25:00.000Z,uyomoruron,loop
2026-06-24T08:25:02.000Z,ishimime,loop
```
To check which field mappings are available for an index: To check which field mappings are available for an index:
```console ```console
$ esctl index show foo2 $ esctl index show foo2

View File

@@ -21,7 +21,6 @@ import (
"fmt" "fmt"
golog "log" golog "log"
"os" "os"
"runtime/debug"
"runtime/pprof" "runtime/pprof"
"strings" "strings"
@@ -119,12 +118,6 @@ func Main() int {
Usage: "enable HTTP debugging", Usage: "enable HTTP debugging",
Destination: &conf.DebugHTTP, Destination: &conf.DebugHTTP,
}, },
&cli.BoolFlag{
Name: "debug-goroutines",
Value: false,
Usage: "enable goroutine debugging",
Destination: &conf.DebugGoRoutines,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "align-ints", Name: "align-ints",
Aliases: []string{"I"}, Aliases: []string{"I"},
@@ -260,12 +253,6 @@ func Version(conf *cfg.Config) *cli.Command {
Usage: "show esctl version information", Usage: "show esctl version information",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
info, _ := debug.ReadBuildInfo()
if strings.Contains(info.Main.Version, "+dirty") {
cfg.COMMIT += "+dirty"
}
_, err := fmt.Printf(versionFmt, _, err := fmt.Printf(versionFmt,
cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION) cfg.Version, cfg.BUILD, cfg.BRANCH, cfg.COMMIT, cfg.GOVERSION, cfg.APIVERSION)

View File

@@ -194,18 +194,18 @@ func (cluster *Cluster) getDefaultOptions() []elasticsearch.Option {
} }
func (cluster *Cluster) getTransport() elastictransport.Option { func (cluster *Cluster) getTransport() elastictransport.Option {
transport := new(http.Transport{ transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}) }
if cluster.DebugHTTP { if cluster.DebugHTTP {
return elastictransport.WithTransport( return elastictransport.WithTransport(
new(DebugTransport{Transport: transport}), &DebugTransport{Transport: transport},
) )
} }
return elastictransport.WithTransport( return elastictransport.WithTransport(
new(CompatibilityTransport{Transport: transport}), &CompatibilityTransport{Transport: transport},
) )
} }

View File

@@ -28,7 +28,7 @@ import (
) )
const ( const (
Version string = `v0.0.27` Version string = `v0.0.26`
) )
var ( var (
@@ -89,13 +89,12 @@ type Config struct {
Force bool // ccr follower renew: -f Force bool // ccr follower renew: -f
DebugHTTP bool // root: --debug-http DebugHTTP bool // root: --debug-http
DebugGoRoutines bool // root: --debug-goroutines Separator string // role diff: -s
Separator string // role diff: -s NotDeployed bool // role diff: -n
NotDeployed bool // role diff: -n Undefined bool // role diff: -u
Undefined bool // role diff: -u Diff bool // role diff: -D
Diff bool // role diff: -D Hidden bool // ds ls: -H
Hidden bool // ds ls: -H
// rollover // rollover
MaxAge string MaxAge string
@@ -114,7 +113,7 @@ type Config struct {
} }
func NewConfig() *Config { func NewConfig() *Config {
return new(Config{Clusters: map[string]*Cluster{}}) return &Config{Clusters: map[string]*Cluster{}}
} }
func getDefaultPath() string { func getDefaultPath() string {
@@ -217,7 +216,7 @@ func (conf *Config) LoadConfig() error {
return fmt.Errorf("failed to read config file: %w", err) return fmt.Errorf("failed to read config file: %w", err)
} }
newconf := new(Config{}) newconf := &Config{}
err = yaml.Unmarshal(data, newconf) err = yaml.Unmarshal(data, newconf)
if err != nil { if err != nil {

View File

@@ -28,7 +28,6 @@ import (
"io" "io"
"log" "log"
"log/slog" "log/slog"
"maps"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -159,7 +158,7 @@ func ApiRepl(conf *cfg.Config) error {
} }
func pageJsonOutput(conf *cfg.Config, raw []byte) { func pageJsonOutput(conf *cfg.Config, raw []byte) {
tmpconf := new(cfg.Config{HaveJQ: conf.HaveJQ}) tmpconf := &cfg.Config{HaveJQ: conf.HaveJQ}
if conf.Pager != "" { if conf.Pager != "" {
tmpconf.HaveJQ = false tmpconf.HaveJQ = false
@@ -207,16 +206,16 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) {
verb = strings.ToUpper(verb) verb = strings.ToUpper(verb)
// we're using port-forwards anyway // we're using port-forwards anyway
noVerifyTransport := new(http.Transport{ noVerifyTransport := &http.Transport{
TLSClientConfig: new(tls.Config{InsecureSkipVerify: true}), TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}) }
client := new(http.Client{Transport: noVerifyTransport}) client := &http.Client{Transport: noVerifyTransport}
if conf.DebugHTTP { if conf.DebugHTTP {
client = new(http.Client{ client = &http.Client{
Transport: new(cfg.DebugTransport{ Transport: &cfg.DebugTransport{
Transport: noVerifyTransport})}) Transport: noVerifyTransport}}
} }
req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data))) req, err := http.NewRequest(verb, conf.DefaultCluster.Uri+path, bytes.NewBuffer([]byte(data)))
@@ -367,7 +366,16 @@ func ApiList(conf *cfg.Config, pattern string) error {
func ApiPathNames() []string { func ApiPathNames() []string {
assets.LoadAssetOpenApi() assets.LoadAssetOpenApi()
return slices.Collect(maps.Keys(assets.OpenAPI.Spec().Paths.Paths)) paths := make([]string, len(assets.OpenAPI.Spec().Paths.Paths))
idx := 0
for path := range assets.OpenAPI.Spec().Paths.Paths {
paths[idx] = path
idx++
}
return paths
} }
func ApiShow(conf *cfg.Config, showpath, verb string) error { func ApiShow(conf *cfg.Config, showpath, verb string) error {
@@ -528,7 +536,7 @@ func getApiExample(op *Op) string {
// otherwise showpath+verb have to match precisely. // otherwise showpath+verb have to match precisely.
func matchOperation(showpath, verb string) (*Op, error) { func matchOperation(showpath, verb string) (*Op, error) {
ops := []*Op{} ops := []*Op{}
op := new(Op{}) op := &Op{}
var found bool var found bool

View File

@@ -17,10 +17,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package es package es
import ( import (
// "encoding/json/jsontext"
// "encoding/json/v2"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -53,22 +49,11 @@ func getHealthReport(conf *cfg.Config) (*HealthReport, error) {
return nil, err return nil, err
} }
report := new(HealthReport{}) report := HealthReport{}
// FIXME: use this once jsonv2 is no more experimental it already
// builds and works like intended, but golangci-lint doesn't
// recognize it with: go: unknown GOEXPERIMENT jsonv2
//
// if err := json.UnmarshalDecode(
// jsontext.NewDecoder(
// bytes.NewBuffer(raw)),
// &report); err != nil {
// return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
// }
if err := json.Unmarshal(raw, &report); err != nil { if err := json.Unmarshal(raw, &report); err != nil {
return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err) return nil, fmt.Errorf("failed to unmarshal healthreport response: %w", err)
} }
return report, nil return &report, nil
} }

View File

@@ -101,39 +101,23 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
es := conf.DefaultCluster.ES() es := conf.DefaultCluster.ES()
responses := make(chan apiResponse, gocount) responses := make(chan apiResponse, gocount)
wg := new(sync.WaitGroup{}) wg := &sync.WaitGroup{}
wg.Go(func() { wg.Add(gocount)
getApiData(conf, es, responses, "health") go getApiData(conf, es, wg, responses, "health")
}) go getApiData(conf, es, wg, responses, "healthreport")
go getApiData(conf, es, wg, responses, "info")
wg.Go(func() { go getApiData(conf, es, wg, responses, "ccr")
getApiData(conf, es, responses, "healthreport") go getApiData(conf, es, wg, responses, "indices")
}) go getApiData(conf, es, wg, responses, "tasks")
wg.Go(func() {
getApiData(conf, es, responses, "info")
})
wg.Go(func() {
getApiData(conf, es, responses, "ccr")
})
wg.Go(func() {
getApiData(conf, es, responses, "indices")
})
wg.Go(func() {
getApiData(conf, es, responses, "tasks")
})
if conf.Verbose { if conf.Verbose {
getApiData(conf, es, responses, "stats") go getApiData(conf, es, wg, responses, "stats")
} }
wg.Wait() wg.Wait()
all := new(apiResponse{}) all := apiResponse{}
var err error var err error
@@ -160,7 +144,7 @@ func getClusterStatus(conf *cfg.Config) (*apiResponse, error) {
} }
} }
return all, err return &all, err
} }
func ClusterStatus(conf *cfg.Config) error { func ClusterStatus(conf *cfg.Config) error {

View File

@@ -29,12 +29,12 @@ func ClusterRerouteMove(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
moveCommand := new(types.CommandMoveAction{ moveCommand := &types.CommandMoveAction{
Shard: conf.Shards, Shard: conf.Shards,
FromNode: conf.FromNode, FromNode: conf.FromNode,
ToNode: conf.ToNode, ToNode: conf.ToNode,
Index: index, Index: index,
}) }
commands.CommandCaster().Move = moveCommand commands.CommandCaster().Move = moveCommand
@@ -52,11 +52,11 @@ func ClusterRerouteAllocateReplica(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
allocCommand := new(types.CommandAllocateReplicaAction{ allocCommand := &types.CommandAllocateReplicaAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
}) }
commands.CommandCaster().AllocateReplica = allocCommand commands.CommandCaster().AllocateReplica = allocCommand
@@ -74,12 +74,12 @@ func ClusterRerouteCancel(conf *cfg.Config, index string) error {
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
cancelCommand := new(types.CommandCancelAction{ cancelCommand := &types.CommandCancelAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
AllowPrimary: &conf.AllowPrimary, AllowPrimary: &conf.AllowPrimary,
}) }
commands.CommandCaster().Cancel = cancelCommand commands.CommandCaster().Cancel = cancelCommand
@@ -97,12 +97,12 @@ func ClusterRerouteAllocatePrimary(conf *cfg.Config, index string, stale bool) e
move := conf.DefaultCluster.ES().Cluster.Reroute() move := conf.DefaultCluster.ES().Cluster.Reroute()
commands := esdsl.NewCommand() commands := esdsl.NewCommand()
allocCommand := new(types.CommandAllocatePrimaryAction{ allocCommand := &types.CommandAllocatePrimaryAction{
Shard: conf.Shards, Shard: conf.Shards,
Node: conf.ToNode, Node: conf.ToNode,
Index: index, Index: index,
AcceptDataLoss: conf.AcceptDataLoss, AcceptDataLoss: conf.AcceptDataLoss,
}) }
if stale { if stale {
commands.CommandCaster().AllocateStalePrimary = allocCommand commands.CommandCaster().AllocateStalePrimary = allocCommand

View File

@@ -101,7 +101,7 @@ func DocDelete(conf *cfg.Config, queries []string) error {
return nil return nil
} }
req := new(deletebyquery.Request{}) req := &deletebyquery.Request{}
if len(queries) == 0 && conf.All { if len(queries) == 0 && conf.All {
req.Query = esdsl.NewMatchAllQuery().QueryCaster() req.Query = esdsl.NewMatchAllQuery().QueryCaster()

View File

@@ -22,8 +22,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"slices"
"strings" "strings"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
@@ -64,7 +62,13 @@ func IlmNames(conf *cfg.Config) ([]string, error) {
return nil, fmt.Errorf("failed to get ilm policies: %w", esErrorString(err)) return nil, fmt.Errorf("failed to get ilm policies: %w", esErrorString(err))
} }
names := slices.Collect(maps.Keys(res)) names := make([]string, len(res))
idx := 0
for name := range res {
names[idx] = name
idx++
}
return names, nil return names, nil
} }
@@ -347,7 +351,7 @@ func IlmCreate(conf *cfg.Config, policyname string) error {
var actions types.IlmActionsVariant = esdsl.NewIlmActions() var actions types.IlmActionsVariant = esdsl.NewIlmActions()
rollover := new(types.RolloverAction{}) rollover := &types.RolloverAction{}
haveroll := false haveroll := false
if policy != nil { if policy != nil {
@@ -509,8 +513,8 @@ func IlmCreate(conf *cfg.Config, policyname string) error {
phases.PhasesCaster().Delete = policy.Phases.Delete phases.PhasesCaster().Delete = policy.Phases.Delete
} }
put := new(putlifecycle.Request{}) put := &putlifecycle.Request{}
newpolicy := new(types.IlmPolicy{}) newpolicy := &types.IlmPolicy{}
newpolicy.IlmPolicyCaster().Phases = *phases.PhasesCaster() newpolicy.IlmPolicyCaster().Phases = *phases.PhasesCaster()
put.Policy = newpolicy put.Policy = newpolicy

View File

@@ -185,19 +185,12 @@ func virtualAge(phase *PhaseData) time.Duration {
// Retrieve all index, ilm-explain and ilm-policies in parallel // Retrieve all index, ilm-explain and ilm-policies in parallel
func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) { func getIlmPhaseData(conf *cfg.Config) ([]PhaseData, error) {
responses := make(chan apiResponse, 3) responses := make(chan apiResponse, 3)
wg := new(sync.WaitGroup{}) wg := &sync.WaitGroup{}
wg.Add(3)
wg.Go(func() { go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "indicesbytes")
getApiData(conf, conf.DefaultCluster.ES(), responses, "indicesbytes") go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "explain")
}) go getApiData(conf, conf.DefaultCluster.ES(), wg, responses, "policies")
wg.Go(func() {
getApiData(conf, conf.DefaultCluster.ES(), responses, "explain")
})
wg.Go(func() {
getApiData(conf, conf.DefaultCluster.ES(), responses, "policies")
})
wg.Wait() wg.Wait()
@@ -338,7 +331,7 @@ func findNextPhase(policy types.IlmPolicy, currentPhase string) *NextPhase {
// phase list to determine which comes next // phase list to determine which comes next
phases, start := registerPhases(policy, currentPhase) phases, start := registerPhases(policy, currentPhase)
nextPhase := new(NextPhase{}) nextPhase := &NextPhase{}
// finally determine which phase comes next // finally determine which phase comes next
// exception: hot, where we look for rollover rules // exception: hot, where we look for rollover rules

View File

@@ -19,6 +19,7 @@ package es
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"github.com/elastic/go-elasticsearch/v9" "github.com/elastic/go-elasticsearch/v9"
@@ -60,7 +61,14 @@ type apiResponse struct {
which int which int
} }
func getApiData(conf *cfg.Config, es *elasticsearch.TypedClient, reschan chan apiResponse, which string) { func getApiData(
conf *cfg.Config,
es *elasticsearch.TypedClient,
wg *sync.WaitGroup,
reschan chan apiResponse,
which string) {
defer wg.Done()
apiRes := apiResponse{} apiRes := apiResponse{}
var arerr error var arerr error

View File

@@ -20,8 +20,6 @@ import (
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"slices"
"codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/cfg"
"codeberg.org/scip/esctl/pkg/printer" "codeberg.org/scip/esctl/pkg/printer"
@@ -35,7 +33,15 @@ func RoleNames(conf *cfg.Config) ([]string, error) {
return nil, fmt.Errorf("failed to get roles: %w", esErrorString(err)) return nil, fmt.Errorf("failed to get roles: %w", esErrorString(err))
} }
return slices.Collect(maps.Keys(res)), nil roles := make([]string, len(res))
idx := 0
for name := range res {
roles[idx] = name
idx++
}
return roles, nil
} }
func RoleList(conf *cfg.Config) error { func RoleList(conf *cfg.Config) error {

View File

@@ -127,7 +127,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) {
}() }()
scanner := bufio.NewScanner(fd) scanner := bufio.NewScanner(fd)
record := new(Record{role: rolename}) record := Record{role: rolename}
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
@@ -150,7 +150,7 @@ func getCsvRecord(conf *cfg.Config, csvfile, rolename string) (*Record, error) {
} }
} }
return record, nil return &record, nil
} }
func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register { func diffRoles(conf *cfg.Config, records map[string]Record, res getrole.Response) []Register {

View File

@@ -56,7 +56,7 @@ func Search(conf *cfg.Config, queries []string) error {
return err return err
} }
req := new(search.Request{Query: queryCaster}) req := &search.Request{Query: queryCaster}
searchEs.Request(req) searchEs.Request(req)
@@ -128,7 +128,7 @@ func validateSearch(conf *cfg.Config, queries []string) error {
return err return err
} }
req := new(validatequery.Request{Query: queryCaster}) req := &validatequery.Request{Query: queryCaster}
validate.Request(req) validate.Request(req)

View File

@@ -80,7 +80,7 @@ func NewFilter(query string) (*filter, error) {
return nil, errors.New("search queries must be in the form field<sep>pattern where <sep> must be one of: = or !=") return nil, errors.New("search queries must be in the form field<sep>pattern where <sep> must be one of: = or !=")
} }
flt := new(filter{term: part[0], filter: part[1], criteria: criteria}) flt := &filter{term: part[0], filter: part[1], criteria: criteria}
if strings.Contains(part[0], ",") { if strings.Contains(part[0], ",") {
// a MultiMatchQuery, match across multiple fields at once // a MultiMatchQuery, match across multiple fields at once

View File

@@ -66,13 +66,13 @@ func SnapshotList(conf *cfg.Config) error {
snapshots := []*Snapshot{} // original snapshot names snapshots := []*Snapshot{} // original snapshot names
for _, snapshot := range sres { for _, snapshot := range sres {
snap := new(Snapshot{ snap := &Snapshot{
Name: *snapshot.Id, Name: *snapshot.Id,
Status: *snapshot.Status, Status: *snapshot.Status,
Start: fmt.Sprintf("%s", snapshot.StartTime), Start: fmt.Sprintf("%s", snapshot.StartTime),
Forindex: indexFromSnapshot(*snapshot.Id), Forindex: indexFromSnapshot(*snapshot.Id),
Orphaned: "no", Orphaned: "no",
}) }
_, exists := indicies[snap.Forindex] _, exists := indicies[snap.Forindex]
if !exists { if !exists {

View File

@@ -29,13 +29,13 @@ import (
const LevelNotice = slog.Level(2) const LevelNotice = slog.Level(2)
func Init(conf *cfg.Config) { func Init(conf *cfg.Config) {
logLevel := new(slog.LevelVar{}) logLevel := &slog.LevelVar{}
opts := new(yadu.Options{ opts := &yadu.Options{
Level: logLevel, Level: logLevel,
AddSource: true, AddSource: true,
NoColor: !isatty.IsTerminal(os.Stdout.Fd()), NoColor: !isatty.IsTerminal(os.Stdout.Fd()),
}) }
buildInfo, _ := debug.ReadBuildInfo() buildInfo, _ := debug.ReadBuildInfo()

View File

@@ -27,7 +27,7 @@ func (b *ByteSize) String() string {
} }
func Bytes(size int64) *ByteSize { func Bytes(size int64) *ByteSize {
return new(ByteSize{size: uint64(size)}) return &ByteSize{size: uint64(size)}
} }
func ByteString(size int64) string { func ByteString(size int64) string {

View File

@@ -35,20 +35,15 @@ type Table struct {
RawHeaders []string RawHeaders []string
Entries [][]any Entries [][]any
rows [][]string // representation used for printing rows [][]string // representation used for printing
processed bool processed bool
lenHeaders []int lenHeaders []int
alignInts bool alignInts bool
maxwidth int maxwidth int
debugGoRoutines bool
} }
func NewTable(conf *cfg.Config, columns, rows int) *Table { func NewTable(conf *cfg.Config, columns, rows int) *Table {
table := new(Table{ table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
Mode: conf.Output,
maxwidth: cfg.GetTermWidth(),
debugGoRoutines: conf.DebugGoRoutines,
})
table.Headers = make([]string, columns) table.Headers = make([]string, columns)
table.RawHeaders = make([]string, columns) table.RawHeaders = make([]string, columns)
@@ -56,18 +51,14 @@ func NewTable(conf *cfg.Config, columns, rows int) *Table {
table.lenHeaders = make([]int, columns) table.lenHeaders = make([]int, columns)
table.alignInts = conf.AlignInts table.alignInts = conf.AlignInts
return table return &table
} }
func NewTableEmpty(conf *cfg.Config) *Table { func NewTableEmpty(conf *cfg.Config) *Table {
table := new(Table{ table := Table{Mode: conf.Output, maxwidth: cfg.GetTermWidth()}
Mode: conf.Output,
maxwidth: cfg.GetTermWidth(),
debugGoRoutines: conf.DebugGoRoutines,
})
table.alignInts = conf.AlignInts table.alignInts = conf.AlignInts
return table return &table
} }
func (table *Table) WithHeaders(headers ...string) *Table { func (table *Table) WithHeaders(headers ...string) *Table {
@@ -84,24 +75,16 @@ func (table *Table) WithHeaders(headers ...string) *Table {
} }
func (table *Table) Print() error { func (table *Table) Print() error {
var err error
switch table.Mode { switch table.Mode {
case "json": case "json":
err = table.PrintJSON() return table.PrintJSON()
case "yaml": case "yaml":
err = table.PrintYAML() return table.PrintYAML()
case "csv": case "csv":
err = table.PrintCSV() return table.PrintCSV()
default: default:
err = table.PrintTSV() return table.PrintTSV()
} }
if table.debugGoRoutines {
printGoRoutineMetrics()
}
return err
} }
var ( var (
@@ -166,11 +149,9 @@ func (table *Table) PrintTSV() error {
wrapped := wrapper(entry) wrapped := wrapper(entry)
// and indent it // and indent it
first := true for idx, line := range strings.Split(wrapped, "\n") {
for line := range strings.Lines(wrapped) { if idx == 0 {
if first {
entry = line entry = line
first = false
} else { } else {
entry += "\n " + strings.Repeat(" ", currentWidth) + line entry += "\n " + strings.Repeat(" ", currentWidth) + line
} }