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