1// Package table provides a convenient way to generate tabular output of any 2// data, primarily useful for CLI tools. 3// 4// Columns are left-aligned and padded to accomodate the largest cell in that 5// column. 6// 7// Source: https://github.com/rodaine/table 8// 9// table.DefaultHeaderFormatter = func(format string, vals ...interface{}) string { 10// return strings.ToUpper(fmt.Sprintf(format, vals...)) 11// } 12// 13// tbl := table.New("ID", "Name", "Cost ($)") 14// 15// for _, widget := range Widgets { 16// tbl.AddRow(widget.ID, widget.Name, widget.Cost) 17// } 18// 19// tbl.Print() 20// 21// // Output: 22// // ID NAME COST ($) 23// // 1 Foobar 1.23 24// // 2 Fizzbuzz 4.56 25// // 3 Gizmo 78.90 26package table 27 28import ( 29 "fmt" 30 "io" 31 "os" 32 "strings" 33 "unicode/utf8" 34) 35 36// These are the default properties for all Tables created from this package 37// and can be modified. 38var ( 39 // DefaultPadding specifies the number of spaces between columns in a table. 40 DefaultPadding = 2 41 42 // DefaultWriter specifies the output io.Writer for the Table.Print method. 43 DefaultWriter io.Writer = os.Stdout 44 45 // DefaultHeaderFormatter specifies the default Formatter for the table header. 46 DefaultHeaderFormatter Formatter 47 48 // DefaultFirstColumnFormatter specifies the default Formatter for the first column cells. 49 DefaultFirstColumnFormatter Formatter 50 51 // DefaultWidthFunc specifies the default WidthFunc for calculating column widths 52 DefaultWidthFunc WidthFunc = utf8.RuneCountInString 53) 54 55// Formatter functions expose a fmt.Sprintf signature that can be used to modify 56// the display of the text in either the header or first column of a Table. 57// The formatter should not change the width of original text as printed since 58// column widths are calculated pre-formatting (though this issue can be mitigated 59// with increased padding). 60// 61// tbl.WithHeaderFormatter(func(format string, vals ...interface{}) string { 62// return strings.ToUpper(fmt.Sprintf(format, vals...)) 63// }) 64// 65// A good use case for formatters is to use ANSI escape codes to color the cells 66// for a nicer interface. The package color (https://github.com/fatih/color) makes 67// it easy to generate these automatically: http://godoc.org/github.com/fatih/color#Color.SprintfFunc 68type Formatter func(string, ...interface{}) string 69 70// A WidthFunc calculates the width of a string. By default, the number of runes 71// is used but this may not be appropriate for certain character sets. The 72// package runewidth (https://github.com/mattn/go-runewidth) could be used to 73// accomodate multi-cell characters (such as emoji or CJK characters). 74type WidthFunc func(string) int 75 76// Table describes the interface for building up a tabular representation of data. 77// It exposes fluent/chainable methods for convenient table building. 78// 79// WithHeaderFormatter and WithFirstColumnFormatter sets the Formatter for the 80// header and first column, respectively. If nil is passed in (the default), no 81// formatting will be applied. 82// 83// New("foo", "bar").WithFirstColumnFormatter(func(f string, v ...interface{}) string { 84// return strings.ToUpper(fmt.Sprintf(f, v...)) 85// }) 86// 87// WithPadding specifies the minimum padding between cells in a row and defaults 88// to DefaultPadding. Padding values less than or equal to zero apply no extra 89// padding between the columns. 90// 91// New("foo", "bar").WithPadding(3) 92// 93// WithWriter modifies the writer which Print outputs to, defaulting to DefaultWriter 94// when instantiated. If nil is passed, os.Stdout will be used. 95// 96// New("foo", "bar").WithWriter(os.Stderr) 97// 98// WithWidthFunc sets the function used to calculate the width of the string in 99// a column. By default, the number of utf8 runes in the string is used. 100// 101// AddRow adds another row of data to the table. Any values can be passed in and 102// will be output as its string representation as described in the fmt standard 103// package. Rows can have less cells than the total number of columns in the table; 104// subsequent cells will be rendered empty. Rows with more cells than the total 105// number of columns will be truncated. References to the data are not held, so 106// the passed in values can be modified without affecting the table's output. 107// 108// New("foo", "bar").AddRow("fizz", "buzz").AddRow(time.Now()).AddRow(1, 2, 3).Print() 109// // Output: 110// // foo bar 111// // fizz buzz 112// // 2006-01-02 15:04:05.0 -0700 MST 113// // 1 2 114// 115// Print writes the string representation of the table to the provided writer. 116// Print can be called multiple times, even after subsequent mutations of the 117// provided data. The output is always preceded and followed by a new line. 118type Table interface { 119 WithHeaderFormatter(f Formatter) Table 120 WithFirstColumnFormatter(f Formatter) Table 121 WithPadding(p int) Table 122 WithWriter(w io.Writer) Table 123 WithWidthFunc(f WidthFunc) Table 124 125 AddRow(vals ...interface{}) Table 126 Print(includeHeader bool) 127} 128 129// New creates a Table instance with the specified header(s) provided. The number 130// of columns is fixed at this point to len(columnHeaders) and the defined defaults 131// are set on the instance. 132func New(columnHeaders ...interface{}) Table { 133 t := table{header: make([]string, len(columnHeaders))} 134 135 t.WithPadding(DefaultPadding) 136 t.WithWriter(DefaultWriter) 137 t.WithHeaderFormatter(DefaultHeaderFormatter) 138 t.WithFirstColumnFormatter(DefaultFirstColumnFormatter) 139 t.WithWidthFunc(DefaultWidthFunc) 140 141 for i, col := range columnHeaders { 142 t.header[i] = fmt.Sprint(col) 143 } 144 145 return &t 146} 147 148type table struct { 149 FirstColumnFormatter Formatter 150 HeaderFormatter Formatter 151 Padding int 152 Writer io.Writer 153 Width WidthFunc 154 155 header []string 156 rows [][]string 157 widths []int 158} 159 160func (t *table) WithHeaderFormatter(f Formatter) Table { 161 t.HeaderFormatter = f 162 return t 163} 164 165func (t *table) WithFirstColumnFormatter(f Formatter) Table { 166 t.FirstColumnFormatter = f 167 return t 168} 169 170func (t *table) WithPadding(p int) Table { 171 if p < 0 { 172 p = 0 173 } 174 175 t.Padding = p 176 return t 177} 178 179func (t *table) WithWriter(w io.Writer) Table { 180 if w == nil { 181 w = os.Stdout 182 } 183 184 t.Writer = w 185 return t 186} 187 188func (t *table) WithWidthFunc(f WidthFunc) Table { 189 t.Width = f 190 return t 191} 192 193func (t *table) AddRow(vals ...interface{}) Table { 194 row := make([]string, len(t.header)) 195 for i, val := range vals { 196 if i >= len(t.header) { 197 break 198 } 199 row[i] = fmt.Sprint(val) 200 } 201 t.rows = append(t.rows, row) 202 203 return t 204} 205 206func (t *table) Print(includeHeader bool) { 207 format := strings.Repeat("%s", len(t.header)) + "\n" 208 t.calculateWidths() 209 210 if includeHeader { 211 t.printHeader(format) 212 } 213 for _, row := range t.rows { 214 t.printRow(format, row) 215 } 216} 217 218func (t *table) printHeader(format string) { 219 vals := t.applyWidths(t.header, t.widths) 220 if t.HeaderFormatter != nil { 221 txt := t.HeaderFormatter(format, vals...) 222 fmt.Fprint(t.Writer, txt) 223 } else { 224 fmt.Fprintf(t.Writer, format, vals...) 225 } 226} 227 228func (t *table) printRow(format string, row []string) { 229 vals := t.applyWidths(row, t.widths) 230 231 if t.FirstColumnFormatter != nil { 232 vals[0] = t.FirstColumnFormatter("%s", vals[0]) 233 } 234 235 fmt.Fprintf(t.Writer, format, vals...) 236} 237 238func (t *table) calculateWidths() { 239 t.widths = make([]int, len(t.header)) 240 for _, row := range t.rows { 241 for i, v := range row { 242 if w := t.Width(v) + t.Padding; w > t.widths[i] { 243 t.widths[i] = w 244 } 245 } 246 } 247 248 for i, v := range t.header { 249 if w := t.Width(v) + t.Padding; w > t.widths[i] { 250 t.widths[i] = w 251 } 252 } 253} 254 255func (t *table) applyWidths(row []string, widths []int) []interface{} { 256 out := make([]interface{}, len(row)) 257 for i, s := range row { 258 out[i] = s + t.lenOffset(s, widths[i]) 259 } 260 return out 261} 262 263func (t *table) lenOffset(s string, w int) string { 264 l := w - t.Width(s) 265 if l <= 0 { 266 return "" 267 } 268 return strings.Repeat(" ", l) 269} 270