Files
steampipe/pkg/display/display.go

344 lines
9.3 KiB
Go

package display
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strings"
"unicode/utf8"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/karrick/gows"
"github.com/spf13/viper"
"github.com/turbot/steampipe/pkg/cmdconfig"
"github.com/turbot/steampipe/pkg/constants"
"github.com/turbot/steampipe/pkg/query/queryresult"
"github.com/turbot/steampipe/pkg/utils"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
// ShowOutput displays the output using the proper formatter as applicable
func ShowOutput(ctx context.Context, result *queryresult.Result) {
output := cmdconfig.Viper().GetString(constants.ArgOutput)
if output == constants.OutputFormatJSON {
displayJSON(ctx, result)
} else if output == constants.OutputFormatCSV {
displayCSV(ctx, result)
} else if output == constants.OutputFormatLine {
displayLine(ctx, result)
} else {
// default
displayTable(ctx, result)
}
}
func ShowWrappedTable(headers []string, rows [][]string, autoMerge bool) {
t := table.NewWriter()
t.SetStyle(table.StyleDefault)
t.Style().Format.Header = text.FormatDefault
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(ctx context.Context, result *queryresult.Result) {
colNames := ColumnNames(result.ColTypes)
maxColNameLength := 0
for _, colName := range colNames {
thisLength := utf8.RuneCountInString(colName)
if thisLength > maxColNameLength {
maxColNameLength = thisLength
}
}
itemIdx := 0
// define a function to display each row
rowFunc := func(row []interface{}, result *queryresult.Result) {
recordAsString, _ := ColumnValuesAsString(row, result.ColTypes)
requiredTerminalColumnsForValuesOfRecord := 0
for _, colValue := range recordAsString {
colRequired := getTerminalColumnsRequiredForString(colValue)
if requiredTerminalColumnsForValuesOfRecord < colRequired {
requiredTerminalColumnsForValuesOfRecord = colRequired
}
}
lineFormat := fmt.Sprintf("%%-%ds | %%s\n", maxColNameLength)
multiLineFormat := fmt.Sprintf("%%-%ds | %%-%ds", maxColNameLength, requiredTerminalColumnsForValuesOfRecord)
fmt.Printf("-[ RECORD %-2d ]%s\n", (itemIdx + 1), strings.Repeat("-", 75))
for idx, column := range recordAsString {
lines := strings.Split(column, "\n")
if len(lines) == 1 {
fmt.Printf(lineFormat, colNames[idx], lines[0])
} else {
for lineIdx, line := range lines {
if lineIdx == 0 {
// the first line
fmt.Printf(multiLineFormat, colNames[idx], line)
} else {
// next lines
fmt.Printf(multiLineFormat, "", line)
}
// is this not the last line of value?
if lineIdx < len(lines)-1 {
fmt.Printf(" +\n")
} else {
fmt.Printf("\n")
}
}
}
}
itemIdx++
}
// call this function for each row
if err := iterateResults(result, rowFunc); err != nil {
utils.ShowError(ctx, err)
return
}
}
func getTerminalColumnsRequiredForString(str string) int {
colsRequired := 0
for _, line := range strings.Split(str, "\n") {
if colsRequired < utf8.RuneCountInString(line) {
colsRequired = utf8.RuneCountInString(line)
}
}
return colsRequired
}
func displayJSON(ctx context.Context, result *queryresult.Result) {
var jsonOutput []map[string]interface{}
// define function to add each row to the JSON output
rowFunc := func(row []interface{}, result *queryresult.Result) {
record := map[string]interface{}{}
for idx, colType := range result.ColTypes {
value, _ := ParseJSONOutputColumnValue(row[idx], colType)
record[colType.Name()] = value
}
jsonOutput = append(jsonOutput, record)
}
// call this function for each row
if err := iterateResults(result, rowFunc); err != nil {
utils.ShowError(ctx, err)
return
}
// display the JSON
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
encoder.SetEscapeHTML(false)
if err := encoder.Encode(jsonOutput); err != nil {
fmt.Print("Error displaying result as JSON", err)
return
}
}
func displayCSV(ctx context.Context, result *queryresult.Result) {
csvWriter := csv.NewWriter(os.Stdout)
csvWriter.Comma = []rune(cmdconfig.Viper().GetString(constants.ArgSeparator))[0]
if cmdconfig.Viper().GetBool(constants.ArgHeader) {
_ = csvWriter.Write(ColumnNames(result.ColTypes))
}
// print the data as it comes
// define function display each csv row
rowFunc := func(row []interface{}, result *queryresult.Result) {
rowAsString, _ := ColumnValuesAsString(row, result.ColTypes)
_ = csvWriter.Write(rowAsString)
}
// call this function for each row
if err := iterateResults(result, rowFunc); err != nil {
utils.ShowError(ctx, err)
return
}
csvWriter.Flush()
if csvWriter.Error() != nil {
utils.ShowErrorWithMessage(ctx, csvWriter.Error(), "unable to print csv")
}
}
func displayTable(ctx context.Context, result *queryresult.Result) {
// the buffer to put the output data in
outbuf := bytes.NewBufferString("")
// the table
t := table.NewWriter()
t.SetOutputMirror(outbuf)
t.SetStyle(table.StyleDefault)
t.Style().Format.Header = text.FormatDefault
colConfigs := []table.ColumnConfig{}
headers := make(table.Row, len(result.ColTypes))
for idx, column := range result.ColTypes {
headers[idx] = column.Name()
colConfigs = append(colConfigs, table.ColumnConfig{
Name: column.Name(),
Number: idx + 1,
WidthMax: constants.MaxColumnWidth,
})
}
t.SetColumnConfigs(colConfigs)
if viper.GetBool(constants.ArgHeader) {
t.AppendHeader(headers)
}
// define a function to execute for each row
rowFunc := func(row []interface{}, result *queryresult.Result) {
rowAsString, _ := ColumnValuesAsString(row, result.ColTypes)
rowObj := table.Row{}
for _, col := range rowAsString {
rowObj = append(rowObj, col)
}
t.AppendRow(rowObj)
}
// iterate each row, adding each to the table
err := iterateResults(result, rowFunc)
if err != nil {
// display the error
fmt.Println()
utils.ShowError(ctx, err)
fmt.Println()
}
// write out the table to the buffer
t.Render()
// page out the table
ShowPaged(ctx, outbuf.String())
// if timer is turned on
if cmdconfig.Viper().GetBool(constants.ArgTiming) {
displayTiming(result)
}
}
func displayTiming(result *queryresult.Result) {
timingResult := <-result.TimingResult
var sb strings.Builder
// large numbers should be formatted with commas
p := message.NewPrinter(language.English)
milliseconds := float64(timingResult.Duration.Microseconds()) / 1000
seconds := timingResult.Duration.Seconds()
if seconds < 0.5 {
sb.WriteString(p.Sprintf("\nTime: %dms.", int64(milliseconds)))
} else {
sb.WriteString(p.Sprintf("\nTime: %.1fs.", seconds))
}
if timingMetadata := timingResult.Metadata; timingMetadata != nil {
totalRows := timingMetadata.RowsFetched + timingMetadata.CachedRowsFetched
sb.WriteString(" Rows fetched: ")
if totalRows == 0 {
sb.WriteString("0")
} else {
if totalRows > 0 {
sb.WriteString(p.Sprintf("%d", timingMetadata.RowsFetched+timingMetadata.CachedRowsFetched))
}
if timingMetadata.CachedRowsFetched > 0 {
if timingMetadata.RowsFetched == 0 {
sb.WriteString(" (cached)")
} else {
sb.WriteString(p.Sprintf(" (%d cached)", timingMetadata.CachedRowsFetched))
}
}
}
sb.WriteString(p.Sprintf(". Hydrate calls: %d.", timingMetadata.HydrateCalls))
}
fmt.Println(sb.String())
}
type displayResultsFunc func(row []interface{}, result *queryresult.Result)
// call func displayResult for each row of results
func iterateResults(result *queryresult.Result, displayResult displayResultsFunc) error {
for row := range *result.RowChan {
if row == nil {
return nil
}
if row.Error != nil {
return row.Error
}
displayResult(row.Data, result)
}
// we will not get here
return nil
}