From 174fe58122da080fa26e2807b0fd0ce5aba04d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 14:55:20 +0200 Subject: [PATCH] Use alternative readline library --- go.mod | 3 +- go.sum | 2 + rline/new_readline.go | 172 ++++++++++++++++++++++++++++++++++++++++++ rline/readline.go | 48 +++++++----- rline/rline.go | 16 ---- 5 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 rline/new_readline.go diff --git a/go.mod b/go.mod index 09dd5436596..3bb8cd77f61 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/nakagami/firebirdsql v0.9.6 github.com/ory/dockertest/v3 v3.10.0 github.com/prestodb/presto-go-client v0.0.0-20230524183650-a1a0bac0f63e + github.com/reeflective/readline v1.0.8 github.com/sijms/go-ora/v2 v2.7.22 github.com/sirupsen/logrus v1.9.3 github.com/snowflakedb/gosnowflake v1.6.25 @@ -56,6 +57,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e github.com/yookoala/realpath v1.0.0 github.com/ziutek/mymysql v1.5.4 + golang.org/x/term v0.14.0 gorm.io/driver/bigquery v1.2.0 modernc.org/ql v1.4.7 modernc.org/sqlite v1.27.0 @@ -260,7 +262,6 @@ require ( golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.4.0 // indirect golang.org/x/tools v0.15.0 // indirect diff --git a/go.sum b/go.sum index 43dc89bea66..ea0aec00cb0 100644 --- a/go.sum +++ b/go.sum @@ -896,6 +896,8 @@ github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGy github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/reeflective/readline v1.0.8 h1:VuDGI82lAwl1H5by+hpW4OQgM+9ikh6EuOySQUGP3sI= +github.com/reeflective/readline v1.0.8/go.mod h1:5JgnHb/ZCvp/6RUA59HEansPBxWTkyBO4hJ5LL9Fp1Y= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= diff --git a/rline/new_readline.go b/rline/new_readline.go new file mode 100644 index 00000000000..b780f9d8fb5 --- /dev/null +++ b/rline/new_readline.go @@ -0,0 +1,172 @@ +//go:build new_readline + +package rline + +import ( + "github.com/reeflective/readline/inputrc" + "golang.org/x/term" + "io" + "os" + "os/signal" + "syscall" + + "github.com/mattn/go-isatty" + "github.com/reeflective/readline" +) + +var ( + // ErrInterrupt is the interrupt error. + ErrInterrupt = readline.ErrInterrupt +) + +// baseRline should be embedded in a struct implementing the IO interface, +// as it keeps implementation specific state. +type baseRline struct { + instance *readline.Shell + prompt string +} + +// Prompt sets the prompt for the next interactive line read. +func (l *rline) Prompt(s string) { + l.prompt = s +} + +// Completer sets the auto-completer. +func (l *rline) Completer(a Completer) { + l.instance.Completer = func(line []rune, cursor int) readline.Completions { + candidates, _ := a.Complete(line, cursor) + values := make([]string, len(candidates)) + for candidate := range candidates { + values = append(values, string(candidate)) + } + return readline.CompleteValues(values...) + } +} + +// SetOutput sets the output format func. +func (l *rline) SetOutput(f func(string) string) { + l.instance.SyntaxHighlighter = func(line []rune) string { + return f(string(line)) + } +} + +// New readline input/output handler. +func New(forceNonInteractive bool, out, histfile string) (IO, error) { + // determine if interactive + interactive, cygwin := false, false + if !forceNonInteractive { + interactive = isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd()) + cygwin = isatty.IsCygwinTerminal(os.Stdout.Fd()) && isatty.IsCygwinTerminal(os.Stdin.Fd()) + } + var stdout io.WriteCloser + var closers []func() error + switch { + case out != "": + var err error + stdout, err = os.OpenFile(out, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return nil, err + } + closers = append(closers, stdout.Close) + interactive = false + default: + stdout = os.Stdout + } + // configure stderr + var stderr io.Writer = os.Stderr + // TODO handle interrupts? + options := []inputrc.Option{inputrc.WithName("usql")} + /* + &readline.Config{ + HistoryFile: histfile, + DisableAutoSaveHistory: true, + InterruptPrompt: "^C", + HistorySearchFold: true, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + FuncIsTerminal: func() bool { + return interactive || cygwin + }, + FuncFilterInputRune: func(r rune) (rune, bool) { + if r == readline.CharCtrlZ { + return r, false + } + return r, true + }, + } + */ + // create readline instance + shell := readline.NewShell(options...) + var history readline.History + if histfile != "" { + history, err := readline.NewHistoryFromFile(histfile) + if err != nil { + return nil, err + } + shell.History.Add("default", history) + } + + n := func() ([]rune, error) { + line, err := shell.Readline() + return []rune(line), err + } + pw := func(prompt string) (string, error) { + _, err := shell.Printf(prompt) + if err != nil { + return "", err + } + return readPassword() + } + if forceNonInteractive { + n, pw = nil, nil + } + result := &rline{ + baseRline: baseRline{instance: shell}, + nextLine: n, + close: func() error { + for _, f := range closers { + _ = f() + } + return nil + }, + stdout: stdout, + stderr: stderr, + isInteractive: interactive || cygwin, + passwordPrompt: pw, + } + shell.Prompt.Primary(func() string { + return result.prompt + }) + if history != nil { + result.saveHistory = func(input string) error { + _, err := history.Write(input) + return err + } + } + return result, nil +} + +func readPassword() (string, error) { + stdin := syscall.Stdin + oldState, err := term.GetState(stdin) + if err != nil { + return "", err + } + defer term.Restore(stdin, oldState) + + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, os.Interrupt) + go func() { + for _ = range sigch { + term.Restore(stdin, oldState) + os.Exit(1) + } + }() + + password, err := term.ReadPassword(stdin) + if err != nil { + return "", err + } + return string(password), nil +} diff --git a/rline/readline.go b/rline/readline.go index 074f1c64987..91d9f6b6982 100644 --- a/rline/readline.go +++ b/rline/readline.go @@ -18,7 +18,29 @@ var ( // baseRline should be embedded in a struct implementing the IO interface, // as it keeps implementation specific state. type baseRline struct { - instance *readline.Instance + instance *readline.Instance + prompt func(string) + completer func(Completer) +} + +// Prompt sets the prompt for the next interactive line read. +func (l *rline) Prompt(s string) { + l.instance.SetPrompt(s) +} + +// Completer sets the auto-completer. +func (l *rline) Completer(a Completer) { + cfg := l.instance.Config.Clone() + cfg.AutoComplete = readlineCompleter{c: a} + l.instance.SetConfig(cfg) +} + +type readlineCompleter struct { + c Completer +} + +func (r readlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) { + return r.c.Complete(line, pos) } // SetOutput sets the output format func. @@ -102,7 +124,9 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { n, pw = nil, nil } return &rline{ - instance: l, + baseRline: baseRline{ + instance: l, + }, nextLine: n, close: func() error { for _, f := range closers { @@ -110,24 +134,10 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { } return nil }, - stdout: stdout, - stderr: stderr, - isInteractive: interactive || cygwin, - prompt: l.SetPrompt, - completer: func(a Completer) { - cfg := l.Config.Clone() - cfg.AutoComplete = readlineCompleter{c: a} - l.SetConfig(cfg) - }, + stdout: stdout, + stderr: stderr, + isInteractive: interactive || cygwin, saveHistory: l.SaveHistory, passwordPrompt: pw, }, nil } - -type readlineCompleter struct { - c Completer -} - -func (r readlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) { - return r.c.Complete(line, pos) -} diff --git a/rline/rline.go b/rline/rline.go index dfbc213445b..c2c68bdf9e6 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -51,8 +51,6 @@ type rline struct { stdout io.Writer stderr io.Writer isInteractive bool - prompt func(string) - completer func(Completer) saveHistory func(string) error passwordPrompt passwordPrompt } @@ -90,20 +88,6 @@ func (l *rline) Interactive() bool { return l.isInteractive } -// Prompt sets the prompt for the next interactive line read. -func (l *rline) Prompt(s string) { - if l.prompt != nil { - l.prompt(s) - } -} - -// Completer sets the auto-completer. -func (l *rline) Completer(a Completer) { - if l.completer != nil { - l.completer(a) - } -} - // Save saves a line of history. func (l *rline) Save(s string) error { if l.saveHistory != nil {