Add REPL autocompletion & go-prompt (#392)

* add v1-compatible query path and refactor other paths to de-duplicate "/query"

* add initial influxQL repl

* add ping endpoint to schema

* improve prompt UX, implement some commands

* fix json column type in schema and improve completion

* feat: add table formatter and move to forked go-prompt

* improve formatting and add table pagination

* implement more REPL commands, including insert and history

* implement "INSERT INTO"

* move repl command to "v1 repl"

* refactor and improve documentation

* clean up v1_repl cmd

* update to latest openapi, use some openapi paths instead of overrides

* remove additional files that were moved to openapi

* compute historyFilePath at REPL start

* clean up REPL use command logic flow

* clean up comments for TODOs now in issues

* move gopher (chonky boi)

* remove autocompletion for separate PR

* run go mod tidy

* add back autocompletion & go-prompt

* add rfc3339 precision option

* allow left and right column scrolling to display whole table

* add error to JSON query response

* add tags and partial to JSON response series schema

* fix csv formatting and add column formatting

* remove table format for separate PR

* fix getDatabases

* move from write to legacy write endpoint for INSERT

* remove history vestiges

* allow multiple spaces in INSERT commands

* add precision comment

* remove auth for separate PR

* separate parseInsert and add unit test

* add additional test case and improve error messages

* fix missing errors import

* fix format suggestion

* re-add history implementation with history limit

* build: upgrade to Go 1.18.3 (#395)

* feat: add back the InfluxQL REPL (#386)

* add v1-compatible query path and refactor other paths to de-duplicate "/query"

* add initial influxQL repl

* add ping endpoint to schema

* improve prompt UX, implement some commands

* fix json column type in schema and improve completion

* feat: add table formatter and move to forked go-prompt

* improve formatting and add table pagination

* implement more REPL commands, including insert and history

* implement "INSERT INTO"

* move repl command to "v1 repl"

* refactor and improve documentation

* clean up v1_repl cmd

* update to latest openapi, use some openapi paths instead of overrides

* remove additional files that were moved to openapi

* compute historyFilePath at REPL start

* clean up REPL use command logic flow

* clean up comments for TODOs now in issues

* move gopher (chonky boi)

* remove autocompletion for separate PR

* run go mod tidy

* add rfc3339 precision option

* allow left and right column scrolling to display whole table

* add error to JSON query response

* add tags and partial to JSON response series schema

* fix csv formatting and add column formatting

* remove table format for separate PR

* fix getDatabases

* move from write to legacy write endpoint for INSERT

* remove history vestiges

* allow multiple spaces in INSERT commands

* add precision comment

* remove auth for separate PR

* separate parseInsert and add unit test

* add additional test case and improve error messages

* fix missing errors import

* print rfc3339 precision

* add rfc3339 to help output

* run tidy

* restructure autocomplete and handle review items

* improve autocompletion with leftover handling

* improve comments and add autocomplete for DELETE & DROP MEASUREMENT

* rename repl to shell

* remove unsupported CREATE & DROP autocompletions

* additional refactor for autocompletion

Co-authored-by: Dane Strandboge <dstrandboge@influxdata.com>
This commit is contained in:
Andrew Lee 2022-06-14 15:18:27 -06:00 committed by GitHub
parent 0c17ebd621
commit c695e601a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 324 additions and 32 deletions

@ -1 +1 @@
Subproject commit 9b93b9e414c45537f6ebed0757ff4f764518e2b5
Subproject commit 920d508ef5bb61e09885bd625c6e81734fc5cc99

View File

@ -0,0 +1,213 @@
package v1shell
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/influxdata/go-prompt"
)
type subsuggestFnType = func(string) (map[string]SuggestNode, string)
type SuggestNode struct {
Description string
subsuggestFn subsuggestFnType
}
func (c *Client) suggestUse(remainder string) (map[string]SuggestNode, string) {
s := map[string]SuggestNode{}
for _, db := range c.Databases {
s["\""+db+"\""] = SuggestNode{Description: "Table Name"}
}
return s, remainder
}
func (c *Client) suggestDropMeasurement(remainder string) (map[string]SuggestNode, string) {
s := map[string]SuggestNode{}
if c.Database != "" && c.RetentionPolicy != "" {
for _, m := range c.Measurements {
s["\""+m+"\""] = SuggestNode{Description: fmt.Sprintf("Measurement on \"%s\".\"%s\"", c.Database, c.RetentionPolicy)}
}
}
return s, remainder
}
func (c *Client) suggestDelete(remainder string) (map[string]SuggestNode, string) {
s := map[string]SuggestNode{}
fromReg := regexp.MustCompile(`(?i)FROM(\s+)` + identRegex("from_clause") + `?$`)
matches := reSubMatchMap(fromReg, remainder)
if matches != nil {
if c.Database != "" && c.RetentionPolicy != "" {
for _, m := range c.Measurements {
s["\""+m+"\""] = SuggestNode{Description: fmt.Sprintf("Measurement on \"%s\".\"%s\"", c.Database, c.RetentionPolicy)}
}
}
return s, getIdentFromMatches(matches, "from_clause")
}
return s, remainder
}
func (c *Client) suggestSelect(remainder string) (map[string]SuggestNode, string) {
s := map[string]SuggestNode{}
fromReg := regexp.MustCompile(`(?i)FROM(\s+)` + identRegex("from_clause") + `?$`)
matches := reSubMatchMap(fromReg, remainder)
if matches != nil {
if c.Database != "" && c.RetentionPolicy != "" {
for _, m := range c.Measurements {
s["\""+m+"\""] = SuggestNode{Description: fmt.Sprintf("Measurement on \"%s\".\"%s\"", c.Database, c.RetentionPolicy)}
}
}
if c.Database != "" {
for _, rp := range c.RetentionPolicies {
s["\""+rp+"\""] = SuggestNode{Description: "Retention Policy on " + c.Database}
}
}
for _, db := range c.Databases {
s["\""+db+"\""] = SuggestNode{Description: "Table Name"}
}
return s, getIdentFromMatches(matches, "from_clause")
}
return s, remainder
}
func getSuggestions(remainder string, s map[string]SuggestNode) ([]prompt.Suggest, string) {
if len(remainder) > 0 {
for term, node := range s {
if strings.HasPrefix(strings.ToLower(remainder), term) || strings.HasPrefix(strings.ToUpper(remainder), term) {
// if the cursor is at the end of a just completed word without trailing space, don't give autosuggestions yet
if len(term) >= len(remainder) {
return []prompt.Suggest{}, ""
}
// remainder is everything after the just-matched term
rem := strings.TrimLeft(remainder[len(term):], " ")
// if sugsuggestFn is nil, this is a terminating suggestion (leaf node with no more following suggestions)
if node.subsuggestFn == nil {
return []prompt.Suggest{}, rem
}
sugs, rem := node.subsuggestFn(rem)
return getSuggestions(rem, sugs)
}
}
}
// if no match was found, convert the s map into suggestions and filter by the remaining characters
var sugs []prompt.Suggest
for text, node := range s {
sugs = append(sugs, prompt.Suggest{Text: text, Description: node.Description})
}
return prompt.FilterFuzzy(sugs, remainder, true), remainder
}
// recursively creates a SuggestNode for each space-delimited word for each subsuggestion
func newSugNodeFn(subsugs ...string) subsuggestFnType {
s := make(map[string]SuggestNode)
for _, sug := range subsugs {
word, rest, found := strings.Cut(sug, " ")
if !found {
s[word] = SuggestNode{}
} else {
s[word] = SuggestNode{subsuggestFn: newSugNodeFn(rest)}
}
}
return func(rem string) (map[string]SuggestNode, string) {
return s, rem
}
}
func (c *Client) completer(d prompt.Document) []prompt.Suggest {
// the commented-out lines are unsupported in 2.x
suggestions := map[string]SuggestNode{
"use": {Description: "Set current database", subsuggestFn: c.suggestUse},
"pretty": {Description: "Toggle pretty print for the json format"},
"precision": {Description: "Specify the format of the timestamp",
subsuggestFn: newSugNodeFn(
"rfc3339",
"ns",
"u",
"ms",
"s",
"m",
"h",
)},
"history": {Description: "Display shell history"},
"settings": {Description: "Output the current shell settings"},
"clear": {Description: "Clears settings such as database",
subsuggestFn: newSugNodeFn(
"db",
"database",
"retention policy",
"rp",
)},
"exit": {Description: "Exit the InfluxQL shell"},
"quit": {Description: "Exit the InfluxQL shell"},
"gopher": {Description: "Display the Go Gopher"},
"help": {Description: "Display help options"},
"format": {Description: "Specify the data display format",
subsuggestFn: newSugNodeFn(
"column",
"csv",
"json",
)},
"SELECT": {subsuggestFn: c.suggestSelect},
"INSERT": {},
"INSERT INTO": {},
"DELETE": {subsuggestFn: c.suggestDelete},
"SHOW": {subsuggestFn: newSugNodeFn(
// "CONTINUOUS QUERIES",
"DATABASES",
// "DIAGNOSTICS",
"FIELD KEY CARDINALITY",
"FIELD KEYS",
// "GRANTS",
// "MEASUREMENT CARDINALITY",
"MEASUREMENT EXACT CARDINALITY",
"MEASUREMENTS",
// "QUERIES",
// "RETENTION POLICIES",
"SERIES",
// "SERIES CARDINALITY",
"SERIES EXACT CARDINALITY",
// "SHARD GROUPS",
// "SHARDS",
// "STATS",
// "SUBSCRIPTIONS",
"TAG KEY CARDINALITY",
"TAG KEY EXACT CARDINALITY",
"TAG KEYS",
"TAG VALUES",
// "USERS",
)},
// "CREATE": {subsuggestFn: newSugNodeFn(
// "CONTINUOUS QUERY",
// "DATABASE",
// "USER",
// "RETENTION POLICY",
// "SUBSCRIPTION",
// )},
"DROP": {subsuggestFn: func(rem string) (map[string]SuggestNode, string) {
return map[string]SuggestNode{
// "CONTINUOUS QUERY": {},
// "DATABASE": {},
"MEASUREMENT": {subsuggestFn: c.suggestDropMeasurement},
// "RETENTION POLICY": {},
// "SERIES": {},
// "SHARD": {},
// "SUBSCRIPTION": {},
// "USER": {},
}, rem
}},
"EXPLAIN": {subsuggestFn: newSugNodeFn("ANALYZE")},
// "GRANT": {},
// "REVOKE": {},
// "ALTER RETENTION POLICY": {},
// "SET PASSOWRD FOR": {},
// "KILL QUERY": {},
}
line := d.CurrentLineBeforeCursor()
currentSuggestions, _ := getSuggestions(line, suggestions)
sort.Slice(currentSuggestions, func(i, j int) bool {
return strings.ToLower(currentSuggestions[i].Text) < strings.ToLower(currentSuggestions[j].Text)
})
return currentSuggestions
}

View File

@ -1,4 +1,4 @@
package v1repl
package v1shell
var Gopher string = `
.-::-::://:-::- .:/++/'

View File

@ -1,4 +1,4 @@
package v1repl
package v1shell
import (
"bufio"
@ -10,13 +10,16 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"sort"
"strings"
"text/tabwriter"
"github.com/fatih/color"
"github.com/influxdata/go-prompt"
"github.com/influxdata/influx-cli/v2/api"
"github.com/influxdata/influx-cli/v2/clients"
)
@ -39,11 +42,59 @@ type PersistentQueryParams struct {
Format FormatType
Pretty bool
// Autocompletion Storage
historyFilePath string
historyLimit int
Databases []string
RetentionPolicies []string
Measurements []string
}
func DefaultPersistentQueryParams() PersistentQueryParams {
return PersistentQueryParams{
Format: ColumnFormat,
Precision: "ns",
historyLimit: 1000,
}
}
func (c *Client) readHistory() []string {
// Attempt to load the history file.
if c.historyFilePath != "" {
if historyFile, err := os.Open(c.historyFilePath); err == nil {
var history []string
scanner := bufio.NewScanner(historyFile)
for scanner.Scan() {
history = append(history, scanner.Text())
}
historyFile.Close()
// Limit to last n elements
if len(history) > c.historyLimit {
history = history[len(history)-c.historyLimit:]
}
return history
}
}
return []string{}
}
func (c *Client) rewriteHistoryFile(history []string) {
if c.historyFilePath != "" {
if historyFile, err := os.Create(c.historyFilePath); err == nil {
historyFile.WriteString(strings.Join(history, "\n"))
historyFile.Close()
}
}
}
func (c *Client) writeCommandToHistory(cmd string) {
if c.historyFilePath != "" {
if historyFile, err := os.OpenFile(c.historyFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666); err == nil {
historyFile.WriteString(cmd + "\n")
historyFile.Close()
}
}
}
func (c *Client) clear(cmd string) {
args := strings.Split(strings.TrimSuffix(strings.TrimSpace(cmd), ";"), " ")
v := strings.ToLower(strings.Join(args[1:], " "))
@ -73,13 +124,6 @@ func (c *Client) clear(cmd string) {
}
}
func DefaultPersistentQueryParams() PersistentQueryParams {
return PersistentQueryParams{
Format: ColumnFormat,
Precision: "ns",
}
}
func (c *Client) Create(ctx context.Context) error {
res, err := c.GetPing(ctx).ExecuteWithHttpInfo()
if err != nil {
@ -90,15 +134,35 @@ func (c *Client) Create(ctx context.Context) error {
version := res.Header.Get("X-Influxdb-Version")
color.Cyan("Connected to InfluxDB %s %s", build, version)
c.Databases, _ = c.GetDatabases(ctx)
color.Cyan("InfluxQL Shell")
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("%s", color.GreenString("> "))
for scanner.Scan() {
c.executor(scanner.Text())
fmt.Printf("%s", color.GreenString("> "))
// compute historyFilePath at REPL start
// Only load/write history if HOME environment variable is set.
var historyDir string
if runtime.GOOS == "windows" {
if userDir := os.Getenv("USERPROFILE"); userDir != "" {
historyDir = userDir
}
}
if homeDir := os.Getenv("HOME"); homeDir != "" {
historyDir = homeDir
}
var history []string
if historyDir != "" {
c.historyFilePath = filepath.Join(historyDir, ".influx_history")
history = c.readHistory()
// rewriting history now truncates the history file down to c.historyLimit lines of history
c.rewriteHistoryFile(history)
}
p := prompt.New(c.executor,
c.completer,
prompt.OptionTitle("InfluxQL Shell"),
prompt.OptionHistory(history),
prompt.OptionDescriptionTextColor(prompt.Cyan),
prompt.OptionPrefixTextColor(prompt.Green),
prompt.OptionCompletionWordSeparator(" ", "."),
)
c.Databases, _ = c.GetDatabases(ctx)
p.Run()
return nil
}
@ -111,6 +175,7 @@ func (c *Client) executor(cmd string) {
if cmd == "" {
return
}
defer c.writeCommandToHistory(cmd)
cmdArgs := strings.Split(cmd, " ")
switch strings.ToLower(cmdArgs[0]) {
case "quit", "exit":
@ -125,7 +190,7 @@ func (c *Client) executor(cmd string) {
case "help":
c.help()
case "history":
color.Yellow("The 'history' command is not yet implemented in 2.x")
color.HiBlack(strings.Join(c.readHistory(), "\n"))
case "format":
c.setFormat(cmdArgs)
case "precision":
@ -225,7 +290,7 @@ func (c Client) insert(cmd string) {
switch c.Precision {
case "h", "m", "rfc3339":
color.Red("Current precision %q unsupported for writes. Use [s, ms, ns, us]", c.Precision)
color.Red("Current precision %q unsupported for writes. Use precision [s, ms, ns, us]", c.Precision)
return
}
ctx := context.Background()

View File

@ -1,9 +1,9 @@
package v1repl_test
package v1shell_test
import (
"testing"
v1repl "github.com/influxdata/influx-cli/v2/clients/v1_repl"
v1shell "github.com/influxdata/influx-cli/v2/clients/v1_shell"
)
var point = `weather,location=us-midwest temperature=82 1465839830100400200`
@ -20,7 +20,7 @@ type InsertTestCase struct {
}
func (it *InsertTestCase) Test(t *testing.T) {
db, rp, point, isInsert := v1repl.ParseInsert(it.cmd)
db, rp, point, isInsert := v1shell.ParseInsert(it.cmd)
if !isInsert {
t.Errorf("%q should be a valid INSERT command", it.cmd)
} else {
@ -135,7 +135,7 @@ func TestParseInsertInto(t *testing.T) {
func TestParseInsertInvalid(t *testing.T) {
t.Parallel()
for _, cmd := range invalidCmds {
if _, _, _, isValid := v1repl.ParseInsert(cmd); isValid {
if _, _, _, isValid := v1shell.ParseInsert(cmd); isValid {
t.Errorf("%q should be an invalid INSERT command", cmd)
}
}

View File

@ -13,7 +13,7 @@ func newV1SubCommand() cli.Command {
Subcommands: []cli.Command{
newV1DBRPCmd(),
newV1AuthCommand(),
newV1ReplCmd(),
newV1ShellCmd(),
},
}
}

View File

@ -4,7 +4,7 @@ import (
"github.com/fatih/color"
"github.com/influxdata/influx-cli/v2/api"
"github.com/influxdata/influx-cli/v2/clients"
repl "github.com/influxdata/influx-cli/v2/clients/v1_repl"
shell "github.com/influxdata/influx-cli/v2/clients/v1_shell"
"github.com/influxdata/influx-cli/v2/pkg/cli/middleware"
"github.com/urfave/cli"
)
@ -14,13 +14,13 @@ type Client struct {
api.LegacyQueryApi
}
func newV1ReplCmd() cli.Command {
func newV1ShellCmd() cli.Command {
var orgParams clients.OrgParams
persistentQueryParams := repl.DefaultPersistentQueryParams()
persistentQueryParams := shell.DefaultPersistentQueryParams()
return cli.Command{
Name: "repl",
Usage: "Start an InfluxQL REPL",
Description: "Start an InfluxQL REPL",
Name: "shell",
Usage: "Start an InfluxQL shell",
Description: "Start an InfluxQL shell",
Before: middleware.WithBeforeFns(withCli(), withApi(true)),
Flags: append(commonFlagsNoPrint(), getOrgFlags(&orgParams)...),
Action: func(ctx *cli.Context) error {
@ -28,7 +28,7 @@ func newV1ReplCmd() cli.Command {
return err
}
api := getAPI(ctx)
c := repl.Client{
c := shell.Client{
CLI: getCLI(ctx),
PersistentQueryParams: persistentQueryParams,
PingApi: api.PingApi,

3
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d
github.com/golang/mock v1.5.0
github.com/google/go-jsonnet v0.17.0
github.com/influxdata/go-prompt v0.2.7
github.com/mattn/go-isatty v0.0.14
github.com/olekukonko/tablewriter v0.0.5
github.com/stretchr/testify v1.7.0
@ -30,7 +31,9 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-tty v0.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect

11
go.sum
View File

@ -33,6 +33,8 @@ github.com/google/go-jsonnet v0.17.0 h1:/9NIEfhK1NQRKl3sP2536b2+x5HnZMdql7x3yK/l
github.com/google/go-jsonnet v0.17.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/influxdata/go-prompt v0.2.7 h1:LoJCo+imRHw4Md1Y6SWFtLRpuukAHXkQ6pWS+DVTiMM=
github.com/influxdata/go-prompt v0.2.7/go.mod h1:sK0TeJSbAWCKVby14Tx85GJE4BZpIZpguBBmFqAqruE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -45,16 +47,22 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E=
github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@ -92,7 +100,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=