From 4ca3a56280293f57dbcfd226a520e78ffde54885 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sat, 1 Oct 2022 14:14:53 +0200 Subject: [PATCH] refactored output printer, now using tablewriter --- cmd/root.go | 17 ++++- go.mod | 2 + go.sum | 4 ++ lib/common.go | 6 ++ lib/helpers.go | 33 ++++++++- lib/io.go | 2 +- lib/parser.go | 2 +- lib/printer.go | 188 +++++++++++++++++++++++++++++++++---------------- tablizer.pod | 19 +++-- 9 files changed, 203 insertions(+), 70 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 4ab2b73..fd0446d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,6 +33,13 @@ var rootCmd = &cobra.Command{ return nil } + lib.PrepareColumns() + + err := lib.PrepareModeFlags() + if err != nil { + return err + } + return lib.ProcessFiles(args) }, } @@ -46,9 +53,17 @@ func Execute() { func init() { rootCmd.PersistentFlags().BoolVarP(&lib.Debug, "debug", "d", false, "Enable debugging") - rootCmd.PersistentFlags().BoolVarP(&lib.XtendedOut, "extended", "x", false, "Enable extended output") rootCmd.PersistentFlags().BoolVarP(&lib.NoNumbering, "no-numbering", "n", false, "Disable header numbering") rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "v", false, "Print program version") rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator") rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") + + // output flags, only 1 allowed + rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output") + rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table output") + rootCmd.PersistentFlags().BoolVarP(&lib.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output") + rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl") + + // same thing but more common, takes precedence over above group + rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, ascii(default)") } diff --git a/go.mod b/go.mod index 1e8b3fa..38b9a5b 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.18 require ( github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.5.0 ) require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.0 // indirect ) diff --git a/go.sum b/go.sum index 2e9690d..6ec3e3c 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/lib/common.go b/lib/common.go index 980d54f..104d452 100644 --- a/lib/common.go +++ b/lib/common.go @@ -17,6 +17,7 @@ along with this program. If not, see . package lib +// command line flags var Debug bool var XtendedOut bool var NoNumbering bool @@ -24,5 +25,10 @@ var ShowVersion bool var Columns string var UseColumns []int var Separator string +var OutflagExtended bool +var OutflagMarkdown bool +var OutflagOrgtable bool +var OutputMode string var Version = "v1.0.2" +var validOutputmodes = "(orgtbl|markdown|extended|ascii)" diff --git a/lib/helpers.go b/lib/helpers.go index 08ab0de..5dab33f 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -18,8 +18,10 @@ along with this program. If not, see . package lib import ( + "errors" "fmt" "os" + "regexp" "strconv" "strings" ) @@ -38,7 +40,7 @@ func contains(s []int, e int) bool { return false } -func prepareColumns() { +func PrepareColumns() { if len(Columns) > 0 { for _, use := range strings.Split(Columns, ",") { usenum, err := strconv.Atoi(use) @@ -49,3 +51,32 @@ func prepareColumns() { } } } + +func PrepareModeFlags() error { + if len(OutputMode) == 0 { + switch { + case OutflagExtended: + OutputMode = "extended" + case OutflagMarkdown: + OutputMode = "markdown" + case OutflagOrgtable: + OutputMode = "orgtbl" + default: + OutputMode = "ascii" + } + } else { + r, err := regexp.Compile(validOutputmodes) + + if err != nil { + return errors.New("Failed to validate output mode spec!") + } + + match := r.MatchString(OutputMode) + + if !match { + return errors.New("Invalid output mode!") + } + } + + return nil +} diff --git a/lib/io.go b/lib/io.go index 1bb5caf..3665dbf 100644 --- a/lib/io.go +++ b/lib/io.go @@ -27,7 +27,7 @@ func ProcessFiles(args []string) error { var pattern string havefiles := false - prepareColumns() + //prepareColumns() if len(args) > 0 { if _, err := os.Stat(args[0]); err != nil { diff --git a/lib/parser.go b/lib/parser.go index 24fac74..eadac31 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -144,7 +144,7 @@ func parseFile(input io.Reader, pattern string) Tabdata { // if Debug { // fmt.Printf("<%s> ", value) // } - values = append(values, value) + values = append(values, strings.TrimSpace(value)) idx++ } diff --git a/lib/printer.go b/lib/printer.go index 2f1233b..2f6a056 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -19,78 +19,146 @@ package lib import ( "fmt" + "github.com/olekukonko/tablewriter" + "os" + "regexp" "strings" ) func printData(data Tabdata) { - if XtendedOut { - printExtendedData(data) - } else { - printTabularData(data) - } -} - -func printTabularData(data Tabdata) { - // needed for data output - var formats []string - - if len(data.entries) > 0 { - // headers + // prepare headers + // FIXME: maybe do this already in parseFile()? + if !NoNumbering { + numberedHeaders := []string{} for i, head := range data.headers { if len(Columns) > 0 { if !contains(UseColumns, i+1) { continue } } - - // calculate column width - var width int - var iwidth int - var format string - - // generate format string - if len(head) > data.maxwidthPerCol[i] { - width = len(head) - } else { - width = data.maxwidthPerCol[i] - } - - if NoNumbering { - iwidth = 0 - } else { - iwidth = len(fmt.Sprintf("%d", i)) // in case i > 9 - } - - format = fmt.Sprintf("%%-%ds", 3+iwidth+width) - - if NoNumbering { - fmt.Printf(format, fmt.Sprintf("%s ", head)) - } else { - fmt.Printf(format, fmt.Sprintf("%s(%d) ", head, i+1)) - } - - // register - formats = append(formats, format) - } - fmt.Println() - - // entries - var idx int - for _, entry := range data.entries { - idx = 0 - //fmt.Println(entry) - for i, value := range entry { - if len(Columns) > 0 { - if !contains(UseColumns, i+1) { - continue - } - } - fmt.Printf(formats[idx], strings.TrimSpace(value)) - idx++ - } - fmt.Println() + numberedHeaders = append(numberedHeaders, fmt.Sprintf("%s(%d)", head, i+1)) } + data.headers = numberedHeaders } + + // prepare data + if len(Columns) > 0 { + reducedEntries := [][]string{} + reducedEntry := []string{} + for _, entry := range data.entries { + reducedEntry = nil + for i, value := range entry { + if !contains(UseColumns, i+1) { + continue + } + + reducedEntry = append(reducedEntry, value) + } + reducedEntries = append(reducedEntries, reducedEntry) + } + data.entries = reducedEntries + } + + switch OutputMode { + case "extended": + printExtendedData(data) + case "ascii": + printAsciiData(data) + case "orgtbl": + printOrgmodeData(data) + case "markdown": + printMarkdownData(data) + default: + printAsciiData(data) + } +} + +func trimRow(row []string) []string { + // FIXME: remove this when we only use Tablewriter and strip in ParseFile()! + var fixedrow []string + for _, cell := range row { + fixedrow = append(fixedrow, strings.TrimSpace(cell)) + } + + return fixedrow +} + +/* + Emacs org-mode compatible table (also orgtbl-mode) +*/ +func printOrgmodeData(data Tabdata) { + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + + table.SetHeader(data.headers) + + for _, row := range data.entries { + table.Append(trimRow(row)) + } + + table.Render() + + /* fix output for org-mode (orgtbl) + tableWriter output: + +------+------+ + | cell | cell | + +------+------+ + + Needed for org-mode compatibility: + |------+------| + | cell | cell | + |------+------| + */ + leftR := regexp.MustCompile("(?m)^\\+") + rightR := regexp.MustCompile("\\+(?m)$") + + fmt.Print(rightR.ReplaceAllString(leftR.ReplaceAllString(tableString.String(), "|"), "|")) +} + +/* + Markdown table +*/ +func printMarkdownData(data Tabdata) { + table := tablewriter.NewWriter(os.Stdout) + + table.SetHeader(data.headers) + + for _, row := range data.entries { + table.Append(trimRow(row)) + } + + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + + table.Render() +} + +/* + Simple ASCII table without any borders etc, just like the input we expect +*/ +func printAsciiData(data Tabdata) { + table := tablewriter.NewWriter(os.Stdout) + + table.SetHeader(data.headers) + table.AppendBulk(data.entries) + + // for _, row := range data.entries { + // table.Append(trimRow(row)) + // } + + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") // pad with tabs + table.SetNoWhiteSpace(true) + + table.Render() } /* diff --git a/tablizer.pod b/tablizer.pod index b837422..bb11ee0 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -10,9 +10,12 @@ tablizer - Manipulate tabular output of other programs Flags: -c, --columns string Only show the speficied columns (separated by ,) -d, --debug Enable debugging - -x, --extended Enable extended output + -X, --extended Enable extended output -h, --help help for tablizer + -M, --markdown Enable markdown table output -n, --no-numbering Disable header numbering + -O, --orgtbl Enable org-mode table output + -o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default) -s, --separator string Custom field separator -v, --version Print program version @@ -70,12 +73,12 @@ The numbering can be suppressed by using the B<-n> option. There might be cases when the tabular output of a program is way too large for your current terminal but you still need to see every -column. In such cases the B<-x> option can be usefull which enables -I. In this mode, each row will be printed vertically, -header left, value right, aligned by the field widths. Here's an -example: +column. In such cases the B<-X> option (or B<-o extended> can be +usefull which enables I. In this mode, each row will be +printed vertically, header left, value right, aligned by the field +widths. Here's an example: - kubectl get pods | ./tablizer -x + kubectl get pods | ./tablizer -X NAME: repldepl-7bcd8d5b64-7zq4l READY: 1/1 STATUS: Running @@ -85,6 +88,10 @@ example: You can of course still use a regex to reduce the number of rows displayed. +Beside normal ascii mode (the default) and extended mode there more +output modes available: B which prints an Emacs org-mode table +and B which prints a Markdown table. + Finally the B<-d> option enables debugging output which is mostly usefull for the developer.