From 7b4e7fb0f1fcbb485b0153cbd9da7172eda263f2 Mon Sep 17 00:00:00 2001 From: Binaek Sarkar Date: Wed, 3 Mar 2021 17:59:10 +0530 Subject: [PATCH] Wrap `plugin list` output to window width. Closes #235 (#244) --- cmd/plugin.go | 13 +-- db/client_execute.go | 11 ++- db/interactive_client.go | 11 ++- db/query.go | 9 +- .../results/queryresult.go | 10 +- .../results/resultstreamer.go | 10 +- display/display.go | 92 ++++++++++++++++--- metaquery/handlers.go | 78 +--------------- 8 files changed, 121 insertions(+), 113 deletions(-) rename db/query_results.go => definitions/results/queryresult.go (75%) rename db/results_streamer.go => definitions/results/resultstreamer.go (74%) diff --git a/cmd/plugin.go b/cmd/plugin.go index 927b4ca9f..42cf06dc6 100644 --- a/cmd/plugin.go +++ b/cmd/plugin.go @@ -4,13 +4,12 @@ import ( "errors" "fmt" "log" - "os" "strings" "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/display" "github.com/turbot/steampipe/statefile" - "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/turbot/steampipe-plugin-sdk/logging" "github.com/turbot/steampipe/cmdconfig" @@ -418,14 +417,12 @@ func runPluginListCmd(cmd *cobra.Command, args []string) { utils.ShowErrorWithMessage(err, fmt.Sprintf("Plugin Listing failed")) } - t := table.NewWriter() - t.SetStyle(table.StyleLight) - t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"Name", "Version", "Connections"}) + headers := []string{"Name", "Version", "Connections"} + rows := [][]string{} for _, item := range list { - t.AppendRow(table.Row{item.Name, item.Version, strings.Join(item.Connections, ",")}) + rows = append(rows, []string{item.Name, item.Version, strings.Join(item.Connections, ",")}) } - t.Render() + display.ShowWrappedTable(headers, rows, false) } func runPluginUninstallCmd(cmd *cobra.Command, args []string) { diff --git a/db/client_execute.go b/db/client_execute.go index 5c80a84b5..a4280fff1 100644 --- a/db/client_execute.go +++ b/db/client_execute.go @@ -10,17 +10,18 @@ import ( "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/definitions/results" "github.com/turbot/steampipe/utils" ) // ExecuteSync :: execute a query against this client and wait for the result -func (c *Client) ExecuteSync(query string) (*SyncQueryResult, error) { +func (c *Client) ExecuteSync(query string) (*results.SyncQueryResult, error) { // https://github.com/golang/go/wiki/CodeReviewComments#indent-error-flow result, err := c.executeQuery(query, false) if err != nil { return nil, err } - syncResult := &SyncQueryResult{ColTypes: result.ColTypes} + syncResult := &results.SyncQueryResult{ColTypes: result.ColTypes} for row := range *result.RowChan { syncResult.Rows = append(syncResult.Rows, row) } @@ -28,9 +29,9 @@ func (c *Client) ExecuteSync(query string) (*SyncQueryResult, error) { return syncResult, nil } -func (c *Client) executeQuery(query string, countStream bool) (*QueryResult, error) { +func (c *Client) executeQuery(query string, countStream bool) (*results.QueryResult, error) { if query == "" { - return &QueryResult{}, nil + return &results.QueryResult{}, nil } start := time.Now() @@ -65,7 +66,7 @@ func (c *Client) executeQuery(query string, countStream bool) (*QueryResult, err } cols, err := rows.Columns() - result := newQueryResult(colTypes) + result := results.NewQueryResult(colTypes) rowCount := 0 diff --git a/db/interactive_client.go b/db/interactive_client.go index 9a08381ed..b6edde6db 100644 --- a/db/interactive_client.go +++ b/db/interactive_client.go @@ -6,6 +6,7 @@ import ( "github.com/turbot/steampipe/autocomplete" "github.com/turbot/steampipe/cmdconfig" + "github.com/turbot/steampipe/definitions/results" "github.com/turbot/steampipe/constants" "github.com/turbot/steampipe/metaquery" @@ -40,7 +41,7 @@ func (c *InteractiveClient) close() { } // InteractiveQuery :: start an interactive prompt and return -func (c *InteractiveClient) InteractiveQuery(resultsStreamer *ResultStreamer, onCompleteCallback func()) { +func (c *InteractiveClient) InteractiveQuery(resultsStreamer *results.ResultStreamer, onCompleteCallback func()) { defer func() { onCompleteCallback() @@ -60,7 +61,7 @@ func (c *InteractiveClient) InteractiveQuery(resultsStreamer *ResultStreamer, on // this needs to be the last thing we do, // as the runQueryCmd uses this as an indication // to quit out of the application - resultsStreamer.close() + resultsStreamer.Close() }() fmt.Printf("Welcome to Steampipe v%s\n", version.String()) @@ -81,7 +82,7 @@ func (c *InteractiveClient) InteractiveQuery(resultsStreamer *ResultStreamer, on } } -func (c *InteractiveClient) runInteractivePrompt(resultsStreamer *ResultStreamer) (ret utils.InteractiveExitStatus) { +func (c *InteractiveClient) runInteractivePrompt(resultsStreamer *results.ResultStreamer) (ret utils.InteractiveExitStatus) { defer func() { // this is to catch the PANIC that gets raised by // the executor of go-prompt @@ -171,7 +172,7 @@ func (c *InteractiveClient) breakMultilinePrompt(buffer *prompt.Buffer) { c.interactiveBuffer = []string{} } -func (c *InteractiveClient) executor(line string, resultsStreamer *ResultStreamer) { +func (c *InteractiveClient) executor(line string, resultsStreamer *results.ResultStreamer) { line = strings.TrimSpace(line) // if it's an empty line, then we don't need to do anything @@ -210,7 +211,7 @@ func (c *InteractiveClient) executor(line string, resultsStreamer *ResultStreame utils.ShowError(err) resultsStreamer.Done() } else { - resultsStreamer.streamResult(result) + resultsStreamer.StreamResult(result) } } diff --git a/db/query.go b/db/query.go index 7c0c131c4..a524a2be6 100644 --- a/db/query.go +++ b/db/query.go @@ -8,11 +8,12 @@ import ( "github.com/turbot/steampipe-plugin-sdk/logging" "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/definitions/results" "github.com/turbot/steampipe/utils" ) // ExecuteQuery :: entry point for executing ad-hoc queries from outside the package -func ExecuteQuery(queryString string) (*ResultStreamer, error) { +func ExecuteQuery(queryString string) (*results.ResultStreamer, error) { var err error logging.LogTime("db.ExecuteQuery start") @@ -48,7 +49,7 @@ func ExecuteQuery(queryString string) (*ResultStreamer, error) { return nil, fmt.Errorf("failed to add functions: %v", err) } - resultsStreamer := newQueryResults() + resultsStreamer := results.NewResultStreamer() // this is a callback to close the db et-al. when things get done - no matter the mode onComplete := func() { Shutdown(client, InvokerQuery) } @@ -69,13 +70,15 @@ func ExecuteQuery(queryString string) (*ResultStreamer, error) { onComplete() return nil, err } - go resultsStreamer.streamSingleResult(result, onComplete) + go resultsStreamer.StreamSingleResult(result, onComplete) } logging.LogTime("db.ExecuteQuery end") return resultsStreamer, nil } +// Shutdown :: closes the client connection and stops the +// database instance if the given `invoker` matches func Shutdown(client *Client, invoker Invoker) { log.Println("[TRACE] shutdown") if client != nil { diff --git a/db/query_results.go b/definitions/results/queryresult.go similarity index 75% rename from db/query_results.go rename to definitions/results/queryresult.go index 00d22cc00..bf3254830 100644 --- a/db/query_results.go +++ b/definitions/results/queryresult.go @@ -1,4 +1,10 @@ -package db +/** + This package is for all interfaces that are imported in multiple packages in the + code base + + This package MUST never import any other `steampipe` package +**/ +package results import ( "database/sql" @@ -29,7 +35,7 @@ func (r QueryResult) StreamError(err error) { *r.RowChan <- &RowResult{Error: err} } -func newQueryResult(colTypes []*sql.ColumnType) *QueryResult { +func NewQueryResult(colTypes []*sql.ColumnType) *QueryResult { rowChan := make(chan *RowResult) return &QueryResult{ RowChan: &rowChan, diff --git a/db/results_streamer.go b/definitions/results/resultstreamer.go similarity index 74% rename from db/results_streamer.go rename to definitions/results/resultstreamer.go index 288222eed..04c238c49 100644 --- a/db/results_streamer.go +++ b/definitions/results/resultstreamer.go @@ -1,11 +1,11 @@ -package db +package results type ResultStreamer struct { Results chan *QueryResult displayReady chan string } -func newQueryResults() *ResultStreamer { +func NewResultStreamer() *ResultStreamer { return &ResultStreamer{ // make buffered channel so we can always stream a single result Results: make(chan *QueryResult, 1), @@ -13,18 +13,18 @@ func newQueryResults() *ResultStreamer { } } -func (q *ResultStreamer) streamResult(result *QueryResult) { +func (q *ResultStreamer) StreamResult(result *QueryResult) { q.Results <- result } -func (q *ResultStreamer) streamSingleResult(result *QueryResult, onComplete func()) { +func (q *ResultStreamer) StreamSingleResult(result *QueryResult, onComplete func()) { q.Results <- result q.Wait() onComplete() close(q.Results) } -func (q *ResultStreamer) close() { +func (q *ResultStreamer) Close() { close(q.Results) } diff --git a/display/display.go b/display/display.go index 192fc8793..3ab73481b 100644 --- a/display/display.go +++ b/display/display.go @@ -11,15 +11,16 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" + "github.com/karrick/gows" "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/constants" - "github.com/turbot/steampipe/db" + "github.com/turbot/steampipe/definitions/results" "github.com/turbot/steampipe/utils" ) // ShowOutput :: displays the output using the proper formatter as applicable -func ShowOutput(result *db.QueryResult) { +func ShowOutput(result *results.QueryResult) { if cmdconfig.Viper().Get(constants.ArgOutput) == constants.ArgJSON { displayJSON(result) } else if cmdconfig.Viper().Get(constants.ArgOutput) == constants.ArgCSV { @@ -32,7 +33,74 @@ func ShowOutput(result *db.QueryResult) { } } -func displayLine(result *db.QueryResult) { +func ShowWrappedTable(headers []string, rows [][]string, autoMerge bool) { + t := table.NewWriter() + t.SetStyle(table.StyleDefault) + t.SetOutputMirror(os.Stdout) + + rowConfig := table.RowConfig{AutoMerge: autoMerge} + colConfigs, headerRow := getColumnSettings(headers, rows) + + t.SetColumnConfigs(colConfigs) + t.AppendHeader(headerRow) + + for _, row := range rows { + rowObj := table.Row{} + for _, col := range row { + rowObj = append(rowObj, col) + } + t.AppendRow(rowObj, rowConfig) + } + t.Render() +} + +// calculate and returns column configuration based on header and row content +func getColumnSettings(headers []string, rows [][]string) ([]table.ColumnConfig, table.Row) { + maxCols, _, _ := gows.GetWinSize() + colConfigs := make([]table.ColumnConfig, len(headers)) + headerRow := make(table.Row, len(headers)) + + sumOfAllCols := 0 + + // account for the spaces around the value of a column and separators + spaceAccounting := ((len(headers) * 3) + 1) + + for idx, colName := range headers { + headerRow[idx] = colName + + // get the maximum len of strings in this column + maxLen := 0 + for _, row := range rows { + colVal := row[idx] + if len(colVal) > maxLen { + maxLen = len(colVal) + } + if len(colName) > maxLen { + maxLen = len(colName) + } + } + colConfigs[idx] = table.ColumnConfig{ + Name: colName, + Number: idx + 1, + WidthMax: maxLen, + WidthMin: maxLen, + } + sumOfAllCols += maxLen + } + + // now that all columns are set to the widths that they need, + // set the last one to occupy as much as is available - no more - no less + sumOfRest := sumOfAllCols - colConfigs[len(colConfigs)-1].WidthMax + + if sumOfAllCols > maxCols { + colConfigs[len(colConfigs)-1].WidthMax = (maxCols - sumOfRest - spaceAccounting) + colConfigs[len(colConfigs)-1].WidthMin = (maxCols - sumOfRest - spaceAccounting) + } + + return colConfigs, headerRow +} + +func displayLine(result *results.QueryResult) { colNames := ColumnNames(result.ColTypes) maxColNameLength := 0 for _, colName := range colNames { @@ -44,7 +112,7 @@ func displayLine(result *db.QueryResult) { itemIdx := 0 // define a function to display each row - rowFunc := func(row []interface{}, result *db.QueryResult) { + rowFunc := func(row []interface{}, result *results.QueryResult) { recordAsString, _ := ColumnValuesAsString(row, result.ColTypes) requiredTerminalColumnsForValuesOfRecord := 0 for _, colValue := range recordAsString { @@ -98,11 +166,11 @@ func getTerminalColumnsRequiredForString(str string) int { return colsRequired } -func displayJSON(result *db.QueryResult) { +func displayJSON(result *results.QueryResult) { var jsonOutput []map[string]interface{} // define function to add each row to the JSON output - rowFunc := func(row []interface{}, result *db.QueryResult) { + rowFunc := func(row []interface{}, result *results.QueryResult) { record := map[string]interface{}{} for idx, colType := range result.ColTypes { value, _ := ParseJSONOutputColumnValue(row[idx], colType) @@ -125,7 +193,7 @@ func displayJSON(result *db.QueryResult) { fmt.Printf("%s\n", string(data)) } -func displayCSV(result *db.QueryResult) { +func displayCSV(result *results.QueryResult) { csvWriter := csv.NewWriter(os.Stdout) csvWriter.Comma = []rune(cmdconfig.Viper().GetString(constants.ArgSeparator))[0] @@ -135,7 +203,7 @@ func displayCSV(result *db.QueryResult) { // print the data as it comes // define function display each csv row - rowFunc := func(row []interface{}, result *db.QueryResult) { + rowFunc := func(row []interface{}, result *results.QueryResult) { rowAsString, _ := ColumnValuesAsString(row, result.ColTypes) _ = csvWriter.Write(rowAsString) } @@ -152,7 +220,7 @@ func displayCSV(result *db.QueryResult) { } } -func displayTable(result *db.QueryResult) { +func displayTable(result *results.QueryResult) { // the buffer to put the output data in outbuf := bytes.NewBufferString("") @@ -178,7 +246,7 @@ func displayTable(result *db.QueryResult) { t.AppendHeader(headers) // define a function to execute for each row - rowFunc := func(row []interface{}, result *db.QueryResult) { + rowFunc := func(row []interface{}, result *results.QueryResult) { rowAsString, _ := ColumnValuesAsString(row, result.ColTypes) rowObj := table.Row{} for _, col := range rowAsString { @@ -205,10 +273,10 @@ func displayTable(result *db.QueryResult) { ShowPaged(outbuf.String()) } -type displayResultsFunc func(row []interface{}, result *db.QueryResult) +type displayResultsFunc func(row []interface{}, result *results.QueryResult) // call func displayResult for each row of results -func iterateResults(result *db.QueryResult, displayResult displayResultsFunc) error { +func iterateResults(result *results.QueryResult, displayResult displayResultsFunc) error { for row := range *result.RowChan { if row == nil { return nil diff --git a/metaquery/handlers.go b/metaquery/handlers.go index 32389a815..d0293f0b8 100644 --- a/metaquery/handlers.go +++ b/metaquery/handlers.go @@ -2,19 +2,18 @@ package metaquery import ( "fmt" - "os" "regexp" "sort" "strings" "github.com/c-bata/go-prompt" "github.com/jedib0t/go-pretty/v6/table" - "github.com/karrick/gows" "github.com/turbot/go-kit/helpers" typeHelpers "github.com/turbot/go-kit/types" "github.com/turbot/steampipe/cmdconfig" "github.com/turbot/steampipe/connection_config" "github.com/turbot/steampipe/constants" + "github.com/turbot/steampipe/display" "github.com/turbot/steampipe/schema" "github.com/turbot/steampipe/utils" ) @@ -174,7 +173,7 @@ To get information about the columns in a table, run '.inspect {connection}.{tab }) rows = append(rows, tables...) } - writeTable(header, rows, true) + display.ShowWrappedTable(header, rows, true) } return nil @@ -233,7 +232,7 @@ func listConnections(input *HandlerInput) error { return rows[i][0] < rows[j][0] }) - writeTable(header, rows, false) + display.ShowWrappedTable(header, rows, false) fmt.Printf(` To get information about the tables in a connection, run '.inspect {connection}' @@ -263,7 +262,7 @@ func inspectConnection(connectionName string, input *HandlerInput) bool { return rows[i][0] < rows[j][0] }) - writeTable(header, rows, false) + display.ShowWrappedTable(header, rows, false) return true } @@ -295,78 +294,11 @@ func inspectTable(connectionName string, tableName string, input *HandlerInput) return rows[i][0] < rows[j][0] }) - writeTable(header, rows, false) + display.ShowWrappedTable(header, rows, false) return nil } -func writeTable(headers []string, rows [][]string, autoMerge bool) { - t := table.NewWriter() - t.SetStyle(table.StyleDefault) - t.SetOutputMirror(os.Stdout) - - rowConfig := table.RowConfig{AutoMerge: autoMerge} - colConfigs, headerRow := getColumnSettings(headers, rows) - - t.SetColumnConfigs(colConfigs) - t.AppendHeader(headerRow) - - for _, row := range rows { - rowObj := table.Row{} - for _, col := range row { - rowObj = append(rowObj, col) - } - t.AppendRow(rowObj, rowConfig) - } - t.Render() -} - -// calculate and returns column configuration based on header and row content -func getColumnSettings(headers []string, rows [][]string) ([]table.ColumnConfig, table.Row) { - maxCols, _, _ := gows.GetWinSize() - colConfigs := make([]table.ColumnConfig, len(headers)) - headerRow := make(table.Row, len(headers)) - - sumOfAllCols := 0 - - // account for the spaces around the value of a column and separators - spaceAccounting := ((len(headers) * 3) + 1) - - for idx, colName := range headers { - headerRow[idx] = colName - - // get the maximum len of strings in this column - maxLen := 0 - for _, row := range rows { - colVal := row[idx] - if len(colVal) > maxLen { - maxLen = len(colVal) - } - if len(colName) > maxLen { - maxLen = len(colName) - } - } - colConfigs[idx] = table.ColumnConfig{ - Name: colName, - Number: idx + 1, - WidthMax: maxLen, - WidthMin: maxLen, - } - sumOfAllCols += maxLen - } - - // now that all columns are set to the widths that they need, - // set the last one to occupy as much as is available - no more - no less - sumOfRest := sumOfAllCols - colConfigs[len(colConfigs)-1].WidthMax - - if sumOfAllCols > maxCols { - colConfigs[len(colConfigs)-1].WidthMax = (maxCols - sumOfRest - spaceAccounting) - colConfigs[len(colConfigs)-1].WidthMin = (maxCols - sumOfRest - spaceAccounting) - } - - return colConfigs, headerRow -} - func buildTable(rows [][]string, autoMerge bool) string { t := table.NewWriter() t.SetStyle(table.StyleDefault)