github.com/CycloneDX/sbom-utility@v0.16.0/cmd/report.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package cmd 20 21 import ( 22 "fmt" 23 "strconv" 24 "strings" 25 26 "github.com/CycloneDX/sbom-utility/common" 27 "github.com/CycloneDX/sbom-utility/utils" 28 "github.com/spf13/cobra" 29 ) 30 31 // Common/reusable Flags used across multiple report commands 32 const ( 33 FLAG_REPORT_WHERE = "where" 34 FLAG_REPORT_WHERE_HELP = "comma-separated list of `key=<regex>` clauses used to filter the result set" 35 ) 36 37 const ( 38 REPORT_LIST_TITLE_ROW_SEPARATOR = "-" 39 REPORT_LIST_VALUE_NONE = "none" 40 ) 41 42 // Markdown report helpers 43 const ( 44 MD_COLUMN_SEPARATOR = "|" 45 MD_ALIGN_LEFT = ":--" 46 MD_ALIGN_CENTER = "-:-" 47 MD_ALIGN_RIGHT = "--:" 48 MD_ALIGN_DEFAULT = MD_ALIGN_LEFT 49 ) 50 51 // Helper function in case displayed table columns become too wide 52 func truncateString(value string, maxLength int, showDetail bool) string { 53 length := len(value) 54 if length > maxLength { 55 value = value[:maxLength] 56 if showDetail { 57 value = fmt.Sprintf("%s (%v/%v)", value, maxLength, length) 58 } 59 } 60 return value 61 } 62 63 // TODO: Allow column format data to include MD_ALIGN_xxx values 64 func createMarkdownColumnAlignmentRow(columns []ColumnFormatData, summarizedReport bool) (alignment []string) { 65 for _, column := range columns { 66 // if it is a summary report being created, but the column is not marked summary data 67 if summarizedReport && !column.IsSummaryData { 68 continue // skip to next column 69 } 70 // it is summary colum data, include it in the alignment row formatting 71 alignment = append(alignment, MD_ALIGN_DEFAULT) 72 } 73 return 74 } 75 76 func createMarkdownRow(data []string) string { 77 return MD_COLUMN_SEPARATOR + 78 strings.Join(data, MD_COLUMN_SEPARATOR) + 79 MD_COLUMN_SEPARATOR 80 } 81 82 // Report processing helpers 83 func processWhereFlag(cmd *cobra.Command) (whereFilters []common.WhereFilter, err error) { 84 // Process flag: --where 85 whereValues, errGet := cmd.Flags().GetString(FLAG_REPORT_WHERE) 86 87 if errGet != nil { 88 err = getLogger().Errorf("failed to read flag `%s` value", FLAG_REPORT_WHERE) 89 return 90 } 91 92 whereFilters, err = retrieveWhereFilters(whereValues) 93 return 94 } 95 96 // Parse "--where" flags on behalf of utility commands that filter output reports (lists) 97 func retrieveWhereFilters(whereValues string) (whereFilters []common.WhereFilter, err error) { 98 // Use common functions for parsing query request clauses 99 wherePredicates := common.ParseWherePredicates(whereValues) 100 whereFilters, err = common.ParseWhereFilters(wherePredicates) 101 return 102 } 103 104 // A generic function that takes variadic "column" data (for a single row) as an interface{} 105 // of either string or []string types and, if needed, "wraps" the single row data into multiple 106 // text rows according to parameterized constraints. 107 // NOTE: Currently, only wraps []string values 108 // TODO: Also wrap on "maxChar" (per column) limit 109 func wrapTableRowText(maxChars int, joinChar string, columns ...interface{}) (tableData [][]string, err error) { 110 111 // calculate column dimension needed as max of slice sizes 112 numColumns := len(columns) 113 114 // Allocate a 1 row table 115 tableData = make([][]string, 1) 116 117 // Allocate the first row of the multi-row "table" 118 var numRowsAllocated int = 1 119 rowData := make([]string, numColumns) 120 tableData[0] = rowData 121 122 // for each column inspect its data and "wrap" as needed 123 // TODO: wrap on macChars using spaces; for now, just support list output 124 for iCol, column := range columns { 125 switch data := column.(type) { 126 case string: 127 // for now, a straightforward copy for string types 128 rowData[iCol] = column.(string) 129 case []string: 130 entries := column.([]string) 131 numRowsNeeded := len(entries) 132 133 // If needed, allocate and append new rows 134 if numRowsNeeded > numRowsAllocated { 135 // as long as we need more rows allocated 136 for ; numRowsAllocated < numRowsNeeded; numRowsAllocated++ { 137 rowData = make([]string, numColumns) 138 tableData = append(tableData, rowData) 139 } 140 getLogger().Debugf("tableData: (%v)", tableData) 141 } 142 143 // Add the multi-line data to appropriate row in the table 144 for i := 0; i < numRowsNeeded; i++ { 145 tableData[i][iCol] = entries[i] 146 } 147 //getLogger().Debugf("tableData: (%v)", tableData) 148 case bool: 149 rowData[iCol] = strconv.FormatBool(data) 150 case int: 151 rowData[iCol] = strconv.Itoa(data) 152 case float64: 153 // NOTE: JSON Unmarshal() always decodes JSON Numbers as "float64" type 154 rowData[iCol] = strconv.FormatFloat(data, 'f', -1, 64) 155 case nil: 156 //getLogger().Tracef("nil value for column: `%v`", columnData.DataKey) 157 rowData[iCol] = REPORT_LIST_VALUE_NONE 158 default: 159 err = getLogger().Errorf("Unexpected type for report data: column: %s, type: `%T`, value: `%v`", rowData[iCol], data, data) 160 } 161 } 162 163 return 164 } 165 166 // Report column data values 167 const REPORT_SUMMARY_DATA = true 168 const REPORT_REPLACE_LINE_FEEDS_TRUE = true 169 const REPORT_DO_NOT_TRUNCATE = -1 170 171 // TODO: Support additional flags to: 172 // - show number of chars shown vs. available when truncated (e.g., (x/y)) 173 // - provide "empty" value to display in column (e.g., "none" or "UNDEFINED") 174 // - inform how to "summarize" (e.g., show-first-only) data if data type is a slice (e.g., []string) 175 // NOTE: if only a subset of entries are shown on a summary, an indication of (x) entries could be shown as well 176 // - Support Markdown column alignment (e.g., MD_ALIGN_xxx values) 177 type ColumnFormatData struct { 178 DataKey string // Note: data key is the column label (where possible) 179 TruncateLength int // truncate character data to this length (default=-1 means don't truncate) 180 IsSummaryData bool // include in `--summary` reports 181 ReplaceLineFeeds bool // replace line feeds with spaces (e.g., for multi-line descriptions) 182 Alignment string // Align column data where possible (i.e., This is primarily for markdown format) 183 } 184 185 func NewColumnFormatData(key string, truncateLen int, isSummary bool, replaceLineFeeds bool) (foo *ColumnFormatData) { 186 foo = new(ColumnFormatData) 187 foo.DataKey = key 188 foo.TruncateLength = truncateLen 189 foo.IsSummaryData = isSummary 190 foo.ReplaceLineFeeds = replaceLineFeeds 191 return 192 } 193 194 func (data *ColumnFormatData) SetAlignment(alignment string) { 195 data.Alignment = alignment 196 } 197 198 func prepareReportTitleData(formatData []ColumnFormatData, summarizedReport bool) (titleData []string, separatorData []string) { 199 var underline string 200 for _, columnData := range formatData { 201 202 // if the report we are preparing is a summarized one (i.e., --summary true) 203 // we will skip appending column data not marked to be included in a summary report 204 if summarizedReport && !columnData.IsSummaryData { 205 continue 206 } 207 titleData = append(titleData, columnData.DataKey) 208 209 underline = strings.Repeat(REPORT_LIST_TITLE_ROW_SEPARATOR, len(columnData.DataKey)) 210 separatorData = append(separatorData, underline) 211 } 212 return 213 } 214 215 func prepareReportLineData(structIn interface{}, formatData []ColumnFormatData, summarizedReport bool) (lineData []string, err error) { 216 var mapStruct map[string]interface{} 217 var data interface{} 218 var dataFound bool 219 var sliceString []string 220 var joinedData string 221 222 mapStruct, err = utils.MarshalStructToJsonMap(structIn) 223 224 for _, columnData := range formatData { 225 // reset local vars 226 sliceString = nil 227 228 // if the report we are preparing is a summarized one (i.e., --summary true) 229 // we will skip appending column data not marked to be included in a summary report 230 if summarizedReport && !columnData.IsSummaryData { 231 continue 232 } 233 234 data, dataFound = mapStruct[columnData.DataKey] 235 236 if !dataFound { 237 // TODO: change back? 238 getLogger().Errorf("data not found in structure: key: `%s`", columnData.DataKey) 239 data = "<error: not found>" 240 //return 241 } 242 243 //fmt.Printf("data: `%v` (%T)\n", data, data) 244 switch typedData := data.(type) { 245 case string: 246 // replace line feeds with spaces in description 247 if typedData != "" { 248 if columnData.ReplaceLineFeeds { 249 // For tabbed text tables, replace line feeds with spaces 250 typedData = strings.ReplaceAll(typedData, "\n", " ") 251 } 252 } 253 lineData = append(lineData, typedData) 254 case bool: 255 lineData = append(lineData, strconv.FormatBool(typedData)) 256 case int: 257 lineData = append(lineData, strconv.Itoa(typedData)) 258 case float64: 259 // NOTE: JSON Unmarshal() always decodes JSON Numbers as "float64" type 260 lineData = append(lineData, strconv.FormatFloat(typedData, 'f', -1, 64)) 261 case []interface{}: 262 // convert to []string 263 for _, value := range typedData { 264 sliceString = append(sliceString, value.(string)) 265 } 266 267 // separate each entry with a comma (and space for readability) 268 joinedData = strings.Join(sliceString, ", ") 269 270 if joinedData != "" { 271 // replace line feeds with spaces in description 272 if columnData.ReplaceLineFeeds { 273 // For tabbed text tables, replace line feeds with spaces 274 joinedData = strings.ReplaceAll(joinedData, "\n", " ") 275 } 276 } 277 278 if summarizedReport { 279 if len(sliceString) > 0 { 280 lineData = append(lineData, sliceString[0]) 281 } 282 continue 283 } 284 285 lineData = append(lineData, joinedData) 286 case nil: 287 //getLogger().Tracef("nil value for column: `%v`", columnData.DataKey) 288 lineData = append(lineData, REPORT_LIST_VALUE_NONE) 289 default: 290 err = getLogger().Errorf("Unexpected type for report data: column: %s, type: `%T`, value: `%v`", columnData.DataKey, data, data) 291 } 292 } 293 294 return 295 }