From 6f600d50ed8af5a0d6e388d2dfc7f36a92d14ade Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 23 Sep 2022 17:41:52 +0200 Subject: [PATCH] clisqlshell: hide the libedit dep behind a go interface In #86457 and related work we will want to offer two editors side-by-side in a transition period, so folk can compare or fall back on something known in case they are not happy with the new stuff. To enable this transition period, this commit hides the editor behind a go interface. This also makes the shell code overall easier to read and understand. Release note: None --- pkg/cli/clisqlshell/BUILD.bazel | 3 + pkg/cli/clisqlshell/context.go | 4 - pkg/cli/clisqlshell/editor.go | 38 ++++ pkg/cli/clisqlshell/editor_bufio.go | 82 ++++++++ pkg/cli/clisqlshell/editor_editline.go | 155 +++++++++++++++ pkg/cli/clisqlshell/sql.go | 232 +++++------------------ pkg/cli/clisqlshell/sql_internal_test.go | 4 +- 7 files changed, 327 insertions(+), 191 deletions(-) create mode 100644 pkg/cli/clisqlshell/editor.go create mode 100644 pkg/cli/clisqlshell/editor_bufio.go create mode 100644 pkg/cli/clisqlshell/editor_editline.go diff --git a/pkg/cli/clisqlshell/BUILD.bazel b/pkg/cli/clisqlshell/BUILD.bazel index d6186e459dd3..6baad29004ea 100644 --- a/pkg/cli/clisqlshell/BUILD.bazel +++ b/pkg/cli/clisqlshell/BUILD.bazel @@ -7,6 +7,9 @@ go_library( "api.go", "context.go", "doc.go", + "editor.go", + "editor_bufio.go", + "editor_editline.go", "parser.go", "sql.go", "statement_diag.go", diff --git a/pkg/cli/clisqlshell/context.go b/pkg/cli/clisqlshell/context.go index 02a2da0f4701..e0061eae2dfc 100644 --- a/pkg/cli/clisqlshell/context.go +++ b/pkg/cli/clisqlshell/context.go @@ -91,10 +91,6 @@ type internalContext struct { // current database name, if known. This is maintained on a best-effort basis. dbName string - // displayPrompt indicates that the prompt should still be displayed, - // even when the line editor is disabled. - displayPrompt bool - // hook to run once, then clear, after running the next batch of statements. afterRun func() diff --git a/pkg/cli/clisqlshell/editor.go b/pkg/cli/clisqlshell/editor.go new file mode 100644 index 000000000000..6417c1f0871a --- /dev/null +++ b/pkg/cli/clisqlshell/editor.go @@ -0,0 +1,38 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clisqlshell + +import "os" + +// editor is the interface between the shell and a line editor. +type editor interface { + init(win, wout, werr *os.File, sqlS sqlShell, maxHistEntries int, histFile string) (cleanupFn func(), err error) + errInterrupted() error + getOutputStream() *os.File + getLine() (string, error) + addHistory(line string) error + canPrompt() bool + setPrompt(prompt string) +} + +type sqlShell interface { + inCopy() bool + runShowCompletions(sql string, offset int) (rows [][]string, err error) + serverSideParse(sql string) (string, error) +} + +// getEditor instantiates an editor compatible with the current configuration. +func getEditor(useEditor bool, displayPrompt bool) editor { + if !useEditor { + return &bufioReader{displayPrompt: displayPrompt} + } + return &editlineReader{} +} diff --git a/pkg/cli/clisqlshell/editor_bufio.go b/pkg/cli/clisqlshell/editor_bufio.go new file mode 100644 index 000000000000..4ab2d8e51987 --- /dev/null +++ b/pkg/cli/clisqlshell/editor_bufio.go @@ -0,0 +1,82 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clisqlshell + +import ( + "bufio" + "fmt" + "io" + "os" + + "github.com/cockroachdb/errors" +) + +// bufioReader implements the editor interface. +type bufioReader struct { + wout *os.File + displayPrompt bool + prompt string + buf *bufio.Reader +} + +var _ editor = (*bufioReader)(nil) + +func (b *bufioReader) init( + win, wout, werr *os.File, _ sqlShell, maxHistEntries int, histFile string, +) (cleanupFn func(), err error) { + b.wout = wout + b.buf = bufio.NewReader(win) + return func() {}, nil +} + +var errBufioInterrupted = errors.New("never happens") + +func (b *bufioReader) errInterrupted() error { + return errBufioInterrupted +} + +func (b *bufioReader) getOutputStream() *os.File { + return b.wout +} + +func (b *bufioReader) addHistory(line string) error { + return nil +} + +func (b *bufioReader) canPrompt() bool { + return b.displayPrompt +} + +func (b *bufioReader) setPrompt(prompt string) { + if b.displayPrompt { + b.prompt = prompt + } +} + +func (b *bufioReader) getLine() (string, error) { + fmt.Fprint(b.wout, b.prompt) + l, err := b.buf.ReadString('\n') + // bufio.ReadString() differs from readline.Readline in the handling of + // EOF. Readline only returns EOF when there is nothing left to read and + // there is no partial line while bufio.ReadString() returns EOF when the + // end of input has been reached but will return the non-empty partial line + // as well. We work around this by converting the bufioReader behavior to match + // the Readline behavior. + if err == io.EOF && len(l) != 0 { + err = nil + } else if err == nil { + // From the bufio.ReadString docs: ReadString returns err != nil if and + // only if the returned data does not end in delim. To match the behavior + // of readline.Readline, we strip off the trailing delimiter. + l = l[:len(l)-1] + } + return l, err +} diff --git a/pkg/cli/clisqlshell/editor_editline.go b/pkg/cli/clisqlshell/editor_editline.go new file mode 100644 index 000000000000..dfa863037a6a --- /dev/null +++ b/pkg/cli/clisqlshell/editor_editline.go @@ -0,0 +1,155 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package clisqlshell + +import ( + "fmt" + "os" + "strings" + + "github.com/cockroachdb/cockroach/pkg/cli/clierror" + "github.com/cockroachdb/errors" + readline "github.com/knz/go-libedit" +) + +// editlineReader implements the editor interface. +type editlineReader struct { + wout *os.File + sql sqlShell + prompt string + ins readline.EditLine +} + +var _ editor = (*editlineReader)(nil) + +func (b *editlineReader) init( + win, wout, werr *os.File, sqlS sqlShell, maxHistEntries int, histFile string, +) (cleanupFn func(), err error) { + cleanupFn = func() {} + + b.ins, err = readline.InitFiles("cockroach", + true /* wideChars */, win, wout, werr) + if errors.Is(err, readline.ErrWidecharNotSupported) { + fmt.Fprintln(werr, "warning: wide character support disabled") + b.ins, err = readline.InitFiles("cockroach", + false, win, wout, werr) + } + if err != nil { + return cleanupFn, err + } + cleanupFn = func() { b.ins.Close() } + b.wout = b.ins.Stdout() + b.sql = sqlS + b.ins.SetCompleter(b) + + // If the user has used bind -v or bind -l in their ~/.editrc, + // this will reset the standard bindings. However we really + // want in this shell that Ctrl+C, tab, Ctrl+Z and Ctrl+R + // always have the same meaning. So reload these bindings + // explicitly no matter what ~/.editrc may have changed. + b.ins.RebindControlKeys() + + if err := b.ins.UseHistory(maxHistEntries, true /*dedup*/); err != nil { + fmt.Fprintf(werr, "warning: cannot enable history: %v\n ", err) + } else if histFile != "" { + err = b.ins.LoadHistory(histFile) + if err != nil { + fmt.Fprintf(werr, "warning: cannot load the command-line history (file corrupted?): %v\n", err) + fmt.Fprintf(werr, "note: the history file will be cleared upon first entry\n") + } + // SetAutoSaveHistory() does two things: + // - it preserves the name of the history file, for use + // by the final SaveHistory() call. + // - it decides whether to save the history to file upon + // every new command. + // We disable the latter, since a history file can grow somewhat + // large and we don't want the excess I/O latency to be interleaved + // in-between every command. + b.ins.SetAutoSaveHistory(histFile, false) + prevCleanup := cleanupFn + cleanupFn = func() { + if err := b.ins.SaveHistory(); err != nil { + fmt.Fprintf(werr, "warning: cannot save command-line history: %v\n", err) + } + prevCleanup() + } + } + + return cleanupFn, nil +} + +func (b *editlineReader) errInterrupted() error { + return readline.ErrInterrupted +} + +func (b *editlineReader) getOutputStream() *os.File { + return b.wout +} + +func (b *editlineReader) addHistory(line string) error { + return b.ins.AddHistory(line) +} + +func (b *editlineReader) canPrompt() bool { + return true +} + +func (b *editlineReader) setPrompt(prompt string) { + b.prompt = prompt + b.ins.SetLeftPrompt(prompt) +} + +func (b *editlineReader) GetCompletions(word string) []string { + if b.sql.inCopy() { + return []string{word + "\t"} + } + sql, offset := b.ins.GetLineInfo() + if !strings.HasSuffix(sql, "??") { + rows, err := b.sql.runShowCompletions(sql, offset) + if err != nil { + clierror.OutputError(b.wout, err, true /*showSeverity*/, false /*verbose*/) + } + + var completions []string + for _, row := range rows { + completions = append(completions, row[0]) + } + + return completions + } + + helpText, err := b.sql.serverSideParse(sql) + if helpText != "" { + // We have a completion suggestion. Use that. + fmt.Fprintf(b.wout, "\nSuggestion:\n%s\n", helpText) + } else if err != nil { + // Some other error. Display it. + fmt.Fprintln(b.wout) + clierror.OutputError(b.wout, err, true /*showSeverity*/, false /*verbose*/) + } + + // After a suggestion or error, redisplay the prompt and current entry. + fmt.Fprint(b.wout, b.prompt, sql) + return nil +} + +func (b *editlineReader) getLine() (string, error) { + l, err := b.ins.GetLine() + if len(l) > 0 && l[len(l)-1] == '\n' { + // Strip the final newline. + l = l[:len(l)-1] + } else { + // There was no newline at the end of the input + // (e.g. Ctrl+C was entered). Force one. + fmt.Fprintln(b.wout) + } + return l, err +} diff --git a/pkg/cli/clisqlshell/sql.go b/pkg/cli/clisqlshell/sql.go index b3e3c1a9a9a8..ba6fae0c29f2 100644 --- a/pkg/cli/clisqlshell/sql.go +++ b/pkg/cli/clisqlshell/sql.go @@ -43,7 +43,6 @@ import ( "github.com/cockroachdb/cockroach/pkg/util/envutil" "github.com/cockroachdb/cockroach/pkg/util/sysutil" "github.com/cockroachdb/errors" - readline "github.com/knz/go-libedit" ) const ( @@ -137,10 +136,8 @@ type cliState struct { iCtx *internalContext conn clisqlclient.Conn - // ins is used to read lines if isInteractive is true. - ins readline.EditLine - // buf is used to read lines if isInteractive is false. - buf *bufio.Reader + // ins is used to read lines. + ins editor // singleStatement is set to true when this state level // is currently processing for runString(). In that mode: // - a missing semicolon at the end of input is ignored: @@ -269,21 +266,13 @@ func (c *cliState) printCliHelp() { fmt.Fprintln(c.iCtx.stdout) } -const noLineEditor readline.EditLine = -1 - -func (c *cliState) hasEditor() bool { - return c.ins != noLineEditor -} - // addHistory persists a line of input to the readline history file. func (c *cliState) addHistory(line string) { - if !c.hasEditor() || len(line) == 0 { + if len(line) == 0 { return } - // ins.AddHistory will push command into memory. err can - // be not nil only if it got a memory error. - if err := c.ins.AddHistory(line); err != nil { + if err := c.ins.addHistory(line); err != nil { fmt.Fprintf(c.iCtx.stderr, "warning: cannot add entry to history: %v\n", err) } } @@ -798,7 +787,7 @@ const unknownTxnStatus = " ?" // doRefreshPrompts refreshes the prompts of the client depending on the // status of the current transaction. func (c *cliState) doRefreshPrompts(nextState cliStateEnum) cliStateEnum { - if !c.iCtx.displayPrompt { + if !c.ins.canPrompt() { return nextState } @@ -812,9 +801,7 @@ func (c *cliState) doRefreshPrompts(nextState cliStateEnum) cliStateEnum { c.continuePrompt = strings.Repeat(" ", len(c.fullPrompt)-3) + "-> " } - if c.hasEditor() { - c.ins.SetLeftPrompt(c.continuePrompt) - } + c.ins.setPrompt(c.continuePrompt) return nextState } @@ -880,9 +867,7 @@ func (c *cliState) doRefreshPrompts(nextState cliStateEnum) cliStateEnum { c.currentPrompt = c.fullPrompt // Configure the editor to use the new prompt. - if c.hasEditor() { - c.ins.SetLeftPrompt(c.currentPrompt) - } + c.ins.setPrompt(c.currentPrompt) return nextState } @@ -953,49 +938,14 @@ func (c *cliState) refreshDatabaseName() string { var cmdHistFile = envutil.EnvOrDefaultString("COCKROACH_SQL_CLI_HISTORY", ".cockroachsql_history") -// GetCompletions implements the readline.CompletionGenerator interface. -func (c *cliState) GetCompletions(s string) []string { - sql, _ := c.ins.GetLineInfo() - - // In COPY mode, just add a tab character. - if c.inCopy() { - return []string{s + "\t"} - } - - if !strings.HasSuffix(sql, "??") { - query := fmt.Sprintf(`SHOW COMPLETIONS AT OFFSET %d FOR %s`, len(sql), lexbase.EscapeSQLString(sql)) - var rows [][]string +func (c *cliState) runShowCompletions(sql string, offset int) (rows [][]string, err error) { + query := fmt.Sprintf(`SHOW COMPLETIONS AT OFFSET %d FOR %s`, offset, lexbase.EscapeSQLString(sql)) + err = c.runWithInterruptableCtx(func(ctx context.Context) error { var err error - err = c.runWithInterruptableCtx(func(ctx context.Context) error { - _, rows, err = c.sqlExecCtx.RunQuery(ctx, c.conn, clisqlclient.MakeQuery(query), true /* showMoreChars */) - return err - }) - - if err != nil { - clierror.OutputError(c.iCtx.stdout, err, true /*showSeverity*/, false /*verbose*/) - } - - var completions []string - for _, row := range rows { - completions = append(completions, row[0]) - } - - return completions - } - - helpText, err := c.serverSideParse(sql) - if helpText != "" { - // We have a completion suggestion. Use that. - fmt.Fprintf(c.iCtx.stdout, "\nSuggestion:\n%s\n", helpText) - } else if err != nil { - // Some other error. Display it. - fmt.Fprintln(c.iCtx.stdout) - clierror.OutputError(c.iCtx.stdout, err, true /*showSeverity*/, false /*verbose*/) - } - - // After the suggestion or error, re-display the prompt and current entry. - fmt.Fprint(c.iCtx.stdout, c.currentPrompt, sql) - return nil + _, rows, err = c.sqlExecCtx.RunQuery(ctx, c.conn, clisqlclient.MakeQuery(query), true /* showMoreChars */) + return err + }) + return rows, err } func (c *cliState) doStart(nextState cliStateEnum) cliStateEnum { @@ -1040,42 +990,7 @@ func (c *cliState) doReadLine(nextState cliStateEnum) cliStateEnum { return nextState } - var l string - var err error - if c.buf == nil { - l, err = c.ins.GetLine() - if len(l) > 0 && l[len(l)-1] == '\n' { - // Strip the final newline. - l = l[:len(l)-1] - } else { - // There was no newline at the end of the input - // (e.g. Ctrl+C was entered). Force one. - fmt.Fprintln(c.iCtx.stdout) - } - } else { - if c.iCtx.displayPrompt { - prompt := c.currentPrompt - if c.useContinuePrompt { - prompt = c.continuePrompt - } - fmt.Fprint(c.iCtx.stdout, prompt) - } - l, err = c.buf.ReadString('\n') - // bufio.ReadString() differs from readline.Readline in the handling of - // EOF. Readline only returns EOF when there is nothing left to read and - // there is no partial line while bufio.ReadString() returns EOF when the - // end of input has been reached but will return the non-empty partial line - // as well. We workaround this by converting the bufio behavior to match - // the Readline behavior. - if err == io.EOF && len(l) != 0 { - err = nil - } else if err == nil { - // From the bufio.ReadString docs: ReadString returns err != nil if and - // only if the returned data does not end in delim. To match the behavior - // of readline.Readline, we strip off the trailing delimiter. - l = l[:len(l)-1] - } - } + l, err := c.ins.getLine() switch { case err == nil: @@ -1089,7 +1004,7 @@ func (c *cliState) doReadLine(nextState cliStateEnum) cliStateEnum { } // In any case, process one line. - case errors.Is(err, readline.ErrInterrupted): + case errors.Is(err, c.ins.errInterrupted()): if !c.cliCtx.IsInteractive { // Ctrl+C terminates non-interactive shells in all cases. c.exitErr = err @@ -1727,11 +1642,9 @@ func (c *cliState) runIncludeInternal( sqlExecCtx: c.sqlExecCtx, sqlCtx: c.sqlCtx, iCtx: c.iCtx, - + ins: &bufioReader{wout: c.iCtx.stdout, buf: input}, conn: c.conn, includeDir: filepath.Dir(filename), - ins: noLineEditor, - buf: input, levels: level, singleStatement: singleStatement, @@ -2144,40 +2057,37 @@ func (c *cliState) configurePreShellDefaults( // there is also a terminal on stdout. canUseEditor := c.cliCtx.IsInteractive && c.sqlExecCtx.TerminalOutput useEditor := canUseEditor && !c.sqlCtx.DisableLineEditor - c.iCtx.displayPrompt = canUseEditor + c.ins = getEditor(useEditor, canUseEditor) + + // maxHistEntries is the maximum number of entries to + // preserve. Note that libedit de-duplicates entries under the + // hood. We expect that folk entering SQL in a shell will often + // reuse the same queries over time, so we don't expect this limit + // to ever be reached in practice, or to be an annoyance to + // anyone. We do prefer a limit however (as opposed to no limit at + // all), to prevent abnormal situation where a history runs into + // megabytes and starts slowing down the shell. + const maxHistEntries = 10000 + var histFile string if useEditor { - // The readline initialization is not placed in - // the doStart() method because of the defer. - c.ins, c.exitErr = readline.InitFiles("cockroach", - true, /* wideChars */ - cmdIn, c.iCtx.stdout, c.iCtx.stderr) - if errors.Is(c.exitErr, readline.ErrWidecharNotSupported) { - fmt.Fprintln(c.iCtx.stderr, "warning: wide character support disabled") - c.ins, c.exitErr = readline.InitFiles("cockroach", - false, cmdIn, c.iCtx.stdout, c.iCtx.stderr) + homeDir, err := envutil.HomeDir() + if err != nil { + fmt.Fprintf(c.iCtx.stderr, "warning: cannot retrieve user information: %v\nwarning: history will not be saved\n", err) + } else { + histFile = filepath.Join(homeDir, cmdHistFile) } - if c.exitErr != nil { - return cleanupFn, c.exitErr - } - // The readline library may have a custom file descriptor for stdout. - // Use that for further output. - c.iCtx.stdout = c.ins.Stdout() - c.iCtx.queryOutputFile = c.ins.Stdout() - - // If the user has used bind -v or bind -l in their ~/.editrc, - // this will reset the standard bindings. However we really - // want in this shell that Ctrl+C, tab, Ctrl+Z and Ctrl+R - // always have the same meaning. So reload these bindings - // explicitly no matter what ~/.editrc may have changed. - c.ins.RebindControlKeys() - cleanupFn = func() { c.ins.Close() } - } else { - c.ins = noLineEditor - c.buf = bufio.NewReader(cmdIn) - cleanupFn = func() {} } - if c.iCtx.displayPrompt { + cleanupFn, c.exitErr = c.ins.init(cmdIn, c.iCtx.stdout, c.iCtx.stderr, c, maxHistEntries, histFile) + if c.exitErr != nil { + return cleanupFn, c.exitErr + } + // The readline library may have a custom file descriptor for stdout. + // Use that for further output. + c.iCtx.stdout = c.ins.getOutputStream() + c.iCtx.queryOutputFile = c.ins.getOutputStream() + + if canUseEditor { // Default prompt is part of the connection URL. eg: "marc@localhost:26257>". c.iCtx.customPromptPattern = defaultPromptPattern if c.sqlConnCtx.DebugMode { @@ -2185,56 +2095,6 @@ func (c *cliState) configurePreShellDefaults( } } - if c.hasEditor() { - // We only enable prompt and history management when the - // interactive input prompter is enabled. This saves on churn and - // memory when e.g. piping a large SQL script through the - // command-line client. - - // maxHistEntries is the maximum number of entries to - // preserve. Note that libedit de-duplicates entries under the - // hood. We expect that folk entering SQL in a shell will often - // reuse the same queries over time, so we don't expect this limit - // to ever be reached in practice, or to be an annoyance to - // anyone. We do prefer a limit however (as opposed to no limit at - // all), to prevent abnormal situation where a history runs into - // megabytes and starts slowing down the shell. - const maxHistEntries = 10000 - - c.ins.SetCompleter(c) - if err := c.ins.UseHistory(maxHistEntries, true /*dedup*/); err != nil { - fmt.Fprintf(c.iCtx.stderr, "warning: cannot enable history: %v\n ", err) - } else { - homeDir, err := envutil.HomeDir() - if err != nil { - fmt.Fprintf(c.iCtx.stderr, "warning: cannot retrieve user information: %v\nwarning: history will not be saved\n", err) - } else { - histFile := filepath.Join(homeDir, cmdHistFile) - err = c.ins.LoadHistory(histFile) - if err != nil { - fmt.Fprintf(c.iCtx.stderr, "warning: cannot load the command-line history (file corrupted?): %v\n", err) - fmt.Fprintf(c.iCtx.stderr, "note: the history file will be cleared upon first entry\n") - } - // SetAutoSaveHistory() does two things: - // - it preserves the name of the history file, for use - // by the final SaveHistory() call. - // - it decides whether to save the history to file upon - // every new command. - // We disable the latter, since a history file can grow somewhat - // large and we don't want the excess I/O latency to be interleaved - // in-between every command. - c.ins.SetAutoSaveHistory(histFile, false) - prevCleanup := cleanupFn - cleanupFn = func() { - if err := c.ins.SaveHistory(); err != nil { - fmt.Fprintf(c.iCtx.stderr, "warning: cannot save command-line history: %v\n", err) - } - prevCleanup() - } - } - } - } - // If any --set flags were set through the command line, // synthetize '-e set=xxx' statements for them at the beginning. c.iCtx.quitAfterExecStmts = len(c.sqlCtx.ExecStmts) > 0 @@ -2380,8 +2240,8 @@ func (c *cliState) maybeHandleInterrupt() func() { c.iCtx.mu.Unlock() if cancelFn == nil { // No query currently executing. - // Do we have a line editor? If so, do nothing. - if c.ins != noLineEditor { + // Are we doing interactive input? If so, do nothing. + if c.cliCtx.IsInteractive { continue } // Otherwise, ctrl+c interrupts the shell. We do this diff --git a/pkg/cli/clisqlshell/sql_internal_test.go b/pkg/cli/clisqlshell/sql_internal_test.go index 1ed172a87997..a3e5bf9e65c9 100644 --- a/pkg/cli/clisqlshell/sql_internal_test.go +++ b/pkg/cli/clisqlshell/sql_internal_test.go @@ -11,6 +11,8 @@ package clisqlshell import ( + "bufio" + "os" "testing" "github.com/cockroachdb/cockroach/pkg/cli/clicfg" @@ -155,7 +157,7 @@ func setupTestCliState() *cliState { } sqlCtx := &Context{} c := NewShell(cliCtx, sqlConnCtx, sqlExecCtx, sqlCtx, nil).(*cliState) - c.ins = noLineEditor + c.ins = &bufioReader{wout: os.Stdout, buf: bufio.NewReader(os.Stdin)} return c }