package table import ( "bytes" "io" "strings" "text/template" ) // Table contains data for a table to be printed. type Table struct { columns []string templates []*template.Template data []interface{} footer []string CellSeparator string PrintHeader func(io.Writer, string) error PrintSeparator func(io.Writer, string) error PrintData func(io.Writer, int, string) error PrintFooter func(io.Writer, string) error } var funcmap = template.FuncMap{ "join": strings.Join, } // New initializes a new Table func New() *Table { p := func(w io.Writer, s string) error { _, err := w.Write(append([]byte(s), '\n')) return err } return &Table{ CellSeparator: " ", PrintHeader: p, PrintSeparator: p, PrintData: func(w io.Writer, _ int, s string) error { return p(w, s) }, PrintFooter: p, } } // AddColumn adds a new header field with the header and format, which is // expected to be template string compatible with text/template. When compiling // the format fails, AddColumn panics. func (t *Table) AddColumn(header, format string) { t.columns = append(t.columns, header) tmpl, err := template.New("template for " + header).Funcs(funcmap).Parse(format) if err != nil { panic(err) } t.templates = append(t.templates, tmpl) } // AddRow adds a new row to the table, which is filled with data. func (t *Table) AddRow(data interface{}) { t.data = append(t.data, data) } // AddFooter prints line after the table func (t *Table) AddFooter(line string) { t.footer = append(t.footer, line) } func printLine(w io.Writer, print func(io.Writer, string) error, sep string, data []string, widths []int) error { var fields [][]string maxLines := 1 for _, d := range data { lines := strings.Split(d, "\n") if len(lines) > maxLines { maxLines = len(lines) } fields = append(fields, lines) } for i := 0; i < maxLines; i++ { var s string for fieldNum, lines := range fields { var v string if i < len(lines) { v += lines[i] } // apply padding pad := widths[fieldNum] - len(v) if pad > 0 { v += strings.Repeat(" ", pad) } if fieldNum > 0 { v = sep + v } s += v } err := print(w, strings.TrimRight(s, " ")) if err != nil { return err } } return nil } // Write prints the table to w. func (t *Table) Write(w io.Writer) error { columns := len(t.templates) if columns == 0 { return nil } // collect all data fields from all columns lines := make([][]string, 0, len(t.data)) buf := bytes.NewBuffer(nil) for _, data := range t.data { row := make([]string, 0, len(t.templates)) for _, tmpl := range t.templates { err := tmpl.Execute(buf, data) if err != nil { return err } row = append(row, buf.String()) buf.Reset() } lines = append(lines, row) } // find max width for each cell columnWidths := make([]int, columns) for i, desc := range t.columns { for _, line := range strings.Split(desc, "\n") { if columnWidths[i] < len(line) { columnWidths[i] = len(desc) } } } for _, line := range lines { for i, content := range line { for _, l := range strings.Split(content, "\n") { if columnWidths[i] < len(l) { columnWidths[i] = len(l) } } } } // calculate the total width of the table totalWidth := 0 for _, width := range columnWidths { totalWidth += width } totalWidth += (columns - 1) * len(t.CellSeparator) // write header if len(t.columns) > 0 { err := printLine(w, t.PrintHeader, t.CellSeparator, t.columns, columnWidths) if err != nil { return err } // draw separation line err = t.PrintSeparator(w, strings.Repeat("-", totalWidth)) if err != nil { return err } } // write all the lines for i, line := range lines { printer := func(w io.Writer, s string) error { return t.PrintData(w, i, s) } err := printLine(w, printer, t.CellSeparator, line, columnWidths) if err != nil { return err } } // draw separation line err := t.PrintSeparator(w, strings.Repeat("-", totalWidth)) if err != nil { return err } if len(t.footer) > 0 { // write the footer for _, line := range t.footer { err := t.PrintFooter(w, line) if err != nil { return err } } } return nil }