blob: 4adedb1b10ad190801cc38d1bdcb3b44c4a3d559 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
2 * Copyright 2019-present Ciena Corporation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package format
17
18import (
Scott Bakered4efab2020-01-13 19:12:25 -080019 "bytes"
20 "errors"
21 "fmt"
Zack Williamse940c7a2019-08-21 14:25:39 -070022 "io"
23 "reflect"
24 "regexp"
Scott Bakered4efab2020-01-13 19:12:25 -080025 "strconv"
Zack Williamse940c7a2019-08-21 14:25:39 -070026 "strings"
27 "text/tabwriter"
28 "text/template"
29 "text/template/parse"
30)
31
32var nameFinder = regexp.MustCompile(`\.([\._A-Za-z0-9]*)}}`)
33
34type Format string
35
Scott Bakered4efab2020-01-13 19:12:25 -080036/* TrimAndPad
37 *
38 * Modify `s` so that it is exactly `l` characters long, removing
39 * characters from the end, or adding spaces as necessary.
40 */
41
42func TrimAndPad(s string, l int) string {
43 // TODO: support right justification if a negative number is passed
44 if len(s) > l {
45 s = s[:l]
46 }
47 return s + strings.Repeat(" ", l-len(s))
48}
49
50/* GetHeaderString
51 *
52 * From a template, extract the set of column names.
53 */
54
55func GetHeaderString(tmpl *template.Template, nameLimit int) string {
56 var header string
57 for _, n := range tmpl.Tree.Root.Nodes {
58 switch n.Type() {
59 case parse.NodeText:
60 header += n.String()
61 case parse.NodeString:
62 header += n.String()
63 case parse.NodeAction:
64 found := nameFinder.FindStringSubmatch(n.String())
65 if len(found) == 2 {
66 if nameLimit > 0 {
67 parts := strings.Split(found[1], ".")
68 start := len(parts) - nameLimit
69 if start < 0 {
70 start = 0
71 }
72 header += strings.ToUpper(strings.Join(parts[start:], "."))
73 } else {
74 header += strings.ToUpper(found[1])
75 }
76 }
77 }
78 }
79 return header
80}
81
Zack Williamse940c7a2019-08-21 14:25:39 -070082func (f Format) IsTable() bool {
83 return strings.HasPrefix(string(f), "table")
84}
85
86func (f Format) Execute(writer io.Writer, withHeaders bool, nameLimit int, data interface{}) error {
87 var tabWriter *tabwriter.Writer = nil
88 format := f
89
90 if f.IsTable() {
91 tabWriter = tabwriter.NewWriter(writer, 0, 4, 4, ' ', 0)
92 format = Format(strings.TrimPrefix(string(f), "table"))
93 }
94
Scott Baker9173ed82020-05-19 08:30:12 -070095 funcmap := template.FuncMap{
96 "timestamp": formatTimestamp,
David K. Bainbridgebd6b2882021-08-26 13:31:02 +000097 "since": formatSince,
98 "gosince": formatGoSince}
Scott Baker9173ed82020-05-19 08:30:12 -070099
100 tmpl, err := template.New("output").Funcs(funcmap).Parse(string(format))
Zack Williamse940c7a2019-08-21 14:25:39 -0700101 if err != nil {
102 return err
103 }
104
105 if f.IsTable() && withHeaders {
Scott Bakered4efab2020-01-13 19:12:25 -0800106 header := GetHeaderString(tmpl, nameLimit)
107
David Bainbridge12f036f2019-10-15 22:09:04 +0000108 if _, err = tabWriter.Write([]byte(header)); err != nil {
109 return err
110 }
111 if _, err = tabWriter.Write([]byte("\n")); err != nil {
112 return err
113 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700114
115 slice := reflect.ValueOf(data)
116 if slice.Kind() == reflect.Slice {
117 for i := 0; i < slice.Len(); i++ {
David Bainbridge12f036f2019-10-15 22:09:04 +0000118 if err = tmpl.Execute(tabWriter, slice.Index(i).Interface()); err != nil {
119 return err
120 }
121 if _, err = tabWriter.Write([]byte("\n")); err != nil {
122 return err
123 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700124 }
125 } else {
David Bainbridge12f036f2019-10-15 22:09:04 +0000126 if err = tmpl.Execute(tabWriter, data); err != nil {
127 return err
128 }
129 if _, err = tabWriter.Write([]byte("\n")); err != nil {
130 return err
131 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700132 }
133 tabWriter.Flush()
134 return nil
135 }
136
137 slice := reflect.ValueOf(data)
138 if slice.Kind() == reflect.Slice {
139 for i := 0; i < slice.Len(); i++ {
David Bainbridge12f036f2019-10-15 22:09:04 +0000140 if err = tmpl.Execute(writer, slice.Index(i).Interface()); err != nil {
141 return err
142 }
143 if _, err = writer.Write([]byte("\n")); err != nil {
144 return err
145 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700146 }
147 } else {
David Bainbridge12f036f2019-10-15 22:09:04 +0000148 if err = tmpl.Execute(writer, data); err != nil {
149 return err
150 }
151 if _, err = writer.Write([]byte("\n")); err != nil {
152 return err
153 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700154 }
155 return nil
156
157}
Scott Bakered4efab2020-01-13 19:12:25 -0800158
159/*
160 * ExecuteFixedWidth
161 *
162 * Formats a table row using a set of fixed column widths. Used for streaming
163 * output where column widths cannot be automatically determined because only
164 * one line of the output is available at a time.
165 *
166 * Assumes the format uses tab as a field delimiter.
167 *
168 * columnWidths: struct that contains column widths
169 * header: If true return the header. If false then evaluate data and return data.
170 * data: Data to evaluate
171 */
172
173func (f Format) ExecuteFixedWidth(columnWidths interface{}, header bool, data interface{}) (string, error) {
174 if !f.IsTable() {
175 return "", errors.New("Fixed width is only available on table format")
176 }
177
178 outputAs := strings.TrimPrefix(string(f), "table")
179 tmpl, err := template.New("output").Parse(string(outputAs))
180 if err != nil {
181 return "", fmt.Errorf("Failed to parse template: %v", err)
182 }
183
184 var buf bytes.Buffer
185 var tabSepOutput string
186
187 if header {
188 // Caller wants the table header.
189 tabSepOutput = GetHeaderString(tmpl, 1)
190 } else {
191 // Caller wants the data.
192 err = tmpl.Execute(&buf, data)
193 if err != nil {
194 return "", fmt.Errorf("Failed to execute template: %v", err)
195 }
196 tabSepOutput = buf.String()
197 }
198
199 // Extract the column width constants by running the template on the
200 // columnWidth structure. This will cause text.template to split the
201 // column widths exactly like it did the output (i.e. separated by
202 // tab characters)
203 buf.Reset()
204 err = tmpl.Execute(&buf, columnWidths)
205 if err != nil {
206 return "", fmt.Errorf("Failed to execute template on widths: %v", err)
207 }
208 tabSepWidth := buf.String()
209
210 // Loop through the fields and widths, printing each field to the
211 // preset width.
212 output := ""
213 outParts := strings.Split(tabSepOutput, "\t")
214 widthParts := strings.Split(tabSepWidth, "\t")
215 for i, outPart := range outParts {
216 width, err := strconv.Atoi(widthParts[i])
217 if err != nil {
218 return "", fmt.Errorf("Failed to parse width %s: %v", widthParts[i], err)
219 }
220 output = output + TrimAndPad(outPart, width) + " "
221 }
222
223 // remove any trailing spaces
224 output = strings.TrimRight(output, " ")
225
226 return output, nil
227}