From 3d77ff1267766d1434613e54f0d83d4d24e7cede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 10:50:46 +0200 Subject: [PATCH 1/8] Ignore more IDEs configuration --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a985dbeee82..9e3df120c52 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ /*.pc .vscode/ +.idea/ From f0b4194aec857b51e612ffefbfebe18ad75246c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 11:09:34 +0200 Subject: [PATCH 2/8] Avoid accessling Rline struct from outside of package --- handler/handler.go | 69 +---------------------------------------- rline/rline.go | 76 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 69 deletions(-) diff --git a/handler/handler.go b/handler/handler.go index 4929d7c5b7c..d3f52494eb7 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1325,41 +1325,7 @@ func (h *Handler) Include(path string, relative bool) error { } defer f.Close() r := bufio.NewReader(f) - // setup rline - l := &rline.Rline{ - N: func() ([]rune, error) { - buf := new(bytes.Buffer) - var b []byte - var isPrefix bool - var err error - for { - // read - b, isPrefix, err = r.ReadLine() - // when not EOF - if err != nil && err != io.EOF { - return nil, err - } - // append - if _, werr := buf.Write(b); werr != nil { - return nil, werr - } - // end of line - if !isPrefix || err != nil { - break - } - } - // peek and read possible line ending \n or \r\n - if err != io.EOF { - if err := peekEnding(buf, r); err != nil { - return nil, err - } - } - return []rune(buf.String()), err - }, - Out: h.l.Stdout(), - Err: h.l.Stderr(), - Pw: h.l.Password, - } + l := rline.NewFromReader(r, h.l.Stdout(), h.l.Stderr(), h.l.Password) p := New(l, h.user, filepath.Dir(path), h.nopw) p.db, p.u = h.db, h.u drivers.ConfigStmt(p.u, p.buf) @@ -1408,39 +1374,6 @@ func readerOpts() []metadata.ReaderOption { return opts } -// peekEnding peeks to see if the next successive bytes in r is \n or \r\n, -// writing to w if it is. Does not advance r if the next bytes are not \n or -// \r\n. -func peekEnding(w io.Writer, r *bufio.Reader) error { - // peek first byte - buf, err := r.Peek(1) - switch { - case err != nil && err != io.EOF: - return err - case err == nil && buf[0] == '\n': - if _, rerr := r.ReadByte(); err != nil && err != io.EOF { - return rerr - } - _, werr := w.Write([]byte{'\n'}) - return werr - case err == nil && buf[0] != '\r': - return nil - } - // peek second byte - buf, err = r.Peek(1) - switch { - case err != nil && err != io.EOF: - return err - case err == nil && buf[0] != '\n': - return nil - } - if _, rerr := r.ReadByte(); err != nil && err != io.EOF { - return rerr - } - _, werr := w.Write([]byte{'\n'}) - return werr -} - // grab grabs i from r, or returns 0 if i >= end. func grab(r []rune, i, end int) rune { if i < end { diff --git a/rline/rline.go b/rline/rline.go index 0bb155d3602..98542ab99c5 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -2,6 +2,8 @@ package rline import ( + "bufio" + "bytes" "errors" "io" "os" @@ -43,6 +45,8 @@ type IO interface { SetOutput(func(string) string) } +type PasswordPrompt func(string) (string, error) + // Rline provides a type compatible with the IO interface. type Rline struct { Inst *readline.Instance @@ -55,7 +59,7 @@ type Rline struct { P func(string) A func(readline.AutoCompleter) S func(string) error - Pw func(string) (string, error) + Pw PasswordPrompt } // Next returns the next line of runes (excluding '\n') from the input. @@ -227,3 +231,73 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { Pw: pw, }, nil } + +func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw PasswordPrompt) IO { + return &Rline{ + N: func() ([]rune, error) { + buf := new(bytes.Buffer) + var b []byte + var isPrefix bool + var err error + for { + // read + b, isPrefix, err = reader.ReadLine() + // when not EOF + if err != nil && err != io.EOF { + return nil, err + } + // append + if _, werr := buf.Write(b); werr != nil { + return nil, werr + } + // end of line + if !isPrefix || err != nil { + break + } + } + // peek and read possible line ending \n or \r\n + if err != io.EOF { + if err := peekEnding(buf, reader); err != nil { + return nil, err + } + } + return []rune(buf.String()), err + }, + Out: out, + Err: err, + Pw: pw, + } +} + +// peekEnding peeks to see if the next successive bytes in r is \n or \r\n, +// writing to w if it is. Does not advance r if the next bytes are not \n or +// \r\n. +func peekEnding(w io.Writer, r *bufio.Reader) error { + // peek first byte + buf, err := r.Peek(1) + switch { + case err != nil && err != io.EOF: + return err + case err == nil && buf[0] == '\n': + if _, rerr := r.ReadByte(); err != nil && err != io.EOF { + return rerr + } + _, werr := w.Write([]byte{'\n'}) + return werr + case err == nil && buf[0] != '\r': + return nil + } + // peek second byte + buf, err = r.Peek(1) + switch { + case err != nil && err != io.EOF: + return err + case err == nil && buf[0] != '\n': + return nil + } + if _, rerr := r.ReadByte(); err != nil && err != io.EOF { + return rerr + } + _, werr := w.Write([]byte{'\n'}) + return werr +} From 1865eefe270e54d697a04cb5870e95cd6e1e2b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 11:15:54 +0200 Subject: [PATCH 3/8] Don't export rline struct --- rline/rline.go | 116 ++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/rline/rline.go b/rline/rline.go index 98542ab99c5..7726fcfe581 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -47,90 +47,90 @@ type IO interface { type PasswordPrompt func(string) (string, error) -// Rline provides a type compatible with the IO interface. -type Rline struct { - Inst *readline.Instance - N func() ([]rune, error) - C func() error - Out io.Writer - Err io.Writer - Int bool - Cyg bool - P func(string) - A func(readline.AutoCompleter) - S func(string) error - Pw PasswordPrompt +// rline provides a type compatible with the IO interface. +type rline struct { + instance *readline.Instance + nextLine func() ([]rune, error) + close func() error + stdout io.Writer + stderr io.Writer + isInteractive bool + isCygwin bool + prompt func(string) + completer func(readline.AutoCompleter) + saveHistory func(string) error + passwordPrompt PasswordPrompt } // Next returns the next line of runes (excluding '\n') from the input. -func (l *Rline) Next() ([]rune, error) { - if l.N != nil { - return l.N() +func (l *rline) Next() ([]rune, error) { + if l.nextLine != nil { + return l.nextLine() } return nil, io.EOF } // Close closes the IO. -func (l *Rline) Close() error { - if l.C != nil { - return l.C() +func (l *rline) Close() error { + if l.close != nil { + return l.close() } return nil } // Stdout is the IO's standard out. -func (l *Rline) Stdout() io.Writer { - return l.Out +func (l *rline) Stdout() io.Writer { + return l.stdout } // Stderr is the IO's standard error out. -func (l *Rline) Stderr() io.Writer { - return l.Err +func (l *rline) Stderr() io.Writer { + return l.stderr } // Interactive determines if the IO is an interactive terminal. -func (l *Rline) Interactive() bool { - return l.Int +func (l *rline) Interactive() bool { + return l.isInteractive } // Cygwin determines if the IO is a Cygwin interactive terminal. -func (l *Rline) Cygwin() bool { - return l.Cyg +func (l *rline) Cygwin() bool { + return l.isCygwin } // Prompt sets the prompt for the next interactive line read. -func (l *Rline) Prompt(s string) { - if l.P != nil { - l.P(s) +func (l *rline) Prompt(s string) { + if l.prompt != nil { + l.prompt(s) } } // Completer sets the auto-completer. -func (l *Rline) Completer(a readline.AutoCompleter) { - if l.A != nil { - l.A(a) +func (l *rline) Completer(a readline.AutoCompleter) { + if l.completer != nil { + l.completer(a) } } // Save saves a line of history. -func (l *Rline) Save(s string) error { - if l.S != nil { - return l.S(s) +func (l *rline) Save(s string) error { + if l.saveHistory != nil { + return l.saveHistory(s) } return nil } // Password prompts for a password. -func (l *Rline) Password(prompt string) (string, error) { - if l.Pw != nil { - return l.Pw(prompt) +func (l *rline) Password(prompt string) (string, error) { + if l.passwordPrompt != nil { + return l.passwordPrompt(prompt) } return "", ErrPasswordNotAvailable } // SetOutput sets the output format func. -func (l *Rline) SetOutput(f func(string) string) { - l.Inst.Config.Output = f +func (l *rline) SetOutput(f func(string) string) { + l.instance.Config.Output = f } // New creates a new readline input/output handler. @@ -208,33 +208,33 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { if forceNonInteractive { n, pw = nil, nil } - return &Rline{ - Inst: l, - N: n, - C: func() error { + return &rline{ + instance: l, + nextLine: n, + close: func() error { for _, f := range closers { _ = f() } return nil }, - Out: stdout, - Err: stderr, - Int: interactive || cygwin, - Cyg: cygwin, - P: l.SetPrompt, - A: func(a readline.AutoCompleter) { + stdout: stdout, + stderr: stderr, + isInteractive: interactive || cygwin, + isCygwin: cygwin, + prompt: l.SetPrompt, + completer: func(a readline.AutoCompleter) { cfg := l.Config.Clone() cfg.AutoComplete = a l.SetConfig(cfg) }, - S: l.SaveHistory, - Pw: pw, + saveHistory: l.SaveHistory, + passwordPrompt: pw, }, nil } func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw PasswordPrompt) IO { - return &Rline{ - N: func() ([]rune, error) { + return &rline{ + nextLine: func() ([]rune, error) { buf := new(bytes.Buffer) var b []byte var isPrefix bool @@ -263,9 +263,9 @@ func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw PasswordPrompt) } return []rune(buf.String()), err }, - Out: out, - Err: err, - Pw: pw, + stdout: out, + stderr: err, + passwordPrompt: pw, } } From 2460c0b22e857e795f7fd2a4be71033ca5380357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 11:18:42 +0200 Subject: [PATCH 4/8] Remove unused rline field --- rline/rline.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rline/rline.go b/rline/rline.go index 7726fcfe581..e1671b1b15e 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -31,8 +31,6 @@ type IO interface { Stderr() io.Writer // Interactive determines if the IO is an interactive terminal. Interactive() bool - // Cygwin determines if the IO is a Cygwin interactive terminal. - Cygwin() bool // Prompt sets the prompt for the next interactive line read. Prompt(string) // Completer sets the auto-completer. @@ -55,7 +53,6 @@ type rline struct { stdout io.Writer stderr io.Writer isInteractive bool - isCygwin bool prompt func(string) completer func(readline.AutoCompleter) saveHistory func(string) error @@ -93,11 +90,6 @@ func (l *rline) Interactive() bool { return l.isInteractive } -// Cygwin determines if the IO is a Cygwin interactive terminal. -func (l *rline) Cygwin() bool { - return l.isCygwin -} - // Prompt sets the prompt for the next interactive line read. func (l *rline) Prompt(s string) { if l.prompt != nil { @@ -220,7 +212,6 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { stdout: stdout, stderr: stderr, isInteractive: interactive || cygwin, - isCygwin: cygwin, prompt: l.SetPrompt, completer: func(a readline.AutoCompleter) { cfg := l.Config.Clone() From 14d5465bcf5c073f4f4d082c7dd933b699e85e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 11:36:21 +0200 Subject: [PATCH 5/8] fixup! Avoid accessling Rline struct from outside of package --- rline/rline.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rline/rline.go b/rline/rline.go index e1671b1b15e..a19f423d405 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -43,8 +43,6 @@ type IO interface { SetOutput(func(string) string) } -type PasswordPrompt func(string) (string, error) - // rline provides a type compatible with the IO interface. type rline struct { instance *readline.Instance @@ -56,9 +54,11 @@ type rline struct { prompt func(string) completer func(readline.AutoCompleter) saveHistory func(string) error - passwordPrompt PasswordPrompt + passwordPrompt passwordPrompt } +type passwordPrompt func(string) (string, error) + // Next returns the next line of runes (excluding '\n') from the input. func (l *rline) Next() ([]rune, error) { if l.nextLine != nil { @@ -223,7 +223,7 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { }, nil } -func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw PasswordPrompt) IO { +func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw passwordPrompt) IO { return &rline{ nextLine: func() ([]rune, error) { buf := new(bytes.Buffer) From c6bef0e2d1b13a3237bb9d637185387f9c4c142d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 12:01:40 +0200 Subject: [PATCH 6/8] Remove readline references from all packages except rlineo --- drivers/completer/completer.go | 6 +++--- drivers/drivers.go | 6 +++--- drivers/metadata/mysql/metadata.go | 4 ++-- rline/rline.go | 24 +++++++++++++++++++----- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/drivers/completer/completer.go b/drivers/completer/completer.go index 4294eb00fef..3b789b9fba9 100644 --- a/drivers/completer/completer.go +++ b/drivers/completer/completer.go @@ -3,6 +3,7 @@ package completer import ( "fmt" + "github.com/xo/usql/rline" "log" "os" "path/filepath" @@ -10,7 +11,6 @@ import ( "strings" "unicode" - "github.com/gohxs/readline" "github.com/xo/usql/drivers/metadata" "github.com/xo/usql/env" "github.com/xo/usql/text" @@ -111,7 +111,7 @@ var ( } ) -func NewDefaultCompleter(opts ...Option) readline.AutoCompleter { +func NewDefaultCompleter(opts ...Option) rline.Completer { c := completer{ // an empty struct satisfies the metadata.Reader interface, because it is actually empty reader: struct{}{}, @@ -277,7 +277,7 @@ type logger interface { Println(...interface{}) } -func (c completer) Do(line []rune, start int) (newLine [][]rune, length int) { +func (c completer) Complete(line []rune, start int) (newLine [][]rune, length int) { var i int for i = start - 1; i > 0; i-- { if strings.ContainsRune(WORD_BREAKS, line[i]) { diff --git a/drivers/drivers.go b/drivers/drivers.go index dc8e21d5f96..772d200e0d4 100644 --- a/drivers/drivers.go +++ b/drivers/drivers.go @@ -7,6 +7,7 @@ import ( "database/sql" "encoding/json" "fmt" + "github.com/xo/usql/rline" "io" "reflect" "strings" @@ -15,7 +16,6 @@ import ( "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/lexers" - "github.com/gohxs/readline" "github.com/xo/dburl" "github.com/xo/usql/drivers/completer" "github.com/xo/usql/drivers/metadata" @@ -101,7 +101,7 @@ type Driver struct { // NewMetadataWriter returns a db metadata printer. NewMetadataWriter func(db DB, w io.Writer, opts ...metadata.ReaderOption) metadata.Writer // NewCompleter returns a db auto-completer. - NewCompleter func(db DB, opts ...completer.Option) readline.AutoCompleter + NewCompleter func(db DB, opts ...completer.Option) rline.Completer // Copy rows into the database table Copy func(ctx context.Context, db *sql.DB, rows *sql.Rows, table string) (int64, error) } @@ -477,7 +477,7 @@ func NewMetadataWriter(ctx context.Context, u *dburl.URL, db DB, w io.Writer, op // NewCompleter creates a metadata completer for a driver and database // connection. -func NewCompleter(ctx context.Context, u *dburl.URL, db DB, readerOpts []metadata.ReaderOption, opts ...completer.Option) readline.AutoCompleter { +func NewCompleter(ctx context.Context, u *dburl.URL, db DB, readerOpts []metadata.ReaderOption, opts ...completer.Option) rline.Completer { d, ok := drivers[u.Driver] if !ok { return nil diff --git a/drivers/metadata/mysql/metadata.go b/drivers/metadata/mysql/metadata.go index b12b64bae73..83f6ea3ea19 100644 --- a/drivers/metadata/mysql/metadata.go +++ b/drivers/metadata/mysql/metadata.go @@ -1,9 +1,9 @@ package mysql import ( + "github.com/xo/usql/rline" "time" - "github.com/gohxs/readline" "github.com/xo/usql/drivers" "github.com/xo/usql/drivers/completer" "github.com/xo/usql/drivers/metadata" @@ -30,7 +30,7 @@ var ( infos.WithUsagePrivileges(false), ) // NewCompleter for MySQL databases - NewCompleter = func(db drivers.DB, opts ...completer.Option) readline.AutoCompleter { + NewCompleter = func(db drivers.DB, opts ...completer.Option) rline.Completer { readerOpts := []metadata.ReaderOption{ // this needs to be relatively low, since autocomplete is very interactive metadata.WithTimeout(3 * time.Second), diff --git a/rline/rline.go b/rline/rline.go index a19f423d405..9bb7a72b3ac 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -34,7 +34,7 @@ type IO interface { // Prompt sets the prompt for the next interactive line read. Prompt(string) // Completer sets the auto-completer. - Completer(readline.AutoCompleter) + Completer(Completer) // Save saves a line of history. Save(string) error // Password prompts for a password. @@ -43,6 +43,12 @@ type IO interface { SetOutput(func(string) string) } +// Completer returns candidates matching current input +type Completer interface { + // Complete current input with matching commands + Complete(line []rune, pos int) (newLine [][]rune, length int) +} + // rline provides a type compatible with the IO interface. type rline struct { instance *readline.Instance @@ -52,7 +58,7 @@ type rline struct { stderr io.Writer isInteractive bool prompt func(string) - completer func(readline.AutoCompleter) + completer func(Completer) saveHistory func(string) error passwordPrompt passwordPrompt } @@ -98,7 +104,7 @@ func (l *rline) Prompt(s string) { } // Completer sets the auto-completer. -func (l *rline) Completer(a readline.AutoCompleter) { +func (l *rline) Completer(a Completer) { if l.completer != nil { l.completer(a) } @@ -213,9 +219,9 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { stderr: stderr, isInteractive: interactive || cygwin, prompt: l.SetPrompt, - completer: func(a readline.AutoCompleter) { + completer: func(a Completer) { cfg := l.Config.Clone() - cfg.AutoComplete = a + cfg.AutoComplete = readlineCompleter{c: a} l.SetConfig(cfg) }, saveHistory: l.SaveHistory, @@ -223,6 +229,14 @@ func New(forceNonInteractive bool, out, histfile string) (IO, error) { }, nil } +type readlineCompleter struct { + c Completer +} + +func (r readlineCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) { + return r.c.Complete(line, pos) +} + func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw passwordPrompt) IO { return &rline{ nextLine: func() ([]rune, error) { From 5b1c526037b97fa5f7c7dd1ba8f590f79c589e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wa=C5=9B?= Date: Mon, 12 Jun 2023 13:23:23 +0200 Subject: [PATCH 7/8] Extract readline specific code to a separate file --- rline/readline.go | 133 ++++++++++++++++++++++++++++++++++++++++++++++ rline/rline.go | 119 +---------------------------------------- 2 files changed, 134 insertions(+), 118 deletions(-) create mode 100644 rline/readline.go diff --git a/rline/readline.go b/rline/readline.go new file mode 100644 index 00000000000..074f1c64987 --- /dev/null +++ b/rline/readline.go @@ -0,0 +1,133 @@ +//go:build !new_readline + +package rline + +import ( + "io" + "os" + + "github.com/gohxs/readline" + "github.com/mattn/go-isatty" +) + +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.Instance +} + +// SetOutput sets the output format func. +func (l *rline) SetOutput(f func(string) string) { + l.instance.Config.Output = f +} + +// New readline input/output handler. +func New(forceNonInteractive bool, out, histfile string) (IO, error) { + // determine if interactive + interactive := isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd()) + cygwin := isatty.IsCygwinTerminal(os.Stdout.Fd()) && isatty.IsCygwinTerminal(os.Stdin.Fd()) + var closers []func() error + // configure stdin + var stdin io.ReadCloser + switch { + case forceNonInteractive: + interactive, cygwin = false, false + case cygwin: + stdin = os.Stdin + default: + stdin = readline.Stdin + } + // configure stdout + var stdout io.WriteCloser + 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 + case cygwin: + stdout = os.Stdout + default: + stdout = readline.Stdout + } + // configure stderr + var stderr io.Writer = os.Stderr + if !cygwin { + stderr = readline.Stderr + } + if interactive { + // wrap it with cancelable stdin + stdin = readline.NewCancelableStdin(stdin) + } + // create readline instance + l, err := readline.NewEx(&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 + }, + }) + if err != nil { + return nil, err + } + closers = append(closers, l.Close) + n := l.Operation.Runes + pw := func(prompt string) (string, error) { + buf, err := l.ReadPassword(prompt) + if err != nil { + return "", err + } + return string(buf), nil + } + if forceNonInteractive { + n, pw = nil, nil + } + return &rline{ + instance: l, + nextLine: n, + close: func() error { + for _, f := range closers { + _ = f() + } + 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) + }, + 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 9bb7a72b3ac..dfbc213445b 100644 --- a/rline/rline.go +++ b/rline/rline.go @@ -6,15 +6,9 @@ import ( "bytes" "errors" "io" - "os" - - "github.com/gohxs/readline" - isatty "github.com/mattn/go-isatty" ) var ( - // ErrInterrupt is the interrupt error. - ErrInterrupt = readline.ErrInterrupt // ErrPasswordNotAvailable is the password not available error. ErrPasswordNotAvailable = errors.New("password not available") ) @@ -51,7 +45,7 @@ type Completer interface { // rline provides a type compatible with the IO interface. type rline struct { - instance *readline.Instance + baseRline nextLine func() ([]rune, error) close func() error stdout io.Writer @@ -126,117 +120,6 @@ func (l *rline) Password(prompt string) (string, error) { return "", ErrPasswordNotAvailable } -// SetOutput sets the output format func. -func (l *rline) SetOutput(f func(string) string) { - l.instance.Config.Output = f -} - -// New creates a new readline input/output handler. -func New(forceNonInteractive bool, out, histfile string) (IO, error) { - // determine if interactive - interactive := isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd()) - cygwin := isatty.IsCygwinTerminal(os.Stdout.Fd()) && isatty.IsCygwinTerminal(os.Stdin.Fd()) - var closers []func() error - // configure stdin - var stdin io.ReadCloser - switch { - case forceNonInteractive: - interactive, cygwin = false, false - case cygwin: - stdin = os.Stdin - default: - stdin = readline.Stdin - } - // configure stdout - var stdout io.WriteCloser - 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 - case cygwin: - stdout = os.Stdout - default: - stdout = readline.Stdout - } - // configure stderr - var stderr io.Writer = os.Stderr - if !cygwin { - stderr = readline.Stderr - } - if interactive { - // wrap it with cancelable stdin - stdin = readline.NewCancelableStdin(stdin) - } - // create readline instance - l, err := readline.NewEx(&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 - }, - }) - if err != nil { - return nil, err - } - closers = append(closers, l.Close) - n := l.Operation.Runes - pw := func(prompt string) (string, error) { - buf, err := l.ReadPassword(prompt) - if err != nil { - return "", err - } - return string(buf), nil - } - if forceNonInteractive { - n, pw = nil, nil - } - return &rline{ - instance: l, - nextLine: n, - close: func() error { - for _, f := range closers { - _ = f() - } - 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) - }, - 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) -} - func NewFromReader(reader *bufio.Reader, out, err io.Writer, pw passwordPrompt) IO { return &rline{ nextLine: func() ([]rune, error) { 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 8/8] 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 {