github.com/anchore/syft@v1.38.2/syft/format/table/encoder.go (about) 1 package table 2 3 import ( 4 "fmt" 5 "io" 6 "sort" 7 "strings" 8 9 "github.com/charmbracelet/lipgloss" 10 "github.com/olekukonko/tablewriter" 11 "github.com/olekukonko/tablewriter/renderer" 12 "github.com/olekukonko/tablewriter/tw" 13 14 "github.com/anchore/syft/syft/sbom" 15 ) 16 17 const ID sbom.FormatID = "syft-table" 18 19 type encoder struct { 20 } 21 22 func NewFormatEncoder() sbom.FormatEncoder { 23 return encoder{} 24 } 25 26 func (e encoder) ID() sbom.FormatID { 27 return ID 28 } 29 30 func (e encoder) Aliases() []string { 31 return []string{ 32 "table", 33 } 34 } 35 36 func (e encoder) Version() string { 37 return sbom.AnyVersion 38 } 39 40 func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error { 41 var rows [][]string 42 43 columns := []string{"Name", "Version", "Type"} 44 for _, p := range s.Artifacts.Packages.Sorted() { 45 row := []string{ 46 p.Name, 47 p.Version, 48 string(p.Type), 49 } 50 rows = append(rows, row) 51 } 52 53 if len(rows) == 0 { 54 _, err := fmt.Fprintln(writer, "No packages discovered") 55 return err 56 } 57 58 // sort by name, version, then type 59 sort.SliceStable(rows, func(i, j int) bool { 60 for col := 0; col < len(columns); col++ { 61 if rows[i][col] != rows[j][col] { 62 return rows[i][col] < rows[j][col] 63 } 64 } 65 return false 66 }) 67 68 columns = append(columns, "") // add a column for duplicate annotations 69 rows = markDuplicateRows(rows) 70 71 table := newTableWriter(writer, columns) 72 73 if err := table.Bulk(rows); err != nil { 74 return fmt.Errorf("failed to add table rows: %w", err) 75 } 76 77 return table.Render() 78 } 79 80 func newTableWriter(writer io.Writer, columns []string) *tablewriter.Table { 81 // Here’s a simplified diagram of a table with a header, rows, and footer: 82 // 83 // [Borders.Top] 84 // | Header1 | Header2 | (Line below header: Lines.ShowTop) 85 // [Separators.BetweenRows] 86 // | Row1 | Row1 | 87 // [Separators.BetweenRows] 88 // | Row2 | Row2 | 89 // [Lines.ShowBottom] 90 // | Footer1 | Footer2 | 91 // [Borders.Bottom] 92 // 93 // So for example: 94 // ┌──────┬─────┐ <- Borders.Top 95 // │ NAME │ AGE │ 96 // ├──────┼─────┤ <- Lines.ShowTop 97 // │ Alice│ 25 │ 98 // ├──────┼─────┤ <- Separators.BetweenRows 99 // │ Bob │ 30 │ 100 // ├──────┼─────┤ <- Lines.ShowBottom 101 // │ Total│ 2 │ 102 // └──────┴─────┘ <- Borders.Bottom 103 104 return tablewriter.NewTable(writer, 105 tablewriter.WithHeader(columns), 106 tablewriter.WithHeaderAutoFormat(tw.On), 107 tablewriter.WithHeaderAutoWrap(tw.WrapNone), 108 tablewriter.WithHeaderAlignment(tw.AlignLeft), 109 tablewriter.WithRowAutoFormat(tw.Off), 110 tablewriter.WithRowAutoWrap(tw.WrapNone), 111 tablewriter.WithRowAlignment(tw.AlignLeft), 112 tablewriter.WithTrimSpace(tw.On), 113 tablewriter.WithAutoHide(tw.On), 114 tablewriter.WithRenderer(renderer.NewBlueprint()), 115 tablewriter.WithBehavior( 116 tw.Behavior{ 117 TrimSpace: tw.On, 118 AutoHide: tw.On, 119 }, 120 ), 121 tablewriter.WithPadding( 122 tw.Padding{ 123 Left: "", 124 Right: " ", 125 Top: "", 126 Bottom: "", 127 }, 128 ), 129 tablewriter.WithRendition( 130 tw.Rendition{ 131 Symbols: tw.NewSymbols(tw.StyleNone), 132 Borders: tw.Border{ 133 Left: tw.Off, 134 Top: tw.Off, 135 Right: tw.Off, 136 Bottom: tw.Off, 137 }, 138 Settings: tw.Settings{ 139 Separators: tw.Separators{ 140 ShowHeader: tw.Off, 141 ShowFooter: tw.Off, 142 BetweenRows: tw.Off, 143 BetweenColumns: tw.Off, 144 }, 145 Lines: tw.Lines{ 146 ShowTop: tw.Off, 147 ShowBottom: tw.Off, 148 ShowHeaderLine: tw.Off, 149 ShowFooterLine: tw.Off, 150 }, 151 }, 152 }, 153 ), 154 ) 155 } 156 157 func markDuplicateRows(items [][]string) [][]string { 158 seen := map[string]int{} 159 var result [][]string 160 161 for _, v := range items { 162 key := strings.Join(v, "|") 163 if _, ok := seen[key]; ok { 164 // dup! 165 seen[key]++ 166 continue 167 } 168 169 seen[key] = 1 170 result = append(result, v) 171 } 172 173 style := lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) 174 for i, v := range result { 175 key := strings.Join(v, "|") 176 // var name string 177 var annotation string 178 switch seen[key] { 179 case 0, 1: 180 case 2: 181 annotation = "(+1 duplicate)" 182 default: 183 annotation = fmt.Sprintf("(+%d duplicates)", seen[key]-1) 184 } 185 186 annotation = style.Render(annotation) 187 result[i] = append(v, annotation) 188 } 189 190 return result 191 }