From 1fc2004474fd592e9e2fe7a5bd6e2a16286d9a5c Mon Sep 17 00:00:00 2001 From: "T. von Dein" Date: Fri, 3 Jul 2026 10:07:11 +0200 Subject: [PATCH] add external pager support via ES_JSON_PAGER (#65) --- README.md | 10 ++++-- cmd/api.go | 10 ++++++ pkg/cfg/config.go | 2 ++ pkg/cfg/term.go | 11 ++++++ pkg/es/api.go | 57 ++++++++++++++++++++++++------- pkg/es/index_template_settings.go | 4 ++- 6 files changed, 77 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index aee6bec..199d215 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Features: - Shell completion support (bash, zsh and fish). Put this into your rc: `source <(esctl completion bash)`. - Cluster settings can be viewed and modified. +- Comprehensive cluster status. +- Cluster reroute support. - Search: you can search indices using full text or by fields, select logical condition (OR, AND), use PIT, limit datetime (ES date math can be used), etc. It is however not yet possible to create @@ -44,10 +46,12 @@ Features: - API documentation (`api list` and `api show `) with interactive markdown pager for endpoint documentation. - Repl: this is an interactive REPL (read eval print loop) towards the - elasticsearch API. You can run API calls on the current selected + elasticsearch API. You can run API calls on the current selected cluster w/o the hassle to specify the whole url, credentials etc. It - has line editing and history support. If `jq` is installed output - JSON will be syntax highlighted. + has line editing and history support. If `jq` is installed output + JSON will be syntax highlighted. A simple internal pager will be + used if output exceeds the terminal height. You can tweak this using + the `$ES_JSON_PAGER` environment variable (I'd recommend [fx](https://fx.wtf/)). - Doc support. You can put, delete and show docs for an index. Very handy if you want to play with it. Just create a new index: `esctl index create foo` and then insert docs into it for search diff --git a/cmd/api.go b/cmd/api.go index 1d329a9..5ccd5d0 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -83,6 +83,16 @@ func ApiRepl(conf *cfg.Config) *cli.Command { Aliases: []string{"shell"}, Usage: "interactive API repl", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pager", + Usage: "external viewer program (default:internal)", + Destination: &conf.Pager, + Aliases: []string{"p"}, + Sources: cli.EnvVars("PAGER", "ES_JSON_PAGER"), + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { return es.ApiRepl(conf) }, diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 191d33b..a5aa3cd 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -106,6 +106,8 @@ type Config struct { FromNode, ToNode string // cluster reroute move: -f + -t AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a + + Pager string // api repl: -p || PAGER } func NewConfig() *Config { diff --git a/pkg/cfg/term.go b/pkg/cfg/term.go index c05069f..23ebf95 100644 --- a/pkg/cfg/term.go +++ b/pkg/cfg/term.go @@ -36,3 +36,14 @@ func GetTermWidth() int { return 80 } + +func GetTermHeight() int { + if term.IsTerminal(int(os.Stdout.Fd())) { + _, height, err := term.GetSize(int(os.Stdout.Fd())) + if err == nil { + return height + } + } + + return 25 +} diff --git a/pkg/es/api.go b/pkg/es/api.go index ab06457..8d226e1 100644 --- a/pkg/es/api.go +++ b/pkg/es/api.go @@ -141,14 +141,50 @@ func ApiRepl(conf *cfg.Config) error { fmt.Printf("failed to call API: %s\n", esErrorString(err)) } - if err := prettyfiJson(conf, raw); err != nil { - fmt.Println(err) - } + pageJsonOutput(conf, raw) } return nil } +func pageJsonOutput(conf *cfg.Config, raw []byte) { + tmpconf := &cfg.Config{HaveJQ: conf.HaveJQ} + + if conf.Pager != "" { + tmpconf.HaveJQ = false + } + + output, err := prettyfiJson(tmpconf, raw) + if err != nil { + fmt.Println(err) + } + + lines := len(strings.Split(output, "\n")) + height := cfg.GetTermHeight() + + if lines > height { + if conf.Pager != "" { + cmd := strings.Split(conf.Pager, " ") + pager := exec.Command(cmd[0], cmd[1:]...) + + var buf bytes.Buffer + buf.WriteString(output) + + pager.Stdout = os.Stdout + pager.Stdin = &buf + pager.Stderr = os.Stderr + + err := pager.Run() + + if err != nil { + fmt.Printf("failed to execute pager '%s': %s", conf.Pager, err) + } + } else { + printer.Pager("json output", output) + } + } +} + func encodeAuth(username, password string) string { return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) } @@ -197,7 +233,7 @@ func CallAPI(conf *cfg.Config, verb, path, data string) ([]byte, error) { return body, nil } -func prettyfiJson(conf *cfg.Config, raw []byte) error { +func prettyfiJson(conf *cfg.Config, raw []byte) (string, error) { if conf.HaveJQ { cmd := exec.CommandContext(context.Background(), "jq", "-C") cmd.Stdin = bytes.NewReader(raw) @@ -207,23 +243,18 @@ func prettyfiJson(conf *cfg.Config, raw []byte) error { err := cmd.Run() if err != nil { - return err + return "", err } - fmt.Println(out.String()) - - return nil + return out.String(), nil } - var pretty bytes.Buffer err := json.Indent(&pretty, raw, "", "\t") if err != nil { - return fmt.Errorf("json parse error: %s", err) + return "", fmt.Errorf("json parse error: %s", err) } - fmt.Println(pretty.String()) - - return nil + return pretty.String(), nil } // interactively read arbitrary JSON data from STDIN, which is diff --git a/pkg/es/index_template_settings.go b/pkg/es/index_template_settings.go index 5923e83..983920f 100644 --- a/pkg/es/index_template_settings.go +++ b/pkg/es/index_template_settings.go @@ -39,9 +39,11 @@ func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.T } if conf.Debug { - if err := prettyfiJson(conf, raw); err != nil { + output, err := prettyfiJson(conf, raw) + if err != nil { return err } + fmt.Println(output) } if len(data.IndexTemplates) == 0 {