build(deps): bump github.com/olekukonko/tablewriter from 1.0.8 to 1.0.9

Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 1.0.8 to 1.0.9.
- [Commits](https://github.com/olekukonko/tablewriter/compare/v1.0.8...v1.0.9)

---
updated-dependencies:
- dependency-name: github.com/olekukonko/tablewriter
  dependency-version: 1.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-08-26 06:40:11 +00:00
committed by GitHub
parent db583c4644
commit 5e6fc50e5e
23 changed files with 2385 additions and 383 deletions
+3 -3
View File
@@ -28,7 +28,7 @@ go get github.com/olekukonko/tablewriter@v0.0.5
#### Latest Version
The latest stable version
```bash
go get github.com/olekukonko/tablewriter@v1.0.7
go get github.com/olekukonko/tablewriter@v1.0.9
```
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@@ -62,7 +62,7 @@ func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.0.7", "latest"},
{"tablewriter", "v1.0.9", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.0.7 │ latest │
│ tablewriter │ v1.0.9 │ latest │
└─────────────┴─────────┴────────┘
```
+6
View File
@@ -688,6 +688,12 @@ func (bb *BehaviorConfigBuilder) WithCompactMerge(state tw.State) *BehaviorConfi
return bb
}
// WithAutoHeader enables/disables automatic header extraction for structs in Bulk.
func (bb *BehaviorConfigBuilder) WithAutoHeader(state tw.State) *BehaviorConfigBuilder {
bb.config.Structs.AutoHeader = state
return bb
}
// ColumnConfigBuilder configures column-specific settings
type ColumnConfigBuilder struct {
parent *ConfigBuilder
+12
View File
@@ -717,6 +717,10 @@ func defaultConfig() Config {
Behavior: tw.Behavior{
AutoHide: tw.Off,
TrimSpace: tw.On,
Structs: tw.Struct{
AutoHeader: tw.Off,
Tags: []string{"json", "db"},
},
},
}
}
@@ -844,6 +848,14 @@ func mergeConfig(dst, src Config) Config {
dst.Behavior.Compact = src.Behavior.Compact
dst.Behavior.Header = src.Behavior.Header
dst.Behavior.Footer = src.Behavior.Footer
dst.Behavior.Footer = src.Behavior.Footer
dst.Behavior.Structs.AutoHeader = src.Behavior.Structs.AutoHeader
// check lent of tags
if len(src.Behavior.Structs.Tags) > 0 {
dst.Behavior.Structs.Tags = src.Behavior.Structs.Tags
}
if src.Widths.Global != 0 {
dst.Widths.Global = src.Widths.Global
+14 -1
View File
@@ -523,9 +523,22 @@ func (f *Blueprint) renderLine(ctx tw.Formatting) {
isTotalPattern := false
// Case-insensitive check for "total"
if isHMergeStart && colIndex > 0 {
if prevCellCtx, ok := ctx.Row.Current[colIndex-1]; ok {
if strings.Contains(strings.ToLower(prevCellCtx.Data), "total") {
isTotalPattern = true
f.logger.Debugf("renderLine: total pattern in row in %d", colIndex)
}
}
}
// Get the alignment from the configuration
align = cellCtx.Align
// Override alignment for footer merged cells
if (ctx.Row.Position == tw.Footer && isHMergeStart) || isTotalPattern {
if align != tw.AlignRight {
if align == tw.AlignNone {
f.logger.Debugf("renderLine: Applying AlignRight HMerge/TOTAL override for Footer col %d. Original/default align was: %s", colIndex, align)
align = tw.AlignRight
}
+3 -4
View File
@@ -1,7 +1,6 @@
package tablewriter
import (
"fmt"
"github.com/olekukonko/errors"
"github.com/olekukonko/tablewriter/pkg/twwidth"
"github.com/olekukonko/tablewriter/tw"
@@ -90,7 +89,7 @@ func (t *Table) Start() error {
if !t.renderer.Config().Streaming {
// Check if the configured renderer actually supports streaming.
t.logger.Error("Configured renderer does not support streaming.")
return fmt.Errorf("renderer does not support streaming")
return errors.Newf("renderer does not support streaming")
}
//t.renderer.Start(t.writer)
@@ -208,7 +207,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
rawCellsSlice, err := t.convertCellsToStrings(row, t.config.Row)
if err != nil {
t.logger.Errorf("streamAppendRow: Failed to convert row to strings: %v", err)
return fmt.Errorf("failed to convert row to strings: %w", err)
return errors.Newf("failed to convert row to strings").Wrap(err)
}
if len(rawCellsSlice) == 0 {
@@ -221,7 +220,7 @@ func (t *Table) streamAppendRow(row interface{}) error {
}
if err := t.ensureStreamWidthsCalculated(rawCellsSlice, t.config.Row); err != nil {
return fmt.Errorf("failed to establish stream column count/widths: %w", err)
return errors.New("failed to establish stream column count/widths").Wrap(err)
}
// Now, check for column mismatch if a column count has been established.
+45 -24
View File
@@ -2,7 +2,6 @@ package tablewriter
import (
"bytes"
"fmt"
"github.com/olekukonko/errors"
"github.com/olekukonko/ll"
"github.com/olekukonko/ll/lh"
@@ -180,65 +179,87 @@ func (t *Table) Caption(caption tw.Caption) *Table { // This is the one we modif
// This method always contributes to a single logical row in the table.
// To add multiple distinct rows, call Append multiple times (once for each row's data)
// or use the Bulk() method if providing a slice where each element is a row.
func (t *Table) Append(rows ...interface{}) error { // rows is already []interface{}
func (t *Table) Append(rows ...interface{}) error {
t.ensureInitialized()
if t.config.Stream.Enable && t.hasPrinted {
// Streaming logic remains unchanged, as AutoHeader is a batch-mode concept.
t.logger.Debugf("Append() called in streaming mode with %d items for a single row", len(rows))
var rowItemForStream interface{}
if len(rows) == 1 {
rowItemForStream = rows[0]
} else {
rowItemForStream = rows // Pass the slice of items if multiple args
rowItemForStream = rows
}
if err := t.streamAppendRow(rowItemForStream); err != nil {
t.logger.Errorf("Error rendering streaming row: %v", err)
return fmt.Errorf("failed to stream append row: %w", err)
return errors.Newf("failed to stream append row").Wrap(err)
}
return nil
}
//Batch Mode Logic
// Batch Mode Logic
t.logger.Debugf("Append (Batch) received %d arguments: %v", len(rows), rows)
var cellsSource interface{}
if len(rows) == 1 {
cellsSource = rows[0]
t.logger.Debug("Append (Batch): Single argument provided. Treating it as the source for row cells.")
} else {
cellsSource = rows // 'rows' is []interface{} containing all arguments
t.logger.Debug("Append (Batch): Multiple arguments provided. Treating them directly as cells for one row.")
cellsSource = rows
}
// Check if we should attempt to auto-generate headers from this append operation.
// Conditions: AutoHeader is on, no headers are set yet, and this is the first data row.
isFirstRow := len(t.rows) == 0
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 && isFirstRow {
t.logger.Debug("Append: Triggering AutoHeader for the first row.")
headers := t.extractHeadersFromStruct(cellsSource)
if len(headers) > 0 {
// Set the extracted headers. The Header() method handles the rest.
t.Header(headers)
}
}
if err := t.appendSingle(cellsSource); err != nil {
// The rest of the function proceeds as before, converting the data to string lines.
lines, err := t.toStringLines(cellsSource, t.config.Row)
if err != nil {
t.logger.Errorf("Append (Batch) failed for cellsSource %v: %v", cellsSource, err)
return err
}
t.rows = append(t.rows, lines)
t.logger.Debugf("Append (Batch) completed for one row, total rows in table: %d", len(t.rows))
return nil
}
// Bulk adds multiple rows from a slice to the table (legacy method).
// Parameter rows must be a slice compatible with stringer or []string.
// Returns an error if the input is invalid or appending fails.
// Bulk adds multiple rows from a slice to the table.
// If Behavior.AutoHeader is enabled, no headers set, and rows is a slice of structs,
// automatically extracts/sets headers from the first struct.
func (t *Table) Bulk(rows interface{}) error {
t.logger.Debug("Starting Bulk operation")
rv := reflect.ValueOf(rows)
if rv.Kind() != reflect.Slice {
err := errors.Newf("Bulk expects a slice, got %T", rows)
t.logger.Debugf("Bulk error: %v", err)
return err
return errors.Newf("Bulk expects a slice, got %T", rows)
}
if rv.Len() == 0 {
return nil
}
// AutoHeader logic remains here, as it's a "Bulk" operation concept.
if t.config.Behavior.Structs.AutoHeader.Enabled() && len(t.headers) == 0 {
first := rv.Index(0).Interface()
// We can now correctly get headers from pointers or embedded structs
headers := t.extractHeadersFromStruct(first)
if len(headers) > 0 {
t.Header(headers)
}
}
// The rest of the logic is now just a loop over Append.
for i := 0; i < rv.Len(); i++ {
row := rv.Index(i).Interface()
t.logger.Debugf("Processing bulk row %d: %v", i, row)
if err := t.appendSingle(row); err != nil {
t.logger.Debugf("Bulk append failed at index %d: %v", i, err)
if err := t.Append(row); err != nil { // Use Append
return err
}
}
t.logger.Debugf("Bulk completed, processed %d rows", rv.Len())
return nil
}
@@ -1383,13 +1404,13 @@ func (t *Table) render() error {
if err != nil {
t.writer = originalWriter
t.logger.Errorf("prepareContexts failed: %v", err)
return fmt.Errorf("failed to prepare table contexts: %w", err)
return errors.Newf("failed to prepare table contexts").Wrap(err)
}
if err := ctx.renderer.Start(t.writer); err != nil {
t.writer = originalWriter
t.logger.Errorf("Renderer Start() error: %v", err)
return fmt.Errorf("renderer start failed: %w", err)
return errors.Newf("renderer start failed").Wrap(err)
}
renderError := false
@@ -1404,7 +1425,7 @@ func (t *Table) render() error {
if renderErr := renderFn(ctx, mctx); renderErr != nil {
t.logger.Errorf("Renderer section error (%s): %v", sectionName, renderErr)
if !renderError {
firstRenderErr = fmt.Errorf("failed to render %s section: %w", sectionName, renderErr)
firstRenderErr = errors.Newf("failed to render %s section", sectionName).Wrap(renderErr)
}
renderError = true
break
@@ -1414,7 +1435,7 @@ func (t *Table) render() error {
if closeErr := ctx.renderer.Close(); closeErr != nil {
t.logger.Errorf("Renderer Close() error: %v", closeErr)
if !renderError {
firstRenderErr = fmt.Errorf("renderer close failed: %w", closeErr)
firstRenderErr = errors.Newf("renderer close failed").Wrap(closeErr)
}
renderError = true
}
+15
View File
@@ -141,6 +141,18 @@ type Compact struct {
Merge State // Merge enables compact width calculation during cell merging, optimizing space allocation.
}
// Struct holds settings for struct-based operations like AutoHeader.
type Struct struct {
// AutoHeader automatically extracts and sets headers from struct fields when Bulk is called with a slice of structs.
// Uses JSON tags if present, falls back to field names (title-cased). Skips unexported or json:"-" fields.
// Enabled by default for convenience.
AutoHeader State
// Tags is a priority-ordered list of struct tag keys to check for header names.
// The first tag found on a field will be used. Defaults to ["json", "db"].
Tags []string
}
// Behavior defines settings that control table rendering behaviors, such as column visibility and content formatting.
type Behavior struct {
AutoHide State // AutoHide determines whether empty columns are hidden. Ignored in streaming mode.
@@ -152,6 +164,9 @@ type Behavior struct {
// Compact enables optimized width calculation for merged cells, such as in horizontal merges,
// by systematically determining the most efficient width instead of scaling by the number of columns.
Compact Compact
// Structs contains settings for how struct data is processed.
Structs Struct
}
// Padding defines the spacing characters around cell content in all four directions.
+106 -72
View File
@@ -1197,6 +1197,10 @@ func (t *Table) convertToString(value interface{}) string {
// convertItemToCells is responsible for converting a single input item (which could be
// a struct, a basic type, or an item implementing Stringer/Formatter) into a slice
// of strings, where each string represents a cell for the table row.
// zoo.go
// convertItemToCells is responsible for converting a single input item into a slice of strings.
// It now uses the unified struct parser for structs.
func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
t.logger.Debugf("convertItemToCells: Converting item of type %T", item)
@@ -1204,10 +1208,10 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
if t.stringer != nil {
res, err := t.convertToStringer(item)
if err == nil {
t.logger.Debugf("convertItemToCells: Used custom table stringer (t.stringer) for type %T. Produced %d cells: %v", item, len(res), res)
t.logger.Debugf("convertItemToCells: Used custom table stringer for type %T. Produced %d cells: %v", item, len(res), res)
return res, nil
}
t.logger.Warnf("convertItemToCells: Custom table stringer (t.stringer) was set but incompatible or errored for type %T: %v. Will attempt other conversion methods.", item, err)
t.logger.Warnf("convertItemToCells: Custom table stringer was set but incompatible for type %T: %v. Will attempt other methods.", item, err)
}
// 2. Handle untyped nil directly.
@@ -1216,85 +1220,26 @@ func (t *Table) convertItemToCells(item interface{}) ([]string, error) {
return []string{""}, nil
}
itemValue := reflect.ValueOf(item)
itemType := itemValue.Type()
// 3. Handle pointers: Dereference pointers to get to the underlying struct or value.
if itemType.Kind() == reflect.Ptr {
if itemValue.IsNil() {
t.logger.Debugf("convertItemToCells: Item is a nil pointer of type %s. Returning single empty cell.", itemType.String())
return []string{""}, nil
}
itemValue = itemValue.Elem()
itemType = itemValue.Type()
t.logger.Debugf("convertItemToCells: Dereferenced pointer, now processing type %s.", itemType.String())
// 3. Use the new unified struct parser. It handles pointers and embedding.
// We only care about the values it returns.
_, values := t.extractFieldsAndValuesFromStruct(item)
if values != nil {
t.logger.Debugf("convertItemToCells: Structs %T reflected into %d cells: %v", item, len(values), values)
return values, nil
}
// 4. Special handling for structs:
if itemType.Kind() == reflect.Struct {
// Check if the original item (before potential dereference) implements Formatter or Stringer.
if formatter, ok := item.(tw.Formatter); ok {
t.logger.Debugf("convertItemToCells: Struct item (type %s) is tw.Formatter. Using Format(). Resulting in 1 cell.", itemType.Name())
return []string{formatter.Format()}, nil
}
if stringer, ok := item.(fmt.Stringer); ok {
t.logger.Debugf("convertItemToCells: Struct item (type %s) is fmt.Stringer. Using String(). Resulting in 1 cell.", itemType.Name())
return []string{stringer.String()}, nil
}
t.logger.Debugf("convertItemToCells: Item is a struct (type %s). Attempting generic field reflection to expand into multiple cells.", itemType.Name())
numFields := itemValue.NumField()
structCells := make([]string, 0, numFields)
hasProcessableFields := false
for i := 0; i < numFields; i++ {
fieldMeta := itemType.Field(i)
if fieldMeta.PkgPath != "" {
t.logger.Debugf("convertItemToCells: Skipping unexported field %s in struct %s", fieldMeta.Name, itemType.Name())
continue
}
hasProcessableFields = true // Mark true if we encounter any exported field
jsonTag := fieldMeta.Tag.Get("json")
if jsonTag == "-" {
t.logger.Debugf("convertItemToCells: Skipping field %s in struct %s due to json:\"-\" tag", fieldMeta.Name, itemType.Name())
continue
}
fieldReflectedValue := itemValue.Field(i)
if strings.Contains(jsonTag, ",omitempty") && fieldReflectedValue.IsZero() {
t.logger.Debugf("convertItemToCells: Omitting zero value for field %s in struct %s due to omitempty tag", fieldMeta.Name, itemType.Name())
structCells = append(structCells, "")
continue
}
structCells = append(structCells, t.convertToString(fieldReflectedValue.Interface()))
}
// Only return expanded cells if there were processable fields.
// If a struct has no exported fields, or all were skipped via json:"-",
// it should still produce output (e.g. fmt.Sprintf of the struct) rather than an empty row.
if hasProcessableFields {
t.logger.Debugf("convertItemToCells: Struct %s reflected into %d cells: %v", itemType.Name(), len(structCells), structCells)
return structCells, nil
}
t.logger.Warnf("convertItemToCells: Struct %s has no processable exported fields. Falling back to Sprintf for the whole item (resulting in 1 cell).", itemType.Name())
return []string{t.convertToString(item)}, nil // 'item' is the original potentially pointer type
}
// 5. Item is NOT a struct. It might be a basic type or a non-struct type implementing Formatter/Stringer.
// These should all result in a single cell.
// 4. Fallback for any other single item (e.g., basic types, or types that implement Stringer/Formatter).
// This code path is now for non-struct types.
if formatter, ok := item.(tw.Formatter); ok {
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format(). Resulting in 1 cell.", item)
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is tw.Formatter. Using Format().", item)
return []string{formatter.Format()}, nil
}
if stringer, ok := item.(fmt.Stringer); ok {
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String(). Resulting in 1 cell.", item)
t.logger.Debugf("convertItemToCells: Item (non-struct, type %T) is fmt.Stringer. Using String().", item)
return []string{stringer.String()}, nil
}
// 6. Fallback for any other single item (e.g., basic types like int, string, bool):
t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type or unhandled by other mechanisms. Treating as single cell via convertToString.", item)
t.logger.Debugf("convertItemToCells: Item (type %T) is a basic type. Treating as single cell via convertToString.", item)
return []string{t.convertToString(item)}, nil
}
@@ -1694,3 +1639,92 @@ func (t *Table) updateWidths(row []string, widths tw.Mapper[int, int], padding t
}
}
}
// extractHeadersFromStruct is now a thin wrapper around the new unified function.
// It only cares about the header names.
func (t *Table) extractHeadersFromStruct(sample interface{}) []string {
headers, _ := t.extractFieldsAndValuesFromStruct(sample)
return headers
}
// extractFieldsAndValuesFromStruct is the new single source of truth for struct reflection.
// It recursively processes a struct, handling pointers and embedded structs,
// and returns two slices: one for header names and one for string-converted values.
func (t *Table) extractFieldsAndValuesFromStruct(sample interface{}) ([]string, []string) {
v := reflect.ValueOf(sample)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, nil
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, nil
}
typ := v.Type()
headers := make([]string, 0, typ.NumField())
values := make([]string, 0, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldValue := v.Field(i)
// Skip unexported fields
if field.PkgPath != "" {
continue
}
// Handle embedded structs recursively
if field.Anonymous {
h, val := t.extractFieldsAndValuesFromStruct(fieldValue.Interface())
if h != nil {
headers = append(headers, h...)
values = append(values, val...)
}
continue
}
var tagName string
skipField := false
// Loop through the priority list of configured tags (e.g., ["json", "db"])
for _, tagKey := range t.config.Behavior.Structs.Tags {
tagValue := field.Tag.Get(tagKey)
// If a tag is found...
if tagValue != "" {
// If the tag is "-", this field should be skipped entirely.
if tagValue == "-" {
skipField = true
break // Stop processing tags for this field.
}
// Otherwise, we've found our highest-priority tag. Store it and stop.
tagName = tagValue
break // Stop processing tags for this field.
}
}
// If the field was marked for skipping, continue to the next field.
if skipField {
continue
}
// Determine header name from the tag or fallback to the field name
headerName := field.Name
if tagName != "" {
headerName = strings.Split(tagName, ",")[0]
}
headers = append(headers, tw.Title(headerName))
// Determine value, respecting omitempty from the found tag
value := ""
if !strings.Contains(tagName, ",omitempty") || !fieldValue.IsZero() {
value = t.convertToString(fieldValue.Interface())
}
values = append(values, value)
}
return headers, values
}