olekukonko/tablewriter
GitHub: olekukonko/tablewriter
一个 Go 语言的富文本表格生成库,支持多种输出格式,用于在终端和 Web 应用中优雅地渲染结构化数据。
Stars: 4804 | Forks: 396
# Go 的 TableWriter
[](https://github.com/olekukonko/tablewriter/actions/workflows/go.yml)
[](https://pkg.go.dev/github.com/olekukonko/tablewriter)
[](https://goreportcard.com/report/github.com/olekukonko/tablewriter)
[](LICENSE)
[](README.md#benchmarks)
`tablewriter` 是一个 Go 库,用于生成**富文本表格**,支持多种输出格式,包括 ASCII、Unicode、Markdown、HTML 和彩色终端。非常适合用于 CLI 工具、日志和 Web 应用程序。
### 主要特性
- **多格式渲染**:ASCII、Unicode、Markdown、HTML、ANSI 彩色
- **高级样式**:单元格合并、对齐、内边距、边框
- **灵活输入**:CSV、结构体、切片或流式数据
- **高性能**:极少的内存分配,缓冲区复用
- **现代特性**:支持泛型、分层合并、实时流式处理
### 安装
#### 旧版本 (v0.0.5)
适用于旧版应用程序:
```
go get github.com/olekukonko/tablewriter@v0.0.5
```
#### 最新版本
最新稳定版
```
go get github.com/olekukonko/tablewriter@v1.1.4
```
**警告:** `v1.0.0` 版本存在功能缺失,请勿使用。
### 为什么选择 TableWriter?
- **适配 CLI**:即时兼容终端输出
- **对数据库友好**:原生支持 `sql.Null*` 类型
- **安全**:自动转义 HTML/Markdown
- **可扩展**:支持自定义渲染器和格式化器
### 快速示例
```
package main
import (
"github.com/olekukonko/tablewriter"
"os"
)
func main() {
data := [][]string{
{"Package", "Version", "Status"},
{"tablewriter", "v0.0.5", "legacy"},
{"tablewriter", "v1.1.4", "latest"},
}
table := tablewriter.NewWriter(os.Stdout)
table.Header(data[0])
table.Bulk(data[1:])
table.Render()
}
```
**输出**:
```
┌─────────────┬─────────┬────────┐
│ PACKAGE │ VERSION │ STATUS │
├─────────────┼─────────┼────────┤
│ tablewriter │ v0.0.5 │ legacy │
│ tablewriter │ v1.1.4 │ latest │
└─────────────┴─────────┴────────┘
```
## 详细用法
使用 `NewTable` 或 `NewWriter` 创建表格,通过选项或 `Config` 结构体进行配置,使用 `Append` 或 `Bulk` 添加数据,并将其渲染到 `io.Writer`。使用像 `Blueprint` (ASCII)、`HTML`、`Markdown`、`Colorized` 或 `Ocean` (流式) 这样的渲染器。
以下是 API 原语映射到生成的 ASCII 表格的方式:
```
API Call ASCII Table Component
-------- ---------------------
table.Header([]string{"NAME", "AGE"}) ┌──────┬─────┐ ← Borders.Top
│ NAME │ AGE │ ← Header row
├──────┼─────┤ ← Lines.ShowTop (header separator)
table.Append([]string{"Alice", "25"}) │ Alice│ 25 │ ← Data row
├──────┼─────┤ ← Separators.BetweenRows
table.Append([]string{"Bob", "30"}) │ Bob │ 30 │ ← Data row
├──────┼─────┤ ← Lines.ShowBottom (footer separator)
table.Footer([]string{"Total", "2"}) │ Total│ 2 │ ← Footer row
└──────┴─────┘ ← Borders.Bottom
```
核心组件包括:
- **Renderer** - 实现了将表格数据转换为输出格式的核心接口。可用的渲染器包括 Blueprint (ASCII)、HTML、Markdown、Colorized (带颜色的 ASCII)、Ocean (流式 ASCII) 和 SVG。
- **Config** - 根配置结构体,控制所有表格的行为和外观
- **Behavior** - 控制高级渲染行为,包括自动隐藏空列、修剪行空白字符、页眉/页脚可见性,以及用于优化合并单元格计算的紧凑模式
- **CellConfig** - 用于表格部分(页眉、行、页脚)的综合配置模板。结合了格式化、内边距、对齐、过滤、回调和宽度约束,并支持全局和按列控制
- **StreamConfig** - 流式模式的配置,包括启用/禁用状态和严格的列验证
- **Rendition** - 定义渲染器如何格式化表格,并包含完整的视觉样式配置
- **Borders** - 控制表格的外部边框(顶部、底部、左侧、右侧边缘)的可见性
- **Lines** - 控制分隔不同表格部分的水平边界线(页眉上方/下方、页脚上方)
- **Separators** - 控制表格内容中行与行、列与列之间分隔符的可见性
- **Symbols** - 定义用于绘制表格边框、角和连接处的字符
在创建新表格时,可以使用各种 `tablewriter.With*()` 函数式选项对这些组件进行配置。
## 示例
### 基础示例
#### 1. 简单表格
创建一个带有页眉和行的基础表格。
##### 默认
```
package main
import (
"fmt"
"github.com/olekukonko/tablewriter"
"os"
)
type Age int
func (a Age) String() string {
return fmt.Sprintf("%d yrs", a)
}
func main() {
data := [][]any{
{"Alice", Age(25), "New York"},
{"Bob", Age(30), "Boston"},
}
table := tablewriter.NewTable(os.Stdout)
table.Header("Name", "Age", "City")
table.Bulk(data)
table.Render()
}
```
**输出**:
```
┌───────┬────────┬──────────┐
│ NAME │ AGE │ CITY │
├───────┼────────┼──────────┤
│ Alice │ 25 yrs │ New York │
│ Bob │ 30 yrs │ Boston │
└───────┴────────┴──────────┘
```
##### 带有自定义配置
```
package main
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
type Age int
func (a Age) String() string {
return fmt.Sprintf("%d yrs", a)
}
func main() {
data := [][]any{
{"Alice", Age(25), "New York"},
{"Bob", Age(30), "Boston"},
}
symbols := tw.NewSymbolCustom("Nature").
WithRow("~").
WithColumn("|").
WithTopLeft("🌱").
WithTopMid("🌿").
WithTopRight("🌱").
WithMidLeft("🍃").
WithCenter("❀").
WithMidRight("🍃").
WithBottomLeft("🌻").
WithBottomMid("🌾").
WithBottomRight("🌻")
table := tablewriter.NewTable(os.Stdout, tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{Symbols: symbols})))
table.Header("Name", "Age", "City")
table.Bulk(data)
table.Render()
}
```
```
🌱~~~~~~❀~~~~~~~~❀~~~~~~~~~🌱
| NAME | AGE | CITY |
🍃~~~~~~❀~~~~~~~~❀~~~~~~~~~🍃
| Alice | 25 yrs | New York |
| Bob | 30 yrs | Boston |
🌻~~~~~~❀~~~~~~~~❀~~~~~~~~~🌻
```
查看更多内容请参见[符号示例](https://github.com/olekukonko/tablewriter/blob/master/_example/symbols/main.go)
#### 2. Markdown 表格
生成用于文档的 Markdown 表格。
```
package main
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"os"
"strings"
"unicode"
)
type Name struct {
First string
Last string
}
// this will be ignored since Format() is present
func (n Name) String() string {
return fmt.Sprintf("%s %s", n.First, n.Last)
}
// Note: Format() overrides String() if both exist.
func (n Name) Format() string {
return fmt.Sprintf("%s %s", n.clean(n.First), n.clean(n.Last))
}
// clean ensures the first letter is capitalized and the rest are lowercase
func (n Name) clean(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
words := strings.Fields(s)
s = strings.Join(words, "")
if s == "" {
return s
}
// Capitalize the first letter
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}
type Age int
// Age int will be ignore and string will be used
func (a Age) String() string {
return fmt.Sprintf("%d yrs", a)
}
func main() {
data := [][]any{
{Name{"Al i CE", " Ma SK"}, Age(25), "New York"},
{Name{"bOb", "mar le y"}, Age(30), "Boston"},
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewMarkdown()),
)
table.Header([]string{"Name", "Age", "City"})
table.Bulk(data)
table.Render()
}
```
**输出**:
```
| NAME | AGE | CITY |
|:----------:|:------:|:--------:|
| Alice Mask | 25 yrs | New York |
| Bob Marley | 30 yrs | Boston |
```
#### 3. CSV 输入
从 CSV 文件创建表格并带有自定义行对齐方式。
```
package main
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"log"
"os"
)
func main() {
// Assuming "test.csv" contains: "First Name,Last Name,SSN\nJohn,Barry,123456\nKathy,Smith,687987"
table, err := tablewriter.NewCSV(os.Stdout, "test.csv", true)
if err != nil {
log.Fatalf("Error: %v", err)
}
table.Configure(func(config *tablewriter.Config) {
config.Row.Alignment.Global = tw.AlignLeft
})
table.Render()
}
```
**输出**:
```
┌────────────┬───────────┬─────────┐
│ FIRST NAME │ LAST NAME │ SSN │
├────────────┼───────────┼─────────┤
│ John │ Barry │ 123456 │
│ Kathy │ Smith │ 687987 │
└────────────┴───────────┴─────────┘
```
### 高级示例
#### 4. 带有长值的彩色表格
创建一个带有自动换行长值、按列设置颜色以及带有样式页脚的彩色表格(灵感来自 `TestColorizedLongValues` 和 `TestColorizedCustomColors`)。
```
package main
import (
"github.com/fatih/color"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
func main() {
data := [][]string{
{"1", "This is a very long description that needs wrapping for readability", "OK"},
{"2", "Short description", "DONE"},
{"3", "Another lengthy description requiring truncation or wrapping", "ERROR"},
}
// Configure colors: green headers, cyan/magenta rows, yellow footer
colorCfg := renderer.ColorizedConfig{
Header: renderer.Tint{
FG: renderer.Colors{color.FgGreen, color.Bold}, // Green bold headers
BG: renderer.Colors{color.BgHiWhite},
},
Column: renderer.Tint{
FG: renderer.Colors{color.FgCyan}, // Default cyan for rows
Columns: []renderer.Tint{
{FG: renderer.Colors{color.FgMagenta}}, // Magenta for column 0
{}, // Inherit default (cyan)
{FG: renderer.Colors{color.FgHiRed}}, // High-intensity red for column 2
},
},
Footer: renderer.Tint{
FG: renderer.Colors{color.FgYellow, color.Bold}, // Yellow bold footer
Columns: []renderer.Tint{
{}, // Inherit default
{FG: renderer.Colors{color.FgHiYellow}}, // High-intensity yellow for column 1
{}, // Inherit default
},
},
Border: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White borders
Separator: renderer.Tint{FG: renderer.Colors{color.FgWhite}}, // White separators
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewColorized(colorCfg)),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Formatting: tw.CellFormatting{AutoWrap: tw.WrapNormal}, // Wrap long content
Alignment: tw.CellAlignment{Global: tw.AlignLeft}, // Left-align rows
ColMaxWidths: tw.CellWidth{Global: 25},
},
Footer: tw.CellConfig{
Alignment: tw.CellAlignment{Global: tw.AlignRight},
},
}),
)
table.Header([]string{"ID", "Description", "Status"})
table.Bulk(data)
table.Footer([]string{"", "Total", "3"})
table.Render()
}
```
**输出**(颜色在兼容 ANSI 的终端中可见):

#### 5. 带有截断的流式表格
增量流式传输带有截断和页脚的表格,模拟实时数据源(灵感来自 `TestOceanStreamTruncation` 和 `TestOceanStreamSlowOutput`)。
```
package main
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"log"
"os"
"time"
)
func main() {
table := tablewriter.NewTable(os.Stdout, tablewriter.WithStreaming(tw.StreamConfig{Enable: true}))
// Start streaming
if err := table.Start(); err != nil {
log.Fatalf("Start failed: %v", err)
}
defer table.Close()
// Stream header
table.Header([]string{"ID", "Description", "Status"})
// Stream rows with simulated delay
data := [][]string{
{"1", "This description is too long", "OK"},
{"2", "Short desc", "DONE"},
{"3", "Another long description here", "ERROR"},
}
for _, row := range data {
table.Append(row)
time.Sleep(500 * time.Millisecond) // Simulate real-time data feed
}
// Stream footer
table.Footer([]string{"", "Total", "3"})
}
```
**输出**(增量显示):
```
┌────────┬───────────────┬──────────┐
│ ID │ DESCRIPTION │ STATUS │
├────────┼───────────────┼──────────┤
│ 1 │ This │ OK │
│ │ description │ │
│ │ is too long │ │
│ 2 │ Short desc │ DONE │
│ 3 │ Another long │ ERROR │
│ │ description │ │
│ │ here │ │
├────────┼───────────────┼──────────┤
│ │ Total │ 3 │
└────────┴───────────────┴──────────┘
```
**注意**:由于固定的列宽,长描述会被 `…` 截断。输出逐行显示,模拟实时数据源。
#### 6. 用于组织数据的分层合并
展示树状结构(例如组织架构)的分层合并(灵感来自 `TestMergeHierarchicalUnicode`)。
```
package main
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
func main() {
data := [][]string{
{"Engineering", "Backend", "API Team", "Alice"},
{"Engineering", "Backend", "Database Team", "Bob"},
{"Engineering", "Frontend", "UI Team", "Charlie"},
{"Marketing", "Digital", "SEO Team", "Dave"},
{"Marketing", "Digital", "Content Team", "Eve"},
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
Row: tw.CellConfig{
Merging: tw.CellMerging{Mode: tw.MergeHierarchical},
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
}),
)
table.Header([]string{"Department", "Division", "Team", "Lead"})
table.Bulk(data)
table.Render()
}
```
**输出**:
```
┌────────────┬──────────┬──────────────┬────────┐
│ DEPARTMENT │ DIVISION │ TEAM │ LEAD │
├────────────┼──────────┼──────────────┼────────┤
│ Engineering│ Backend │ API Team │ Alice │
│ │ ├──────────────┼────────┤
│ │ │ Database Team│ Bob │
│ │ Frontend ├──────────────┼────────┤
│ │ │ UI Team │ Charlie│
├────────────┼──────────┼──────────────┼────────┤
│ Marketing │ Digital │ SEO Team │ Dave │
│ │ ├──────────────┼────────┤
│ │ │ Content Team │ Eve │
└────────────┴──────────┴──────────────┴────────┘
```
**注意**:分层合并会对重复的值进行分组(例如,“Engineering” 跨越多行,“Backend” 跨越两个团队),从而创建出树状结构。
#### 7. 带有合并的自定义内边距
展示自定义内边距以及水平和垂直合并的组合(灵感来自 `merge_test.go` 中的 `TestMergeWithPadding`)。
```
package main
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
func main() {
data := [][]string{
{"1/1/2014", "Domain name", "Successful", "Successful"},
{"1/1/2014", "Domain name", "Pending", "Waiting"},
{"1/1/2014", "Domain name", "Successful", "Rejected"},
{"", "", "TOTAL", "$145.93"},
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Merging: tw.CellMerging{Mode: tw.MergeBoth},
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
},
Footer: tw.CellConfig{
Padding: tw.CellPadding{
Global: tw.Padding{Left: "*", Right: "*"},
PerColumn: []tw.Padding{{}, {}, {Bottom: "^"}, {Bottom: "^"}},
},
Alignment: tw.CellAlignment{PerColumn: []tw.Align{tw.Skip, tw.Skip, tw.AlignRight, tw.AlignLeft}},
},
}),
)
table.Header([]string{"Date", "Description", "Status", "Conclusion"})
table.Bulk(data)
table.Render()
}
```
**输出**:
```
┌──────────┬─────────────┬────────────┬────────────┐
│ DATE │ DESCRIPTION │ STATUS │ CONCLUSION │
├──────────┼─────────────┼────────────┴────────────┤
│ 1/1/2014 │ Domain name │ Successful │
│ │ ├────────────┬────────────┤
│ │ │ Pending │ Waiting │
│ │ ├────────────┼────────────┤
│ │ │ Successful │ Rejected │
├──────────┼─────────────┼────────────┼────────────┤
│ │ │ TOTAL │ $145.93 │
│ │ │^^^^^^^^^^^^│^^^^^^^^^^^^│
└──────────┴─────────────┴────────────┴────────────┘
```
#### 8. 嵌套表格
创建一个包含嵌套子表格的表格以实现复杂布局(灵感来自 `extra_test.go` 中的 `TestMasterClass`)。
```
package main
import (
"bytes"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
func main() {
// Helper to create a sub-table
createSubTable := func(s string) string {
var buf bytes.Buffer
table := tablewriter.NewTable(&buf,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Symbols: tw.NewSymbols(tw.StyleASCII),
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.On},
Lines: tw.Lines{ShowFooterLine: tw.On},
},
})),
tablewriter.WithConfig(tablewriter.Config{
MaxWidth: 10,
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
}),
)
table.Append([]string{s, s})
table.Append([]string{s, s})
table.Render()
return buf.String()
}
// Main table
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Settings: tw.Settings{Separators: tw.Separators{BetweenColumns: tw.On}},
})),
tablewriter.WithConfig(tablewriter.Config{
MaxWidth: 30,
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignCenter}},
}),
)
table.Append([]string{createSubTable("A"), createSubTable("B")})
table.Append([]string{createSubTable("C"), createSubTable("D")})
table.Render()
}
```
**输出**:
```
A | A │ B | B
---+--- │ ---+---
A | A │ B | B
C | C │ D | D
---+--- │ ---+---
C | C │ D | D
```
#### 9. 带有数据库的结构体
从结构体切片渲染表格,模拟数据库查询(灵感来自 `struct_test.go` 中的 `TestStructTableWithDB`)。
```
package main
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
type Employee struct {
ID int
Name string
Age int
Department string
Salary float64
}
func employeeStringer(e interface{}) []string {
emp, ok := e.(Employee)
if !ok {
return []string{"Error: Invalid type"}
}
return []string{
fmt.Sprintf("%d", emp.ID),
emp.Name,
fmt.Sprintf("%d", emp.Age),
emp.Department,
fmt.Sprintf("%.2f", emp.Salary),
}
}
func main() {
employees := []Employee{
{ID: 1, Name: "Alice Smith", Age: 28, Department: "Engineering", Salary: 75000.50},
{ID: 2, Name: "Bob Johnson", Age: 34, Department: "Marketing", Salary: 62000.00},
{ID: 3, Name: "Charlie Brown", Age: 45, Department: "HR", Salary: 80000.75},
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Symbols: tw.NewSymbols(tw.StyleRounded),
})),
tablewriter.WithStringer(employeeStringer),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{AutoFormat: tw.On},
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
},
Row: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignLeft}},
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
}),
)
table.Header([]string{"ID", "Name", "Age", "Department", "Salary"})
for _, emp := range employees {
table.Append(emp)
}
totalSalary := 0.0
for _, emp := range employees {
totalSalary += emp.Salary
}
table.Footer([]string{"", "", "", "Total", fmt.Sprintf("%.2f", totalSalary)})
table.Render()
}
```
**输出**:
```
╭────┬───────────────┬─────┬─────────────┬───────────╮
│ ID │ NAME │ AGE │ DEPARTMENT │ SALARY │
├────┼───────────────┼─────┼─────────────┼───────────┤
│ 1 │ Alice Smith │ 28 │ Engineering │ 75000.50 │
│ 2 │ Bob Johnson │ 34 │ Marketing │ 62000.00 │
│ 3 │ Charlie Brown │ 45 │ HR │ 80000.75 │
├────┼───────────────┼─────┼─────────────┼───────────┤
│ │ │ │ Total │ 217001.25 │
╰────┴───────────────┴─────┴─────────────┴───────────╯
```
#### 10. 简单的 HTML 表格
```
package main
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"os"
)
func main() {
data := [][]string{
{"North", "Q1 & Q2", "Q1 & Q2", "$2200.00"},
{"South", "Q1", "Q1", "$1000.00"},
{"South", "Q2", "Q2", "$1200.00"},
}
// Configure HTML with custom CSS classes and content escaping
htmlCfg := renderer.HTMLConfig{
TableClass: "sales-table",
HeaderClass: "table-header",
BodyClass: "table-body",
FooterClass: "table-footer",
RowClass: "table-row",
HeaderRowClass: "header-row",
FooterRowClass: "footer-row",
EscapeContent: true, // Escape HTML characters (e.g., "&" to "&")
}
table := tablewriter.NewTable(os.Stdout,
tablewriter.WithRenderer(renderer.NewHTML(htmlCfg)),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Merging: tw.CellMerging{Mode: tw.MergeHorizontal}, // Merge identical header cells
Alignment: tw.CellAlignment{Global: tw.AlignCenter},
},
Row: tw.CellConfig{
Merging: tw.CellMerging{Mode: tw.MergeHorizontal}, // Merge identical row cells
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
},
Footer: tw.CellConfig{Alignment: tw.CellAlignment{Global: tw.AlignRight}},
}),
)
table.Header([]string{"Region", "Quarter", "Quarter", "Sales"})
table.Bulk(data)
table.Footer([]string{"", "", "Total", "$4400.00"})
table.Render()
}
```
**输出**:
```
```
#### 11. SVG 支持
```
package main
import (
"fmt"
"github.com/olekukonko/ll"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"os"
)
type Age int
func (a Age) String() string {
return fmt.Sprintf("%d yrs", a)
}
func main() {
data := [][]any{
{"Alice", Age(25), "New York"},
{"Bob", Age(30), "Boston"},
}
file, err := os.OpenFile("out.svg", os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
ll.Fatal(err)
}
defer file.Close()
table := tablewriter.NewTable(file, tablewriter.WithRenderer(renderer.NewSVG()))
table.Header("Name", "Age", "City")
table.Bulk(data)
table.Render()
}
```
```
```
#### 12 简单应用
```
package main
import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
)
const (
folder = "📁"
file = "📄"
baseDir = "../"
indentStr = " "
)
func main() {
table := tablewriter.NewTable(os.Stdout, tablewriter.WithTrimSpace(tw.Off))
table.Header([]string{"Tree", "Size", "Permissions", "Modified"})
err := filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.Name() == "." || d.Name() == ".." {
return nil
}
// Calculate relative path depth
relPath, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
depth := 0
if relPath != "." {
depth = len(strings.Split(relPath, string(filepath.Separator))) - 1
}
indent := strings.Repeat(indentStr, depth)
var name string
if d.IsDir() {
name = fmt.Sprintf("%s%s %s", indent, folder, d.Name())
} else {
name = fmt.Sprintf("%s%s %s", indent, file, d.Name())
}
info, err := d.Info()
if err != nil {
return err
}
table.Append([]string{
name,
Size(info.Size()).String(),
info.Mode().String(),
Time(info.ModTime()).Format(),
})
return nil
})
if err != nil {
fmt.Fprintf(os.Stdout, "Error: %v\n", err)
return
}
table.Render()
}
const (
KB = 1024
MB = KB * 1024
GB = MB * 1024
TB = GB * 1024
)
type Size int64
func (s Size) String() string {
switch {
case s < KB:
return fmt.Sprintf("%d B", s)
case s < MB:
return fmt.Sprintf("%.2f KB", float64(s)/KB)
case s < GB:
return fmt.Sprintf("%.2f MB", float64(s)/MB)
case s < TB:
return fmt.Sprintf("%.2f GB", float64(s)/GB)
default:
return fmt.Sprintf("%.2f TB", float64(s)/TB)
}
}
type Time time.Time
func (t Time) Format() string {
now := time.Now()
diff := now.Sub(time.Time(t))
if diff.Seconds() < 60 {
return "just now"
} else if diff.Minutes() < 60 {
return fmt.Sprintf("%d minutes ago", int(diff.Minutes()))
} else if diff.Hours() < 24 {
return fmt.Sprintf("%d hours ago", int(diff.Hours()))
} else if diff.Hours() < 24*7 {
return fmt.Sprintf("%d days ago", int(diff.Hours()/24))
} else {
return time.Time(t).Format("Jan 2, 2006")
}
}
```
```
┌──────────────────┬─────────┬─────────────┬──────────────┐
│ TREE │ SIZE │ PERMISSIONS │ MODIFIED │
├──────────────────┼─────────┼─────────────┼──────────────┤
│ 📁 filetable │ 160 B │ drwxr-xr-x │ just now │
│ 📄 main.go │ 2.19 KB │ -rw-r--r-- │ 22 hours ago │
│ 📄 out.txt │ 0 B │ -rw-r--r-- │ just now │
│ 📁 testdata │ 128 B │ drwxr-xr-x │ 1 days ago │
│ 📄 a.txt │ 11 B │ -rw-r--r-- │ 1 days ago │
│ 📄 b.txt │ 17 B │ -rw-r--r-- │ 1 days ago │
│ 📁 symbols │ 128 B │ drwxr-xr-x │ just now │
│ 📄 main.go │ 4.58 KB │ -rw-r--r-- │ 1 hours ago │
│ 📄 out.txt │ 8.72 KB │ -rw-r--r-- │ just now │
└──────────────────┴─────────┴─────────────┴──────────────┘
```
## 变更
- `AutoFormat` 的变更请参见 [#261](https://github.com/olekukonko/tablewriter/issues/261)
## 新特性
- `Counting` 的变更请参见 [#294](https://github.com/olekukonko/tablewriter/issues/294)
## 命令行工具
`csv2table` 工具可将 CSV 文件转换为 ASCII 表格。详情请参见 `cmd/csv2table/csv2table.go`。
使用示例:
```
csv2table -f test.csv -h true -a left
```
## 贡献
欢迎各种贡献!请向 [GitHub 仓库](https://github.com/olekukonko/tablewriter)提交 issue 或 pull request。
## 许可证
MIT 许可证。详情请参见 [LICENSE](LICENSE) 文件。
| REGION | QUARTER | SALES | |
|---|---|---|---|
| North | Q1 & Q2 | $2200.00 | |
| South | Q1 | $1000.00 | |
| South | Q2 | $1200.00 | |
| Total | $4400.00 | ||
标签:ASCII, CLI, EVTX分析, Go, HTML, Markdown, Ruby工具, WiFi技术, 日志审计