olekukonko/tablewriter

GitHub: olekukonko/tablewriter

一个 Go 语言的富文本表格生成库,支持多种输出格式,用于在终端和 Web 应用中优雅地渲染结构化数据。

Stars: 4804 | Forks: 396

# Go 的 TableWriter [![Go](https://static.pigsec.cn/wp-content/uploads/repos/cas/31/31c96a8fe552af584b5983afc39ddf2e71ca6806185aad0eda564c77331e5b3e.svg)](https://github.com/olekukonko/tablewriter/actions/workflows/go.yml) [![Go 参考](https://pkg.go.dev/badge/github.com/olekukonko/tablewriter.svg)](https://pkg.go.dev/github.com/olekukonko/tablewriter) [![Go 报告卡](https://goreportcard.com/badge/github.com/olekukonko/tablewriter)](https://goreportcard.com/report/github.com/olekukonko/tablewriter) [![许可证](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![基准测试](https://img.shields.io/badge/benchmarks-included-success)](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 的终端中可见): ![带有长值的彩色表格](_readme/color_1.png "Title") #### 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() } ``` **输出**: ```
REGION QUARTER SALES
North Q1 & Q2 $2200.00
South Q1 $1000.00
South Q2 $1200.00
``` #### 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() } ``` ``` NAME AGE CITY Alice 25 yrs New York Bob 30 yrs Boston ``` #### 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) 文件。
标签:ASCII, CLI, EVTX分析, Go, HTML, Markdown, Ruby工具, WiFi技术, 日志审计