mirror of
				https://gitea.com/gitea/tea.git
				synced 2025-10-31 01:05:26 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			214 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2020 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package print
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"regexp"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/olekukonko/tablewriter"
 | |
| )
 | |
| 
 | |
| // table provides infrastructure to easily print (sorted) lists in different formats
 | |
| type table struct {
 | |
| 	headers    []string
 | |
| 	values     [][]string
 | |
| 	sortDesc   bool // used internally by sortable interface
 | |
| 	sortColumn uint // ↑
 | |
| }
 | |
| 
 | |
| // printable can be implemented for structs to put fields dynamically into a table
 | |
| type printable interface {
 | |
| 	FormatField(field string, machineReadable bool) string
 | |
| }
 | |
| 
 | |
| // high level api to print a table of items with dynamic fields
 | |
| func tableFromItems(fields []string, values []printable, machineReadable bool) table {
 | |
| 	t := table{headers: fields}
 | |
| 	for _, v := range values {
 | |
| 		row := make([]string, len(fields))
 | |
| 		for i, f := range fields {
 | |
| 			row[i] = v.FormatField(f, machineReadable)
 | |
| 		}
 | |
| 		t.addRowSlice(row)
 | |
| 	}
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| func tableWithHeader(header ...string) table {
 | |
| 	return table{headers: header}
 | |
| }
 | |
| 
 | |
| // it's the callers responsibility to ensure row length is equal to header length!
 | |
| func (t *table) addRow(row ...string) {
 | |
| 	t.addRowSlice(row)
 | |
| }
 | |
| 
 | |
| // it's the callers responsibility to ensure row length is equal to header length!
 | |
| func (t *table) addRowSlice(row []string) {
 | |
| 	t.values = append(t.values, row)
 | |
| }
 | |
| 
 | |
| func (t *table) sort(column uint, desc bool) {
 | |
| 	t.sortColumn = column
 | |
| 	t.sortDesc = desc
 | |
| 	sort.Stable(t) // stable to allow multiple calls to sort
 | |
| }
 | |
| 
 | |
| // sortable interface
 | |
| func (t table) Len() int      { return len(t.values) }
 | |
| func (t table) Swap(i, j int) { t.values[i], t.values[j] = t.values[j], t.values[i] }
 | |
| func (t table) Less(i, j int) bool {
 | |
| 	if t.sortDesc {
 | |
| 		i, j = j, i
 | |
| 	}
 | |
| 	return t.values[i][t.sortColumn] < t.values[j][t.sortColumn]
 | |
| }
 | |
| 
 | |
| func (t *table) print(output string) {
 | |
| 	t.fprint(os.Stdout, output)
 | |
| }
 | |
| 
 | |
| func (t *table) fprint(f io.Writer, output string) {
 | |
| 	switch output {
 | |
| 	case "", "table":
 | |
| 		outputTable(f, t.headers, t.values)
 | |
| 	case "csv":
 | |
| 		outputDsv(f, t.headers, t.values, ",")
 | |
| 	case "simple":
 | |
| 		outputSimple(f, t.headers, t.values)
 | |
| 	case "tsv":
 | |
| 		outputDsv(f, t.headers, t.values, "\t")
 | |
| 	case "yml", "yaml":
 | |
| 		outputYaml(f, t.headers, t.values)
 | |
| 	case "json":
 | |
| 		outputJSON(f, t.headers, t.values)
 | |
| 	default:
 | |
| 		fmt.Fprintf(f, `"unknown output type '%s', available types are:
 | |
| - csv: comma-separated values
 | |
| - simple: space-separated values
 | |
| - table: auto-aligned table format (default)
 | |
| - tsv: tab-separated values
 | |
| - yaml: YAML format
 | |
| - json: JSON format
 | |
| `, output)
 | |
| 		os.Exit(1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // outputTable prints structured data as table
 | |
| func outputTable(f io.Writer, headers []string, values [][]string) error {
 | |
| 	table := tablewriter.NewWriter(f)
 | |
| 	if len(headers) > 0 {
 | |
| 		table.Header(headers)
 | |
| 	}
 | |
| 	for _, value := range values {
 | |
| 		if err := table.Append(value); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return table.Render()
 | |
| }
 | |
| 
 | |
| // outputSimple prints structured data as space delimited value
 | |
| func outputSimple(f io.Writer, headers []string, values [][]string) {
 | |
| 	for _, value := range values {
 | |
| 		fmt.Fprint(f, strings.Join(value, " "))
 | |
| 		fmt.Fprintf(f, "\n")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // outputDsv prints structured data as delimiter separated value format
 | |
| func outputDsv(f io.Writer, headers []string, values [][]string, delimiterOpt ...string) {
 | |
| 	delimiter := ","
 | |
| 	if len(delimiterOpt) > 0 {
 | |
| 		delimiter = delimiterOpt[0]
 | |
| 	}
 | |
| 	fmt.Fprintln(f, "\""+strings.Join(headers, "\""+delimiter+"\"")+"\"")
 | |
| 	for _, value := range values {
 | |
| 		fmt.Fprintf(f, "\"")
 | |
| 		fmt.Fprint(f, strings.Join(value, "\""+delimiter+"\""))
 | |
| 		fmt.Fprintf(f, "\"")
 | |
| 		fmt.Fprintf(f, "\n")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // outputYaml prints structured data as yaml
 | |
| func outputYaml(f io.Writer, headers []string, values [][]string) {
 | |
| 	for _, value := range values {
 | |
| 		fmt.Fprintln(f, "-")
 | |
| 		for j, val := range value {
 | |
| 			intVal, _ := strconv.Atoi(val)
 | |
| 			if strconv.Itoa(intVal) == val {
 | |
| 				fmt.Fprintf(f, "  %s: %s\n", headers[j], val)
 | |
| 			} else {
 | |
| 				fmt.Fprintf(f, "  %s: '%s'\n", headers[j], strings.ReplaceAll(val, "'", "''"))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
 | |
| 	matchAllCap   = regexp.MustCompile("([a-z0-9])([A-Z])")
 | |
| )
 | |
| 
 | |
| func toSnakeCase(str string) string {
 | |
| 	snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
 | |
| 	snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
 | |
| 	return strings.ToLower(snake)
 | |
| }
 | |
| 
 | |
| // outputJSON prints structured data as json
 | |
| // Since golang's map is unordered, we need to ensure consistent ordering, we have
 | |
| // to output the JSON ourselves.
 | |
| func outputJSON(f io.Writer, headers []string, values [][]string) {
 | |
| 	fmt.Fprintln(f, "[")
 | |
| 	itemCount := len(values)
 | |
| 	headersCount := len(headers)
 | |
| 	const space = "  "
 | |
| 	for i, value := range values {
 | |
| 		fmt.Fprintf(f, "%s{\n", space)
 | |
| 		for j, val := range value {
 | |
| 			v, err := json.Marshal(val)
 | |
| 			if err != nil {
 | |
| 				fmt.Printf("Failed to format JSON for value '%s': %v\n", val, err)
 | |
| 				return
 | |
| 			}
 | |
| 			key, err := json.Marshal(toSnakeCase(headers[j]))
 | |
| 			if err != nil {
 | |
| 				fmt.Printf("Failed to format JSON for header '%s': %v\n", headers[j], err)
 | |
| 				return
 | |
| 			}
 | |
| 			fmt.Fprintf(f, "%s:%s", key, v)
 | |
| 			if j != headersCount-1 {
 | |
| 				fmt.Fprintln(f, ",")
 | |
| 			} else {
 | |
| 				fmt.Fprintln(f)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if i != itemCount-1 {
 | |
| 			fmt.Fprintf(f, "%s},\n", space)
 | |
| 		} else {
 | |
| 			fmt.Fprintf(f, "%s}\n", space)
 | |
| 		}
 | |
| 	}
 | |
| 	fmt.Fprintln(f, "]")
 | |
| }
 | |
| 
 | |
| func isMachineReadable(outputFormat string) bool {
 | |
| 	switch outputFormat {
 | |
| 	case "yml", "yaml", "csv", "tsv", "json":
 | |
| 		return true
 | |
| 	}
 | |
| 	return false
 | |
| }
 | 
