blob: 2b744d1f587429122cff6738b17809e84267f37c [file] [log] [blame]
/*
* Copyright 2019-present Ciena Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package format
import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"text/template/parse"
)
var nameFinder = regexp.MustCompile(`\.([\._A-Za-z0-9]*)}}`)
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")
}
func (f Format) Execute(writer io.Writer, withHeaders bool, nameLimit int, data interface{}) error {
var tabWriter *tabwriter.Writer = nil
format := f
if f.IsTable() {
tabWriter = tabwriter.NewWriter(writer, 0, 4, 4, ' ', 0)
format = Format(strings.TrimPrefix(string(f), "table"))
}
funcmap := template.FuncMap{
"timestamp": formatTimestamp,
"since": formatSince}
tmpl, err := template.New("output").Funcs(funcmap).Parse(string(format))
if err != nil {
return err
}
if f.IsTable() && withHeaders {
header := GetHeaderString(tmpl, nameLimit)
if _, err = tabWriter.Write([]byte(header)); err != nil {
return err
}
if _, err = tabWriter.Write([]byte("\n")); err != nil {
return err
}
slice := reflect.ValueOf(data)
if slice.Kind() == reflect.Slice {
for i := 0; i < slice.Len(); i++ {
if err = tmpl.Execute(tabWriter, slice.Index(i).Interface()); err != nil {
return err
}
if _, err = tabWriter.Write([]byte("\n")); err != nil {
return err
}
}
} else {
if err = tmpl.Execute(tabWriter, data); err != nil {
return err
}
if _, err = tabWriter.Write([]byte("\n")); err != nil {
return err
}
}
tabWriter.Flush()
return nil
}
slice := reflect.ValueOf(data)
if slice.Kind() == reflect.Slice {
for i := 0; i < slice.Len(); i++ {
if err = tmpl.Execute(writer, slice.Index(i).Interface()); err != nil {
return err
}
if _, err = writer.Write([]byte("\n")); err != nil {
return err
}
}
} else {
if err = tmpl.Execute(writer, data); err != nil {
return err
}
if _, err = writer.Write([]byte("\n")); err != nil {
return err
}
}
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
}