VOL-2496 Add "event listen" command to voltctl

Change-Id: I8f1fb34b55f56c8125142ac289e2f19fc170d804
diff --git a/pkg/format/formatter.go b/pkg/format/formatter.go
index eaa42eb..c42b8ea 100644
--- a/pkg/format/formatter.go
+++ b/pkg/format/formatter.go
@@ -16,9 +16,13 @@
 package format
 
 import (
+	"bytes"
+	"errors"
+	"fmt"
 	"io"
 	"reflect"
 	"regexp"
+	"strconv"
 	"strings"
 	"text/tabwriter"
 	"text/template"
@@ -29,6 +33,52 @@
 
 type Format string
 
+/* TrimAndPad
+ *
+ * Modify `s` so that it is exactly `l` characters long, removing
+ * characters from the end, or adding spaces as necessary.
+ */
+
+func TrimAndPad(s string, l int) string {
+	// TODO: support right justification if a negative number is passed
+	if len(s) > l {
+		s = s[:l]
+	}
+	return s + strings.Repeat(" ", l-len(s))
+}
+
+/* GetHeaderString
+ *
+ * From a template, extract the set of column names.
+ */
+
+func GetHeaderString(tmpl *template.Template, nameLimit int) string {
+	var header string
+	for _, n := range tmpl.Tree.Root.Nodes {
+		switch n.Type() {
+		case parse.NodeText:
+			header += n.String()
+		case parse.NodeString:
+			header += n.String()
+		case parse.NodeAction:
+			found := nameFinder.FindStringSubmatch(n.String())
+			if len(found) == 2 {
+				if nameLimit > 0 {
+					parts := strings.Split(found[1], ".")
+					start := len(parts) - nameLimit
+					if start < 0 {
+						start = 0
+					}
+					header += strings.ToUpper(strings.Join(parts[start:], "."))
+				} else {
+					header += strings.ToUpper(found[1])
+				}
+			}
+		}
+	}
+	return header
+}
+
 func (f Format) IsTable() bool {
 	return strings.HasPrefix(string(f), "table")
 }
@@ -48,29 +98,8 @@
 	}
 
 	if f.IsTable() && withHeaders {
-		var header string
-		for _, n := range tmpl.Tree.Root.Nodes {
-			switch n.Type() {
-			case parse.NodeText:
-				header += n.String()
-			case parse.NodeString:
-				header += n.String()
-			case parse.NodeAction:
-				found := nameFinder.FindStringSubmatch(n.String())
-				if len(found) == 2 {
-					if nameLimit > 0 {
-						parts := strings.Split(found[1], ".")
-						start := len(parts) - nameLimit
-						if start < 0 {
-							start = 0
-						}
-						header += strings.ToUpper(strings.Join(parts[start:], "."))
-					} else {
-						header += strings.ToUpper(found[1])
-					}
-				}
-			}
-		}
+		header := GetHeaderString(tmpl, nameLimit)
+
 		if _, err = tabWriter.Write([]byte(header)); err != nil {
 			return err
 		}
@@ -121,3 +150,73 @@
 	return nil
 
 }
+
+/*
+ * ExecuteFixedWidth
+ *
+ * Formats a table row using a set of fixed column widths. Used for streaming
+ * output where column widths cannot be automatically determined because only
+ * one line of the output is available at a time.
+ *
+ * Assumes the format uses tab as a field delimiter.
+ *
+ * columnWidths: struct that contains column widths
+ * header: If true return the header. If false then evaluate data and return data.
+ * data: Data to evaluate
+ */
+
+func (f Format) ExecuteFixedWidth(columnWidths interface{}, header bool, data interface{}) (string, error) {
+	if !f.IsTable() {
+		return "", errors.New("Fixed width is only available on table format")
+	}
+
+	outputAs := strings.TrimPrefix(string(f), "table")
+	tmpl, err := template.New("output").Parse(string(outputAs))
+	if err != nil {
+		return "", fmt.Errorf("Failed to parse template: %v", err)
+	}
+
+	var buf bytes.Buffer
+	var tabSepOutput string
+
+	if header {
+		// Caller wants the table header.
+		tabSepOutput = GetHeaderString(tmpl, 1)
+	} else {
+		// Caller wants the data.
+		err = tmpl.Execute(&buf, data)
+		if err != nil {
+			return "", fmt.Errorf("Failed to execute template: %v", err)
+		}
+		tabSepOutput = buf.String()
+	}
+
+	// Extract the column width constants by running the template on the
+	// columnWidth structure. This will cause text.template to split the
+	// column widths exactly like it did the output (i.e. separated by
+	// tab characters)
+	buf.Reset()
+	err = tmpl.Execute(&buf, columnWidths)
+	if err != nil {
+		return "", fmt.Errorf("Failed to execute template on widths: %v", err)
+	}
+	tabSepWidth := buf.String()
+
+	// Loop through the fields and widths, printing each field to the
+	// preset width.
+	output := ""
+	outParts := strings.Split(tabSepOutput, "\t")
+	widthParts := strings.Split(tabSepWidth, "\t")
+	for i, outPart := range outParts {
+		width, err := strconv.Atoi(widthParts[i])
+		if err != nil {
+			return "", fmt.Errorf("Failed to parse width %s: %v", widthParts[i], err)
+		}
+		output = output + TrimAndPad(outPart, width) + " "
+	}
+
+	// remove any trailing spaces
+	output = strings.TrimRight(output, " ")
+
+	return output, nil
+}