blob: 2b744d1f587429122cff6738b17809e84267f37c [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,
97 "since": formatSince}
98
99 tmpl, err := template.New("output").Funcs(funcmap).Parse(string(format))
Zack Williamse940c7a2019-08-21 14:25:39 -0700100 if err != nil {
101 return err
102 }
103
104 if f.IsTable() && withHeaders {
Scott Bakered4efab2020-01-13 19:12:25 -0800105 header := GetHeaderString(tmpl, nameLimit)
106
David Bainbridge12f036f2019-10-15 22:09:04 +0000107 if _, err = tabWriter.Write([]byte(header)); err != nil {
108 return err
109 }
110 if _, err = tabWriter.Write([]byte("\n")); err != nil {
111 return err
112 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700113
114 slice := reflect.ValueOf(data)
115 if slice.Kind() == reflect.Slice {
116 for i := 0; i < slice.Len(); i++ {
David Bainbridge12f036f2019-10-15 22:09:04 +0000117 if err = tmpl.Execute(tabWriter, slice.Index(i).Interface()); err != nil {
118 return err
119 }
120 if _, err = tabWriter.Write([]byte("\n")); err != nil {
121 return err
122 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700123 }
124 } else {
David Bainbridge12f036f2019-10-15 22:09:04 +0000125 if err = tmpl.Execute(tabWriter, data); err != nil {
126 return err
127 }
128 if _, err = tabWriter.Write([]byte("\n")); err != nil {
129 return err
130 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700131 }
132 tabWriter.Flush()
133 return nil
134 }
135
136 slice := reflect.ValueOf(data)
137 if slice.Kind() == reflect.Slice {
138 for i := 0; i < slice.Len(); i++ {
David Bainbridge12f036f2019-10-15 22:09:04 +0000139 if err = tmpl.Execute(writer, slice.Index(i).Interface()); err != nil {
140 return err
141 }
142 if _, err = writer.Write([]byte("\n")); err != nil {
143 return err
144 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700145 }
146 } else {
David Bainbridge12f036f2019-10-15 22:09:04 +0000147 if err = tmpl.Execute(writer, data); err != nil {
148 return err
149 }
150 if _, err = writer.Write([]byte("\n")); err != nil {
151 return err
152 }
Zack Williamse940c7a2019-08-21 14:25:39 -0700153 }
154 return nil
155
156}
Scott Bakered4efab2020-01-13 19:12:25 -0800157
158/*
159 * ExecuteFixedWidth
160 *
161 * Formats a table row using a set of fixed column widths. Used for streaming
162 * output where column widths cannot be automatically determined because only
163 * one line of the output is available at a time.
164 *
165 * Assumes the format uses tab as a field delimiter.
166 *
167 * columnWidths: struct that contains column widths
168 * header: If true return the header. If false then evaluate data and return data.
169 * data: Data to evaluate
170 */
171
172func (f Format) ExecuteFixedWidth(columnWidths interface{}, header bool, data interface{}) (string, error) {
173 if !f.IsTable() {
174 return "", errors.New("Fixed width is only available on table format")
175 }
176
177 outputAs := strings.TrimPrefix(string(f), "table")
178 tmpl, err := template.New("output").Parse(string(outputAs))
179 if err != nil {
180 return "", fmt.Errorf("Failed to parse template: %v", err)
181 }
182
183 var buf bytes.Buffer
184 var tabSepOutput string
185
186 if header {
187 // Caller wants the table header.
188 tabSepOutput = GetHeaderString(tmpl, 1)
189 } else {
190 // Caller wants the data.
191 err = tmpl.Execute(&buf, data)
192 if err != nil {
193 return "", fmt.Errorf("Failed to execute template: %v", err)
194 }
195 tabSepOutput = buf.String()
196 }
197
198 // Extract the column width constants by running the template on the
199 // columnWidth structure. This will cause text.template to split the
200 // column widths exactly like it did the output (i.e. separated by
201 // tab characters)
202 buf.Reset()
203 err = tmpl.Execute(&buf, columnWidths)
204 if err != nil {
205 return "", fmt.Errorf("Failed to execute template on widths: %v", err)
206 }
207 tabSepWidth := buf.String()
208
209 // Loop through the fields and widths, printing each field to the
210 // preset width.
211 output := ""
212 outParts := strings.Split(tabSepOutput, "\t")
213 widthParts := strings.Split(tabSepWidth, "\t")
214 for i, outPart := range outParts {
215 width, err := strconv.Atoi(widthParts[i])
216 if err != nil {
217 return "", fmt.Errorf("Failed to parse width %s: %v", widthParts[i], err)
218 }
219 output = output + TrimAndPad(outPart, width) + " "
220 }
221
222 // remove any trailing spaces
223 output = strings.TrimRight(output, " ")
224
225 return output, nil
226}