Skip to content

Commit 285bd2b

Browse files
nordlundjclaude
andauthored
Generic table output (#13)
This pull request refactors the way tabular data is printed in several CLI commands by introducing a new reusable table printer utility. Previously, each command manually calculated column widths and formatted output using `fmt.Printf`. Now, all commands use the new `table` package for dynamic and consistent table formatting, improving maintainability and output readability. Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2cd4047 commit 285bd2b

8 files changed

Lines changed: 203 additions & 73 deletions

File tree

cmd/instance_list.go

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
67

78
"cloudamqp-cli/client"
9+
"cloudamqp-cli/internal/table"
810
"github.com/spf13/cobra"
911
)
1012

@@ -33,50 +35,17 @@ var instanceListCmd = &cobra.Command{
3335
return nil
3436
}
3537

36-
// Calculate column widths
37-
idWidth := len("ID")
38-
nameWidth := len("NAME")
39-
planWidth := len("PLAN")
40-
regionWidth := len("REGION")
41-
42-
for _, instance := range instances {
43-
idLen := len(strconv.Itoa(instance.ID))
44-
if idLen > idWidth {
45-
idWidth = idLen
46-
}
47-
if len(instance.Name) > nameWidth {
48-
nameWidth = len(instance.Name)
49-
}
50-
if len(instance.Plan) > planWidth {
51-
planWidth = len(instance.Plan)
52-
}
53-
if len(instance.Region) > regionWidth {
54-
regionWidth = len(instance.Region)
55-
}
56-
}
57-
58-
// Add padding
59-
idWidth += 2
60-
nameWidth += 2
61-
planWidth += 2
62-
regionWidth += 2
63-
64-
// Create format strings
65-
headerFormat := fmt.Sprintf("%%-%ds %%-%ds %%-%ds %%-%ds\n", idWidth, nameWidth, planWidth, regionWidth)
66-
rowFormat := fmt.Sprintf("%%-%dd %%-%ds %%-%ds %%-%ds\n", idWidth, nameWidth, planWidth, regionWidth)
67-
68-
// Print table header
69-
fmt.Printf(headerFormat, "ID", "NAME", "PLAN", "REGION")
70-
fmt.Printf(headerFormat, "--", "----", "----", "------")
71-
72-
// Print instance data
38+
// Create table and populate data
39+
t := table.New(os.Stdout, "ID", "NAME", "PLAN", "REGION")
7340
for _, instance := range instances {
74-
fmt.Printf(rowFormat,
75-
instance.ID,
41+
t.AddRow(
42+
strconv.Itoa(instance.ID),
7643
instance.Name,
7744
instance.Plan,
78-
instance.Region)
45+
instance.Region,
46+
)
7947
}
48+
t.Print()
8049

8150
return nil
8251
},

cmd/instance_nodes.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56

67
"cloudamqp-cli/client"
8+
"cloudamqp-cli/internal/table"
79
"github.com/spf13/cobra"
810
)
911

@@ -47,11 +49,8 @@ var instanceNodesListCmd = &cobra.Command{
4749
return nil
4850
}
4951

50-
// Print table header
51-
fmt.Printf("%-20s %-12s %-10s %-10s %-15s\n", "NAME", "CONFIGURED", "RUNNING", "DISK_SIZE", "RABBITMQ_VERSION")
52-
fmt.Printf("%-20s %-12s %-10s %-10s %-15s\n", "----", "----------", "-------", "---------", "----------------")
53-
54-
// Print node data
52+
// Create table and populate data
53+
t := table.New(os.Stdout, "NAME", "CONFIGURED", "RUNNING", "DISK_SIZE", "RABBITMQ_VERSION")
5554
for _, node := range nodes {
5655
configured := "No"
5756
if node.Configured {
@@ -62,13 +61,15 @@ var instanceNodesListCmd = &cobra.Command{
6261
running = "Yes"
6362
}
6463
totalDisk := node.DiskSize + node.AdditionalDiskSize
65-
fmt.Printf("%-20s %-12s %-10s %-10dGB %-15s\n",
64+
t.AddRow(
6665
node.Name,
6766
configured,
6867
running,
69-
totalDisk,
70-
node.RabbitMQVersion)
68+
fmt.Sprintf("%d GB", totalDisk),
69+
node.RabbitMQVersion,
70+
)
7171
}
72+
t.Print()
7273

7374
return nil
7475
},

cmd/instance_plugins.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
56

67
"cloudamqp-cli/client"
8+
"cloudamqp-cli/internal/table"
79
"github.com/spf13/cobra"
810
)
911

@@ -47,18 +49,16 @@ var instancePluginsListCmd = &cobra.Command{
4749
return nil
4850
}
4951

50-
// Print table header
51-
fmt.Printf("%-30s %-10s\n", "NAME", "ENABLED")
52-
fmt.Printf("%-30s %-10s\n", "----", "-------")
53-
54-
// Print plugin data
52+
// Create table and populate data
53+
t := table.New(os.Stdout, "NAME", "ENABLED")
5554
for _, plugin := range plugins {
5655
enabled := "No"
5756
if plugin.Enabled {
5857
enabled = "Yes"
5958
}
60-
fmt.Printf("%-30s %-10s\n", plugin.Name, enabled)
59+
t.AddRow(plugin.Name, enabled)
6160
}
61+
t.Print()
6262

6363
return nil
6464
},

cmd/plans.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package cmd
22

33
import (
4-
"encoding/json"
54
"fmt"
5+
"os"
66

77
"cloudamqp-cli/client"
8+
"cloudamqp-cli/internal/table"
89
"github.com/spf13/cobra"
910
)
1011

@@ -36,12 +37,25 @@ var plansCmd = &cobra.Command{
3637
return nil
3738
}
3839

39-
output, err := json.MarshalIndent(plans, "", " ")
40-
if err != nil {
41-
return fmt.Errorf("failed to format response: %v", err)
40+
// Create table and populate data
41+
t := table.New(os.Stdout, "NAME", "PRICE", "BACKEND", "SHARED")
42+
for _, plan := range plans {
43+
shared := "No"
44+
if plan.Shared {
45+
shared = "Yes"
46+
}
47+
price := fmt.Sprintf("$%.2f", plan.Price)
48+
if plan.Price == 0 {
49+
price = "Free"
50+
}
51+
t.AddRow(
52+
plan.Name,
53+
price,
54+
plan.Backend,
55+
shared,
56+
)
4257
}
43-
44-
fmt.Printf("Available plans:\n%s\n", string(output))
58+
t.Print()
4559
return nil
4660
},
4761
}

cmd/team_list.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
6+
"strings"
57

68
"cloudamqp-cli/client"
9+
"cloudamqp-cli/internal/table"
710
"github.com/spf13/cobra"
811
)
912

@@ -32,14 +35,20 @@ var teamListCmd = &cobra.Command{
3235
return nil
3336
}
3437

35-
// Print table header
36-
fmt.Printf("%-40s\n", "EMAIL")
37-
fmt.Printf("%-40s\n", "-----")
38-
39-
// Print team member data
38+
// Create table and populate data
39+
t := table.New(os.Stdout, "EMAIL", "ROLES", "2FA")
4040
for _, member := range members {
41-
fmt.Printf("%-40s\n", member.Email)
41+
roles := strings.Join(member.Roles, ", ")
42+
if roles == "" {
43+
roles = "-"
44+
}
45+
tfa := "No"
46+
if member.TFAAuthEnabled {
47+
tfa = "Yes"
48+
}
49+
t.AddRow(member.Email, roles, tfa)
4250
}
51+
t.Print()
4352

4453
return nil
4554
},

cmd/vpc_list.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package cmd
22

33
import (
44
"fmt"
5+
"os"
6+
"strconv"
57

68
"cloudamqp-cli/client"
9+
"cloudamqp-cli/internal/table"
710
"github.com/spf13/cobra"
811
)
912

@@ -32,17 +35,17 @@ var vpcListCmd = &cobra.Command{
3235
return nil
3336
}
3437

35-
// Print table header
36-
fmt.Printf("%-20s %-18s %-30s\n", "NAME", "SUBNET", "REGION")
37-
fmt.Printf("%-20s %-18s %-30s\n", "----", "------", "------")
38-
39-
// Print VPC data
38+
// Create table and populate data
39+
t := table.New(os.Stdout, "ID", "NAME", "SUBNET", "REGION")
4040
for _, vpc := range vpcs {
41-
fmt.Printf("%-20s %-18s %-30s\n",
41+
t.AddRow(
42+
strconv.Itoa(vpc.ID),
4243
vpc.Name,
4344
vpc.Subnet,
44-
vpc.Region)
45+
vpc.Region,
46+
)
4547
}
48+
t.Print()
4649

4750
return nil
4851
},

internal/table/table.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
)
8+
9+
// Column represents a column in the table
10+
type Column struct {
11+
Header string
12+
Width int
13+
}
14+
15+
// Printer handles dynamic table printing with automatic width calculation
16+
type Printer struct {
17+
columns []Column
18+
rows [][]string
19+
writer io.Writer
20+
}
21+
22+
// New creates a new table printer
23+
func New(writer io.Writer, headers ...string) *Printer {
24+
columns := make([]Column, len(headers))
25+
for i, header := range headers {
26+
columns[i] = Column{
27+
Header: header,
28+
Width: len(header),
29+
}
30+
}
31+
return &Printer{
32+
columns: columns,
33+
rows: make([][]string, 0),
34+
writer: writer,
35+
}
36+
}
37+
38+
// AddRow adds a row of data to the table
39+
func (p *Printer) AddRow(values ...string) error {
40+
if len(values) != len(p.columns) {
41+
return fmt.Errorf("expected %d columns, got %d", len(p.columns), len(values))
42+
}
43+
44+
// Update column widths based on this row's values
45+
for i, value := range values {
46+
if len(value) > p.columns[i].Width {
47+
p.columns[i].Width = len(value)
48+
}
49+
}
50+
51+
p.rows = append(p.rows, values)
52+
return nil
53+
}
54+
55+
// Print outputs the table with calculated column widths
56+
func (p *Printer) Print() {
57+
// Add padding to widths
58+
for i := range p.columns {
59+
p.columns[i].Width += 2
60+
}
61+
62+
// Build format string
63+
formatParts := make([]string, len(p.columns))
64+
for i, col := range p.columns {
65+
formatParts[i] = fmt.Sprintf("%%-%ds", col.Width)
66+
}
67+
format := strings.Join(formatParts, " ") + "\n"
68+
69+
// Print header
70+
headers := make([]interface{}, len(p.columns))
71+
separators := make([]interface{}, len(p.columns))
72+
for i, col := range p.columns {
73+
headers[i] = col.Header
74+
separators[i] = strings.Repeat("-", col.Width)
75+
}
76+
fmt.Fprintf(p.writer, format, headers...)
77+
fmt.Fprintf(p.writer, format, separators...)
78+
79+
// Print rows
80+
for _, row := range p.rows {
81+
rowInterface := make([]interface{}, len(row))
82+
for i, v := range row {
83+
rowInterface[i] = v
84+
}
85+
fmt.Fprintf(p.writer, format, rowInterface...)
86+
}
87+
}

0 commit comments

Comments
 (0)