diff --git a/autocomplete/table.go b/autocomplete/table.go index b759afb3ca..9f1858c4aa 100644 --- a/autocomplete/table.go +++ b/autocomplete/table.go @@ -7,8 +7,10 @@ import ( "github.com/c-bata/go-prompt" "github.com/turbot/go-kit/helpers" + "github.com/turbot/steampipe/db/db_common" "github.com/turbot/steampipe/schema" "github.com/turbot/steampipe/steampipeconfig" + "github.com/turbot/steampipe/utils" ) // GetTableAutoCompleteSuggestions :: derives and returns tables for typeahead @@ -47,7 +49,7 @@ func GetTableAutoCompleteSuggestions(schema *schema.Metadata, connectionMap *ste // add qualified names of all tables for tableName := range schemaDetails { if !isTemporarySchema { - qualifiedTablesToAdd = append(qualifiedTablesToAdd, fmt.Sprintf("%s.%s", schemaName, tableName)) + qualifiedTablesToAdd = append(qualifiedTablesToAdd, fmt.Sprintf("%s.%s", schemaName, escapeIfRequired(tableName))) } } @@ -71,15 +73,17 @@ func GetTableAutoCompleteSuggestions(schema *schema.Metadata, connectionMap *ste sort.Strings(qualifiedTablesToAdd) for _, schema := range schemasToAdd { - s = append(s, prompt.Suggest{Text: schema, Description: "Schema"}) + // we don't need to escape schema names, since schema names are derived from connection names + // which are validated so that we don't end up with names which need it + s = append(s, prompt.Suggest{Text: schema, Description: "Schema", Output: schema}) } for _, table := range unqualifiedTablesToAdd { - s = append(s, prompt.Suggest{Text: table, Description: "Table"}) + s = append(s, prompt.Suggest{Text: table, Description: "Table", Output: escapeIfRequired(table)}) } for _, table := range qualifiedTablesToAdd { - s = append(s, prompt.Suggest{Text: table, Description: "Table"}) + s = append(s, prompt.Suggest{Text: table, Description: "Table", Output: escapeIfRequired(table)}) } return s @@ -88,3 +92,15 @@ func GetTableAutoCompleteSuggestions(schema *schema.Metadata, connectionMap *ste func stripVersionFromPluginName(pluginName string) string { return strings.Split(pluginName, "@")[0] } + +func escapeIfRequired(strToEscape string) string { + tokens := utils.SplitByRune(strToEscape, '.') + escaped := []string{} + for _, token := range tokens { + if strings.ContainsAny(token, " -") { + token = db_common.PgEscapeName(token) + } + escaped = append(escaped, token) + } + return strings.Join(escaped, ".") +} diff --git a/go.mod b/go.mod index defbde3425..812740055d 100644 --- a/go.mod +++ b/go.mod @@ -73,4 +73,4 @@ require ( sigs.k8s.io/yaml v1.1.0 ) -replace github.com/c-bata/go-prompt => github.com/turbot/go-prompt v0.2.6-steampipe.0.20210830083819-c872df2bdcc9 +replace github.com/c-bata/go-prompt => github.com/turbot/go-prompt v0.2.6-steampipe.0.20211124090719-0709bc8d8ce2 diff --git a/go.sum b/go.sum index cd7bbcc057..dc46cec3e9 100644 --- a/go.sum +++ b/go.sum @@ -909,8 +909,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/tombuildsstuff/giovanni v0.15.1/go.mod h1:0TZugJPEtqzPlMpuJHYfXY6Dq2uLPrXf98D2XQSxNbA= github.com/turbot/go-kit v0.3.0 h1:o4zZIO1ovdmJ2bHWOdXnnt8jJMIDGqYSkZvBREzFeMQ= github.com/turbot/go-kit v0.3.0/go.mod h1:SBdPRngbEfYubiR81iAVtO43oPkg1+ASr+XxvgbH7/k= -github.com/turbot/go-prompt v0.2.6-steampipe.0.20210830083819-c872df2bdcc9 h1:mcDQVuT3E9tQtCB7sdLgEm3uC/eGGtBr27WwQCU6j+s= -github.com/turbot/go-prompt v0.2.6-steampipe.0.20210830083819-c872df2bdcc9/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= +github.com/turbot/go-prompt v0.2.6-steampipe.0.20211124090719-0709bc8d8ce2 h1:mydbzShy5MB1XMDu2S4DocAo8/Jl8vRmEa37FvpY2jY= +github.com/turbot/go-prompt v0.2.6-steampipe.0.20211124090719-0709bc8d8ce2/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/turbot/steampipe-plugin-sdk v1.8.0 h1:bPHlCnAg66UTMz7W7AwR8jCszOwwe6Bfmvpiko7VB2k= github.com/turbot/steampipe-plugin-sdk v1.8.0/go.mod h1:76H3wr6KB6t+kDS38EEOZAsw61Ie/q7/IV9X0kv5NjI= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= diff --git a/interactive/interactive_client.go b/interactive/interactive_client.go index 2b83e78911..99c29f8904 100644 --- a/interactive/interactive_client.go +++ b/interactive/interactive_client.go @@ -482,7 +482,7 @@ func (c *InteractiveClient) queryCompleter(d prompt.Document) []prompt.Suggest { //named queries s = append(s, c.namedQuerySuggestions()...) // "select" - s = append(s, prompt.Suggest{Text: "select"}) + s = append(s, prompt.Suggest{Text: "select", Output: "select"}, prompt.Suggest{Text: "with", Output: "with"}) // metaqueries s = append(s, metaquery.PromptSuggestions()...) @@ -528,7 +528,7 @@ func (c *InteractiveClient) namedQuerySuggestions() []prompt.Suggest { if q.Description != nil { description += fmt.Sprintf(": %s", *q.Description) } - res = append(res, prompt.Suggest{Text: queryName, Description: description}) + res = append(res, prompt.Suggest{Text: queryName, Output: queryName, Description: description}) } // add all the controls in the workspace for controlName, c := range c.workspace().GetControlMap() { diff --git a/query/metaquery/completers.go b/query/metaquery/completers.go index a9a64d3ff5..e409f7ab0a 100644 --- a/query/metaquery/completers.go +++ b/query/metaquery/completers.go @@ -38,7 +38,7 @@ func completerFromArgsOf(cmd string) completer { metaQueryDefinition, _ := metaQueryDefinitions[cmd] suggestions := make([]prompt.Suggest, len(metaQueryDefinition.args)) for idx, arg := range metaQueryDefinition.args { - suggestions[idx] = prompt.Suggest{Text: arg.value, Description: arg.description} + suggestions[idx] = prompt.Suggest{Text: arg.value, Description: arg.description, Output: arg.value} } return suggestions } diff --git a/query/metaquery/definitions.go b/query/metaquery/definitions.go index a7dcc7b5a2..9ab2c50baf 100644 --- a/query/metaquery/definitions.go +++ b/query/metaquery/definitions.go @@ -111,9 +111,12 @@ func init() { completer: completerFromArgsOf(constants.CmdCache), }, constants.CmdInspect: { - title: constants.CmdInspect, - handler: inspect, - validator: atMostNArgs(1), + title: constants.CmdInspect, + handler: inspect, + // .inspect only supports a single arg, however the arg validation code cannot understand escaped arguments + // e.g. it will treat csv."my table" as 2 args + // the logic to handle this escaping is lower down so we just validate to ensure at least one argument has been provided + validator: atLeastNArgs(0), description: "View connections, tables & column information", completer: inspectCompleter, }, diff --git a/query/metaquery/handlers.go b/query/metaquery/handlers.go index a0da4ef777..a3479ac43a 100644 --- a/query/metaquery/handlers.go +++ b/query/metaquery/handlers.go @@ -260,9 +260,23 @@ func inspect(input *HandlerInput) error { if len(input.args()) == 0 { return listConnections(input) } - // arg can be one of or . tableOrConnection := input.args()[0] + if len(input.args()) > 0 { + // this should be one argument, but may have been split by the tokenizer + // because of the escape characters that autocomplete puts in + // join them up + tableOrConnection = strings.Join(input.args(), " ") + } + // arg can be one of or . split := strings.Split(tableOrConnection, ".") + for i, s := range split { + // trim escaping + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, `"`) + s = strings.TrimSuffix(s, `"`) + + split[i] = s + } if len(split) == 1 { // only a connection name (or maybe unqualified table name) diff --git a/query/metaquery/utils.go b/query/metaquery/utils.go index b1ac19a5f8..08de9cab56 100644 --- a/query/metaquery/utils.go +++ b/query/metaquery/utils.go @@ -1,11 +1,11 @@ package metaquery import ( - "encoding/csv" "sort" "strings" "github.com/c-bata/go-prompt" + "github.com/turbot/steampipe/utils" ) // IsMetaQuery :: returns true if the query is a metaquery, false otherwise @@ -23,7 +23,7 @@ func IsMetaQuery(query string) bool { func getCmdAndArgs(query string) (string, []string) { query = strings.TrimSuffix(query, ";") - split := splitByWhitespace(query) + split := utils.SplitByWhitespace(query) cmd := split[0] args := []string{} if len(split) > 1 { @@ -32,24 +32,11 @@ func getCmdAndArgs(query string) (string, []string) { return cmd, args } -// splitByWhitespace uses the CSV decoder, using '\s' as the separator rune -// this enables us to parse out the tokens - even if they are quoted and/or escaped -func splitByWhitespace(str string) (s []string) { - csvDecoder := csv.NewReader(strings.NewReader(str)) - csvDecoder.Comma = ' ' - csvDecoder.LazyQuotes = true - csvDecoder.TrimLeadingSpace = true - // Read can never error, because we are passing in a StringReader - // lookup csv.Reader.Read - split, _ := csvDecoder.Read() - return split -} - // PromptSuggestions :: Returns a list of the suggestions for go-prompt func PromptSuggestions() []prompt.Suggest { suggestions := make([]prompt.Suggest, 0, len(metaQueryDefinitions)) for k, definition := range metaQueryDefinitions { - suggestions = append(suggestions, prompt.Suggest{Text: k, Description: definition.description}) + suggestions = append(suggestions, prompt.Suggest{Text: k, Description: definition.description, Output: k}) } sort.SliceStable(suggestions[:], func(i, j int) bool { diff --git a/query/metaquery/validators.go b/query/metaquery/validators.go index 60d3318a57..a686cc218b 100644 --- a/query/metaquery/validators.go +++ b/query/metaquery/validators.go @@ -91,6 +91,18 @@ func validatorFromArgsOf(cmd string) validator { } } +var atLeastNArgs = func(n int) validator { + return func(args []string) ValidationResult { + numArgs := len(args) + if numArgs < n { + return ValidationResult{ + Err: fmt.Errorf("command needs at least %d argument(s) - got %d", n, numArgs), + } + } + return ValidationResult{ShouldRun: true} + } +} + var atMostNArgs = func(n int) validator { return func(args []string) ValidationResult { numArgs := len(args) diff --git a/utils/string.go b/utils/string.go new file mode 100644 index 0000000000..a01d5fe953 --- /dev/null +++ b/utils/string.go @@ -0,0 +1,21 @@ +package utils + +import ( + "encoding/csv" + "strings" +) + +// SplitByRune uses the CSV decoder to parse out the tokens - even if they are quoted and/or escaped +func SplitByRune(str string, r rune) []string { + csvDecoder := csv.NewReader(strings.NewReader(str)) + csvDecoder.Comma = r + csvDecoder.LazyQuotes = true + csvDecoder.TrimLeadingSpace = true + split, _ := csvDecoder.Read() + return split +} + +// SplitByWhitespace splits by the ' ' rune +func SplitByWhitespace(str string) []string { + return SplitByRune(str, ' ') +}