From e7b15b3cde3a19350097a88a06ab8550247e7f82 Mon Sep 17 00:00:00 2001 From: Neil Hanlon Date: Fri, 26 Jul 2024 22:17:05 -0400 Subject: [PATCH] feat(peridot-cli/task-info): fetch and display task details given a task ID, fetch its details and display them to a table or to json with `-o json`. Table view also adds a calculated task duration and can optionally include the submitter information as well as a link to logs for the task. * --no-color - to skip colorizing output * --L|--logs - include column with link to logs * --submitter - show submitter * --no-wait - control whether to wait until a task completes to output (table mode) --- go.mod | 2 + go.sum | 2 + peridot/cmd/v1/peridot/BUILD.bazel | 4 +- peridot/cmd/v1/peridot/main.go | 12 + peridot/cmd/v1/peridot/task_info.go | 310 +++++ peridot/cmd/v1/peridot/utils.go | 17 + repositories.bzl | 6 + .../olekukonko/tablewriter/BUILD.bazel | 27 + .../olekukonko/tablewriter/LICENSE.md | 19 + .../olekukonko/tablewriter/README.md | 431 ++++++ .../github.com/olekukonko/tablewriter/csv.go | 52 + .../tablewriter/csv2table/BUILD.bazel | 16 + .../tablewriter/csv2table/README.md | 43 + .../tablewriter/csv2table/csv2table.go | 85 ++ .../github.com/olekukonko/tablewriter/go.mod | 5 + .../github.com/olekukonko/tablewriter/go.sum | 2 + .../olekukonko/tablewriter/table.go | 967 ++++++++++++++ .../olekukonko/tablewriter/table_test.go | 1182 +++++++++++++++++ .../tablewriter/table_with_color.go | 136 ++ .../olekukonko/tablewriter/testdata/test.csv | 4 + .../tablewriter/testdata/test_info.csv | 4 + .../github.com/olekukonko/tablewriter/util.go | 93 ++ .../github.com/olekukonko/tablewriter/wrap.go | 99 ++ .../olekukonko/tablewriter/wrap_test.go | 58 + vendor/modules.txt | 3 + 25 files changed, 3578 insertions(+), 1 deletion(-) create mode 100644 peridot/cmd/v1/peridot/task_info.go create mode 100644 vendor/github.com/olekukonko/tablewriter/BUILD.bazel create mode 100644 vendor/github.com/olekukonko/tablewriter/LICENSE.md create mode 100644 vendor/github.com/olekukonko/tablewriter/README.md create mode 100644 vendor/github.com/olekukonko/tablewriter/csv.go create mode 100644 vendor/github.com/olekukonko/tablewriter/csv2table/BUILD.bazel create mode 100644 vendor/github.com/olekukonko/tablewriter/csv2table/README.md create mode 100644 vendor/github.com/olekukonko/tablewriter/csv2table/csv2table.go create mode 100644 vendor/github.com/olekukonko/tablewriter/go.mod create mode 100644 vendor/github.com/olekukonko/tablewriter/go.sum create mode 100644 vendor/github.com/olekukonko/tablewriter/table.go create mode 100644 vendor/github.com/olekukonko/tablewriter/table_test.go create mode 100644 vendor/github.com/olekukonko/tablewriter/table_with_color.go create mode 100644 vendor/github.com/olekukonko/tablewriter/testdata/test.csv create mode 100644 vendor/github.com/olekukonko/tablewriter/testdata/test_info.csv create mode 100644 vendor/github.com/olekukonko/tablewriter/util.go create mode 100644 vendor/github.com/olekukonko/tablewriter/wrap.go create mode 100644 vendor/github.com/olekukonko/tablewriter/wrap_test.go diff --git a/go.mod b/go.mod index 3ee9b577..2425cfe8 100644 --- a/go.mod +++ b/go.mod @@ -117,6 +117,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -184,4 +185,5 @@ replace ( peridot.resf.org/peridot/pb => ./bazel-bin/peridot/proto/v1/peridotpb_go_proto_/peridot.resf.org/peridot/pb peridot.resf.org/peridot/yumrepofs/pb => ./bazel-bin/peridot/proto/v1/yumrepofs/yumrepofspb_go_proto_/peridot.resf.org/peridot/yumrepofs/pb ) + // sync-replace-end diff --git a/go.sum b/go.sum index df3efe8c..160f3a4f 100644 --- a/go.sum +++ b/go.sum @@ -452,6 +452,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= diff --git a/peridot/cmd/v1/peridot/BUILD.bazel b/peridot/cmd/v1/peridot/BUILD.bazel index 8b415878..eb2e0fed 100644 --- a/peridot/cmd/v1/peridot/BUILD.bazel +++ b/peridot/cmd/v1/peridot/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "project_list.go", "task.go", "task_logs.go", + "task_info.go", "utils.go", ], data = [ @@ -39,6 +40,7 @@ go_library( "//vendor/github.com/spf13/cobra", "//vendor/github.com/spf13/viper", "//vendor/openapi.peridot.resf.org/peridotopenapi", + "//vendor/github.com/olekukonko/tablewriter", "@org_golang_x_oauth2//:oauth2", "@org_golang_x_oauth2//clientcredentials", ], @@ -67,7 +69,7 @@ pkg_rpm( architecture = "x86_64", description = "A command line interface to interact with the Peridot build system", license = "MIT", - release = "1", + release = "2", source_date_epoch = 0, summary = "Peridot Command Line Interface", version = "0.2.3", diff --git a/peridot/cmd/v1/peridot/main.go b/peridot/cmd/v1/peridot/main.go index af3123fb..9833449d 100644 --- a/peridot/cmd/v1/peridot/main.go +++ b/peridot/cmd/v1/peridot/main.go @@ -54,7 +54,10 @@ func init() { root.PersistentFlags().String("client-secret", "", "Client secret for authentication") root.PersistentFlags().String("project-id", "", "Peridot project ID") root.PersistentFlags().Bool("debug", false, "Debug mode") + root.PersistentFlags().StringP("output", "o", "table", "Output format (table|json)") + root.PersistentFlags().Bool("no-color", false, "don't colorize output") + root.PersistentFlags().Bool("no-wait", false, "don't wait for completion") root.AddCommand(lookaside) lookaside.AddCommand(lookasideUpload) @@ -65,6 +68,7 @@ func init() { root.AddCommand(task) task.AddCommand(taskLogs) + task.AddCommand(taskInfo) root.AddCommand(project) project.AddCommand(projectInfo) @@ -125,3 +129,11 @@ func debug() bool { func output() string { return viper.GetString("output") } + +func color() bool { + return !viper.GetBool("no-color") +} + +func wait() bool { + return !viper.GetBool("no-wait") +} diff --git a/peridot/cmd/v1/peridot/task_info.go b/peridot/cmd/v1/peridot/task_info.go new file mode 100644 index 00000000..d01b3440 --- /dev/null +++ b/peridot/cmd/v1/peridot/task_info.go @@ -0,0 +1,310 @@ +// Copyright (c) All respective contributors to the Peridot Project. All rights reserved. +// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved. +// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package main + +import ( + "errors" + "fmt" + "log" + "os" + "slices" + "time" + + "github.com/google/uuid" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" + "openapi.peridot.resf.org/peridotopenapi" +) + +var taskInfo = &cobra.Command{ + Use: "info [name-or-buildId]", + Args: cobra.ExactArgs(1), + Run: taskInfoMn, +} + +var ( + showLogLink bool + showSubmitterInfo bool + showDuration bool +) + +func init() { + taskInfo.Flags().BoolVar(&succeeded, "succeeded", true, "only query successful tasks") + taskInfo.Flags().BoolVar(&cancelled, "cancelled", false, "only query cancelled tasks") + taskInfo.Flags().BoolVar(&failed, "failed", false, "only query failed tasks") + taskInfo.MarkFlagsMutuallyExclusive("cancelled", "failed", "succeeded") + + taskInfo.Flags().BoolVarP(&showLogLink, "logs", "L", false, "include log link in output (table format only)") + taskInfo.Flags().BoolVar(&showSubmitterInfo, "submitter", false, "include submitter details (table format only)") + taskInfo.Flags().BoolVar(&showDuration, "duration", true, "include duration from start to stop (table format only)") +} + +func getNextColor(colors tablewriter.Colors) tablewriter.Colors { + bgColor := getNextBackgroundColor(colors[0]) + fgColor := colors[1] + if bgColor == -1 { + fgColor = getNextForegroundColor(0) + bgColor = getNextBackgroundColor(0) + } + return tablewriter.Colors{bgColor, fgColor} +} + +func getNextForegroundColor(color int) int { + switch color { + case 0: + return tablewriter.FgGreenColor + case tablewriter.FgCyanColor: + return tablewriter.FgHiGreenColor + case tablewriter.FgHiCyanColor: + return tablewriter.FgGreenColor + default: + color++ + return color + } +} + +func getNextBackgroundColor(color int) int { + switch color { + case 0: + return tablewriter.BgRedColor + case tablewriter.BgCyanColor: + return tablewriter.BgHiRedColor + case tablewriter.BgHiCyanColor: + return -1 + default: + color++ + return color + } +} + +func buildHeaderAndAutoMergeCells() ([]string, []int) { + header := []string{"ptid", "tid", "status", "type", "arch", "created", "finished"} + mergableNames := []string{"ptid", "type", "arch"} + var autoMergeCells []int + + // Conditional appending to header + if showDuration { + header = append(header, "duration") + mergableNames = append(mergableNames, "duration") + } + if showSubmitterInfo { + header = append(header, "submitter") + mergableNames = append(mergableNames, "submitter") + } + if showLogLink { + header = append(header, "logs") + } + + // Determine dynamic indices for auto-merge cells + for _, itemName := range mergableNames { + index := slices.Index(header, itemName) + if index != -1 { + autoMergeCells = append(autoMergeCells, index) + } + } + + return header, autoMergeCells +} + +func convertSubTaskSliceToCSV(task peridotopenapi.V1AsyncTask) { + subtasks, ok := task.GetSubtasksOk() + if !ok { + errFatal(fmt.Errorf("error getting subtasks: %v", ok)) + } + + var parentTask = (*subtasks)[0] + + var table = tablewriter.NewWriter(os.Stdout) + // var data [][]string + + var header, autoMergeCells = buildHeaderAndAutoMergeCells() + + var lastColor = tablewriter.Colors{0, tablewriter.FgWhiteColor} + var seenTasksColors = make(map[string]tablewriter.Colors) + + var parentTaskIds []string // cache parentTaskIds for colorizing + + // precache all the subtask's parent tasks so we know if we should color them + for _, subtask := range *subtasks { + parentTaskIds = append(parentTaskIds, subtask.GetParentTaskId()) + } + + for _, subtask := range *subtasks { + json, err := subtask.MarshalJSON() + if err != nil { + errFatal(err) + } + + if debug() { + err = PrettyPrintJSON(json) + if err != nil { + errFatal(err) + } + // taskResponse, _ := subtask.GetResponse().MarshalJSON() + // taskMetadata, _ := subtask.GetMetadata().MarshalJSON() + } + + subtaskId := subtask.GetId() + subtaskParentTaskId := subtask.GetParentTaskId() + createdAt := subtask.GetCreatedAt() + finishedAt := subtask.GetFinishedAt() + + row := []string{ + subtaskParentTaskId, + subtaskId, + string(subtask.GetStatus()), + string(subtask.GetType()), + subtask.GetArch(), + formatTime(createdAt), + formatTime(finishedAt), + } + + if showDuration { + row = append(row, formatDuration(createdAt, finishedAt)) + } + + if showSubmitterInfo { + effectiveSubmitter := fmt.Sprintf("%s <%s>", parentTask.GetSubmitterId(), parentTask.GetSubmitterEmail()) + row = append(row, effectiveSubmitter) + } + + if showLogLink { + row = append(row, getLogLink(subtaskId)) + } + + if !color() { + table.Append(row) + continue + } + + nextColor := tablewriter.Colors{tablewriter.BgBlackColor, tablewriter.FgWhiteColor} + needsColor := hasAny(parentTaskIds, subtaskId) + if _, seen := seenTasksColors[subtaskId]; !seen && needsColor { + debugP("before: lastcolor: %v next: %v", lastColor, nextColor) + nextColor = getNextColor(lastColor) + debugP("after: lastcolor: %v next: %v", lastColor, nextColor) + lastColor = nextColor + seenTasksColors[subtaskId] = nextColor + } + + tidColors := nextColor + + ptidColors := tablewriter.Colors{tablewriter.FgWhiteColor, tablewriter.BgBlackColor} + if seenColor, seen := seenTasksColors[subtaskParentTaskId]; seen { + ptidColors = seenColor + } + + var colors = make([]tablewriter.Colors, len(row)) + + debugP("tidcolor: %v ptidcolor %d", tidColors, ptidColors) + for i, v := range header { + switch v { + case "ptid": + colors[i] = ptidColors + case "tid": + colors[i] = tidColors + default: + colors[i] = tablewriter.Colors{} + } + } + + table.Rich(row, colors) + } + + table.SetHeader(header) + table.SetAutoMergeCellsByColumnIndex(autoMergeCells) + table.SetRowLine(true) + table.Render() + +} + +func debugP(s string, args ...any) { + if debug() { + log.Printf(s, args...) + } +} + +func hasAny(slice []string, target string) bool { + if idx := slices.Index(slice, target); idx >= 0 { + return true + } + return false +} + +func taskInfoMn(_ *cobra.Command, args []string) { + // Ensure project id exists + projectId := mustGetProjectID() + + taskId := args[0] + + err := uuid.Validate(taskId) + if err != nil { + errFatal(errors.New("invalid task id")) + } + + taskCl := getClient(serviceTask).(peridotopenapi.TaskServiceApi) + log.Printf("Searching for task %s in project %s\n", taskId, projectId) + + var waiting = false + for { + res, _, err := taskCl.GetTask(getContext(), projectId, taskId).Execute() + if err != nil { + errFatal(fmt.Errorf("error getting task: %s", err.Error())) + } + + task := res.GetTask() + + switch output() { + case "table": + if !waiting || task.GetDone() { + convertSubTaskSliceToCSV(task) + } + if wait() && !task.GetDone() { + waiting = true + log.Printf("Waiting for task %s to complete", task.GetTaskId()) + time.Sleep(5 * time.Second) + continue + } + + case "json": + taskJSON, err := res.MarshalJSON() + if err != nil { + errFatal(err) + } + + err = PrettyPrintJSON(taskJSON) + if err != nil { + errFatal(err) + } + } + break + } +} diff --git a/peridot/cmd/v1/peridot/utils.go b/peridot/cmd/v1/peridot/utils.go index c860fb0e..2e8027ed 100644 --- a/peridot/cmd/v1/peridot/utils.go +++ b/peridot/cmd/v1/peridot/utils.go @@ -39,6 +39,8 @@ import ( "log" "net/http" "strconv" + "strings" + "time" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -208,3 +210,18 @@ func PrettyPrintJSON(data []byte) error { fmt.Println(string(formattedJSON)) return nil } + +func formatTime(t time.Time) string { + return t.Format("2006-01-02 15:04:05") +} + +func formatDuration(start, end time.Time) string { + duration := end.Sub(start) + return time.Time{}.Add(duration).Format("15:04:05") +} + +func getLogLink(subtaskId string) string { + return fmt.Sprintf("https://%s/api/v1/projects/%s/tasks/%s/logs", + strings.Replace(endpoint(), "-api", "", 1), + mustGetProjectID(), subtaskId) +} diff --git a/repositories.bzl b/repositories.bzl index f631f902..dfde5787 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -1068,6 +1068,12 @@ def go_repositories(): sum = "h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=", version = "v1.3.1", ) + go_repository( + name = "com_github_olekukonko_tablewriter", + importpath = "github.com/olekukonko/tablewriter", + sum = "h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=", + version = "v0.0.5", + ) go_repository( name = "com_github_oneofone_xxhash", importpath = "github.com/OneOfOne/xxhash", diff --git a/vendor/github.com/olekukonko/tablewriter/BUILD.bazel b/vendor/github.com/olekukonko/tablewriter/BUILD.bazel new file mode 100644 index 00000000..40a82e0b --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/BUILD.bazel @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "tablewriter", + srcs = [ + "csv.go", + "table.go", + "table_with_color.go", + "util.go", + "wrap.go", + ], + importmap = "peridot.resf.org/vendor/github.com/olekukonko/tablewriter", + importpath = "github.com/olekukonko/tablewriter", + visibility = ["//visibility:public"], + deps = ["//vendor/github.com/mattn/go-runewidth"], +) + +go_test( + name = "tablewriter_test", + srcs = [ + "table_test.go", + "wrap_test.go", + ], + data = glob(["testdata/**"]), + embed = [":tablewriter"], + deps = ["//vendor/github.com/mattn/go-runewidth"], +) diff --git a/vendor/github.com/olekukonko/tablewriter/LICENSE.md b/vendor/github.com/olekukonko/tablewriter/LICENSE.md new file mode 100644 index 00000000..a0769b5c --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 by Oleku Konko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md new file mode 100644 index 00000000..f06530d7 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -0,0 +1,431 @@ +ASCII Table Writer +========= + +[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) +[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter) +[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter) + +Generate ASCII table on the fly ... Installation is simple as + + go get github.com/olekukonko/tablewriter + + +#### Features +- Automatic Padding +- Support Multiple Lines +- Supports Alignment +- Support Custom Separators +- Automatic Alignment of numbers & percentage +- Write directly to http , file etc via `io.Writer` +- Read directly from CSV file +- Optional row line via `SetRowLine` +- Normalise table header +- Make CSV Headers optional +- Enable or disable table border +- Set custom footer support +- Optional identical cells merging +- Set custom caption +- Optional reflowing of paragraphs in multi-line cells. + +#### Example 1 - Basic +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +##### Output 1 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +``` + +#### Example 2 - Without Border / Footer / Bulk Append +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 2 +``` + + DATE | DESCRIPTION | CV2 | AMOUNT +-----------+--------------------------+-------+---------- + 1/1/2014 | Domain name | 2233 | $10.98 + 1/1/2014 | January Hosting | 2233 | $54.95 + 1/4/2014 | February Hosting | 2233 | $51.00 + 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 +-----------+--------------------------+-------+---------- + TOTAL | $146 93 + --------+---------- + +``` + + +#### Example 3 - CSV +```go +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true) +table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment +table.Render() +``` + +##### Output 3 +``` ++----------+--------------+------+-----+---------+----------------+ +| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA | ++----------+--------------+------+-----+---------+----------------+ +| user_id | smallint(5) | NO | PRI | NULL | auto_increment | +| username | varchar(10) | NO | | NULL | | +| password | varchar(100) | NO | | NULL | | ++----------+--------------+------+-----+---------+----------------+ +``` + +#### Example 4 - Custom Separator +```go +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true) +table.SetRowLine(true) // Enable row line + +// Change table lines +table.SetCenterSeparator("*") +table.SetColumnSeparator("╪") +table.SetRowSeparator("-") + +table.SetAlignment(tablewriter.ALIGN_LEFT) +table.Render() +``` + +##### Output 4 +``` +*------------*-----------*---------* +╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪ +*------------*-----------*---------* +╪ John ╪ Barry ╪ 123456 ╪ +*------------*-----------*---------* +╪ Kathy ╪ Smith ╪ 687987 ╪ +*------------*-----------*---------* +╪ Bob ╪ McCornick ╪ 3979870 ╪ +*------------*-----------*---------* +``` + +#### Example 5 - Markdown Format +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) +table.SetCenterSeparator("|") +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 5 +``` +| DATE | DESCRIPTION | CV2 | AMOUNT | +|----------|--------------------------|------|--------| +| 1/1/2014 | Domain name | 2233 | $10.98 | +| 1/1/2014 | January Hosting | 2233 | $54.95 | +| 1/4/2014 | February Hosting | 2233 | $51.00 | +| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | +``` + +#### Example 6 - Identical cells merging +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "1234", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2345", "$54.95"}, + []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) +table.SetAutoMergeCells(true) +table.SetRowLine(true) +table.AppendBulk(data) +table.Render() +``` + +##### Output 6 +``` ++----------+--------------------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+--------------------------+-------+---------+ +| 1/1/2014 | Domain name | 1234 | $10.98 | ++ +--------------------------+-------+---------+ +| | January Hosting | 2345 | $54.95 | ++----------+--------------------------+-------+---------+ +| 1/4/2014 | February Hosting | 3456 | $51.00 | ++ +--------------------------+-------+---------+ +| | February Extra Bandwidth | 4567 | $30.00 | ++----------+--------------------------+-------+---------+ +| TOTAL | $146 93 | ++----------+--------------------------+-------+---------+ +``` + +#### Example 7 - Identical cells merging (specify the column index to merge) +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "1234", "$10.98"}, + []string{"1/1/2014", "January Hosting", "1234", "$10.98"}, + []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) +table.SetAutoMergeCellsByColumnIndex([]int{2, 3}) +table.SetRowLine(true) +table.AppendBulk(data) +table.Render() +``` + +##### Output 7 +``` ++----------+--------------------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+--------------------------+-------+---------+ +| 1/1/2014 | Domain name | 1234 | $10.98 | ++----------+--------------------------+ + + +| 1/1/2014 | January Hosting | | | ++----------+--------------------------+-------+---------+ +| 1/4/2014 | February Hosting | 3456 | $51.00 | ++----------+--------------------------+-------+---------+ +| 1/4/2014 | February Extra Bandwidth | 4567 | $30.00 | ++----------+--------------------------+-------+---------+ +| TOTAL | $146.93 | ++----------+--------------------------+-------+---------+ +``` + + +#### Table with color +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false + +table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, + tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, + tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) + +table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) + +table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.FgHiRedColor}) + +table.AppendBulk(data) +table.Render() +``` + +#### Table with color Output +![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png) + +#### Example - 8 Table Cells with Color + +Individual Cell Colors from `func Rich` take precedence over Column Colors + +```go +data := [][]string{ + []string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"}, + []string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"}, + []string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"}, + []string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"}, + []string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"}, + []string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"}, + []string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"}, + []string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"}, + []string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"}, + []string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"}) +table.SetFooter([]string{"", "", "Footer3", "Footer4"}) +table.SetBorder(false) + +table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, + tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, + tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) + +table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) + +table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.FgHiRedColor}) + +colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"} +colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"} + +for i, row := range data { + if i == 4 { + table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}}) + table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}}) + } + table.Append(row) +} + +table.SetAutoMergeCells(true) +table.Render() + +``` + +##### Table cells with color Output +![Table cells with Color](https://user-images.githubusercontent.com/9064687/63969376-bcd88d80-ca6f-11e9-9466-c3d954700b25.png) + +#### Example 9 - Set table caption +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) +table.SetCaption(true, "Movie ratings.") + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +Note: Caption text will wrap with total width of rendered table. + +##### Output 9 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +Movie ratings. +``` + +#### Example 10 - Set NoWhiteSpace and TablePadding option +```go +data := [][]string{ + {"node1.example.com", "Ready", "compute", "1.11"}, + {"node2.example.com", "Ready", "compute", "1.11"}, + {"node3.example.com", "Ready", "compute", "1.11"}, + {"node4.example.com", "NotReady", "compute", "1.11"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Status", "Role", "Version"}) +table.SetAutoWrapText(false) +table.SetAutoFormatHeaders(true) +table.SetHeaderAlignment(ALIGN_LEFT) +table.SetAlignment(ALIGN_LEFT) +table.SetCenterSeparator("") +table.SetColumnSeparator("") +table.SetRowSeparator("") +table.SetHeaderLine(false) +table.SetBorder(false) +table.SetTablePadding("\t") // pad with tabs +table.SetNoWhiteSpace(true) +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 10 +``` +NAME STATUS ROLE VERSION +node1.example.com Ready compute 1.11 +node2.example.com Ready compute 1.11 +node3.example.com Ready compute 1.11 +node4.example.com NotReady compute 1.11 +``` + +#### Render table into a string + +Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the `strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example: + +```go +package main + +import ( + "strings" + "fmt" + + "github.com/olekukonko/tablewriter" +) + +func main() { + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + + /* + * Code to fill the table + */ + + table.Render() + + fmt.Println(tableString.String()) +} +``` + +#### TODO +- ~~Import Directly from CSV~~ - `done` +- ~~Support for `SetFooter`~~ - `done` +- ~~Support for `SetBorder`~~ - `done` +- ~~Support table with uneven rows~~ - `done` +- ~~Support custom alignment~~ +- General Improvement & Optimisation +- `NewHTML` Parse table from HTML diff --git a/vendor/github.com/olekukonko/tablewriter/csv.go b/vendor/github.com/olekukonko/tablewriter/csv.go new file mode 100644 index 00000000..98878303 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/csv.go @@ -0,0 +1,52 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "encoding/csv" + "io" + "os" +) + +// Start A new table by importing from a CSV file +// Takes io.Writer and csv File name +func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) { + file, err := os.Open(fileName) + if err != nil { + return &Table{}, err + } + defer file.Close() + csvReader := csv.NewReader(file) + t, err := NewCSVReader(writer, csvReader, hasHeader) + return t, err +} + +// Start a New Table Writer with csv.Reader +// This enables customisation such as reader.Comma = ';' +// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94 +func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) { + t := NewWriter(writer) + if hasHeader { + // Read the first row + headers, err := csvReader.Read() + if err != nil { + return &Table{}, err + } + t.SetHeader(headers) + } + for { + record, err := csvReader.Read() + if err == io.EOF { + break + } else if err != nil { + return &Table{}, err + } + t.Append(record) + } + return t, nil +} diff --git a/vendor/github.com/olekukonko/tablewriter/csv2table/BUILD.bazel b/vendor/github.com/olekukonko/tablewriter/csv2table/BUILD.bazel new file mode 100644 index 00000000..d4139989 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/csv2table/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "csv2table_lib", + srcs = ["csv2table.go"], + importmap = "peridot.resf.org/vendor/github.com/olekukonko/tablewriter/csv2table", + importpath = "github.com/olekukonko/tablewriter/csv2table", + visibility = ["//visibility:private"], + deps = ["//vendor/github.com/olekukonko/tablewriter"], +) + +go_binary( + name = "csv2table", + embed = [":csv2table_lib"], + visibility = ["//visibility:public"], +) diff --git a/vendor/github.com/olekukonko/tablewriter/csv2table/README.md b/vendor/github.com/olekukonko/tablewriter/csv2table/README.md new file mode 100644 index 00000000..6cf5628a --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/csv2table/README.md @@ -0,0 +1,43 @@ +ASCII Table Writer Tool +========= + +Generate ASCII table on the fly via command line ... Installation is simple as + +#### Get Tool + + go get github.com/olekukonko/tablewriter/csv2table + +#### Install Tool + + go install github.com/olekukonko/tablewriter/csv2table + + +#### Usage + + csv2table -f test.csv + +#### Support for Piping + + cat test.csv | csv2table -p=true + +#### Output + +``` ++------------+-----------+---------+ +| FIRST NAME | LAST NAME | SSN | ++------------+-----------+---------+ +| John | Barry | 123456 | +| Kathy | Smith | 687987 | +| Bob | McCornick | 3979870 | ++------------+-----------+---------+ +``` + +#### Another Piping with Header set to `false` + + echo dance,with,me | csv2table -p=true -h=false + +#### Output + + +-------+------+-----+ + | dance | with | me | + +-------+------+-----+ diff --git a/vendor/github.com/olekukonko/tablewriter/csv2table/csv2table.go b/vendor/github.com/olekukonko/tablewriter/csv2table/csv2table.go new file mode 100644 index 00000000..05c0b09b --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/csv2table/csv2table.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/csv" + "flag" + "fmt" + "io" + "os" + "unicode/utf8" + + "github.com/olekukonko/tablewriter" +) + +var ( + fileName = flag.String("f", "", "Set file with eg. sample.csv") + delimiter = flag.String("d", ",", "Set CSV File delimiter eg. ,|;|\t ") + header = flag.Bool("h", true, "Set header options eg. true|false ") + align = flag.String("a", "none", "Set alignment with eg. none|left|right|center") + pipe = flag.Bool("p", false, "Support for Piping from STDIN") + border = flag.Bool("b", true, "Enable / disable table border") +) + +func main() { + flag.Parse() + fmt.Println() + if *pipe || hasArg("-p") { + process(os.Stdin) + } else { + if *fileName == "" { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + fmt.Println() + os.Exit(1) + } + processFile() + } + fmt.Println() +} + +func hasArg(name string) bool { + for _, v := range os.Args { + if name == v { + return true + } + } + return false +} +func processFile() { + r, err := os.Open(*fileName) + if err != nil { + exit(err) + } + defer r.Close() + process(r) +} +func process(r io.Reader) { + csvReader := csv.NewReader(r) + rune, size := utf8.DecodeRuneInString(*delimiter) + if size == 0 { + rune = ',' + } + csvReader.Comma = rune + + table, err := tablewriter.NewCSVReader(os.Stdout, csvReader, *header) + + if err != nil { + exit(err) + } + + switch *align { + case "left": + table.SetAlignment(tablewriter.ALIGN_LEFT) + case "right": + table.SetAlignment(tablewriter.ALIGN_RIGHT) + case "center": + table.SetAlignment(tablewriter.ALIGN_CENTER) + } + table.SetBorder(*border) + table.Render() +} + +func exit(err error) { + fmt.Fprintf(os.Stderr, "#Error : %s", err) + os.Exit(1) +} diff --git a/vendor/github.com/olekukonko/tablewriter/go.mod b/vendor/github.com/olekukonko/tablewriter/go.mod new file mode 100644 index 00000000..484ab01f --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/go.mod @@ -0,0 +1,5 @@ +module github.com/olekukonko/tablewriter + +go 1.12 + +require github.com/mattn/go-runewidth v0.0.9 diff --git a/vendor/github.com/olekukonko/tablewriter/go.sum b/vendor/github.com/olekukonko/tablewriter/go.sum new file mode 100644 index 00000000..4a94bf58 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go new file mode 100644 index 00000000..f913149c --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table.go @@ -0,0 +1,967 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +// Create & Generate text based table +package tablewriter + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strings" +) + +const ( + MAX_ROW_WIDTH = 30 +) + +const ( + CENTER = "+" + ROW = "-" + COLUMN = "|" + SPACE = " " + NEWLINE = "\n" +) + +const ( + ALIGN_DEFAULT = iota + ALIGN_CENTER + ALIGN_RIGHT + ALIGN_LEFT +) + +var ( + decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`) + percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`) +) + +type Border struct { + Left bool + Right bool + Top bool + Bottom bool +} + +type Table struct { + out io.Writer + rows [][]string + lines [][][]string + cs map[int]int + rs map[int]int + headers [][]string + footers [][]string + caption bool + captionText string + autoFmt bool + autoWrap bool + reflowText bool + mW int + pCenter string + pRow string + pColumn string + tColumn int + tRow int + hAlign int + fAlign int + align int + newLine string + rowLine bool + autoMergeCells bool + columnsToAutoMergeCells map[int]bool + noWhiteSpace bool + tablePadding string + hdrLine bool + borders Border + colSize int + headerParams []string + columnsParams []string + footerParams []string + columnsAlign []int +} + +// Start New Table +// Take io.Writer Directly +func NewWriter(writer io.Writer) *Table { + t := &Table{ + out: writer, + rows: [][]string{}, + lines: [][][]string{}, + cs: make(map[int]int), + rs: make(map[int]int), + headers: [][]string{}, + footers: [][]string{}, + caption: false, + captionText: "Table caption.", + autoFmt: true, + autoWrap: true, + reflowText: true, + mW: MAX_ROW_WIDTH, + pCenter: CENTER, + pRow: ROW, + pColumn: COLUMN, + tColumn: -1, + tRow: -1, + hAlign: ALIGN_DEFAULT, + fAlign: ALIGN_DEFAULT, + align: ALIGN_DEFAULT, + newLine: NEWLINE, + rowLine: false, + hdrLine: true, + borders: Border{Left: true, Right: true, Bottom: true, Top: true}, + colSize: -1, + headerParams: []string{}, + columnsParams: []string{}, + footerParams: []string{}, + columnsAlign: []int{}} + return t +} + +// Render table output +func (t *Table) Render() { + if t.borders.Top { + t.printLine(true) + } + t.printHeading() + if t.autoMergeCells { + t.printRowsMergeCells() + } else { + t.printRows() + } + if !t.rowLine && t.borders.Bottom { + t.printLine(true) + } + t.printFooter() + + if t.caption { + t.printCaption() + } +} + +const ( + headerRowIdx = -1 + footerRowIdx = -2 +) + +// Set table header +func (t *Table) SetHeader(keys []string) { + t.colSize = len(keys) + for i, v := range keys { + lines := t.parseDimension(v, i, headerRowIdx) + t.headers = append(t.headers, lines) + } +} + +// Set table Footer +func (t *Table) SetFooter(keys []string) { + //t.colSize = len(keys) + for i, v := range keys { + lines := t.parseDimension(v, i, footerRowIdx) + t.footers = append(t.footers, lines) + } +} + +// Set table Caption +func (t *Table) SetCaption(caption bool, captionText ...string) { + t.caption = caption + if len(captionText) == 1 { + t.captionText = captionText[0] + } +} + +// Turn header autoformatting on/off. Default is on (true). +func (t *Table) SetAutoFormatHeaders(auto bool) { + t.autoFmt = auto +} + +// Turn automatic multiline text adjustment on/off. Default is on (true). +func (t *Table) SetAutoWrapText(auto bool) { + t.autoWrap = auto +} + +// Turn automatic reflowing of multiline text when rewrapping. Default is on (true). +func (t *Table) SetReflowDuringAutoWrap(auto bool) { + t.reflowText = auto +} + +// Set the Default column width +func (t *Table) SetColWidth(width int) { + t.mW = width +} + +// Set the minimal width for a column +func (t *Table) SetColMinWidth(column int, width int) { + t.cs[column] = width +} + +// Set the Column Separator +func (t *Table) SetColumnSeparator(sep string) { + t.pColumn = sep +} + +// Set the Row Separator +func (t *Table) SetRowSeparator(sep string) { + t.pRow = sep +} + +// Set the center Separator +func (t *Table) SetCenterSeparator(sep string) { + t.pCenter = sep +} + +// Set Header Alignment +func (t *Table) SetHeaderAlignment(hAlign int) { + t.hAlign = hAlign +} + +// Set Footer Alignment +func (t *Table) SetFooterAlignment(fAlign int) { + t.fAlign = fAlign +} + +// Set Table Alignment +func (t *Table) SetAlignment(align int) { + t.align = align +} + +// Set No White Space +func (t *Table) SetNoWhiteSpace(allow bool) { + t.noWhiteSpace = allow +} + +// Set Table Padding +func (t *Table) SetTablePadding(padding string) { + t.tablePadding = padding +} + +func (t *Table) SetColumnAlignment(keys []int) { + for _, v := range keys { + switch v { + case ALIGN_CENTER: + break + case ALIGN_LEFT: + break + case ALIGN_RIGHT: + break + default: + v = ALIGN_DEFAULT + } + t.columnsAlign = append(t.columnsAlign, v) + } +} + +// Set New Line +func (t *Table) SetNewLine(nl string) { + t.newLine = nl +} + +// Set Header Line +// This would enable / disable a line after the header +func (t *Table) SetHeaderLine(line bool) { + t.hdrLine = line +} + +// Set Row Line +// This would enable / disable a line on each row of the table +func (t *Table) SetRowLine(line bool) { + t.rowLine = line +} + +// Set Auto Merge Cells +// This would enable / disable the merge of cells with identical values +func (t *Table) SetAutoMergeCells(auto bool) { + t.autoMergeCells = auto +} + +// Set Auto Merge Cells By Column Index +// This would enable / disable the merge of cells with identical values for specific columns +// If cols is empty, it is the same as `SetAutoMergeCells(true)`. +func (t *Table) SetAutoMergeCellsByColumnIndex(cols []int) { + t.autoMergeCells = true + + if len(cols) > 0 { + m := make(map[int]bool) + for _, col := range cols { + m[col] = true + } + t.columnsToAutoMergeCells = m + } +} + +// Set Table Border +// This would enable / disable line around the table +func (t *Table) SetBorder(border bool) { + t.SetBorders(Border{border, border, border, border}) +} + +func (t *Table) SetBorders(border Border) { + t.borders = border +} + +// Append row to table +func (t *Table) Append(row []string) { + rowSize := len(t.headers) + if rowSize > t.colSize { + t.colSize = rowSize + } + + n := len(t.lines) + line := [][]string{} + for i, v := range row { + + // Detect string width + // Detect String height + // Break strings into words + out := t.parseDimension(v, i, n) + + // Append broken words + line = append(line, out) + } + t.lines = append(t.lines, line) +} + +// Append row to table with color attributes +func (t *Table) Rich(row []string, colors []Colors) { + rowSize := len(t.headers) + if rowSize > t.colSize { + t.colSize = rowSize + } + + n := len(t.lines) + line := [][]string{} + for i, v := range row { + + // Detect string width + // Detect String height + // Break strings into words + out := t.parseDimension(v, i, n) + + if len(colors) > i { + color := colors[i] + out[0] = format(out[0], color) + } + + // Append broken words + line = append(line, out) + } + t.lines = append(t.lines, line) +} + +// Allow Support for Bulk Append +// Eliminates repeated for loops +func (t *Table) AppendBulk(rows [][]string) { + for _, row := range rows { + t.Append(row) + } +} + +// NumLines to get the number of lines +func (t *Table) NumLines() int { + return len(t.lines) +} + +// Clear rows +func (t *Table) ClearRows() { + t.lines = [][][]string{} +} + +// Clear footer +func (t *Table) ClearFooter() { + t.footers = [][]string{} +} + +// Center based on position and border. +func (t *Table) center(i int) string { + if i == -1 && !t.borders.Left { + return t.pRow + } + + if i == len(t.cs)-1 && !t.borders.Right { + return t.pRow + } + + return t.pCenter +} + +// Print line based on row width +func (t *Table) printLine(nl bool) { + fmt.Fprint(t.out, t.center(-1)) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.center(i)) + } + if nl { + fmt.Fprint(t.out, t.newLine) + } +} + +// Print line based on row width with our without cell separator +func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { + fmt.Fprint(t.out, t.pCenter) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + if i > len(displayCellSeparator) || displayCellSeparator[i] { + // Display the cell separator + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.pCenter) + } else { + // Don't display the cell separator for this cell + fmt.Fprintf(t.out, "%s%s", + strings.Repeat(" ", v+2), + t.pCenter) + } + } + if nl { + fmt.Fprint(t.out, t.newLine) + } +} + +// Return the PadRight function if align is left, PadLeft if align is right, +// and Pad by default +func pad(align int) func(string, string, int) string { + padFunc := Pad + switch align { + case ALIGN_LEFT: + padFunc = PadRight + case ALIGN_RIGHT: + padFunc = PadLeft + } + return padFunc +} + +// Print heading information +func (t *Table) printHeading() { + // Check if headers is available + if len(t.headers) < 1 { + return + } + + // Identify last column + end := len(t.cs) - 1 + + // Get pad function + padFunc := pad(t.hAlign) + + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.headerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[headerRowIdx] + + // Print Heading + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + } + + for y := 0; y <= end; y++ { + v := t.cs[y] + h := "" + + if y < len(t.headers) && x < len(t.headers[y]) { + h = t.headers[y][x] + } + if t.autoFmt { + h = Title(h) + } + pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn) + if t.noWhiteSpace { + pad = ConditionString((y == end && !t.borders.Left), SPACE, t.tablePadding) + } + if is_esc_seq { + if !t.noWhiteSpace { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } else { + fmt.Fprintf(t.out, "%s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } + } else { + if !t.noWhiteSpace { + fmt.Fprintf(t.out, " %s %s", + padFunc(h, SPACE, v), + pad) + } else { + // the spaces between breaks the kube formatting + fmt.Fprintf(t.out, "%s%s", + padFunc(h, SPACE, v), + pad) + } + } + } + // Next line + fmt.Fprint(t.out, t.newLine) + } + if t.hdrLine { + t.printLine(true) + } +} + +// Print heading information +func (t *Table) printFooter() { + // Check if headers is available + if len(t.footers) < 1 { + return + } + + // Only print line if border is not set + if !t.borders.Bottom { + t.printLine(true) + } + + // Identify last column + end := len(t.cs) - 1 + + // Get pad function + padFunc := pad(t.fAlign) + + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.footerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[footerRowIdx] + + // Print Footer + erasePad := make([]bool, len(t.footers)) + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + f := "" + if y < len(t.footers) && x < len(t.footers[y]) { + f = t.footers[y][x] + } + if t.autoFmt { + f = Title(f) + } + pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn) + + if erasePad[y] || (x == 0 && len(f) == 0) { + pad = SPACE + erasePad[y] = true + } + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(f, SPACE, v), + t.footerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(f, SPACE, v), + pad) + } + + //fmt.Fprintf(t.out, " %s %s", + // padFunc(f, SPACE, v), + // pad) + } + // Next line + fmt.Fprint(t.out, t.newLine) + //t.printLine(true) + } + + hasPrinted := false + + for i := 0; i <= end; i++ { + v := t.cs[i] + pad := t.pRow + center := t.pCenter + length := len(t.footers[i][0]) + + if length > 0 { + hasPrinted = true + } + + // Set center to be space if length is 0 + if length == 0 && !t.borders.Right { + center = SPACE + } + + // Print first junction + if i == 0 { + if length > 0 && !t.borders.Left { + center = t.pRow + } + fmt.Fprint(t.out, center) + } + + // Pad With space of length is 0 + if length == 0 { + pad = SPACE + } + // Ignore left space as it has printed before + if hasPrinted || t.borders.Left { + pad = t.pRow + center = t.pCenter + } + + // Change Center end position + if center != SPACE { + if i == end && !t.borders.Right { + center = t.pRow + } + } + + // Change Center start position + if center == SPACE { + if i < end && len(t.footers[i+1][0]) != 0 { + if !t.borders.Left { + center = t.pRow + } else { + center = t.pCenter + } + } + } + + // Print the footer + fmt.Fprintf(t.out, "%s%s%s%s", + pad, + strings.Repeat(string(pad), v), + pad, + center) + + } + + fmt.Fprint(t.out, t.newLine) +} + +// Print caption text +func (t Table) printCaption() { + width := t.getTableWidth() + paragraph, _ := WrapString(t.captionText, width) + for linecount := 0; linecount < len(paragraph); linecount++ { + fmt.Fprintln(t.out, paragraph[linecount]) + } +} + +// Calculate the total number of characters in a row +func (t Table) getTableWidth() int { + var chars int + for _, v := range t.cs { + chars += v + } + + // Add chars, spaces, seperators to calculate the total width of the table. + // ncols := t.colSize + // spaces := ncols * 2 + // seps := ncols + 1 + + return (chars + (3 * t.colSize) + 2) +} + +func (t Table) printRows() { + for i, lines := range t.lines { + t.printRow(lines, i) + } +} + +func (t *Table) fillAlignment(num int) { + if len(t.columnsAlign) < num { + t.columnsAlign = make([]int, num) + for i := range t.columnsAlign { + t.columnsAlign[i] = t.align + } + } +} + +// Print Row Information +// Adjust column alignment based on type + +func (t *Table) printRow(columns [][]string, rowIdx int) { + // Get Maximum Height + max := t.rs[rowIdx] + total := len(columns) + + // TODO Fix uneven col size + // if total < t.colSize { + // for n := t.colSize - total; n < t.colSize ; n++ { + // columns = append(columns, []string{SPACE}) + // t.cs[n] = t.mW + // } + //} + + // Pad Each Height + pads := []int{} + + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + t.fillAlignment(total) + + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + //fmt.Println(max, "\n") + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) + fmt.Fprintf(t.out, SPACE) + } + + str := columns[y][x] + + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + + // This would print alignment + // Default alignment would use multiple configuration + switch t.columnsAlign[y] { + case ALIGN_CENTER: // + fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + + // TODO Custom alignment per column + //if max == 1 || pads[y] > 0 { + // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + //} else { + // fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + //} + + } + } + if !t.noWhiteSpace { + fmt.Fprintf(t.out, SPACE) + } else { + fmt.Fprintf(t.out, t.tablePadding) + } + } + // Check if border is set + // Replace with space if not set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + } + fmt.Fprint(t.out, t.newLine) + } + + if t.rowLine { + t.printLine(true) + } +} + +// Print the rows of the table and merge the cells that are identical +func (t *Table) printRowsMergeCells() { + var previousLine []string + var displayCellBorder []bool + var tmpWriter bytes.Buffer + for i, lines := range t.lines { + // We store the display of the current line in a tmp writer, as we need to know which border needs to be print above + previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine) + if i > 0 { //We don't need to print borders above first line + if t.rowLine { + t.printLineOptionalCellSeparators(true, displayCellBorder) + } + } + tmpWriter.WriteTo(t.out) + } + //Print the end of the table + if t.rowLine { + t.printLine(true) + } +} + +// Print Row Information to a writer and merge identical cells. +// Adjust column alignment based on type + +func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) { + // Get Maximum Height + max := t.rs[rowIdx] + total := len(columns) + + // Pad Each Height + pads := []int{} + + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + + var displayCellBorder []bool + t.fillAlignment(total) + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) + + fmt.Fprintf(writer, SPACE) + + str := columns[y][x] + + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + + if t.autoMergeCells { + var mergeCell bool + if t.columnsToAutoMergeCells != nil { + // Check to see if the column index is in columnsToAutoMergeCells. + if t.columnsToAutoMergeCells[y] { + mergeCell = true + } + } else { + // columnsToAutoMergeCells was not set. + mergeCell = true + } + //Store the full line to merge mutli-lines cells + fullLine := strings.TrimRight(strings.Join(columns[y], " "), " ") + if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" && mergeCell { + // If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty. + displayCellBorder = append(displayCellBorder, false) + str = "" + } else { + // First line or different content, keep the content and print the cell border + displayCellBorder = append(displayCellBorder, true) + } + } + + // This would print alignment + // Default alignment would use multiple configuration + switch t.columnsAlign[y] { + case ALIGN_CENTER: // + fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + } + } + fmt.Fprintf(writer, SPACE) + } + // Check if border is set + // Replace with space if not set + fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE)) + fmt.Fprint(writer, t.newLine) + } + + //The new previous line is the current one + previousLine = make([]string, total) + for y := 0; y < total; y++ { + previousLine[y] = strings.TrimRight(strings.Join(columns[y], " "), " ") //Store the full line for multi-lines cells + } + //Returns the newly added line and wether or not a border should be displayed above. + return previousLine, displayCellBorder +} + +func (t *Table) parseDimension(str string, colKey, rowKey int) []string { + var ( + raw []string + maxWidth int + ) + + raw = getLines(str) + maxWidth = 0 + for _, line := range raw { + if w := DisplayWidth(line); w > maxWidth { + maxWidth = w + } + } + + // If wrapping, ensure that all paragraphs in the cell fit in the + // specified width. + if t.autoWrap { + // If there's a maximum allowed width for wrapping, use that. + if maxWidth > t.mW { + maxWidth = t.mW + } + + // In the process of doing so, we need to recompute maxWidth. This + // is because perhaps a word in the cell is longer than the + // allowed maximum width in t.mW. + newMaxWidth := maxWidth + newRaw := make([]string, 0, len(raw)) + + if t.reflowText { + // Make a single paragraph of everything. + raw = []string{strings.Join(raw, " ")} + } + for i, para := range raw { + paraLines, _ := WrapString(para, maxWidth) + for _, line := range paraLines { + if w := DisplayWidth(line); w > newMaxWidth { + newMaxWidth = w + } + } + if i > 0 { + newRaw = append(newRaw, " ") + } + newRaw = append(newRaw, paraLines...) + } + raw = newRaw + maxWidth = newMaxWidth + } + + // Store the new known maximum width. + v, ok := t.cs[colKey] + if !ok || v < maxWidth || v == 0 { + t.cs[colKey] = maxWidth + } + + // Remember the number of lines for the row printer. + h := len(raw) + v, ok = t.rs[rowKey] + + if !ok || v < h || v == 0 { + t.rs[rowKey] = h + } + //fmt.Printf("Raw %+v %d\n", raw, len(raw)) + return raw +} diff --git a/vendor/github.com/olekukonko/tablewriter/table_test.go b/vendor/github.com/olekukonko/tablewriter/table_test.go new file mode 100644 index 00000000..fc0d61fe --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table_test.go @@ -0,0 +1,1182 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "bytes" + "fmt" + "io" + "os" + "reflect" + "strings" + "testing" +) + +func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { + if !reflect.DeepEqual(got, want) { + buf := bytes.Buffer{} + buf.WriteString("got:\n[%v]\nwant:\n[%v]\n") + for _, v := range msgs { + buf.WriteString(v.(string)) + } + t.Errorf(buf.String(), got, want) + } +} + +func ExampleShort() { + data := [][]string{ + {"A", "The Good", "500"}, + {"B", "The Very very Bad Man", "288"}, + {"C", "The Ugly", "120"}, + {"D", "The Gopher", "800"}, + } + + table := NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + for _, v := range data { + table.Append(v) + } + table.Render() + + // Output: +------+-----------------------+--------+ + // | NAME | SIGN | RATING | + // +------+-----------------------+--------+ + // | A | The Good | 500 | + // | B | The Very very Bad Man | 288 | + // | C | The Ugly | 120 | + // | D | The Gopher | 800 | + // +------+-----------------------+--------+ +} + +func ExampleLong() { + data := [][]string{ + {"Learn East has computers with adapted keyboards with enlarged print etc", " Some Data ", " Another Data"}, + {"Instead of lining up the letters all ", "the way across, he splits the keyboard in two", "Like most ergonomic keyboards", "See Data"}, + } + + table := NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + table.SetCenterSeparator("*") + table.SetRowSeparator("=") + + for _, v := range data { + table.Append(v) + } + table.Render() + + // Output: *================================*================================*===============================*==========* + // | NAME | SIGN | RATING | | + // *================================*================================*===============================*==========* + // | Learn East has computers | Some Data | Another Data | + // | with adapted keyboards with | | | + // | enlarged print etc | | | + // | Instead of lining up the | the way across, he splits the | Like most ergonomic keyboards | See Data | + // | letters all | keyboard in two | | | + // *================================*================================*===============================*==========* +} + +func ExampleCSV() { + table, _ := NewCSV(os.Stdout, "testdata/test.csv", true) + table.SetCenterSeparator("*") + table.SetRowSeparator("=") + + table.Render() + + // Output: *============*===========*=========* + // | FIRST NAME | LAST NAME | SSN | + // *============*===========*=========* + // | John | Barry | 123456 | + // | Kathy | Smith | 687987 | + // | Bob | McCornick | 3979870 | + // *============*===========*=========* +} + +// TestNumLines to test the numbers of lines +func TestNumLines(t *testing.T) { + data := [][]string{ + {"A", "The Good", "500"}, + {"B", "The Very very Bad Man", "288"}, + {"C", "The Ugly", "120"}, + {"D", "The Gopher", "800"}, + } + + buf := &bytes.Buffer{} + table := NewWriter(buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + for i, v := range data { + table.Append(v) + checkEqual(t, table.NumLines(), i+1, "Number of lines failed") + } + + checkEqual(t, table.NumLines(), len(data), "Number of lines failed") +} + +func TestCSVInfo(t *testing.T) { + buf := &bytes.Buffer{} + table, err := NewCSV(buf, "testdata/test_info.csv", true) + if err != nil { + t.Error(err) + return + } + table.SetAlignment(ALIGN_LEFT) + table.SetBorder(false) + table.Render() + + got := buf.String() + want := ` FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA +-----------+--------------+------+-----+---------+----------------- + user_id | smallint(5) | NO | PRI | NULL | auto_increment + username | varchar(10) | NO | | NULL | + password | varchar(100) | NO | | NULL | +` + checkEqual(t, got, want, "CSV info failed") +} + +func TestCSVSeparator(t *testing.T) { + buf := &bytes.Buffer{} + table, err := NewCSV(buf, "testdata/test.csv", true) + if err != nil { + t.Error(err) + return + } + table.SetRowLine(true) + table.SetCenterSeparator("+") + table.SetColumnSeparator("|") + table.SetRowSeparator("-") + table.SetAlignment(ALIGN_LEFT) + table.Render() + + want := `+------------+-----------+---------+ +| FIRST NAME | LAST NAME | SSN | ++------------+-----------+---------+ +| John | Barry | 123456 | ++------------+-----------+---------+ +| Kathy | Smith | 687987 | ++------------+-----------+---------+ +| Bob | McCornick | 3979870 | ++------------+-----------+---------+ +` + + checkEqual(t, buf.String(), want, "CSV info failed") +} + +func TestNoBorder(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + {"1/1/2014", "January Hosting", "2233", "$54.95"}, + {"", " (empty)\n (empty)", "", ""}, + {"1/4/2014", "February Hosting", "2233", "$51.00"}, + {"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, + {"1/4/2014", " (Discount)", "2233", "-$1.00"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetAutoWrapText(false) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetFooter([]string{"", "", "Total", "$145.93"}) // Add Footer + table.SetBorder(false) // Set Border to false + table.AppendBulk(data) // Add Bulk Data + table.Render() + + want := ` DATE | DESCRIPTION | CV2 | AMOUNT +-----------+--------------------------+-------+---------- + 1/1/2014 | Domain name | 2233 | $10.98 + 1/1/2014 | January Hosting | 2233 | $54.95 + | (empty) | | + | (empty) | | + 1/4/2014 | February Hosting | 2233 | $51.00 + 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 + 1/4/2014 | (Discount) | 2233 | -$1.00 +-----------+--------------------------+-------+---------- + TOTAL | $145.93 + --------+---------- +` + + checkEqual(t, buf.String(), want, "border table rendering failed") +} + +func TestWithBorder(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + {"1/1/2014", "January Hosting", "2233", "$54.95"}, + {"", " (empty)\n (empty)", "", ""}, + {"1/4/2014", "February Hosting", "2233", "$51.00"}, + {"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, + {"1/4/2014", " (Discount)", "2233", "-$1.00"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetAutoWrapText(false) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetFooter([]string{"", "", "Total", "$145.93"}) // Add Footer + table.AppendBulk(data) // Add Bulk Data + table.Render() + + want := `+----------+--------------------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+--------------------------+-------+---------+ +| 1/1/2014 | Domain name | 2233 | $10.98 | +| 1/1/2014 | January Hosting | 2233 | $54.95 | +| | (empty) | | | +| | (empty) | | | +| 1/4/2014 | February Hosting | 2233 | $51.00 | +| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | +| 1/4/2014 | (Discount) | 2233 | -$1.00 | ++----------+--------------------------+-------+---------+ +| TOTAL | $145.93 | ++----------+--------------------------+-------+---------+ +` + + checkEqual(t, buf.String(), want, "border table rendering failed") +} + +func TestPrintingInMarkdown(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + {"1/1/2014", "January Hosting", "2233", "$54.95"}, + {"1/4/2014", "February Hosting", "2233", "$51.00"}, + {"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.AppendBulk(data) // Add Bulk Data + table.SetBorders(Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.Render() + + want := `| DATE | DESCRIPTION | CV2 | AMOUNT | +|----------|--------------------------|------|--------| +| 1/1/2014 | Domain name | 2233 | $10.98 | +| 1/1/2014 | January Hosting | 2233 | $54.95 | +| 1/4/2014 | February Hosting | 2233 | $51.00 | +| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | +` + checkEqual(t, buf.String(), want, "border table rendering failed") +} + +func TestPrintHeading(t *testing.T) { + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.printHeading() + want := `| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | ++---+---+---+---+---+---+---+---+---+---+---+---+ +` + checkEqual(t, buf.String(), want, "header rendering failed") +} + +func TestPrintHeadingWithoutAutoFormat(t *testing.T) { + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.SetAutoFormatHeaders(false) + table.printHeading() + want := `| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | ++---+---+---+---+---+---+---+---+---+---+---+---+ +` + checkEqual(t, buf.String(), want, "header rendering failed") +} + +func TestPrintFooter(t *testing.T) { + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.SetFooter([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.printFooter() + want := `| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | ++---+---+---+---+---+---+---+---+---+---+---+---+ +` + checkEqual(t, buf.String(), want, "footer rendering failed") +} + +func TestPrintFooterWithoutAutoFormat(t *testing.T) { + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetAutoFormatHeaders(false) + table.SetHeader([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.SetFooter([]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c"}) + table.printFooter() + want := `| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | ++---+---+---+---+---+---+---+---+---+---+---+---+ +` + checkEqual(t, buf.String(), want, "footer rendering failed") +} + +func TestPrintShortCaption(t *testing.T) { + var buf bytes.Buffer + data := [][]string{ + {"A", "The Good", "500"}, + {"B", "The Very very Bad Man", "288"}, + {"C", "The Ugly", "120"}, + {"D", "The Gopher", "800"}, + } + + table := NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + table.SetCaption(true, "Short caption.") + + for _, v := range data { + table.Append(v) + } + table.Render() + + want := `+------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +Short caption. +` + checkEqual(t, buf.String(), want, "long caption for short example rendering failed") +} + +func TestPrintLongCaptionWithShortExample(t *testing.T) { + var buf bytes.Buffer + data := [][]string{ + {"A", "The Good", "500"}, + {"B", "The Very very Bad Man", "288"}, + {"C", "The Ugly", "120"}, + {"D", "The Gopher", "800"}, + } + + table := NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + table.SetCaption(true, "This is a very long caption. The text should wrap. If not, we have a problem that needs to be solved.") + + for _, v := range data { + table.Append(v) + } + table.Render() + + want := `+------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +This is a very long caption. The text +should wrap. If not, we have a problem +that needs to be solved. +` + checkEqual(t, buf.String(), want, "long caption for short example rendering failed") +} + +func TestPrintCaptionWithFooter(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + {"1/1/2014", "January Hosting", "2233", "$54.95"}, + {"1/4/2014", "February Hosting", "2233", "$51.00"}, + {"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer + table.SetCaption(true, "This is a very long caption. The text should wrap to the width of the table.") // Add caption + table.SetBorder(false) // Set Border to false + table.AppendBulk(data) // Add Bulk Data + table.Render() + + want := ` DATE | DESCRIPTION | CV2 | AMOUNT +-----------+--------------------------+-------+---------- + 1/1/2014 | Domain name | 2233 | $10.98 + 1/1/2014 | January Hosting | 2233 | $54.95 + 1/4/2014 | February Hosting | 2233 | $51.00 + 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 +-----------+--------------------------+-------+---------- + TOTAL | $146.93 + --------+---------- +This is a very long caption. The text should wrap to the +width of the table. +` + checkEqual(t, buf.String(), want, "border table rendering failed") +} + +func TestPrintLongCaptionWithLongExample(t *testing.T) { + var buf bytes.Buffer + data := [][]string{ + {"Learn East has computers with adapted keyboards with enlarged print etc", "Some Data", "Another Data"}, + {"Instead of lining up the letters all", "the way across, he splits the keyboard in two", "Like most ergonomic keyboards"}, + } + + table := NewWriter(&buf) + table.SetCaption(true, "This is a very long caption. The text should wrap. If not, we have a problem that needs to be solved.") + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + for _, v := range data { + table.Append(v) + } + table.Render() + + want := `+--------------------------------+--------------------------------+-------------------------------+ +| NAME | SIGN | RATING | ++--------------------------------+--------------------------------+-------------------------------+ +| Learn East has computers | Some Data | Another Data | +| with adapted keyboards with | | | +| enlarged print etc | | | +| Instead of lining up the | the way across, he splits the | Like most ergonomic keyboards | +| letters all | keyboard in two | | ++--------------------------------+--------------------------------+-------------------------------+ +This is a very long caption. The text should wrap. If not, we have a problem that needs to be +solved. +` + checkEqual(t, buf.String(), want, "long caption for long example rendering failed") +} + +func Example_autowrap() { + var multiline = `A multiline +string with some lines being really long.` + + const ( + testRow = iota + testHeader + testFooter + testFooter2 + ) + for mode := testRow; mode <= testFooter2; mode++ { + for _, autoFmt := range []bool{false, true} { + if mode == testRow && autoFmt { + // Nothing special to test, skip + continue + } + for _, autoWrap := range []bool{false, true} { + for _, reflow := range []bool{false, true} { + if !autoWrap && reflow { + // Invalid configuration, skip + continue + } + fmt.Println("mode", mode, "autoFmt", autoFmt, "autoWrap", autoWrap, "reflow", reflow) + t := NewWriter(os.Stdout) + t.SetAutoFormatHeaders(autoFmt) + t.SetAutoWrapText(autoWrap) + t.SetReflowDuringAutoWrap(reflow) + if mode == testHeader { + t.SetHeader([]string{"woo", multiline}) + } else { + t.SetHeader([]string{"woo", "waa"}) + } + if mode == testRow { + t.Append([]string{"woo", multiline}) + } else { + t.Append([]string{"woo", "waa"}) + } + if mode == testFooter { + t.SetFooter([]string{"woo", multiline}) + } else if mode == testFooter2 { + t.SetFooter([]string{"", multiline}) + } else { + t.SetFooter([]string{"woo", "waa"}) + } + t.Render() + } + } + } + fmt.Println() + } + + // Output: + // mode 0 autoFmt false autoWrap false reflow false + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | woo | A multiline | + // | | string with some lines being really long. | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // mode 0 autoFmt false autoWrap true reflow false + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | A multiline | + // | | | + // | | string with some lines being | + // | | really long. | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // mode 0 autoFmt false autoWrap true reflow true + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | A multiline string with some | + // | | lines being really long. | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // + // mode 1 autoFmt false autoWrap false reflow false + // +-----+-------------------------------------------+ + // | woo | A multiline | + // | | string with some lines being really long. | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // mode 1 autoFmt false autoWrap true reflow false + // +-----+--------------------------------+ + // | woo | A multiline | + // | | | + // | | string with some lines being | + // | | really long. | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // mode 1 autoFmt false autoWrap true reflow true + // +-----+--------------------------------+ + // | woo | A multiline string with some | + // | | lines being really long. | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // mode 1 autoFmt true autoWrap false reflow false + // +-----+-------------------------------------------+ + // | WOO | A MULTILINE | + // | | STRING WITH SOME LINES BEING REALLY LONG | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | WOO | WAA | + // +-----+-------------------------------------------+ + // mode 1 autoFmt true autoWrap true reflow false + // +-----+--------------------------------+ + // | WOO | A MULTILINE | + // | | | + // | | STRING WITH SOME LINES BEING | + // | | REALLY LONG | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // mode 1 autoFmt true autoWrap true reflow true + // +-----+--------------------------------+ + // | WOO | A MULTILINE STRING WITH SOME | + // | | LINES BEING REALLY LONG | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // + // mode 2 autoFmt false autoWrap false reflow false + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | woo | A multiline | + // | | string with some lines being really long. | + // +-----+-------------------------------------------+ + // mode 2 autoFmt false autoWrap true reflow false + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | A multiline | + // | | | + // | | string with some lines being | + // | | really long. | + // +-----+--------------------------------+ + // mode 2 autoFmt false autoWrap true reflow true + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | A multiline string with some | + // | | lines being really long. | + // +-----+--------------------------------+ + // mode 2 autoFmt true autoWrap false reflow false + // +-----+-------------------------------------------+ + // | WOO | WAA | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | WOO | A MULTILINE | + // | | STRING WITH SOME LINES BEING REALLY LONG | + // +-----+-------------------------------------------+ + // mode 2 autoFmt true autoWrap true reflow false + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | WOO | A MULTILINE | + // | | | + // | | STRING WITH SOME LINES BEING | + // | | REALLY LONG | + // +-----+--------------------------------+ + // mode 2 autoFmt true autoWrap true reflow true + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | WOO | A MULTILINE STRING WITH SOME | + // | | LINES BEING REALLY LONG | + // +-----+--------------------------------+ + // + // mode 3 autoFmt false autoWrap false reflow false + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | A multiline | + // | string with some lines being really long. | + // +-----+-------------------------------------------+ + // mode 3 autoFmt false autoWrap true reflow false + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | A multiline | + // | | + // | string with some lines being | + // | really long. | + // +-----+--------------------------------+ + // mode 3 autoFmt false autoWrap true reflow true + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | A multiline string with some | + // | lines being really long. | + // +-----+--------------------------------+ + // mode 3 autoFmt true autoWrap false reflow false + // +-----+-------------------------------------------+ + // | WOO | WAA | + // +-----+-------------------------------------------+ + // | woo | waa | + // +-----+-------------------------------------------+ + // | A MULTILINE | + // | STRING WITH SOME LINES BEING REALLY LONG | + // +-----+-------------------------------------------+ + // mode 3 autoFmt true autoWrap true reflow false + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | A MULTILINE | + // | | + // | STRING WITH SOME LINES BEING | + // | REALLY LONG | + // +-----+--------------------------------+ + // mode 3 autoFmt true autoWrap true reflow true + // +-----+--------------------------------+ + // | WOO | WAA | + // +-----+--------------------------------+ + // | woo | waa | + // +-----+--------------------------------+ + // | A MULTILINE STRING WITH SOME | + // | LINES BEING REALLY LONG | + // +-----+--------------------------------+ +} + +func TestPrintLine(t *testing.T) { + header := make([]string, 12) + val := " " + want := "" + for i := range header { + header[i] = val + want = fmt.Sprintf("%s+-%s-", want, strings.Replace(val, " ", "-", -1)) + val = val + " " + } + want = want + "+" + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader(header) + table.printLine(false) + checkEqual(t, buf.String(), want, "line rendering failed") +} + +func TestAnsiStrip(t *testing.T) { + header := make([]string, 12) + val := " " + want := "" + for i := range header { + header[i] = "\033[43;30m" + val + "\033[00m" + want = fmt.Sprintf("%s+-%s-", want, strings.Replace(val, " ", "-", -1)) + val = val + " " + } + want = want + "+" + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader(header) + table.printLine(false) + checkEqual(t, buf.String(), want, "line rendering failed") +} + +func NewCustomizedTable(out io.Writer) *Table { + table := NewWriter(out) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetBorder(false) + table.SetAlignment(ALIGN_LEFT) + table.SetHeader([]string{}) + return table +} + +func TestSubclass(t *testing.T) { + buf := new(bytes.Buffer) + table := NewCustomizedTable(buf) + + data := [][]string{ + {"A", "The Good", "500"}, + {"B", "The Very very Bad Man", "288"}, + {"C", "The Ugly", "120"}, + {"D", "The Gopher", "800"}, + } + + for _, v := range data { + table.Append(v) + } + table.Render() + + want := ` A The Good 500 + B The Very very Bad Man 288 + C The Ugly 120 + D The Gopher 800 +` + checkEqual(t, buf.String(), want, "test subclass failed") +} + +func TestAutoMergeRows(t *testing.T) { + data := [][]string{ + {"A", "The Good", "500"}, + {"A", "The Very very Bad Man", "288"}, + {"B", "The Very very Bad Man", "120"}, + {"B", "The Very very Bad Man", "200"}, + } + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + for _, v := range data { + table.Append(v) + } + table.SetAutoMergeCells(true) + table.Render() + want := `+------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| | The Very very Bad Man | 288 | +| B | | 120 | +| | | 200 | ++------+-----------------------+--------+ +` + got := buf.String() + if got != want { + t.Errorf("\ngot:\n%s\nwant:\n%s\n", got, want) + } + + buf.Reset() + table = NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + for _, v := range data { + table.Append(v) + } + table.SetAutoMergeCells(true) + table.SetRowLine(true) + table.Render() + want = `+------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | ++ +-----------------------+--------+ +| | The Very very Bad Man | 288 | ++------+ +--------+ +| B | | 120 | ++ + +--------+ +| | | 200 | ++------+-----------------------+--------+ +` + checkEqual(t, buf.String(), want) + + buf.Reset() + table = NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + dataWithlongText := [][]string{ + {"A", "The Good", "500"}, + {"A", "The Very very very very very Bad Man", "288"}, + {"B", "The Very very very very very Bad Man", "120"}, + {"C", "The Very very Bad Man", "200"}, + } + table.AppendBulk(dataWithlongText) + table.SetAutoMergeCells(true) + table.SetRowLine(true) + table.Render() + want = `+------+--------------------------------+--------+ +| NAME | SIGN | RATING | ++------+--------------------------------+--------+ +| A | The Good | 500 | ++ +--------------------------------+--------+ +| | The Very very very very very | 288 | +| | Bad Man | | ++------+ +--------+ +| B | | 120 | +| | | | ++------+--------------------------------+--------+ +| C | The Very very Bad Man | 200 | ++------+--------------------------------+--------+ +` + checkEqual(t, buf.String(), want) + + buf.Reset() + table = NewWriter(&buf) + table.SetHeader([]string{"Name", "Sign", "Rating"}) + + dataWithlongText2 := [][]string{ + {"A", "The Good", "500"}, + {"A", "The Very very very very very Bad Man", "288"}, + {"B", "The Very very Bad Man", "120"}, + } + table.AppendBulk(dataWithlongText2) + table.SetAutoMergeCells(true) + table.SetRowLine(true) + table.Render() + want = `+------+--------------------------------+--------+ +| NAME | SIGN | RATING | ++------+--------------------------------+--------+ +| A | The Good | 500 | ++ +--------------------------------+--------+ +| | The Very very very very very | 288 | +| | Bad Man | | ++------+--------------------------------+--------+ +| B | The Very very Bad Man | 120 | ++------+--------------------------------+--------+ +` + checkEqual(t, buf.String(), want) +} + +func TestClearRows(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetAutoWrapText(false) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetFooter([]string{"", "", "Total", "$145.93"}) // Add Footer + table.AppendBulk(data) // Add Bulk Data + table.Render() + + originalWant := `+----------+-------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+-------------+-------+---------+ +| 1/1/2014 | Domain name | 2233 | $10.98 | ++----------+-------------+-------+---------+ +| TOTAL | $145.93 | ++----------+-------------+-------+---------+ +` + want := originalWant + + checkEqual(t, buf.String(), want, "table clear rows failed") + + buf.Reset() + table.ClearRows() + table.Render() + + want = `+----------+-------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+-------------+-------+---------+ ++----------+-------------+-------+---------+ +| TOTAL | $145.93 | ++----------+-------------+-------+---------+ +` + + checkEqual(t, buf.String(), want, "table clear rows failed") + + buf.Reset() + table.AppendBulk(data) // Add Bulk Data + table.Render() + + want = `+----------+-------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+-------------+-------+---------+ +| 1/1/2014 | Domain name | 2233 | $10.98 | ++----------+-------------+-------+---------+ +| TOTAL | $145.93 | ++----------+-------------+-------+---------+ +` + + checkEqual(t, buf.String(), want, "table clear rows failed") +} + +func TestClearFooters(t *testing.T) { + data := [][]string{ + {"1/1/2014", "Domain name", "2233", "$10.98"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetAutoWrapText(false) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetFooter([]string{"", "", "Total", "$145.93"}) // Add Footer + table.AppendBulk(data) // Add Bulk Data + table.Render() + + buf.Reset() + table.ClearFooter() + table.Render() + + want := `+----------+-------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+-------------+-------+---------+ +| 1/1/2014 | Domain name | 2233 | $10.98 | ++----------+-------------+-------+---------+ +` + + checkEqual(t, buf.String(), want) +} + +func TestMoreDataColumnsThanHeaders(t *testing.T) { + var ( + buf = &bytes.Buffer{} + table = NewWriter(buf) + header = []string{"A", "B", "C"} + data = [][]string{ + {"a", "b", "c", "d"}, + {"1", "2", "3", "4"}, + } + want = `+---+---+---+---+ +| A | B | C | | ++---+---+---+---+ +| a | b | c | d | +| 1 | 2 | 3 | 4 | ++---+---+---+---+ +` + ) + table.SetHeader(header) + // table.SetFooter(ctx.tableCtx.footer) + table.AppendBulk(data) + table.Render() + + checkEqual(t, buf.String(), want) +} + +func TestMoreFooterColumnsThanHeaders(t *testing.T) { + var ( + buf = &bytes.Buffer{} + table = NewWriter(buf) + header = []string{"A", "B", "C"} + data = [][]string{ + {"a", "b", "c", "d"}, + {"1", "2", "3", "4"}, + } + footer = []string{"a", "b", "c", "d", "e"} + want = `+---+---+---+---+---+ +| A | B | C | | | ++---+---+---+---+---+ +| a | b | c | d | +| 1 | 2 | 3 | 4 | ++---+---+---+---+---+ +| A | B | C | D | E | ++---+---+---+---+---+ +` + ) + table.SetHeader(header) + table.SetFooter(footer) + table.AppendBulk(data) + table.Render() + + checkEqual(t, buf.String(), want) +} + +func TestSetColMinWidth(t *testing.T) { + var ( + buf = &bytes.Buffer{} + table = NewWriter(buf) + header = []string{"AAA", "BBB", "CCC"} + data = [][]string{ + {"a", "b", "c"}, + {"1", "2", "3"}, + } + footer = []string{"a", "b", "cccc"} + want = `+-----+-----+-------+ +| AAA | BBB | CCC | ++-----+-----+-------+ +| a | b | c | +| 1 | 2 | 3 | ++-----+-----+-------+ +| A | B | CCCC | ++-----+-----+-------+ +` + ) + table.SetHeader(header) + table.SetFooter(footer) + table.AppendBulk(data) + table.SetColMinWidth(2, 5) + table.Render() + + checkEqual(t, buf.String(), want) +} + +func TestWrapString(t *testing.T) { + want := []string{"ああああああああああああああああああああああああ", "あああああああ"} + got, _ := WrapString("ああああああああああああああああああああああああ あああああああ", 55) + checkEqual(t, got, want) +} + +func TestNumberAlign(t *testing.T) { + var ( + buf = &bytes.Buffer{} + table = NewWriter(buf) + data = [][]string{ + {"AAAAAAAAAAAAA", "BBBBBBBBBBBBB", "CCCCCCCCCCCCCC"}, + {"A", "B", "C"}, + {"123456789", "2", "3"}, + {"1", "2", "123,456,789"}, + {"1", "123,456.789", "3"}, + {"-123,456", "-2", "-3"}, + } + want = `+---------------+---------------+----------------+ +| AAAAAAAAAAAAA | BBBBBBBBBBBBB | CCCCCCCCCCCCCC | +| A | B | C | +| 123456789 | 2 | 3 | +| 1 | 2 | 123,456,789 | +| 1 | 123,456.789 | 3 | +| -123,456 | -2 | -3 | ++---------------+---------------+----------------+ +` + ) + table.AppendBulk(data) + table.Render() + + checkEqual(t, buf.String(), want) +} + +func TestCustomAlign(t *testing.T) { + var ( + buf = &bytes.Buffer{} + table = NewWriter(buf) + header = []string{"AAA", "BBB", "CCC"} + data = [][]string{ + {"a", "b", "c"}, + {"1", "2", "3"}, + } + footer = []string{"a", "b", "cccc"} + want = `+-----+-----+-------+ +| AAA | BBB | CCC | ++-----+-----+-------+ +| a | b | c | +| 1 | 2 | 3 | ++-----+-----+-------+ +| A | B | CCCC | ++-----+-----+-------+ +` + ) + table.SetHeader(header) + table.SetFooter(footer) + table.AppendBulk(data) + table.SetColMinWidth(2, 5) + table.SetColumnAlignment([]int{ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT}) + table.Render() + + checkEqual(t, buf.String(), want) +} + +func TestTitle(t *testing.T) { + ts := []struct { + text string + want string + }{ + {"", ""}, + {"foo", "FOO"}, + {"Foo", "FOO"}, + {"foO", "FOO"}, + {".foo", "FOO"}, + {"foo.", "FOO"}, + {".foo.", "FOO"}, + {".foo.bar.", "FOO BAR"}, + {"_foo", "FOO"}, + {"foo_", "FOO"}, + {"_foo_", "FOO"}, + {"_foo_bar_", "FOO BAR"}, + {" foo", "FOO"}, + {"foo ", "FOO"}, + {" foo ", "FOO"}, + {" foo bar ", "FOO BAR"}, + {"0.1", "0.1"}, + {"FOO 0.1", "FOO 0.1"}, + {".1 0.1", ".1 0.1"}, + {"1. 0.1", "1. 0.1"}, + {"1. 0.", "1. 0."}, + {".1. 0.", ".1. 0."}, + {".$ . $.", "$ . $"}, + {".$. $.", "$ $"}, + } + for _, tt := range ts { + got := Title(tt.text) + if got != tt.want { + t.Errorf("want %q, bot got %q", tt.want, got) + } + } +} + +func TestKubeFormat(t *testing.T) { + data := [][]string{ + {"1/1/2014", "jan_hosting", "2233", "$10.98"}, + {"1/1/2014", "feb_hosting", "2233", "$54.95"}, + {"1/4/2014", "feb_extra_bandwidth", "2233", "$51.00"}, + {"1/4/2014", "mar_hosting", "2233", "$30.00"}, + } + + var buf bytes.Buffer + table := NewWriter(&buf) + table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(ALIGN_LEFT) + table.SetAlignment(ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + table.AppendBulk(data) // Add Bulk Data + table.Render() + + want := `DATE DESCRIPTION CV2 AMOUNT +1/1/2014 jan_hosting 2233 $10.98 +1/1/2014 feb_hosting 2233 $54.95 +1/4/2014 feb_extra_bandwidth 2233 $51.00 +1/4/2014 mar_hosting 2233 $30.00 +` + + checkEqual(t, buf.String(), want, "kube format rendering failed") +} diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go new file mode 100644 index 00000000..ae7a364a --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table_with_color.go @@ -0,0 +1,136 @@ +package tablewriter + +import ( + "fmt" + "strconv" + "strings" +) + +const ESC = "\033" +const SEP = ";" + +const ( + BgBlackColor int = iota + 40 + BgRedColor + BgGreenColor + BgYellowColor + BgBlueColor + BgMagentaColor + BgCyanColor + BgWhiteColor +) + +const ( + FgBlackColor int = iota + 30 + FgRedColor + FgGreenColor + FgYellowColor + FgBlueColor + FgMagentaColor + FgCyanColor + FgWhiteColor +) + +const ( + BgHiBlackColor int = iota + 100 + BgHiRedColor + BgHiGreenColor + BgHiYellowColor + BgHiBlueColor + BgHiMagentaColor + BgHiCyanColor + BgHiWhiteColor +) + +const ( + FgHiBlackColor int = iota + 90 + FgHiRedColor + FgHiGreenColor + FgHiYellowColor + FgHiBlueColor + FgHiMagentaColor + FgHiCyanColor + FgHiWhiteColor +) + +const ( + Normal = 0 + Bold = 1 + UnderlineSingle = 4 + Italic +) + +type Colors []int + +func startFormat(seq string) string { + return fmt.Sprintf("%s[%sm", ESC, seq) +} + +func stopFormat() string { + return fmt.Sprintf("%s[%dm", ESC, Normal) +} + +// Making the SGR (Select Graphic Rendition) sequence. +func makeSequence(codes []int) string { + codesInString := []string{} + for _, code := range codes { + codesInString = append(codesInString, strconv.Itoa(code)) + } + return strings.Join(codesInString, SEP) +} + +// Adding ANSI escape sequences before and after string +func format(s string, codes interface{}) string { + var seq string + + switch v := codes.(type) { + + case string: + seq = v + case []int: + seq = makeSequence(v) + case Colors: + seq = makeSequence(v) + default: + return s + } + + if len(seq) == 0 { + return s + } + return startFormat(seq) + s + stopFormat() +} + +// Adding header colors (ANSI codes) +func (t *Table) SetHeaderColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of header colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.headerParams = append(t.headerParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetColumnColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of column colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.columnsParams = append(t.columnsParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetFooterColor(colors ...Colors) { + if len(t.footers) != len(colors) { + panic("Number of footer colors must be equal to number of footer.") + } + for i := 0; i < len(colors); i++ { + t.footerParams = append(t.footerParams, makeSequence(colors[i])) + } +} + +func Color(colors ...int) []int { + return colors +} diff --git a/vendor/github.com/olekukonko/tablewriter/testdata/test.csv b/vendor/github.com/olekukonko/tablewriter/testdata/test.csv new file mode 100644 index 00000000..1609327e --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/testdata/test.csv @@ -0,0 +1,4 @@ +first_name,last_name,ssn +John,Barry,123456 +Kathy,Smith,687987 +Bob,McCornick,3979870 \ No newline at end of file diff --git a/vendor/github.com/olekukonko/tablewriter/testdata/test_info.csv b/vendor/github.com/olekukonko/tablewriter/testdata/test_info.csv new file mode 100644 index 00000000..e4c40e98 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/testdata/test_info.csv @@ -0,0 +1,4 @@ +Field,Type,Null,Key,Default,Extra +user_id,smallint(5),NO,PRI,NULL,auto_increment +username,varchar(10),NO,,NULL, +password,varchar(100),NO,,NULL, \ No newline at end of file diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go new file mode 100644 index 00000000..380e7ab3 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/util.go @@ -0,0 +1,93 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "regexp" + "strings" + + "github.com/mattn/go-runewidth" +) + +var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]") + +func DisplayWidth(str string) int { + return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, "")) +} + +// Simple Condition for string +// Returns value based on condition +func ConditionString(cond bool, valid, inValid string) string { + if cond { + return valid + } + return inValid +} + +func isNumOrSpace(r rune) bool { + return ('0' <= r && r <= '9') || r == ' ' +} + +// Format Table Header +// Replace _ , . and spaces +func Title(name string) string { + origLen := len(name) + rs := []rune(name) + for i, r := range rs { + switch r { + case '_': + rs[i] = ' ' + case '.': + // ignore floating number 0.0 + if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) { + rs[i] = ' ' + } + } + } + name = string(rs) + name = strings.TrimSpace(name) + if len(name) == 0 && origLen > 0 { + // Keep at least one character. This is important to preserve + // empty lines in multi-line headers/footers. + name = " " + } + return strings.ToUpper(name) +} + +// Pad String +// Attempts to place string in the center +func Pad(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + gapLeft := int(math.Ceil(float64(gap / 2))) + gapRight := gap - gapLeft + return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight) + } + return s +} + +// Pad String Right position +// This would place string at the left side of the screen +func PadRight(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return s + strings.Repeat(string(pad), gap) + } + return s +} + +// Pad String Left position +// This would place string at the right side of the screen +func PadLeft(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return strings.Repeat(string(pad), gap) + s + } + return s +} diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go new file mode 100644 index 00000000..a092ee1f --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/wrap.go @@ -0,0 +1,99 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "strings" + + "github.com/mattn/go-runewidth" +) + +var ( + nl = "\n" + sp = " " +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapString(s string, lim int) ([]string, int) { + words := strings.Split(strings.Replace(s, nl, sp, -1), sp) + var lines []string + max := 0 + for _, v := range words { + max = runewidth.StringWidth(v) + if max > lim { + lim = max + } + } + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, strings.Join(line, sp)) + } + return lines, lim +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, +// WrapString will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each rune as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words []string, spc, lim, pen int) [][]string { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = runewidth.StringWidth(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j]) + } + } + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + var lines [][]string + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} + +// getLines decomposes a multiline string into a slice of strings. +func getLines(s string) []string { + return strings.Split(s, nl) +} diff --git a/vendor/github.com/olekukonko/tablewriter/wrap_test.go b/vendor/github.com/olekukonko/tablewriter/wrap_test.go new file mode 100644 index 00000000..a03f9fc2 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/wrap_test.go @@ -0,0 +1,58 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "strings" + "testing" + + "github.com/mattn/go-runewidth" +) + +var text = "The quick brown fox jumps over the lazy dog." + +func TestWrap(t *testing.T) { + exp := []string{ + "The", "quick", "brown", "fox", + "jumps", "over", "the", "lazy", "dog."} + + got, _ := WrapString(text, 6) + checkEqual(t, len(got), len(exp)) +} + +func TestWrapOneLine(t *testing.T) { + exp := "The quick brown fox jumps over the lazy dog." + words, _ := WrapString(text, 500) + checkEqual(t, strings.Join(words, string(sp)), exp) + +} + +func TestUnicode(t *testing.T) { + input := "Česká řeřicha" + var wordsUnicode []string + if runewidth.IsEastAsian() { + wordsUnicode, _ = WrapString(input, 14) + } else { + wordsUnicode, _ = WrapString(input, 13) + } + // input contains 13 (or 14 for CJK) runes, so it fits on one line. + checkEqual(t, len(wordsUnicode), 1) +} + +func TestDisplayWidth(t *testing.T) { + input := "Česká řeřicha" + want := 13 + if runewidth.IsEastAsian() { + want = 14 + } + if n := DisplayWidth(input); n != want { + t.Errorf("Wants: %d Got: %d", want, n) + } + input = "\033[43;30m" + input + "\033[00m" + checkEqual(t, DisplayWidth(input), want) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 93b323c7..d8257d59 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1200,3 +1200,6 @@ sigs.k8s.io/yaml # peridot.resf.org/peridot/yumrepofs/pb => ./bazel-bin/peridot/proto/v1/yumrepofs/yumrepofspb_go_proto_/peridot.resf.org/peridot/yumrepofs/pb # peridot.resf.org/common => ./bazel-bin/proto/commonpb_go_proto_/peridot.resf.org/common # github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options => ./bazel-bin/protoc-gen-openapiv2/options/options_go_proto_/github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2/options +# github.com/olekukonko/tablewriter v0.0.5 +## explicit; go 1.13 +github.com/olekukonko/tablewriter