m/cli/metroctl: implement tabular print
Change-Id: I0511d48218bcc7e2e56af66839392bf11643733c
Reviewed-on: https://review.monogon.dev/c/monogon/+/1391
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/metroctl/table.go b/metropolis/cli/metroctl/table.go
new file mode 100644
index 0000000..1d947d9
--- /dev/null
+++ b/metropolis/cli/metroctl/table.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "strings"
+)
+
+// table is a list of entries that form a table with sparse columns. Each entry
+// defines its own columns.
+type table struct {
+ entries []entry
+}
+
+// add an entry to the table.
+func (t *table) add(e entry) {
+ t.entries = append(t.entries, e)
+}
+
+// An entry is made up of column key -> value pairs.
+type entry struct {
+ columns []entryColumn
+}
+
+// add a key/value pair to the entry.
+func (e *entry) add(key, value string) {
+ e.columns = append(e.columns, entryColumn{
+ key: key,
+ value: value,
+ })
+}
+
+// get a value from a given key, returning zero string if not set.
+func (e *entry) get(key string) string {
+ for _, col := range e.columns {
+ if col.key == key {
+ return col.value
+ }
+ }
+ return ""
+}
+
+// An entryColumn is a pair for table column key and entry column value.
+type entryColumn struct {
+ key string
+ value string
+}
+
+// columns returns the keys and widths of columns that are present in the table's
+// entries.
+func (t *table) columns() columns {
+ var res columns
+ for _, e := range t.entries {
+ for _, c := range e.columns {
+ tc := res.upsert(c.key)
+ if len(c.value) > tc.width {
+ tc.width = len(c.value)
+ }
+ }
+ }
+ return res
+}
+
+type columns []*column
+
+// A column in a table, not containing all entries that make up this table, but
+// containing their maximum width (for layout purposes).
+type column struct {
+ // key is the column key.
+ key string
+ // width is the maximum width (in runes) of all the entries' data in this column.
+ width int
+}
+
+// upsert a key into a list of columns, returning the upserted column.
+func (c *columns) upsert(key string) *column {
+ for _, col := range *c {
+ if col.key == key {
+ return col
+ }
+ }
+ col := &column{
+ key: key,
+ width: len(key),
+ }
+ *c = append(*c, col)
+ return col
+}
+
+// filter returns a copy of columns where the only columns present are the ones
+// whose onlyColumns values are true. If only columns is nil, no filtering takes
+// place (all columns are returned).
+func (c columns) filter(onlyColumns map[string]bool) columns {
+ var res []*column
+ for _, cc := range c {
+ if onlyColumns != nil && !onlyColumns[cc.key] {
+ continue
+ }
+ res = append(res, cc)
+ }
+ return res
+}
+
+// printHeader writes a table-like header to the given file, keeping margin
+// spaces between columns.
+func (c columns) printHeader(f io.Writer, margin int) {
+ for _, cc := range c {
+ fmt.Fprintf(f, "%-*s", cc.width+margin, strings.ToUpper(cc.key))
+ }
+ fmt.Fprintf(f, "\n")
+}
+
+// print writes a table-like representation of this table to the given file,
+// first filtering the columns by onlyColumns (if not set, no filtering takes
+// place).
+func (t *table) print(f io.Writer, onlyColumns map[string]bool) {
+ margin := 3
+ cols := t.columns().filter(onlyColumns)
+ cols.printHeader(f, margin)
+
+ for _, e := range t.entries {
+ for _, c := range cols {
+ v := e.get(c.key)
+ fmt.Fprintf(f, "%-*s", c.width+margin, v)
+ }
+ fmt.Fprintf(f, "\n")
+ }
+}