add external pager support via ES_JSON_PAGER (#65)

This commit is contained in:
T. von Dein
2026-07-03 10:07:11 +02:00
parent 7b576c976c
commit 1fc2004474
6 changed files with 77 additions and 17 deletions

View File

@@ -23,6 +23,8 @@ Features:
- Shell completion support (bash, zsh and fish). Put this into your - Shell completion support (bash, zsh and fish). Put this into your
rc: `source <(esctl completion bash)`. rc: `source <(esctl completion bash)`.
- Cluster settings can be viewed and modified. - 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 - Search: you can search indices using full text or by fields, select
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
@@ -47,7 +49,9 @@ Features:
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 cluster w/o the hassle to specify the whole url, credentials etc. It
has line editing and history support. If `jq` is installed output has line editing and history support. If `jq` is installed output
JSON will be syntax highlighted. 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 - 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: 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 `esctl index create foo` and then insert docs into it for search

View File

@@ -83,6 +83,16 @@ func ApiRepl(conf *cfg.Config) *cli.Command {
Aliases: []string{"shell"}, Aliases: []string{"shell"},
Usage: "interactive API repl", 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 { Action: func(ctx context.Context, cmd *cli.Command) error {
return es.ApiRepl(conf) return es.ApiRepl(conf)
}, },

View File

@@ -106,6 +106,8 @@ type Config struct {
FromNode, ToNode string // cluster reroute move: -f + -t FromNode, ToNode string // cluster reroute move: -f + -t
AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a AllowPrimary, AcceptDataLoss bool // cluster reroute cancel: -p,-a
Pager string // api repl: -p || PAGER
} }
func NewConfig() *Config { func NewConfig() *Config {

View File

@@ -36,3 +36,14 @@ func GetTermWidth() int {
return 80 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
}

View File

@@ -141,14 +141,50 @@ func ApiRepl(conf *cfg.Config) error {
fmt.Printf("failed to call API: %s\n", esErrorString(err)) fmt.Printf("failed to call API: %s\n", esErrorString(err))
} }
if err := prettyfiJson(conf, raw); err != nil { pageJsonOutput(conf, raw)
fmt.Println(err)
}
} }
return nil 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 { func encodeAuth(username, password string) string {
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) 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 return body, nil
} }
func prettyfiJson(conf *cfg.Config, raw []byte) error { func prettyfiJson(conf *cfg.Config, raw []byte) (string, error) {
if conf.HaveJQ { if conf.HaveJQ {
cmd := exec.CommandContext(context.Background(), "jq", "-C") cmd := exec.CommandContext(context.Background(), "jq", "-C")
cmd.Stdin = bytes.NewReader(raw) cmd.Stdin = bytes.NewReader(raw)
@@ -207,23 +243,18 @@ func prettyfiJson(conf *cfg.Config, raw []byte) error {
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
return err return "", err
} }
fmt.Println(out.String()) return out.String(), nil
return nil
} }
var pretty bytes.Buffer var pretty bytes.Buffer
err := json.Indent(&pretty, raw, "", "\t") err := json.Indent(&pretty, raw, "", "\t")
if err != nil { if err != nil {
return fmt.Errorf("json parse error: %s", err) return "", fmt.Errorf("json parse error: %s", err)
} }
fmt.Println(pretty.String()) return pretty.String(), nil
return nil
} }
// interactively read arbitrary JSON data from STDIN, which is // interactively read arbitrary JSON data from STDIN, which is

View File

@@ -39,9 +39,11 @@ func getIndexTemplateSettings(conf *cfg.Config, tplname string, table *printer.T
} }
if conf.Debug { if conf.Debug {
if err := prettyfiJson(conf, raw); err != nil { output, err := prettyfiJson(conf, raw)
if err != nil {
return err return err
} }
fmt.Println(output)
} }
if len(data.IndexTemplates) == 0 { if len(data.IndexTemplates) == 0 {