From a8c9ede77e7509ecde5522768f585b647ae6ca9f Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 21 Nov 2023 11:40:55 +0100 Subject: [PATCH 01/13] added -L flag to highligh lines in alternating bg color --- cfg/config.go | 9 +++++++-- cmd/root.go | 1 + cmd/tablizer.go | 12 ++++++------ lib/helpers.go | 27 ++++++++++++++++++++++----- tablizer.1 | 7 ++++--- tablizer.pod | 5 +++-- 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index beb8fad..94e6e20 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -22,7 +22,6 @@ import ( "os" "regexp" - "github.com/glycerine/zygomys/zygo" "github.com/gookit/color" ) @@ -46,6 +45,7 @@ type Config struct { Pattern string PatternR *regexp.Regexp UseFuzzySearch bool + UseHighlight bool SortMode string SortDescending bool @@ -55,7 +55,8 @@ type Config struct { FIXME: make configurable somehow, config file or ENV see https://github.com/gookit/color. */ - ColorStyle color.Style + ColorStyle color.Style + HighlightStyle color.Style NoColor bool @@ -104,13 +105,16 @@ func Colors() map[color.Level]map[string]color.Color { return map[color.Level]map[string]color.Color{ color.Level16: { "bg": color.BgGreen, "fg": color.FgBlack, + "hlbg": color.BgGray, "hlfg": color.FgWhite, }, color.Level256: { "bg": color.BgLightGreen, "fg": color.FgBlack, + "hlbg": color.BgGray, "hlfg": color.FgWhite, }, color.LevelRgb: { // FIXME: maybe use something nicer "bg": color.BgLightGreen, "fg": color.FgBlack, + "hlbg": color.BgGray, "hlfg": color.FgWhite, }, } } @@ -123,6 +127,7 @@ func (c *Config) DetermineColormode() { level := color.TermColorLevel() colors := Colors() c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"]) + c.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"]) } } diff --git a/cmd/root.go b/cmd/root.go index d3bb98b..6557ef3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -118,6 +118,7 @@ func Execute() { rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false, "select non-matching rows") rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page") rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false, "Use fuzzy searching") + rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false, "Use alternating background colors") rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "", "Display completion code") rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator, "Custom field separator") rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 300e56c..8811831 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -22,10 +22,11 @@ SYNOPSIS -X, --extended Enable extended output -M, --markdown Enable markdown table output -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable ouput + -S, --shell Enable shell evaluable output -Y, --yaml Enable yaml output -C, --csv Enable CSV output -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): -a, --sort-age sort according to age (duration) string @@ -38,7 +39,7 @@ SYNOPSIS -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page - -v, --version Print program version + -V, --version Print program version DESCRIPTION Many programs generate tabular output. But sometimes you need to @@ -313,10 +314,11 @@ Output Flags (mutually exclusive): -X, --extended Enable extended output -M, --markdown Enable markdown table output -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable ouput + -S, --shell Enable shell evaluable output -Y, --yaml Enable yaml output -C, --csv Enable CSV output -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): -a, --sort-age sort according to age (duration) string @@ -326,12 +328,10 @@ Sort Mode Flags (mutually exclusive): Other Flags: --completion Generate the autocompletion script for - -l --load-path Where to search for lisp plugins. Maybe a file or - a directory containing files with *.zy extension -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page - -v, --version Print program version + -V, --version Print program version ` diff --git a/lib/helpers.go b/lib/helpers.go index dff6fbc..90707a0 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -20,13 +20,14 @@ package lib import ( "errors" "fmt" - "github.com/gookit/color" - "github.com/tlinden/tablizer/cfg" "os" "regexp" "sort" "strconv" "strings" + + "github.com/gookit/color" + "github.com/tlinden/tablizer/cfg" ) func contains(s []int, e int) bool { @@ -155,9 +156,25 @@ func trimRow(row []string) []string { func colorizeData(c cfg.Config, output string) string { if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { r := regexp.MustCompile("(" + c.Pattern + ")") - return r.ReplaceAllStringFunc(output, func(in string) string { - return c.ColorStyle.Sprint(in) - }) + highlight := true + colorized := "" + + for _, line := range strings.Split(output, "\n") { + if c.UseHighlight { + if highlight { + line = c.HighlightStyle.Sprint(line) + } + highlight = !highlight + } else { + line = r.ReplaceAllStringFunc(line, func(in string) string { + return c.ColorStyle.Sprint(in) + }) + } + + colorized += line + "\n" + } + + return colorized } else { return output } diff --git a/tablizer.1 b/tablizer.1 index c651e8d..d60e3f0 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2023-05-06" "1" "User Commands" +.TH TABLIZER 1 "2023-11-21" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -160,10 +160,11 @@ tablizer \- Manipulate tabular output of other programs \& \-X, \-\-extended Enable extended output \& \-M, \-\-markdown Enable markdown table output \& \-O, \-\-orgtbl Enable org\-mode table output -\& \-S, \-\-shell Enable shell evaluable ouput +\& \-S, \-\-shell Enable shell evaluable output \& \-Y, \-\-yaml Enable yaml output \& \-C, \-\-csv Enable CSV output \& \-A, \-\-ascii Default output mode, ascii tabular +\& \-L, \-\-hightlight\-lines Use alternating background colors for tables \& \& Sort Mode Flags (mutually exclusive): \& \-a, \-\-sort\-age sort according to age (duration) string @@ -176,7 +177,7 @@ tablizer \- Manipulate tabular output of other programs \& \-d, \-\-debug Enable debugging \& \-h, \-\-help help for tablizer \& \-m, \-\-man Display manual page -\& \-v, \-\-version Print program version +\& \-V, \-\-version Print program version .Ve .SH "DESCRIPTION" .IX Header "DESCRIPTION" diff --git a/tablizer.pod b/tablizer.pod index b75c080..7c72985 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -21,10 +21,11 @@ tablizer - Manipulate tabular output of other programs -X, --extended Enable extended output -M, --markdown Enable markdown table output -O, --orgtbl Enable org-mode table output - -S, --shell Enable shell evaluable ouput + -S, --shell Enable shell evaluable output -Y, --yaml Enable yaml output -C, --csv Enable CSV output -A, --ascii Default output mode, ascii tabular + -L, --hightlight-lines Use alternating background colors for tables Sort Mode Flags (mutually exclusive): -a, --sort-age sort according to age (duration) string @@ -37,7 +38,7 @@ tablizer - Manipulate tabular output of other programs -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page - -v, --version Print program version + -V, --version Print program version =head1 DESCRIPTION From 3c910ca08f742510191ea885a9631794db5f13a9 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 21 Nov 2023 17:41:04 +0100 Subject: [PATCH 02/13] works but is ugly :( --- cfg/config.go | 10 +++++----- lib/helpers.go | 17 +++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index 94e6e20..acafbcc 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -104,17 +104,17 @@ var ValidHooks []string func Colors() map[color.Level]map[string]color.Color { return map[color.Level]map[string]color.Color{ color.Level16: { - "bg": color.BgGreen, "fg": color.FgBlack, + "bg": color.BgGreen, "fg": color.FgWhite, "hlbg": color.BgGray, "hlfg": color.FgWhite, }, color.Level256: { - "bg": color.BgLightGreen, "fg": color.FgBlack, - "hlbg": color.BgGray, "hlfg": color.FgWhite, + "bg": color.BgLightGreen, "fg": color.FgWhite, + "hlbg": color.BgLightBlue, "hlfg": color.FgWhite, }, color.LevelRgb: { // FIXME: maybe use something nicer - "bg": color.BgLightGreen, "fg": color.FgBlack, - "hlbg": color.BgGray, "hlfg": color.FgWhite, + "bg": color.BgLightGreen, "fg": color.FgWhite, + "hlbg": color.BgBlue, "hlfg": color.FgWhite, }, } } diff --git a/lib/helpers.go b/lib/helpers.go index 90707a0..9398cb2 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -156,21 +156,18 @@ func trimRow(row []string) []string { func colorizeData(c cfg.Config, output string) string { if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { r := regexp.MustCompile("(" + c.Pattern + ")") + return r.ReplaceAllStringFunc(output, func(in string) string { + return c.ColorStyle.Sprint(in) + }) + } else if c.UseHighlight && color.IsConsole(os.Stdout) { highlight := true colorized := "" for _, line := range strings.Split(output, "\n") { - if c.UseHighlight { - if highlight { - line = c.HighlightStyle.Sprint(line) - } - highlight = !highlight - } else { - line = r.ReplaceAllStringFunc(line, func(in string) string { - return c.ColorStyle.Sprint(in) - }) + if highlight { + line = c.HighlightStyle.Sprint(line) } - + highlight = !highlight colorized += line + "\n" } From 811173ddb469c3d17bc604b97be03849f4725aab Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 10:30:40 +0100 Subject: [PATCH 03/13] fixed alternating highlighting, now looks reasonable --- cfg/config.go | 12 +++++++++--- lib/helpers.go | 32 +++++++++++++++++++++++++------- lib/printer.go | 17 ++++++++++++----- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index acafbcc..82b1c9c 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -55,8 +55,10 @@ type Config struct { FIXME: make configurable somehow, config file or ENV see https://github.com/gookit/color. */ - ColorStyle color.Style - HighlightStyle color.Style + ColorStyle color.Style + HighlightStyle color.Style + NoHighlightStyle color.Style + HighlightHdrStyle color.Style NoColor bool @@ -114,7 +116,9 @@ func Colors() map[color.Level]map[string]color.Color { color.LevelRgb: { // FIXME: maybe use something nicer "bg": color.BgLightGreen, "fg": color.FgWhite, - "hlbg": color.BgBlue, "hlfg": color.FgWhite, + "hlbg": color.BgHiGreen, "hlfg": color.FgWhite, + "nohlbg": color.BgWhite, "nohlfg": color.FgLightGreen, + "hdrbg": color.BgBlue, "hdrfg": color.FgWhite, }, } } @@ -128,6 +132,8 @@ func (c *Config) DetermineColormode() { colors := Colors() c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"]) c.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"]) + c.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"]) + c.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"]) } } diff --git a/lib/helpers.go b/lib/helpers.go index 9398cb2..df87336 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -154,24 +154,42 @@ func trimRow(row []string) []string { } func colorizeData(c cfg.Config, output string) string { - if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { - r := regexp.MustCompile("(" + c.Pattern + ")") - return r.ReplaceAllStringFunc(output, func(in string) string { - return c.ColorStyle.Sprint(in) - }) - } else if c.UseHighlight && color.IsConsole(os.Stdout) { + if c.UseHighlight && color.IsConsole(os.Stdout) { highlight := true colorized := "" + first := true for _, line := range strings.Split(output, "\n") { if highlight { - line = c.HighlightStyle.Sprint(line) + if first { + // we need to add two spaces to the header line + // because tablewriter omits them for some reason + // in pprint mode. This doesn't matter as long as + // we don't use colorization. But with colors the + // missing spaces can be seen. + if c.OutputMode == cfg.Ascii { + line = line + " " + } + + line = c.HighlightHdrStyle.Sprint(line) + first = false + } else { + line = c.HighlightStyle.Sprint(line) + } + } else { + line = c.NoHighlightStyle.Sprint(line) } highlight = !highlight + colorized += line + "\n" } return colorized + } else if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { + r := regexp.MustCompile("(" + c.Pattern + ")") + return r.ReplaceAllStringFunc(output, func(in string) string { + return c.ColorStyle.Sprint(in) + }) } else { return output } diff --git a/lib/printer.go b/lib/printer.go index 1d76222..7b6cdcd 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -20,15 +20,16 @@ package lib import ( "encoding/csv" "fmt" - "github.com/gookit/color" - "github.com/olekukonko/tablewriter" - "github.com/tlinden/tablizer/cfg" - "gopkg.in/yaml.v3" "io" "log" "regexp" "strconv" "strings" + + "github.com/gookit/color" + "github.com/olekukonko/tablewriter" + "github.com/tlinden/tablizer/cfg" + "gopkg.in/yaml.v3" ) func printData(w io.Writer, c cfg.Config, data *Tabdata) { @@ -148,9 +149,15 @@ func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) { table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) - table.SetTablePadding("\t") // pad with tabs table.SetNoWhiteSpace(true) + if !c.UseHighlight { + // the tabs destroy the highlighting + table.SetTablePadding("\t") // pad with tabs + } else { + table.SetTablePadding(" ") + } + table.Render() output(w, color.Sprint(colorizeData(c, tableString.String()))) } From f045adf44194c1b6270d406420f89fc8c9191764 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 13:33:26 +0100 Subject: [PATCH 04/13] added config file support to set custom colors --- cfg/config.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++-- cmd/root.go | 10 +++- cmd/tablizer.go | 2 + config.hcl | 12 +++++ go.mod | 24 +++++++-- go.sum | 54 +++++++++++++++---- tablizer.1 | 3 +- tablizer.pod | 32 ++++++++++++ 8 files changed, 253 insertions(+), 20 deletions(-) create mode 100644 config.hcl diff --git a/cfg/config.go b/cfg/config.go index 82b1c9c..ce1bab5 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -19,20 +19,36 @@ package cfg import ( "errors" "fmt" + "log" "os" "regexp" "github.com/glycerine/zygomys/zygo" "github.com/gookit/color" + "github.com/hashicorp/hcl/v2/hclsimple" ) const DefaultSeparator string = `(\s\s+|\t)` -const Version string = "v1.0.17" +const Version string = "v1.1.0" var DefaultLoadPath string = os.Getenv("HOME") + "/.config/tablizer/lisp" +var DefaultConfigfile string = os.Getenv("HOME") + "/.config/tablizer/config" var VERSION string // maintained by -x +// public config, set via config file or using defaults +type Configuration struct { + FG string `hcl:"FG"` + BG string `hcl:"BG"` + HighlightFG string `hcl:"HighlightFG"` + HighlightBG string `hcl:"HighlightBG"` + NoHighlightFG string `hcl:"NoHighlightFG"` + NoHighlightBG string `hcl:"NoHighlightBG"` + HighlightHdrFG string `hcl:"HighlightHdrFG"` + HighlightHdrBG string `hcl:"HighlightHdrBG"` +} + +// internal config type Config struct { Debug bool NoNumbering bool @@ -68,6 +84,11 @@ type Config struct { // a path containing lisp scripts to be loaded on startup LispLoadPath string + + // config file, optional + Configfile string + + Configuration Configuration } // maps outputmode short flags to output mode, ie. -O => -o orgtbl @@ -103,8 +124,8 @@ type Sortmode struct { var ValidHooks []string // default color schemes -func Colors() map[color.Level]map[string]color.Color { - return map[color.Level]map[string]color.Color{ +func (c *Config) Colors() map[color.Level]map[string]color.Color { + colors := map[color.Level]map[string]color.Color{ color.Level16: { "bg": color.BgGreen, "fg": color.FgWhite, "hlbg": color.BgGray, "hlfg": color.FgWhite, @@ -114,13 +135,62 @@ func Colors() map[color.Level]map[string]color.Color { "hlbg": color.BgLightBlue, "hlfg": color.FgWhite, }, color.LevelRgb: { - // FIXME: maybe use something nicer "bg": color.BgLightGreen, "fg": color.FgWhite, "hlbg": color.BgHiGreen, "hlfg": color.FgWhite, "nohlbg": color.BgWhite, "nohlfg": color.FgLightGreen, "hdrbg": color.BgBlue, "hdrfg": color.FgWhite, }, } + + if len(c.Configuration.BG) > 0 { + colors[color.Level16]["bg"] = ColorStringToBGColor(c.Configuration.BG) + colors[color.Level256]["bg"] = ColorStringToBGColor(c.Configuration.BG) + colors[color.LevelRgb]["bg"] = ColorStringToBGColor(c.Configuration.BG) + } + + if len(c.Configuration.FG) > 0 { + colors[color.Level16]["fg"] = ColorStringToColor(c.Configuration.FG) + colors[color.Level256]["fg"] = ColorStringToColor(c.Configuration.FG) + colors[color.LevelRgb]["fg"] = ColorStringToColor(c.Configuration.FG) + } + + if len(c.Configuration.HighlightBG) > 0 { + colors[color.Level16]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) + colors[color.Level256]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) + colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) + } + + if len(c.Configuration.HighlightFG) > 0 { + colors[color.Level16]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) + colors[color.Level256]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) + colors[color.LevelRgb]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) + } + + if len(c.Configuration.NoHighlightBG) > 0 { + colors[color.Level16]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) + colors[color.Level256]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) + colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) + } + + if len(c.Configuration.NoHighlightFG) > 0 { + colors[color.Level16]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) + colors[color.Level256]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) + colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) + } + + if len(c.Configuration.HighlightHdrBG) > 0 { + colors[color.Level16]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) + colors[color.Level256]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) + colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) + } + + if len(c.Configuration.HighlightHdrFG) > 0 { + colors[color.Level16]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) + colors[color.Level256]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) + colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) + } + + return colors } // find supported color mode, modifies config based on constants @@ -129,7 +199,8 @@ func (c *Config) DetermineColormode() { color.Disable() } else { level := color.TermColorLevel() - colors := Colors() + colors := c.Colors() + c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"]) c.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"]) c.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"]) @@ -228,3 +299,58 @@ func (c *Config) PreparePattern(pattern string) error { return nil } + +func (c *Config) ParseConfigfile() error { + if path, err := os.Stat(c.Configfile); !os.IsNotExist(err) { + if !path.IsDir() { + configstring, err := os.ReadFile(path.Name()) + if err != nil { + return err + } + + err = hclsimple.Decode( + path.Name(), []byte(configstring), + nil, &c.Configuration, + ) + if err != nil { + log.Fatalf("Failed to load configuration: %s", err) + } + } + } + + return nil +} + +// translate color string to internal color value +func ColorStringToColor(colorname string) color.Color { + for name, color := range color.FgColors { + if name == colorname { + return color + } + } + + for name, color := range color.ExFgColors { + if name == colorname { + return color + } + } + + return color.Normal +} + +// same, for background colors +func ColorStringToBGColor(colorname string) color.Color { + for name, color := range color.BgColors { + if name == colorname { + return color + } + } + + for name, color := range color.ExBgColors { + if name == colorname { + return color + } + } + + return color.Normal +} diff --git a/cmd/root.go b/cmd/root.go index 6557ef3..1edfb43 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,6 +92,11 @@ func Execute() { } // Setup + err := conf.ParseConfigfile() + if err != nil { + return err + } + conf.CheckEnv() conf.PrepareModeFlags(modeflag) conf.PrepareSortFlags(sortmode) @@ -99,7 +104,7 @@ func Execute() { conf.ApplyDefaults() // setup lisp env, load plugins etc - err := lib.SetupLisp(&conf) + err = lib.SetupLisp(&conf) if err != nil { return err } @@ -146,6 +151,9 @@ func Execute() { // lisp options rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, "Load path for lisp plugins (expects *.zy files)") + // config file + rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile, "config file (default: ~/.config/tablizer/config)") + rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") err := rootCmd.Execute() diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 8811831..5886c0e 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -36,6 +36,7 @@ SYNOPSIS Other Flags: --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page @@ -328,6 +329,7 @@ Sort Mode Flags (mutually exclusive): Other Flags: --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page diff --git a/config.hcl b/config.hcl new file mode 100644 index 0000000..acd41e7 --- /dev/null +++ b/config.hcl @@ -0,0 +1,12 @@ +# supported colors: +# black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, +# lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, +# magenta, red, white, yellow +BG = "lightGreen" +FG = "white" +HighlightBG = "lightGreen" +HighlightFG = "white" +NoHighlightBG = "white" +NoHighlightFG = "lightGreen" +HighlightHdrBG = "red" +HighlightHdrFG = "white" diff --git a/go.mod b/go.mod index c59f01c..27df626 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,40 @@ go 1.18 require ( github.com/alecthomas/repr v0.1.1 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de + github.com/glycerine/zygomys v5.1.2+incompatible github.com/gookit/color v1.5.2 + github.com/hashicorp/hcl/v2 v2.19.1 + github.com/lithammer/fuzzysearch v1.1.7 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.6.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be // indirect + github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect github.com/glycerine/greenpack v5.1.1+incompatible // indirect github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 // indirect - github.com/glycerine/zygomys v5.1.2+incompatible // indirect - github.com/lithammer/fuzzysearch v1.1.7 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/philhofer/fwd v1.1.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect github.com/shurcooL/go-goon v1.0.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/text v0.8.0 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/text v0.13.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) require ( @@ -34,5 +50,5 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index 9437625..6527d53 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,71 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/repr v0.1.1 h1:87P60cSmareLAxMc4Hro0r2RBY4ROm0dYwkJNpS4pPs= github.com/alecthomas/repr v0.1.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be h1:XBJdPGgA3qqhW+p9CANCAVdF7ZIXdu3pZAkypMkKAjE= github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be/go.mod h1:OSCrScrFAjcBObrulk6BEQlytA462OkG1UGB5NYj9kE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/greenpack v5.1.1+incompatible h1:fDr9i6MkSGZmAy4VXPfJhW+SyK2/LNnzIp5nHyDiaIM= github.com/glycerine/greenpack v5.1.1+incompatible/go.mod h1:us0jVISAESGjsEuLlAfCd5nkZm6W6WQF18HPuOecIg4= github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 h1:4ZegphJXBTc4uFQ08UVoWYmQXorGa+ipXetUj83sMBc= github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0/go.mod h1:AqJLs6UeoC65dnHxyCQ6MO31P5STpjcmgaANAU+No8Q= github.com/glycerine/zygomys v5.1.2+incompatible h1:jmcdmA3XPxgfOunAXFpipE9LQoUL6eX6d2mhYyjV4GE= github.com/glycerine/zygomys v5.1.2+incompatible/go.mod h1:i3SPKZpmy9dwF/3iWrXJ/ZLyzZucegwypwOmqRkUUaQ= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= +github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/lithammer/fuzzysearch v1.1.7 h1:q8rZNmBIUkqxsxb/IlwsXVbCoPIH/0juxjFHY0UIwhU= github.com/lithammer/fuzzysearch v1.1.7/go.mod h1:ZhIlfRGxnD8qa9car/yplC6GmnM14CS07BYAKJJBK2I= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E= @@ -47,8 +78,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= @@ -57,9 +89,12 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1z github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -77,11 +112,10 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= @@ -91,16 +125,18 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tablizer.1 b/tablizer.1 index d60e3f0..c8820a2 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2023-11-21" "1" "User Commands" +.TH TABLIZER 1 "2023-11-22" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -174,6 +174,7 @@ tablizer \- Manipulate tabular output of other programs \& \& Other Flags: \& \-\-completion Generate the autocompletion script for +\& \-f, \-\-config Configuration file (default: ~/.config/tablizer/config) \& \-d, \-\-debug Enable debugging \& \-h, \-\-help help for tablizer \& \-m, \-\-man Display manual page diff --git a/tablizer.pod b/tablizer.pod index 7c72985..8bfcfde 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -35,6 +35,7 @@ tablizer - Manipulate tabular output of other programs Other Flags: --completion Generate the autocompletion script for + -f, --config Configuration file (default: ~/.config/tablizer/config) -d, --debug Enable debugging -h, --help help for tablizer -m, --man Display manual page @@ -294,6 +295,37 @@ and source this file from your PowerShell profile. =back +=head1 CONFIGURATION AND COLORS + +YOu can put certain configuration values into a configuration file in +HCL format. By default tablizer looks for +C<$HOME/.config/tablizer/config>, but you can provide one using the +parameter C<-f>. + +In the configuration the following variables can be defined: + + BG = "lightGreen" + FG = "white" + HighlightBG = "lightGreen" + HighlightFG = "white" + NoHighlightBG = "white" + NoHighlightFG = "lightGreen" + HighlightHdrBG = "red" + HighlightHdrFG = "white" + +The following color definitions are available: + +black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, +lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, +magenta, red, white, yellow + +The Variables B and B are being used to highlight matches. The +other *FG and *BG variables are for colored table output (enabled with +the C<-L> parameter). + +Colorization can be turned off completely either by setting the +parameter C<-N> or the environment variable B to a true value. + =head1 BUGS In order to report a bug, unexpected behavior, feature requests From 76b98fb8ad8e420fe250989402d71e7fbe3780f5 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 13:48:32 +0100 Subject: [PATCH 05/13] upd mods --- go.mod | 34 ++++++++++++-------------------- go.sum | 62 +++++++++++++++++++++------------------------------------- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index 27df626..cebaa78 100644 --- a/go.mod +++ b/go.mod @@ -14,41 +14,31 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/zclconf/go-cty v1.13.2 // indirect + require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be // indirect github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 // indirect github.com/glycerine/greenpack v5.1.1+incompatible // indirect github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.5.6 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-runewidth v0.0.10 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/philhofer/fwd v1.1.2 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.1.0 // indirect github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect github.com/shurcooL/go-goon v1.0.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect - github.com/tinylib/msgp v1.1.8 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - github.com/zclconf/go-cty v1.13.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/text v0.13.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect -) - -require ( - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - - // force release. > 0.4. doesnt build everywhere, see: - // https://github.com/TLINDEN/tablizer/actions/runs/3396457307/jobs/5647544615 - github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.12.0 // indirect + github.com/tinylib/msgp v1.1.9 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index 6527d53..6cfa483 100644 --- a/go.sum +++ b/go.sum @@ -9,11 +9,9 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmms github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be h1:XBJdPGgA3qqhW+p9CANCAVdF7ZIXdu3pZAkypMkKAjE= github.com/glycerine/blake2b v0.0.0-20151022103502-3c8c640cd7be/go.mod h1:OSCrScrFAjcBObrulk6BEQlytA462OkG1UGB5NYj9kE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 h1:gclg6gY70GLy3PbkQ1AERPfmLMMagS60DKF78eWwLn8= @@ -25,8 +23,8 @@ github.com/glycerine/liner v0.0.0-20160121172638-72909af234e0/go.mod h1:AqJLs6Ue github.com/glycerine/zygomys v5.1.2+incompatible h1:jmcdmA3XPxgfOunAXFpipE9LQoUL6eX6d2mhYyjV4GE= github.com/glycerine/zygomys v5.1.2+incompatible/go.mod h1:i3SPKZpmy9dwF/3iWrXJ/ZLyzZucegwypwOmqRkUUaQ= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -37,32 +35,24 @@ github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7P github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/lithammer/fuzzysearch v1.1.7 h1:q8rZNmBIUkqxsxb/IlwsXVbCoPIH/0juxjFHY0UIwhU= github.com/lithammer/fuzzysearch v1.1.7/go.mod h1:ZhIlfRGxnD8qa9car/yplC6GmnM14CS07BYAKJJBK2I= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 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/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= @@ -78,30 +68,24 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= +github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= -github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -112,31 +96,29 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3632de10d7c97b35d63590933d35404083fcbc58 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 13:57:57 +0100 Subject: [PATCH 06/13] try to fix linter --- .github/workflows/ci.yaml | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1e3643..8804f61 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,25 +34,5 @@ jobs: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 - #with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - # version: v1.29 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true + with: + skip-cache: true From ddfbecaa353c7075d369d18ae18dd0b67b380ddc Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 14:08:36 +0100 Subject: [PATCH 07/13] +docs, try linter v1.18 --- .github/workflows/ci.yaml | 2 +- cmd/tablizer.go | 30 ++++++++++++++++++++++++++++++ go.mod | 3 +-- go.sum | 4 ++-- tablizer.1 | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8804f61..4aed93d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.17 + go-version: 1.18 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 5886c0e..e49dc17 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -259,6 +259,36 @@ DESCRIPTION and source this file from your PowerShell profile. +CONFIGURATION AND COLORS + YOu can put certain configuration values into a configuration file in + HCL format. By default tablizer looks for + "$HOME/.config/tablizer/config", but you can provide one using the + parameter "-f". + + In the configuration the following variables can be defined: + + BG = "lightGreen" + FG = "white" + HighlightBG = "lightGreen" + HighlightFG = "white" + NoHighlightBG = "white" + NoHighlightFG = "lightGreen" + HighlightHdrBG = "red" + HighlightHdrFG = "white" + + The following color definitions are available: + + black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, + lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, magenta, + red, white, yellow + + The Variables FG and BG are being used to highlight matches. The other + *FG and *BG variables are for colored table output (enabled with the + "-L" parameter). + + Colorization can be turned off completely either by setting the + parameter "-N" or the environment variable NO_COLOR to a true value. + BUGS In order to report a bug, unexpected behavior, feature requests or to submit a patch, please open an issue on github: diff --git a/go.mod b/go.mod index cebaa78..e87a3a8 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/zclconf/go-cty v1.13.2 // indirect - require ( github.com/agext/levenshtein v1.2.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect @@ -38,6 +36,7 @@ require ( github.com/tinylib/msgp v1.1.9 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index 6cfa483..79ff9d7 100644 --- a/go.sum +++ b/go.sum @@ -77,8 +77,8 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/tablizer.1 b/tablizer.1 index c8820a2..2d8285c 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -446,6 +446,38 @@ To load completions for every new session, run: .Ve .Sp and source this file from your PowerShell profile. +.SH "CONFIGURATION AND COLORS" +.IX Header "CONFIGURATION AND COLORS" +YOu can put certain configuration values into a configuration file in +\&\s-1HCL\s0 format. By default tablizer looks for +\&\f(CW\*(C`$HOME/.config/tablizer/config\*(C'\fR, but you can provide one using the +parameter \f(CW\*(C`\-f\*(C'\fR. +.PP +In the configuration the following variables can be defined: +.PP +.Vb 8 +\& BG = "lightGreen" +\& FG = "white" +\& HighlightBG = "lightGreen" +\& HighlightFG = "white" +\& NoHighlightBG = "white" +\& NoHighlightFG = "lightGreen" +\& HighlightHdrBG = "red" +\& HighlightHdrFG = "white" +.Ve +.PP +The following color definitions are available: +.PP +black, blue, cyan, darkGray, default, green, lightBlue, lightCyan, +lightGreen, lightMagenta, lightRed, lightWhite, lightYellow, +magenta, red, white, yellow +.PP +The Variables \fB\s-1FG\s0\fR and \fB\s-1BG\s0\fR are being used to highlight matches. The +other *FG and *BG variables are for colored table output (enabled with +the \f(CW\*(C`\-L\*(C'\fR parameter). +.PP +Colorization can be turned off completely either by setting the +parameter \f(CW\*(C`\-N\*(C'\fR or the environment variable \fB\s-1NO_COLOR\s0\fR to a true value. .SH "BUGS" .IX Header "BUGS" In order to report a bug, unexpected behavior, feature requests From 0f224579618aa0409e0ed77b49b04b324b791447 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 22 Nov 2023 14:09:49 +0100 Subject: [PATCH 08/13] remove go 1.17 support --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4aed93d..75e6052 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,7 +4,7 @@ jobs: build: strategy: matrix: - version: [1.17, 1.18, 1.19] + version: [1.18, 1.19] os: [ubuntu-latest, windows-latest, macos-latest] name: Build runs-on: ${{ matrix.os }} From 96f7881c162ed1c594625c099b56b409dc8ad6fe Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 7 May 2024 13:26:41 +0200 Subject: [PATCH 09/13] fix #12: only consider -v if there's a pattern, ignore it otherwise --- lib/parser.go | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/parser.go b/lib/parser.go index df241af..28bb2a7 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 Thomas von Dein +Copyright © 2022-2024 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,26 +27,9 @@ import ( "strings" "github.com/alecthomas/repr" - "github.com/lithammer/fuzzysearch/fuzzy" "github.com/tlinden/tablizer/cfg" ) -/* - * [!]Match a line, use fuzzy search for normal pattern strings and - * regexp otherwise. - */ -func matchPattern(c cfg.Config, line string) bool { - if len(c.Pattern) > 0 { - if c.UseFuzzySearch { - return fuzzy.MatchFold(c.Pattern, line) - } else { - return c.PatternR.MatchString(line) - } - } - - return true -} - /* Parser switch */ @@ -73,7 +56,7 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { line := strings.TrimSpace(scanner.Text()) if hadFirst { // don't match 1st line, it's the header - if matchPattern(c, line) == c.InvertMatch { + if c.Pattern != "" && matchPattern(c, line) == c.InvertMatch { // by default -v is false, so if a line does NOT // match the pattern, we will ignore it. However, // if the user specified -v, the matching is inverted, @@ -180,7 +163,7 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { } } else { // data processing - if matchPattern(c, line) == c.InvertMatch { + if c.Pattern != "" && matchPattern(c, line) == c.InvertMatch { // by default -v is false, so if a line does NOT // match the pattern, we will ignore it. However, // if the user specified -v, the matching is inverted, @@ -224,6 +207,15 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err())) } + // filter by field filters, if any + filtereddata, changed, err := FilterByFields(c, data) + if err != nil { + return data, fmt.Errorf("Failed to filter fields: %w", err) + } + if changed { + data = filtereddata + } + // apply user defined lisp process hooks, if any userdata, changed, err := RunProcessHooks(c, data) if err != nil { From ba2a2e8460d5c81743ce36f4e76a29fadeea8351 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 7 May 2024 13:28:11 +0200 Subject: [PATCH 10/13] add -F filter by column flag (closes #13) --- cfg/config.go | 29 +++++++- cmd/root.go | 16 ++++- cmd/tablizer.go | 19 +++++- lib/common.go | 12 +++- lib/filter.go | 82 +++++++++++++++++++++++ lib/filter_test.go | 164 +++++++++++++++++++++++++++++++++++++++++++++ tablizer.1 | 24 +++++-- tablizer.pod | 19 +++++- 8 files changed, 351 insertions(+), 14 deletions(-) create mode 100644 lib/filter.go create mode 100644 lib/filter_test.go diff --git a/cfg/config.go b/cfg/config.go index ce1bab5..dbc3d5c 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 Thomas von Dein +Copyright © 2022-2024 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ import ( "log" "os" "regexp" + "strings" "github.com/glycerine/zygomys/zygo" "github.com/gookit/color" @@ -29,7 +30,7 @@ import ( ) const DefaultSeparator string = `(\s\s+|\t)` -const Version string = "v1.1.0" +const Version string = "v1.2.0" var DefaultLoadPath string = os.Getenv("HOME") + "/.config/tablizer/lisp" var DefaultConfigfile string = os.Getenv("HOME") + "/.config/tablizer/config" @@ -89,6 +90,10 @@ type Config struct { Configfile string Configuration Configuration + + // used for field filtering + Rawfilters []string + Filters map[string]*regexp.Regexp } // maps outputmode short flags to output mode, ie. -O => -o orgtbl @@ -260,6 +265,26 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) { } } +func (conf *Config) PrepareFilters() error { + conf.Filters = make(map[string]*regexp.Regexp, len(conf.Rawfilters)) + + for _, filter := range conf.Rawfilters { + parts := strings.Split(filter, "=") + if len(parts) != 2 { + return errors.New("filter field and value must be separated by =") + } + + reg, err := regexp.Compile(parts[1]) + if err != nil { + return err + } + + conf.Filters[strings.ToLower(parts[0])] = reg + } + + return nil +} + func (c *Config) CheckEnv() { // check for environment vars, command line flags have precedence, // NO_COLOR is being checked by the color module itself. diff --git a/cmd/root.go b/cmd/root.go index 1edfb43..50352b6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 Thomas von Dein +Copyright © 2022-2024 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -100,6 +100,11 @@ func Execute() { conf.CheckEnv() conf.PrepareModeFlags(modeflag) conf.PrepareSortFlags(sortmode) + + if err = conf.PrepareFilters(); err != nil { + return err + } + conf.DetermineColormode() conf.ApplyDefaults() @@ -149,10 +154,15 @@ func Execute() { rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv") // lisp options - rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, "Load path for lisp plugins (expects *.zy files)") + rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, + "Load path for lisp plugins (expects *.zy files)") // config file - rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile, "config file (default: ~/.config/tablizer/config)") + rootCmd.PersistentFlags().StringVarP(&conf.Configfile, "config", "f", cfg.DefaultConfigfile, + "config file (default: ~/.config/tablizer/config)") + + // filters + rootCmd.PersistentFlags().StringArrayVarP(&conf.Rawfilters, "filter", "F", nil, "Filter by field (field=regexp)") rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") diff --git a/cmd/tablizer.go b/cmd/tablizer.go index e49dc17..5f0d23e 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -17,6 +17,7 @@ SYNOPSIS -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) -z, --fuzzy Use fuzzy seach [experimental] + -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): -X, --extended Enable extended output @@ -117,7 +118,7 @@ DESCRIPTION Finally the -d option enables debugging output which is mostly useful for the developer. - PATTERNS + PATTERNS AND FILTERING You can reduce the rows being displayed by using a regular expression pattern. The regexp is PCRE compatible, refer to the syntax cheat sheet here: . If you want to read a @@ -145,6 +146,19 @@ DESCRIPTION -z, in which case the pattern is regarded as a fuzzy search term, not a regexp. + Sometimes you want to filter by one or more columns. You can do that + using the -F option. The option can be specified multiple times and has + the following format: + + fieldname=regexp + + Fieldnames (== columns headers) are case insensitive. + + If you specify more than one filter, both filters have to match (AND + operation). + + If the option -v is specified, the filtering is inverted. + COLUMNS The parameter -c can be used to specify, which columns to display. By default tablizer numerizes the header names and these numbers can be @@ -298,7 +312,7 @@ LICENSE This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3. - Copyright (c) 2023 by Thomas von Dein + Copyright (c) 2022-2024 by Thomas von Dein This software uses the following GO modules: @@ -340,6 +354,7 @@ Operational Flags: -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) -z, --fuzzy Use fuzzy seach [experimental] + -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): -X, --extended Enable extended output diff --git a/lib/common.go b/lib/common.go index 5e6e920..0c9b915 100644 --- a/lib/common.go +++ b/lib/common.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 Thomas von Dein +Copyright © 2022-2024 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,3 +24,13 @@ type Tabdata struct { headers []string // [ "ID", "NAME", ...] entries [][]string } + +func (data *Tabdata) CloneEmpty() Tabdata { + new := Tabdata{ + maxwidthHeader: data.maxwidthHeader, + columns: data.columns, + headers: data.headers, + } + + return new +} diff --git a/lib/filter.go b/lib/filter.go new file mode 100644 index 0000000..022fd91 --- /dev/null +++ b/lib/filter.go @@ -0,0 +1,82 @@ +/* +Copyright © 2022-2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package lib + +import ( + "strings" + + "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/tlinden/tablizer/cfg" +) + +/* + * [!]Match a line, use fuzzy search for normal pattern strings and + * regexp otherwise. + */ +func matchPattern(c cfg.Config, line string) bool { + if c.UseFuzzySearch { + return fuzzy.MatchFold(c.Pattern, line) + } + + return c.PatternR.MatchString(line) +} + +/* + * Filter parsed data by fields. The filter is positive, so if one or + * more filters match on a row, it will be kept, otherwise it will be + * excluded. + */ +func FilterByFields(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { + if len(conf.Filters) == 0 { + // no filters, no checking + return Tabdata{}, false, nil + } + + newdata := data.CloneEmpty() + + for _, row := range data.entries { + keep := true + + for idx, header := range data.headers { + if !Exists(conf.Filters, strings.ToLower(header)) { + // do not filter by unspecified field + continue + } + + if !conf.Filters[strings.ToLower(header)].MatchString(row[idx]) { + // there IS a filter, but it doesn't match + keep = false + break + } + } + + if keep == !conf.InvertMatch { + // also apply -v + newdata.entries = append(newdata.entries, row) + } + } + + return newdata, true, nil +} + +func Exists[K comparable, V any](m map[K]V, v K) bool { + if _, ok := m[v]; ok { + return true + } + return false +} diff --git a/lib/filter_test.go b/lib/filter_test.go new file mode 100644 index 0000000..7124d0b --- /dev/null +++ b/lib/filter_test.go @@ -0,0 +1,164 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package lib + +import ( + "fmt" + "reflect" + "testing" + + "github.com/tlinden/tablizer/cfg" +) + +func TestMatchPattern(t *testing.T) { + var input = []struct { + name string + fuzzy bool + pattern string + line string + }{ + { + name: "normal", + pattern: "haus", + line: "hausparty", + }, + { + name: "fuzzy", + pattern: "hpt", + line: "haus-party-termin", + fuzzy: true, + }, + } + + for _, in := range input { + testname := fmt.Sprintf("match-pattern-%s", in.name) + + t.Run(testname, func(t *testing.T) { + c := cfg.Config{} + + if in.fuzzy { + c.UseFuzzySearch = true + } + + err := c.PreparePattern(in.pattern) + if err != nil { + t.Errorf("PreparePattern returned error: %s", err) + } + + if !matchPattern(c, in.line) { + t.Errorf("matchPattern() did not match\nExp: true\nGot: false\n") + } + }) + } + +} + +func TestFilterByFields(t *testing.T) { + data := Tabdata{ + headers: []string{ + "ONE", "TWO", "THREE", + }, + entries: [][]string{ + {"asd", "igig", "cxxxncnc"}, + {"19191", "EDD 1", "x"}, + {"8d8", "AN 1", "y"}, + }, + } + + var input = []struct { + name string + filter []string + expect Tabdata + invert bool + }{ + { + name: "one-field", + filter: []string{"one=19"}, + expect: Tabdata{ + headers: []string{ + "ONE", "TWO", "THREE", + }, + entries: [][]string{ + {"19191", "EDD 1", "x"}, + }, + }, + }, + + { + name: "one-field-inverted", + filter: []string{"one=19"}, + invert: true, + expect: Tabdata{ + headers: []string{ + "ONE", "TWO", "THREE", + }, + entries: [][]string{ + {"asd", "igig", "cxxxncnc"}, + {"8d8", "AN 1", "y"}, + }, + }, + }, + + { + name: "many-fields", + filter: []string{"one=19", "two=DD"}, + expect: Tabdata{ + headers: []string{ + "ONE", "TWO", "THREE", + }, + entries: [][]string{ + {"19191", "EDD 1", "x"}, + }, + }, + }, + + { + name: "many-fields-inverted", + filter: []string{"one=19", "two=DD"}, + invert: true, + expect: Tabdata{ + headers: []string{ + "ONE", "TWO", "THREE", + }, + entries: [][]string{ + {"asd", "igig", "cxxxncnc"}, + {"8d8", "AN 1", "y"}, + }, + }, + }, + } + + for _, in := range input { + testname := fmt.Sprintf("filter-by-fields-%s", in.name) + + t.Run(testname, func(t *testing.T) { + c := cfg.Config{Rawfilters: in.filter, InvertMatch: in.invert} + + err := c.PrepareFilters() + if err != nil { + t.Errorf("PrepareFilters returned error: %s", err) + } + + data, _, _ := FilterByFields(c, data) + if !reflect.DeepEqual(data, in.expect) { + t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, in.expect) + } + }) + } + +} diff --git a/tablizer.1 b/tablizer.1 index 2d8285c..4ea873f 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "TABLIZER 1" -.TH TABLIZER 1 "2023-11-22" "1" "User Commands" +.TH TABLIZER 1 "2024-05-07" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -155,6 +155,7 @@ tablizer \- Manipulate tabular output of other programs \& \-s, \-\-separator string Custom field separator \& \-k, \-\-sort\-by int Sort by column (default: 1) \& \-z, \-\-fuzzy Use fuzzy seach [experimental] +\& \-F, \-\-filter field=reg Filter given field with regex, can be used multiple times \& \& Output Flags (mutually exclusive): \& \-X, \-\-extended Enable extended output @@ -264,8 +265,8 @@ Sorts timestamps. .PP Finally the \fB\-d\fR option enables debugging output which is mostly useful for the developer. -.SS "\s-1PATTERNS\s0" -.IX Subsection "PATTERNS" +.SS "\s-1PATTERNS AND FILTERING\s0" +.IX Subsection "PATTERNS AND FILTERING" You can reduce the rows being displayed by using a regular expression pattern. The regexp is \s-1PCRE\s0 compatible, refer to the syntax cheat sheet here: . If you want @@ -300,6 +301,21 @@ Example for a case insensitive search: You can use the experimental fuzzy seach feature by providing the option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search term, not a regexp. +.PP +Sometimes you want to filter by one or more columns. You can do that +using the \fB\-F\fR option. The option can be specified multiple times and +has the following format: +.PP +.Vb 1 +\& fieldname=regexp +.Ve +.PP +Fieldnames (== columns headers) are case insensitive. +.PP +If you specify more than one filter, both filters have to match (\s-1AND\s0 +operation). +.PP +If the option \fB\-v\fR is specified, the filtering is inverted. .SS "\s-1COLUMNS\s0" .IX Subsection "COLUMNS" The parameter \fB\-c\fR can be used to specify, which columns to @@ -487,7 +503,7 @@ or to submit a patch, please open an issue on github: .IX Header "LICENSE" This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3. .PP -Copyright (c) 2023 by Thomas von Dein +Copyright (c) 2022\-2024 by Thomas von Dein .PP This software uses the following \s-1GO\s0 modules: .IP "repr (https://github.com/alecthomas/repr)" 4 diff --git a/tablizer.pod b/tablizer.pod index 8bfcfde..79884cd 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -16,6 +16,7 @@ tablizer - Manipulate tabular output of other programs -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) -z, --fuzzy Use fuzzy seach [experimental] + -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): -X, --extended Enable extended output @@ -128,7 +129,7 @@ Sorts timestamps. Finally the B<-d> option enables debugging output which is mostly useful for the developer. -=head2 PATTERNS +=head2 PATTERNS AND FILTERING You can reduce the rows being displayed by using a regular expression pattern. The regexp is PCRE compatible, refer to the syntax cheat @@ -159,6 +160,20 @@ You can use the experimental fuzzy seach feature by providing the option B<-z>, in which case the pattern is regarded as a fuzzy search term, not a regexp. +Sometimes you want to filter by one or more columns. You can do that +using the B<-F> option. The option can be specified multiple times and +has the following format: + + fieldname=regexp + +Fieldnames (== columns headers) are case insensitive. + +If you specify more than one filter, both filters have to match (AND +operation). + +If the option B<-v> is specified, the filtering is inverted. + + =head2 COLUMNS The parameter B<-c> can be used to specify, which columns to @@ -336,7 +351,7 @@ L. This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3. -Copyright (c) 2023 by Thomas von Dein +Copyright (c) 2022-2024 by Thomas von Dein This software uses the following GO modules: From 39609425e53e39bc479baa32ae1621c0807983e8 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 7 May 2024 15:19:54 +0200 Subject: [PATCH 11/13] refactoring and gouncritic, 1st part --- Makefile | 7 ++ cfg/config.go | 176 ++++++++++++++++++++++---------------------- cfg/config_test.go | 25 ++++--- cmd/root.go | 88 ++++++++++++++-------- cmd/tablizer.go | 10 +-- lib/filter.go | 8 +- lib/filter_test.go | 28 +++---- lib/helpers.go | 65 ++++++++-------- lib/helpers_test.go | 37 ++++++---- lib/io.go | 69 +++++++++-------- lib/lisp.go | 87 ++++++++++++---------- lib/parser.go | 51 +++++++------ lib/parser_test.go | 6 +- lib/printer.go | 88 +++++++++++----------- lib/printer_test.go | 6 +- lib/sort.go | 57 +++++++------- tablizer.1 | 4 +- tablizer.pod | 4 +- 18 files changed, 434 insertions(+), 382 deletions(-) diff --git a/Makefile b/Makefile index 2064c0a..a59b7da 100644 --- a/Makefile +++ b/Makefile @@ -91,3 +91,10 @@ show-versions: buildlocal goupdate: go get -t -u=patch ./... + +lint: + golangci-lint run + +lint-full: + golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn + gocritic check -enableAll *.go diff --git a/cfg/config.go b/cfg/config.go index dbc3d5c..51d8347 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -19,7 +19,6 @@ package cfg import ( "errors" "fmt" - "log" "os" "regexp" "strings" @@ -31,14 +30,15 @@ import ( const DefaultSeparator string = `(\s\s+|\t)` const Version string = "v1.2.0" +const MAXPARTS = 2 -var DefaultLoadPath string = os.Getenv("HOME") + "/.config/tablizer/lisp" -var DefaultConfigfile string = os.Getenv("HOME") + "/.config/tablizer/config" +var DefaultLoadPath = os.Getenv("HOME") + "/.config/tablizer/lisp" +var DefaultConfigfile = os.Getenv("HOME") + "/.config/tablizer/config" var VERSION string // maintained by -x // public config, set via config file or using defaults -type Configuration struct { +type Settings struct { FG string `hcl:"FG"` BG string `hcl:"BG"` HighlightFG string `hcl:"HighlightFG"` @@ -89,7 +89,7 @@ type Config struct { // config file, optional Configfile string - Configuration Configuration + Settings Settings // used for field filtering Rawfilters []string @@ -115,7 +115,7 @@ const ( Shell Yaml CSV - Ascii + ASCII ) // various sort types @@ -129,7 +129,7 @@ type Sortmode struct { var ValidHooks []string // default color schemes -func (c *Config) Colors() map[color.Level]map[string]color.Color { +func (conf *Config) Colors() map[color.Level]map[string]color.Color { colors := map[color.Level]map[string]color.Color{ color.Level16: { "bg": color.BgGreen, "fg": color.FgWhite, @@ -147,89 +147,85 @@ func (c *Config) Colors() map[color.Level]map[string]color.Color { }, } - if len(c.Configuration.BG) > 0 { - colors[color.Level16]["bg"] = ColorStringToBGColor(c.Configuration.BG) - colors[color.Level256]["bg"] = ColorStringToBGColor(c.Configuration.BG) - colors[color.LevelRgb]["bg"] = ColorStringToBGColor(c.Configuration.BG) + if len(conf.Settings.BG) > 0 { + colors[color.Level16]["bg"] = ColorStringToBGColor(conf.Settings.BG) + colors[color.Level256]["bg"] = ColorStringToBGColor(conf.Settings.BG) + colors[color.LevelRgb]["bg"] = ColorStringToBGColor(conf.Settings.BG) } - if len(c.Configuration.FG) > 0 { - colors[color.Level16]["fg"] = ColorStringToColor(c.Configuration.FG) - colors[color.Level256]["fg"] = ColorStringToColor(c.Configuration.FG) - colors[color.LevelRgb]["fg"] = ColorStringToColor(c.Configuration.FG) + if len(conf.Settings.FG) > 0 { + colors[color.Level16]["fg"] = ColorStringToColor(conf.Settings.FG) + colors[color.Level256]["fg"] = ColorStringToColor(conf.Settings.FG) + colors[color.LevelRgb]["fg"] = ColorStringToColor(conf.Settings.FG) } - if len(c.Configuration.HighlightBG) > 0 { - colors[color.Level16]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) - colors[color.Level256]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) - colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(c.Configuration.HighlightBG) + if len(conf.Settings.HighlightBG) > 0 { + colors[color.Level16]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG) + colors[color.Level256]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG) + colors[color.LevelRgb]["hlbg"] = ColorStringToBGColor(conf.Settings.HighlightBG) } - if len(c.Configuration.HighlightFG) > 0 { - colors[color.Level16]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) - colors[color.Level256]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) - colors[color.LevelRgb]["hlfg"] = ColorStringToColor(c.Configuration.HighlightFG) + if len(conf.Settings.HighlightFG) > 0 { + colors[color.Level16]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG) + colors[color.Level256]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG) + colors[color.LevelRgb]["hlfg"] = ColorStringToColor(conf.Settings.HighlightFG) } - if len(c.Configuration.NoHighlightBG) > 0 { - colors[color.Level16]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) - colors[color.Level256]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) - colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(c.Configuration.NoHighlightBG) + if len(conf.Settings.NoHighlightBG) > 0 { + colors[color.Level16]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG) + colors[color.Level256]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG) + colors[color.LevelRgb]["nohlbg"] = ColorStringToBGColor(conf.Settings.NoHighlightBG) } - if len(c.Configuration.NoHighlightFG) > 0 { - colors[color.Level16]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) - colors[color.Level256]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) - colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(c.Configuration.NoHighlightFG) + if len(conf.Settings.NoHighlightFG) > 0 { + colors[color.Level16]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG) + colors[color.Level256]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG) + colors[color.LevelRgb]["nohlfg"] = ColorStringToColor(conf.Settings.NoHighlightFG) } - if len(c.Configuration.HighlightHdrBG) > 0 { - colors[color.Level16]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) - colors[color.Level256]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) - colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(c.Configuration.HighlightHdrBG) + if len(conf.Settings.HighlightHdrBG) > 0 { + colors[color.Level16]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG) + colors[color.Level256]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG) + colors[color.LevelRgb]["hdrbg"] = ColorStringToBGColor(conf.Settings.HighlightHdrBG) } - if len(c.Configuration.HighlightHdrFG) > 0 { - colors[color.Level16]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) - colors[color.Level256]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) - colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(c.Configuration.HighlightHdrFG) + if len(conf.Settings.HighlightHdrFG) > 0 { + colors[color.Level16]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG) + colors[color.Level256]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG) + colors[color.LevelRgb]["hdrfg"] = ColorStringToColor(conf.Settings.HighlightHdrFG) } return colors } // find supported color mode, modifies config based on constants -func (c *Config) DetermineColormode() { +func (conf *Config) DetermineColormode() { if !isTerminal(os.Stdout) { color.Disable() } else { level := color.TermColorLevel() - colors := c.Colors() + colors := conf.Colors() - c.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"]) - c.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"]) - c.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"]) - c.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"]) + conf.ColorStyle = color.New(colors[level]["bg"], colors[level]["fg"]) + conf.HighlightStyle = color.New(colors[level]["hlbg"], colors[level]["hlfg"]) + conf.NoHighlightStyle = color.New(colors[level]["nohlbg"], colors[level]["nohlfg"]) + conf.HighlightHdrStyle = color.New(colors[level]["hdrbg"], colors[level]["hdrfg"]) } } // Return true if current terminal is interactive func isTerminal(f *os.File) bool { o, _ := f.Stat() - if (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice { - return true - } else { - return false - } + return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice } +// main program version +// generated version string, used by -v contains lib.Version on +// +// main branch, and lib.Version-$branch-$lastcommit-$date on +// +// development branch func Getversion() string { - // main program version - - // generated version string, used by -v contains lib.Version on - // main branch, and lib.Version-$branch-$lastcommit-$date on - // development branch - return fmt.Sprintf("This is tablizer version %s", VERSION) } @@ -261,7 +257,7 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) { case flag.C: conf.OutputMode = CSV default: - conf.OutputMode = Ascii + conf.OutputMode = ASCII } } @@ -270,13 +266,14 @@ func (conf *Config) PrepareFilters() error { for _, filter := range conf.Rawfilters { parts := strings.Split(filter, "=") - if len(parts) != 2 { + if len(parts) != MAXPARTS { return errors.New("filter field and value must be separated by =") } reg, err := regexp.Compile(parts[1]) if err != nil { - return err + return fmt.Errorf("failed to compile filter regex for field %s: %w", + parts[0], err) } conf.Filters[strings.ToLower(parts[0])] = reg @@ -285,62 +282,69 @@ func (conf *Config) PrepareFilters() error { return nil } -func (c *Config) CheckEnv() { +func (conf *Config) CheckEnv() { // check for environment vars, command line flags have precedence, // NO_COLOR is being checked by the color module itself. - if !c.NoNumbering { + if !conf.NoNumbering { _, set := os.LookupEnv("T_NO_HEADER_NUMBERING") if set { - c.NoNumbering = true + conf.NoNumbering = true } } - if len(c.Columns) == 0 { + if len(conf.Columns) == 0 { cols := os.Getenv("T_COLUMNS") if len(cols) > 1 { - c.Columns = cols + conf.Columns = cols } } } -func (c *Config) ApplyDefaults() { +func (conf *Config) ApplyDefaults() { // mode specific defaults - if c.OutputMode == Yaml || c.OutputMode == CSV { - c.NoNumbering = true + if conf.OutputMode == Yaml || conf.OutputMode == CSV { + conf.NoNumbering = true } ValidHooks = []string{"filter", "process", "transpose", "append"} } -func (c *Config) PreparePattern(pattern string) error { +func (conf *Config) PreparePattern(pattern string) error { PatternR, err := regexp.Compile(pattern) if err != nil { - return errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", c.Pattern, err)) + return fmt.Errorf("regexp pattern %s is invalid: %w", conf.Pattern, err) } - c.PatternR = PatternR - c.Pattern = pattern + conf.PatternR = PatternR + conf.Pattern = pattern return nil } -func (c *Config) ParseConfigfile() error { - if path, err := os.Stat(c.Configfile); !os.IsNotExist(err) { - if !path.IsDir() { - configstring, err := os.ReadFile(path.Name()) - if err != nil { - return err - } +// Parse config file. Ignore if the file doesn't exist but return an +// error if it exists but fails to read or parse +func (conf *Config) ParseConfigfile() error { + path, err := os.Stat(conf.Configfile) - err = hclsimple.Decode( - path.Name(), []byte(configstring), - nil, &c.Configuration, - ) - if err != nil { - log.Fatalf("Failed to load configuration: %s", err) - } - } + if os.IsNotExist(err) || path.IsDir() { + // ignore non-existent or dirs + return nil + } + + configstring, err := os.ReadFile(path.Name()) + if err != nil { + return fmt.Errorf("failed to read config file %s: %w", path.Name(), err) + } + + err = hclsimple.Decode( + path.Name(), + configstring, + nil, + &conf.Settings) + if err != nil { + return fmt.Errorf("failed to load configuration file %s: %w", + path.Name(), err) } return nil diff --git a/cfg/config_test.go b/cfg/config_test.go index de7424c..3bf2732 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -34,7 +34,7 @@ func TestPrepareModeFlags(t *testing.T) { {Modeflag{O: true}, Orgtbl}, {Modeflag{Y: true}, Yaml}, {Modeflag{M: true}, Markdown}, - {Modeflag{}, Ascii}, + {Modeflag{}, ASCII}, } // FIXME: use a map for easier printing @@ -63,15 +63,15 @@ func TestPrepareSortFlags(t *testing.T) { {Sortmode{}, "string"}, } - for _, tt := range tests { - testname := fmt.Sprintf("PrepareSortFlags-expect-%s", tt.expect) + for _, testdata := range tests { + testname := fmt.Sprintf("PrepareSortFlags-expect-%s", testdata.expect) t.Run(testname, func(t *testing.T) { - c := Config{} + conf := Config{} - c.PrepareSortFlags(tt.flag) + conf.PrepareSortFlags(testdata.flag) - if c.SortMode != tt.expect { - t.Errorf("got: %s, expect: %s", c.SortMode, tt.expect) + if conf.SortMode != testdata.expect { + t.Errorf("got: %s, expect: %s", conf.SortMode, testdata.expect) } }) } @@ -86,15 +86,16 @@ func TestPreparePattern(t *testing.T) { {"[a-z", true}, } - for _, tt := range tests { - testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", tt.pattern, tt.wanterr) + for _, testdata := range tests { + testname := fmt.Sprintf("PreparePattern-pattern-%s-wanterr-%t", + testdata.pattern, testdata.wanterr) t.Run(testname, func(t *testing.T) { - c := Config{} + conf := Config{} - err := c.PreparePattern(tt.pattern) + err := conf.PreparePattern(testdata.pattern) if err != nil { - if !tt.wanterr { + if !testdata.wanterr { t.Errorf("PreparePattern returned error: %s", err) } } diff --git a/cmd/root.go b/cmd/root.go index 50352b6..3bf3c8d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,11 +33,11 @@ import ( func man() { man := exec.Command("less", "-") - var b bytes.Buffer - b.Write([]byte(manpage)) + var buffer bytes.Buffer + buffer.Write([]byte(manpage)) man.Stdout = os.Stdout - man.Stdin = &b + man.Stdin = &buffer man.Stderr = os.Stderr err := man.Run() @@ -58,7 +58,7 @@ func completion(cmd *cobra.Command, mode string) error { case "powershell": return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) default: - return errors.New("Invalid shell parameter! Valid ones: bash|zsh|fish|powershell") + return errors.New("invalid shell parameter! Valid ones: bash|zsh|fish|powershell") } } @@ -79,11 +79,13 @@ func Execute() { RunE: func(cmd *cobra.Command, args []string) error { if ShowVersion { fmt.Println(cfg.Getversion()) + return nil } if ShowManual { man() + return nil } @@ -120,38 +122,64 @@ func Execute() { } // options - rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging") - rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false, "Disable header numbering") - rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false, "Disable header display") - rootCmd.PersistentFlags().BoolVarP(&conf.NoColor, "no-color", "N", false, "Disable pattern highlighting") - rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "V", false, "Print program version") - rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false, "select non-matching rows") - rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, "Display manual page") - rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false, "Use fuzzy searching") - rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false, "Use alternating background colors") - rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "", "Display completion code") - rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator, "Custom field separator") - rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") + rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, + "Enable debugging") + rootCmd.PersistentFlags().BoolVarP(&conf.NoNumbering, "no-numbering", "n", false, + "Disable header numbering") + rootCmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "H", false, + "Disable header display") + rootCmd.PersistentFlags().BoolVarP(&conf.NoColor, "no-color", "N", false, + "Disable pattern highlighting") + rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "V", false, + "Print program version") + rootCmd.PersistentFlags().BoolVarP(&conf.InvertMatch, "invert-match", "v", false, + "select non-matching rows") + rootCmd.PersistentFlags().BoolVarP(&ShowManual, "man", "m", false, + "Display manual page") + rootCmd.PersistentFlags().BoolVarP(&conf.UseFuzzySearch, "fuzzy", "z", false, + "Use fuzzy searching") + rootCmd.PersistentFlags().BoolVarP(&conf.UseHighlight, "highlight-lines", "L", false, + "Use alternating background colors") + rootCmd.PersistentFlags().StringVarP(&ShowCompletion, "completion", "", "", + "Display completion code") + rootCmd.PersistentFlags().StringVarP(&conf.Separator, "separator", "s", cfg.DefaultSeparator, + "Custom field separator") + rootCmd.PersistentFlags().StringVarP(&conf.Columns, "columns", "c", "", + "Only show the speficied columns (separated by ,)") // sort options - rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)") + rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, + "Sort by column (default: 1)") // sort mode, only 1 allowed - rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)") - rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false, "sort according to string numerical value") - rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false, "sort according to time string") - rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false, "sort according to age (duration) string") - rootCmd.MarkFlagsMutuallyExclusive("sort-numeric", "sort-time", "sort-age") + rootCmd.PersistentFlags().BoolVarP(&conf.SortDescending, "sort-desc", "D", false, + "Sort in descending order (default: ascending)") + rootCmd.PersistentFlags().BoolVarP(&sortmode.Numeric, "sort-numeric", "i", false, + "sort according to string numerical value") + rootCmd.PersistentFlags().BoolVarP(&sortmode.Time, "sort-time", "t", false, + "sort according to time string") + rootCmd.PersistentFlags().BoolVarP(&sortmode.Age, "sort-age", "a", false, + "sort according to age (duration) string") + rootCmd.MarkFlagsMutuallyExclusive("sort-numeric", "sort-time", + "sort-age") // output flags, only 1 allowed - rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false, "Enable extended output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false, "Enable markdown table output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, "Enable org-mode table output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false, "Enable shell mode output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, "Enable CSV output") - rootCmd.PersistentFlags().BoolVarP(&modeflag.A, "ascii", "A", false, "Enable ASCII output (default)") - rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv") + rootCmd.PersistentFlags().BoolVarP(&modeflag.X, "extended", "X", false, + "Enable extended output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.M, "markdown", "M", false, + "Enable markdown table output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, + "Enable org-mode table output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.S, "shell", "S", false, + "Enable shell mode output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, + "Enable yaml output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, + "Enable CSV output") + rootCmd.PersistentFlags().BoolVarP(&modeflag.A, "ascii", "A", false, + "Enable ASCII output (default)") + rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", + "shell", "yaml", "csv") // lisp options rootCmd.PersistentFlags().StringVarP(&conf.LispLoadPath, "load-path", "l", cfg.DefaultLoadPath, diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 5f0d23e..9e07f94 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -16,7 +16,7 @@ SYNOPSIS -H, --no-headers Disable headers display -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy seach [experimental] + -z, --fuzzy Use fuzzy search [experimental] -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): @@ -142,9 +142,9 @@ DESCRIPTION kubectl get pods -A | tablizer "(?i)account" - You can use the experimental fuzzy seach feature by providing the option - -z, in which case the pattern is regarded as a fuzzy search term, not a - regexp. + You can use the experimental fuzzy search feature by providing the + option -z, in which case the pattern is regarded as a fuzzy search term, + not a regexp. Sometimes you want to filter by one or more columns. You can do that using the -F option. The option can be specified multiple times and has @@ -353,7 +353,7 @@ Operational Flags: -H, --no-headers Disable headers display -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy seach [experimental] + -z, --fuzzy Use fuzzy search [experimental] -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): diff --git a/lib/filter.go b/lib/filter.go index 022fd91..1d6f7a5 100644 --- a/lib/filter.go +++ b/lib/filter.go @@ -28,12 +28,12 @@ import ( * [!]Match a line, use fuzzy search for normal pattern strings and * regexp otherwise. */ -func matchPattern(c cfg.Config, line string) bool { - if c.UseFuzzySearch { - return fuzzy.MatchFold(c.Pattern, line) +func matchPattern(conf cfg.Config, line string) bool { + if conf.UseFuzzySearch { + return fuzzy.MatchFold(conf.Pattern, line) } - return c.PatternR.MatchString(line) + return conf.PatternR.MatchString(line) } /* diff --git a/lib/filter_test.go b/lib/filter_test.go index 7124d0b..227a714 100644 --- a/lib/filter_test.go +++ b/lib/filter_test.go @@ -45,22 +45,22 @@ func TestMatchPattern(t *testing.T) { }, } - for _, in := range input { - testname := fmt.Sprintf("match-pattern-%s", in.name) + for _, inputdata := range input { + testname := fmt.Sprintf("match-pattern-%s", inputdata.name) t.Run(testname, func(t *testing.T) { - c := cfg.Config{} + conf := cfg.Config{} - if in.fuzzy { - c.UseFuzzySearch = true + if inputdata.fuzzy { + conf.UseFuzzySearch = true } - err := c.PreparePattern(in.pattern) + err := conf.PreparePattern(inputdata.pattern) if err != nil { t.Errorf("PreparePattern returned error: %s", err) } - if !matchPattern(c, in.line) { + if !matchPattern(conf, inputdata.line) { t.Errorf("matchPattern() did not match\nExp: true\nGot: false\n") } }) @@ -143,20 +143,20 @@ func TestFilterByFields(t *testing.T) { }, } - for _, in := range input { - testname := fmt.Sprintf("filter-by-fields-%s", in.name) + for _, inputdata := range input { + testname := fmt.Sprintf("filter-by-fields-%s", inputdata.name) t.Run(testname, func(t *testing.T) { - c := cfg.Config{Rawfilters: in.filter, InvertMatch: in.invert} + conf := cfg.Config{Rawfilters: inputdata.filter, InvertMatch: inputdata.invert} - err := c.PrepareFilters() + err := conf.PrepareFilters() if err != nil { t.Errorf("PrepareFilters returned error: %s", err) } - data, _, _ := FilterByFields(c, data) - if !reflect.DeepEqual(data, in.expect) { - t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, in.expect) + data, _, _ := FilterByFields(conf, data) + if !reflect.DeepEqual(data, inputdata.expect) { + t.Errorf("Filtered data does not match expected data:\ngot: %+v\nexp: %+v", data, inputdata.expect) } }) } diff --git a/lib/helpers.go b/lib/helpers.go index df87336..ad3d841 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -41,11 +41,11 @@ func contains(s []int, e int) bool { // parse columns list given with -c, modifies config.UseColumns based // on eventually given regex -func PrepareColumns(c *cfg.Config, data *Tabdata) error { - if len(c.Columns) > 0 { - for _, use := range strings.Split(c.Columns, ",") { +func PrepareColumns(conf *cfg.Config, data *Tabdata) error { + if len(conf.Columns) > 0 { + for _, use := range strings.Split(conf.Columns, ",") { if len(use) == 0 { - msg := fmt.Sprintf("Could not parse columns list %s: empty column", c.Columns) + msg := fmt.Sprintf("Could not parse columns list %s: empty column", conf.Columns) return errors.New(msg) } @@ -54,14 +54,14 @@ func PrepareColumns(c *cfg.Config, data *Tabdata) error { // might be a regexp colPattern, err := regexp.Compile(use) if err != nil { - msg := fmt.Sprintf("Could not parse columns list %s: %v", c.Columns, err) + msg := fmt.Sprintf("Could not parse columns list %s: %v", conf.Columns, err) return errors.New(msg) } // find matching header fields for i, head := range data.headers { if colPattern.MatchString(head) { - c.UseColumns = append(c.UseColumns, i+1) + conf.UseColumns = append(conf.UseColumns, i+1) } } @@ -70,45 +70,45 @@ func PrepareColumns(c *cfg.Config, data *Tabdata) error { // a colum spec is not a number, we process them above // inside the err handler for atoi(). so only add the // number, if it's really just a number. - c.UseColumns = append(c.UseColumns, usenum) + conf.UseColumns = append(conf.UseColumns, usenum) } } // deduplicate: put all values into a map (value gets map key) // thereby removing duplicates, extract keys into new slice // and sort it - imap := make(map[int]int, len(c.UseColumns)) - for _, i := range c.UseColumns { + imap := make(map[int]int, len(conf.UseColumns)) + for _, i := range conf.UseColumns { imap[i] = 0 } - c.UseColumns = nil + conf.UseColumns = nil for k := range imap { - c.UseColumns = append(c.UseColumns, k) + conf.UseColumns = append(conf.UseColumns, k) } - sort.Ints(c.UseColumns) + sort.Ints(conf.UseColumns) } return nil } // prepare headers: add numbers to headers -func numberizeAndReduceHeaders(c cfg.Config, data *Tabdata) { +func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) { numberedHeaders := []string{} maxwidth := 0 // start from scratch, so we only look at displayed column widths - for i, head := range data.headers { + for idx, head := range data.headers { headlen := 0 - if len(c.Columns) > 0 { + if len(conf.Columns) > 0 { // -c specified - if !contains(c.UseColumns, i+1) { + if !contains(conf.UseColumns, idx+1) { // ignore this one continue } } - if c.NoNumbering { + if conf.NoNumbering { numberedHeaders = append(numberedHeaders, head) headlen = len(head) } else { - numhead := fmt.Sprintf("%s(%d)", head, i+1) + numhead := fmt.Sprintf("%s(%d)", head, idx+1) headlen = len(numhead) numberedHeaders = append(numberedHeaders, numhead) } @@ -124,14 +124,14 @@ func numberizeAndReduceHeaders(c cfg.Config, data *Tabdata) { } // exclude columns, if any -func reduceColumns(c cfg.Config, data *Tabdata) { - if len(c.Columns) > 0 { +func reduceColumns(conf cfg.Config, data *Tabdata) { + if len(conf.Columns) > 0 { reducedEntries := [][]string{} var reducedEntry []string for _, entry := range data.entries { reducedEntry = nil for i, value := range entry { - if !contains(c.UseColumns, i+1) { + if !contains(conf.UseColumns, i+1) { continue } @@ -153,8 +153,9 @@ func trimRow(row []string) []string { return fixedrow } -func colorizeData(c cfg.Config, output string) string { - if c.UseHighlight && color.IsConsole(os.Stdout) { +func colorizeData(conf cfg.Config, output string) string { + switch { + case conf.UseHighlight && color.IsConsole(os.Stdout): highlight := true colorized := "" first := true @@ -167,17 +168,17 @@ func colorizeData(c cfg.Config, output string) string { // in pprint mode. This doesn't matter as long as // we don't use colorization. But with colors the // missing spaces can be seen. - if c.OutputMode == cfg.Ascii { - line = line + " " + if conf.OutputMode == cfg.ASCII { + line += " " } - line = c.HighlightHdrStyle.Sprint(line) + line = conf.HighlightHdrStyle.Sprint(line) first = false } else { - line = c.HighlightStyle.Sprint(line) + line = conf.HighlightStyle.Sprint(line) } } else { - line = c.NoHighlightStyle.Sprint(line) + line = conf.NoHighlightStyle.Sprint(line) } highlight = !highlight @@ -185,12 +186,12 @@ func colorizeData(c cfg.Config, output string) string { } return colorized - } else if len(c.Pattern) > 0 && !c.NoColor && color.IsConsole(os.Stdout) { - r := regexp.MustCompile("(" + c.Pattern + ")") + case len(conf.Pattern) > 0 && !conf.NoColor && color.IsConsole(os.Stdout): + r := regexp.MustCompile("(" + conf.Pattern + ")") return r.ReplaceAllStringFunc(output, func(in string) string { - return c.ColorStyle.Sprint(in) + return conf.ColorStyle.Sprint(in) }) - } else { + default: return output } } diff --git a/lib/helpers_test.go b/lib/helpers_test.go index 430514d..f079d8f 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -19,9 +19,10 @@ package lib import ( "fmt" - "github.com/tlinden/tablizer/cfg" "reflect" "testing" + + "github.com/tlinden/tablizer/cfg" ) func TestContains(t *testing.T) { @@ -74,15 +75,15 @@ func TestPrepareColumns(t *testing.T) { for _, tt := range tests { testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror) t.Run(testname, func(t *testing.T) { - c := cfg.Config{Columns: tt.input} - err := PrepareColumns(&c, &data) + conf := cfg.Config{Columns: tt.input} + err := PrepareColumns(&conf, &data) if err != nil { if !tt.wanterror { t.Errorf("got error: %v", err) } } else { - if !reflect.DeepEqual(c.UseColumns, tt.exp) { - t.Errorf("got: %v, expected: %v", c.UseColumns, tt.exp) + if !reflect.DeepEqual(conf.UseColumns, tt.exp) { + t.Errorf("got: %v, expected: %v", conf.UseColumns, tt.exp) } } }) @@ -114,14 +115,16 @@ func TestReduceColumns(t *testing.T) { input := [][]string{{"a", "b", "c"}} - for _, tt := range tests { - testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns) + for _, testdata := range tests { + testname := fmt.Sprintf("reduce-columns-by-%+v", testdata.columns) + t.Run(testname, func(t *testing.T) { - c := cfg.Config{Columns: "x", UseColumns: tt.columns} + c := cfg.Config{Columns: "x", UseColumns: testdata.columns} data := Tabdata{entries: input} reduceColumns(c, &data) - if !reflect.DeepEqual(data.entries, tt.expect) { - t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect) + if !reflect.DeepEqual(data.entries, testdata.expect) { + t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", + data.entries, testdata.expect) } }) } @@ -142,15 +145,17 @@ func TestNumberizeHeaders(t *testing.T) { {[]string{"ONE", "TWO"}, []int{1, 2}, true}, } - for _, tt := range tests { - testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", tt.columns, tt.nonum) + for _, testdata := range tests { + testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", + testdata.columns, testdata.nonum) + t.Run(testname, func(t *testing.T) { - c := cfg.Config{Columns: "x", UseColumns: tt.columns, NoNumbering: tt.nonum} + conf := cfg.Config{Columns: "x", UseColumns: testdata.columns, NoNumbering: testdata.nonum} usedata := data - numberizeAndReduceHeaders(c, &usedata) - if !reflect.DeepEqual(usedata.headers, tt.expect) { + numberizeAndReduceHeaders(conf, &usedata) + if !reflect.DeepEqual(usedata.headers, testdata.expect) { t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v", - usedata.headers, tt.expect) + usedata.headers, testdata.expect) } }) } diff --git a/lib/io.go b/lib/io.go index bd59bbf..5e184ca 100644 --- a/lib/io.go +++ b/lib/io.go @@ -19,40 +19,41 @@ package lib import ( "errors" - "github.com/tlinden/tablizer/cfg" "io" "os" + + "github.com/tlinden/tablizer/cfg" ) -func ProcessFiles(c *cfg.Config, args []string) error { - fds, pattern, err := determineIO(c, args) +func ProcessFiles(conf *cfg.Config, args []string) error { + fds, pattern, err := determineIO(conf, args) if err != nil { return err } - if err := c.PreparePattern(pattern); err != nil { + if err := conf.PreparePattern(pattern); err != nil { return err } for _, fd := range fds { - data, err := Parse(*c, fd) + data, err := Parse(*conf, fd) if err != nil { return err } - err = PrepareColumns(c, &data) + err = PrepareColumns(conf, &data) if err != nil { return err } - printData(os.Stdout, *c, &data) + printData(os.Stdout, *conf, &data) } return nil } -func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) { +func determineIO(conf *cfg.Config, args []string) ([]io.Reader, string, error) { var pattern string var fds []io.Reader var haveio bool @@ -64,45 +65,43 @@ func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) { if len(args) > 0 { // ignore any args > 1 pattern = args[0] - c.Pattern = args[0] // used for colorization by printData() + conf.Pattern = args[0] // used for colorization by printData() } haveio = true - } else { - if len(args) > 0 { - // threre were args left, take a look - if args[0] == "-" { - // in traditional unix programs a dash denotes STDIN (forced) - fds = append(fds, os.Stdin) - haveio = true - } else { - if _, err := os.Stat(args[0]); err != nil { - // first one is not a file, consider it as regexp and - // shift arg list - pattern = args[0] - c.Pattern = args[0] // used for colorization by printData() - args = args[1:] - } + } else if len(args) > 0 { + // threre were args left, take a look + if args[0] == "-" { + // in traditional unix programs a dash denotes STDIN (forced) + fds = append(fds, os.Stdin) + haveio = true + } else { + if _, err := os.Stat(args[0]); err != nil { + // first one is not a file, consider it as regexp and + // shift arg list + pattern = args[0] + conf.Pattern = args[0] // used for colorization by printData() + args = args[1:] + } - if len(args) > 0 { - // consider any other args as files - for _, file := range args { + if len(args) > 0 { + // consider any other args as files + for _, file := range args { - fd, err := os.OpenFile(file, os.O_RDONLY, 0755) + fd, err := os.OpenFile(file, os.O_RDONLY, 0755) - if err != nil { - return nil, "", err - } - - fds = append(fds, fd) - haveio = true + if err != nil { + return nil, "", err } + + fds = append(fds, fd) + haveio = true } } } } if !haveio { - return nil, "", errors.New("No file specified and nothing to read on stdin!") + return nil, "", errors.New("no file specified and nothing to read on stdin") } return fds, pattern, nil diff --git a/lib/lisp.go b/lib/lisp.go index bf4df5a..e13c292 100644 --- a/lib/lisp.go +++ b/lib/lisp.go @@ -52,19 +52,19 @@ func AddHook(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) } hookname = t.Name() default: - return zygo.SexpNull, errors.New("hook name must be a symbol!") + return zygo.SexpNull, errors.New("hook name must be a symbol ") } - switch t := args[1].(type) { + switch sexptype := args[1].(type) { case *zygo.SexpSymbol: _, exists := Hooks[hookname] if !exists { - Hooks[hookname] = []*zygo.SexpSymbol{t} + Hooks[hookname] = []*zygo.SexpSymbol{sexptype} } else { - Hooks[hookname] = append(Hooks[hookname], t) + Hooks[hookname] = append(Hooks[hookname], sexptype) } default: - return zygo.SexpNull, errors.New("hook function must be a symbol!") + return zygo.SexpNull, errors.New("hook function must be a symbol ") } return zygo.SexpNull, nil @@ -86,7 +86,7 @@ func HookExists(key string) bool { /* * Basic sanity checks and load lisp file */ -func LoadFile(env *zygo.Zlisp, path string) error { +func LoadAndEvalFile(env *zygo.Zlisp, path string) error { if strings.HasSuffix(path, `.zy`) { code, err := os.ReadFile(path) if err != nil { @@ -106,32 +106,39 @@ func LoadFile(env *zygo.Zlisp, path string) error { /* * Setup lisp interpreter environment */ -func SetupLisp(c *cfg.Config) error { +func SetupLisp(conf *cfg.Config) error { + // iterate over load-path and evaluate all *.zy files there, if any + // we ignore if load-path does not exist, which is the default anyway + path, err := os.Stat(conf.LispLoadPath) + if os.IsNotExist(err) { + return nil + } + + // init global hooks Hooks = make(map[string][]*zygo.SexpSymbol) + // init sandbox env := zygo.NewZlispSandbox() env.AddFunction("addhook", AddHook) - // iterate over load-path and evaluate all *.zy files there, if any - // we ignore if load-path does not exist, which is the default anyway - if path, err := os.Stat(c.LispLoadPath); !os.IsNotExist(err) { - if !path.IsDir() { - err := LoadFile(env, c.LispLoadPath) - if err != nil { - return err - } - } else { - dir, err := os.ReadDir(c.LispLoadPath) - if err != nil { - return err - } + if !path.IsDir() { + // load single lisp file + err = LoadAndEvalFile(env, conf.LispLoadPath) + if err != nil { + return err + } + } else { + // load all lisp file in load dir + dir, err := os.ReadDir(conf.LispLoadPath) + if err != nil { + return err + } - for _, entry := range dir { - if !entry.IsDir() { - err := LoadFile(env, c.LispLoadPath+"/"+entry.Name()) - if err != nil { - return err - } + for _, entry := range dir { + if !entry.IsDir() { + err := LoadAndEvalFile(env, conf.LispLoadPath+"/"+entry.Name()) + if err != nil { + return err } } } @@ -139,7 +146,7 @@ func SetupLisp(c *cfg.Config) error { RegisterLib(env) - c.Lisp = env + conf.Lisp = env return nil } @@ -155,11 +162,11 @@ returning false wins, that is if each function returns true the line will be kept, if at least one of them returns false, it will be skipped. */ -func RunFilterHooks(c cfg.Config, line string) (bool, error) { +func RunFilterHooks(conf cfg.Config, line string) (bool, error) { for _, hook := range Hooks["filter"] { var result bool - c.Lisp.Clear() - res, err := c.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line)) + conf.Lisp.Clear() + res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line)) if err != nil { return false, err } @@ -197,7 +204,7 @@ The somewhat complicated code is being caused by the fact, that we need to convert our internal structure to a lisp variable and vice versa afterwards. */ -func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) { +func RunProcessHooks(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { var userdata Tabdata lisplist := []zygo.Sexp{} @@ -224,14 +231,14 @@ func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) { } // we need to add it to the env so that the function can use the struct directly - c.Lisp.AddGlobal("data", &zygo.SexpArray{Val: lisplist, Env: c.Lisp}) + conf.Lisp.AddGlobal("data", &zygo.SexpArray{Val: lisplist, Env: conf.Lisp}) // execute the actual hook hook := Hooks["process"][0] var result bool - c.Lisp.Clear() + conf.Lisp.Clear() - res, err := c.Lisp.EvalString(fmt.Sprintf("(%s data)", hook.Name())) + res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s data)", hook.Name())) if err != nil { return userdata, false, err } @@ -243,17 +250,17 @@ func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) { case *zygo.SexpBool: result = th.Val default: - return userdata, false, errors.New("Expect (bool, array(hash)) as return value!") + return userdata, false, errors.New("xpect (bool, array(hash)) as return value") } switch tt := t.Tail.(type) { case *zygo.SexpArray: lisplist = tt.Val default: - return userdata, false, errors.New("Expect (bool, array(hash)) as return value!") + return userdata, false, errors.New("expect (bool, array(hash)) as return value ") } default: - return userdata, false, errors.New("filter hook shall return array of hashes!") + return userdata, false, errors.New("filter hook shall return array of hashes ") } if !result { @@ -268,7 +275,7 @@ func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) { switch hash := item.(type) { case *zygo.SexpHash: for _, header := range data.headers { - entry, err := hash.HashGetDefault(c.Lisp, &zygo.SexpStr{S: header}, &zygo.SexpStr{S: ""}) + entry, err := hash.HashGetDefault(conf.Lisp, &zygo.SexpStr{S: header}, &zygo.SexpStr{S: ""}) if err != nil { return userdata, false, err } @@ -277,11 +284,11 @@ func RunProcessHooks(c cfg.Config, data Tabdata) (Tabdata, bool, error) { case *zygo.SexpStr: row = append(row, t.S) default: - return userdata, false, errors.New("Hash values should be string!") + return userdata, false, errors.New("hsh values should be string ") } } default: - return userdata, false, errors.New("Returned array should contain hashes!") + return userdata, false, errors.New("rturned array should contain hashes ") } userdata.entries = append(userdata.entries, row) diff --git a/lib/parser.go b/lib/parser.go index 28bb2a7..eec0a59 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -20,7 +20,6 @@ package lib import ( "bufio" "encoding/csv" - "errors" "fmt" "io" "regexp" @@ -33,22 +32,22 @@ import ( /* Parser switch */ -func Parse(c cfg.Config, input io.Reader) (Tabdata, error) { - if len(c.Separator) == 1 { - return parseCSV(c, input) +func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) { + if len(conf.Separator) == 1 { + return parseCSV(conf, input) } - return parseTabular(c, input) + return parseTabular(conf, input) } /* Parse CSV input. */ -func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { - var content io.Reader = input +func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) { + var content = input data := Tabdata{} - if len(c.Pattern) > 0 { + if len(conf.Pattern) > 0 { scanner := bufio.NewScanner(input) lines := []string{} hadFirst := false @@ -56,7 +55,7 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { line := strings.TrimSpace(scanner.Text()) if hadFirst { // don't match 1st line, it's the header - if c.Pattern != "" && matchPattern(c, line) == c.InvertMatch { + if conf.Pattern != "" && matchPattern(conf, line) == conf.InvertMatch { // by default -v is false, so if a line does NOT // match the pattern, we will ignore it. However, // if the user specified -v, the matching is inverted, @@ -65,9 +64,9 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { } // apply user defined lisp filters, if any - accept, err := RunFilterHooks(c, line) + accept, err := RunFilterHooks(conf, line) if err != nil { - return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err)) + return data, fmt.Errorf("failed to apply filter hook: %w", err) } if !accept { @@ -83,11 +82,11 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { } csvreader := csv.NewReader(content) - csvreader.Comma = rune(c.Separator[0]) + csvreader.Comma = rune(conf.Separator[0]) records, err := csvreader.ReadAll() if err != nil { - return data, errors.Unwrap(fmt.Errorf("Could not parse CSV input: %w", err)) + return data, fmt.Errorf("could not parse CSV input: %w", err) } if len(records) >= 1 { @@ -108,9 +107,9 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { } // apply user defined lisp process hooks, if any - userdata, changed, err := RunProcessHooks(c, data) + userdata, changed, err := RunProcessHooks(conf, data) if err != nil { - return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err)) + return data, fmt.Errorf("failed to apply filter hook: %w", err) } if changed { data = userdata @@ -122,13 +121,13 @@ func parseCSV(c cfg.Config, input io.Reader) (Tabdata, error) { /* Parse tabular input. */ -func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { +func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { data := Tabdata{} var scanner *bufio.Scanner hadFirst := false - separate := regexp.MustCompile(c.Separator) + separate := regexp.MustCompile(conf.Separator) scanner = bufio.NewScanner(input) @@ -163,7 +162,7 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { } } else { // data processing - if c.Pattern != "" && matchPattern(c, line) == c.InvertMatch { + if conf.Pattern != "" && matchPattern(conf, line) == conf.InvertMatch { // by default -v is false, so if a line does NOT // match the pattern, we will ignore it. However, // if the user specified -v, the matching is inverted, @@ -172,9 +171,9 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { } // apply user defined lisp filters, if any - accept, err := RunFilterHooks(c, line) + accept, err := RunFilterHooks(conf, line) if err != nil { - return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err)) + return data, fmt.Errorf("failed to apply filter hook: %w", err) } if !accept { @@ -204,28 +203,28 @@ func parseTabular(c cfg.Config, input io.Reader) (Tabdata, error) { } if scanner.Err() != nil { - return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err())) + return data, fmt.Errorf("failed to read from io.Reader: %w", scanner.Err()) } // filter by field filters, if any - filtereddata, changed, err := FilterByFields(c, data) + filtereddata, changed, err := FilterByFields(conf, data) if err != nil { - return data, fmt.Errorf("Failed to filter fields: %w", err) + return data, fmt.Errorf("failed to filter fields: %w", err) } if changed { data = filtereddata } // apply user defined lisp process hooks, if any - userdata, changed, err := RunProcessHooks(c, data) + userdata, changed, err := RunProcessHooks(conf, data) if err != nil { - return data, errors.Unwrap(fmt.Errorf("Failed to apply filter hook: %w", err)) + return data, fmt.Errorf("failed to apply filter hook: %w", err) } if changed { data = userdata } - if c.Debug { + if conf.Debug { repr.Print(data) } diff --git a/lib/parser_test.go b/lib/parser_test.go index a42f57a..23aba28 100644 --- a/lib/parser_test.go +++ b/lib/parser_test.go @@ -109,13 +109,13 @@ func TestParserPatternmatching(t *testing.T) { testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t", in.name, tt.pattern, tt.invert) t.Run(testname, func(t *testing.T) { - c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, + conf := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: in.separator} - _ = c.PreparePattern(tt.pattern) + _ = conf.PreparePattern(tt.pattern) readFd := strings.NewReader(strings.TrimSpace(in.text)) - gotdata, err := Parse(c, readFd) + gotdata, err := Parse(conf, readFd) if err != nil { if !tt.want { diff --git a/lib/printer.go b/lib/printer.go index 7b6cdcd..418aaf6 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -32,51 +32,51 @@ import ( "gopkg.in/yaml.v3" ) -func printData(w io.Writer, c cfg.Config, data *Tabdata) { +func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { // some output preparations: // add numbers to headers and remove this we're not interested in - numberizeAndReduceHeaders(c, data) + numberizeAndReduceHeaders(conf, data) // remove unwanted columns, if any - reduceColumns(c, data) + reduceColumns(conf, data) // sort the data - sortTable(c, data) + sortTable(conf, data) - switch c.OutputMode { + switch conf.OutputMode { case cfg.Extended: - printExtendedData(w, c, data) - case cfg.Ascii: - printAsciiData(w, c, data) + printExtendedData(writer, conf, data) + case cfg.ASCII: + printAsciiData(writer, conf, data) case cfg.Orgtbl: - printOrgmodeData(w, c, data) + printOrgmodeData(writer, conf, data) case cfg.Markdown: - printMarkdownData(w, c, data) + printMarkdownData(writer, conf, data) case cfg.Shell: - printShellData(w, c, data) + printShellData(writer, data) case cfg.Yaml: - printYamlData(w, c, data) + printYamlData(writer, data) case cfg.CSV: - printCSVData(w, c, data) + printCSVData(writer, data) default: - printAsciiData(w, c, data) + printAsciiData(writer, conf, data) } } -func output(w io.Writer, str string) { - fmt.Fprint(w, str) +func output(writer io.Writer, str string) { + fmt.Fprint(writer, str) } /* Emacs org-mode compatible table (also orgtbl-mode) */ -func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) { +func printOrgmodeData(writer io.Writer, conf cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) - if !c.NoHeaders { + if !conf.NoHeaders { table.SetHeader(data.headers) } @@ -100,8 +100,8 @@ func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) { leftR := regexp.MustCompile(`(?m)^\\+`) rightR := regexp.MustCompile(`\\+(?m)$`) - output(w, color.Sprint( - colorizeData(c, + output(writer, color.Sprint( + colorizeData(conf, rightR.ReplaceAllString( leftR.ReplaceAllString(tableString.String(), "|"), "|")))) } @@ -109,11 +109,11 @@ func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) { /* Markdown table */ -func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) { +func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) - if !c.NoHeaders { + if !conf.NoHeaders { table.SetHeader(data.headers) } @@ -125,17 +125,17 @@ func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) { table.SetCenterSeparator("|") table.Render() - output(w, color.Sprint(colorizeData(c, tableString.String()))) + output(writer, color.Sprint(colorizeData(conf, tableString.String()))) } /* Simple ASCII table without any borders etc, just like the input we expect */ -func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) { +func printAsciiData(writer io.Writer, conf cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) - if !c.NoHeaders { + if !conf.NoHeaders { table.SetHeader(data.headers) } table.AppendBulk(data.entries) @@ -151,7 +151,7 @@ func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) { table.SetBorder(false) table.SetNoWhiteSpace(true) - if !c.UseHighlight { + if !conf.UseHighlight { // the tabs destroy the highlighting table.SetTablePadding("\t") // pad with tabs } else { @@ -159,13 +159,13 @@ func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) { } table.Render() - output(w, color.Sprint(colorizeData(c, tableString.String()))) + output(writer, color.Sprint(colorizeData(conf, tableString.String()))) } /* We simulate the \x command of psql (the PostgreSQL client) */ -func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) { +func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) { // needed for data output format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) out := "" @@ -179,67 +179,67 @@ func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) { } } - output(w, colorizeData(c, out)) + output(writer, colorizeData(conf, out)) } /* Shell output, ready to be eval'd. Just like FreeBSD stat(1) */ -func printShellData(w io.Writer, c cfg.Config, data *Tabdata) { +func printShellData(writer io.Writer, data *Tabdata) { out := "" if len(data.entries) > 0 { for _, entry := range data.entries { shentries := []string{} - for i, value := range entry { + for idx, value := range entry { shentries = append(shentries, fmt.Sprintf("%s=\"%s\"", - data.headers[i], value)) + data.headers[idx], value)) } out += fmt.Sprint(strings.Join(shentries, " ")) + "\n" } } // no colorization here - output(w, out) + output(writer, out) } -func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) { - type D struct { +func printYamlData(writer io.Writer, data *Tabdata) { + type Data struct { Entries []map[string]interface{} `yaml:"entries"` } - d := D{} + yamlout := Data{} for _, entry := range data.entries { - ml := map[string]interface{}{} + yamldata := map[string]interface{}{} - for i, entry := range entry { + for idx, entry := range entry { style := yaml.TaggedStyle _, err := strconv.Atoi(entry) if err != nil { style = yaml.DoubleQuotedStyle } - ml[strings.ToLower(data.headers[i])] = + yamldata[strings.ToLower(data.headers[idx])] = &yaml.Node{ Kind: yaml.ScalarNode, Style: style, Value: entry} } - d.Entries = append(d.Entries, ml) + yamlout.Entries = append(yamlout.Entries, yamldata) } - yamlstr, err := yaml.Marshal(&d) + yamlstr, err := yaml.Marshal(&yamlout) if err != nil { log.Fatal(err) } - output(w, string(yamlstr)) + output(writer, string(yamlstr)) } -func printCSVData(w io.Writer, c cfg.Config, data *Tabdata) { - csvout := csv.NewWriter(w) +func printCSVData(writer io.Writer, data *Tabdata) { + csvout := csv.NewWriter(writer) if err := csvout.Write(data.headers); err != nil { log.Fatalln("error writing record to csv:", err) diff --git a/lib/printer_test.go b/lib/printer_test.go index 6419e25..008543a 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -20,10 +20,10 @@ package lib import ( "bytes" "fmt" - //"github.com/alecthomas/repr" - "github.com/tlinden/tablizer/cfg" "strings" "testing" + + "github.com/tlinden/tablizer/cfg" ) func newData() Tabdata { @@ -73,7 +73,7 @@ var tests = []struct { }{ // --------------------- Default settings mode tests `` { - mode: cfg.Ascii, + mode: cfg.ASCII, name: "default", expect: ` NAME(1) DURATION(2) COUNT(3) WHEN(4) diff --git a/lib/sort.go b/lib/sort.go index 37dfa79..1a55d80 100644 --- a/lib/sort.go +++ b/lib/sort.go @@ -18,21 +18,22 @@ along with this program. If not, see . package lib import ( - "github.com/araddon/dateparse" - "github.com/tlinden/tablizer/cfg" "regexp" "sort" "strconv" + + "github.com/araddon/dateparse" + "github.com/tlinden/tablizer/cfg" ) -func sortTable(c cfg.Config, data *Tabdata) { - if c.SortByColumn <= 0 { +func sortTable(conf cfg.Config, data *Tabdata) { + if conf.SortByColumn <= 0 { // no sorting wanted return } // slightly modified here to match internal array indicies - col := c.SortByColumn + col := conf.SortByColumn col-- // ui starts counting by 1, but use 0 internally @@ -48,38 +49,38 @@ func sortTable(c cfg.Config, data *Tabdata) { // actual sorting sort.SliceStable(data.entries, func(i, j int) bool { - return compare(&c, data.entries[i][col], data.entries[j][col]) + return compare(&conf, data.entries[i][col], data.entries[j][col]) }) } // config is not modified here, but it would be inefficient to copy it every loop -func compare(c *cfg.Config, a string, b string) bool { +func compare(conf *cfg.Config, left string, right string) bool { var comp bool - switch c.SortMode { + switch conf.SortMode { case "numeric": - left, err := strconv.Atoi(a) + left, err := strconv.Atoi(left) if err != nil { left = 0 } - right, err := strconv.Atoi(b) + right, err := strconv.Atoi(right) if err != nil { right = 0 } comp = left < right case "duration": - left := duration2int(a) - right := duration2int(b) + left := duration2int(left) + right := duration2int(right) comp = left < right case "time": - left, _ := dateparse.ParseAny(a) - right, _ := dateparse.ParseAny(b) + left, _ := dateparse.ParseAny(left) + right, _ := dateparse.ParseAny(right) comp = left.Unix() < right.Unix() default: - comp = a < b + comp = left < right } - if c.SortDescending { + if conf.SortDescending { comp = !comp } @@ -87,15 +88,15 @@ func compare(c *cfg.Config, a string, b string) bool { } /* - We could use time.ParseDuration(), but this doesn't support days. +We could use time.ParseDuration(), but this doesn't support days. - We could also use github.com/xhit/go-str2duration/v2, which does - the job, but it's just another dependency, just for this little - gem. And we don't need a time.Time value. And int is good enough - for duration comparision. +We could also use github.com/xhit/go-str2duration/v2, which does +the job, but it's just another dependency, just for this little +gem. And we don't need a time.Time value. And int is good enough +for duration comparison. - Convert a durartion into an integer. Valid time units are "s", - "m", "h" and "d". +Convert a duration into an integer. Valid time units are "s", +"m", "h" and "d". */ func duration2int(duration string) int { re := regexp.MustCompile(`(\d+)([dhms])`) @@ -103,16 +104,16 @@ func duration2int(duration string) int { for _, match := range re.FindAllStringSubmatch(duration, -1) { if len(match) == 3 { - v, _ := strconv.Atoi(match[1]) + durationvalue, _ := strconv.Atoi(match[1]) switch match[2][0] { case 'd': - seconds += v * 86400 + seconds += durationvalue * 86400 case 'h': - seconds += v * 3600 + seconds += durationvalue * 3600 case 'm': - seconds += v * 60 + seconds += durationvalue * 60 case 's': - seconds += v + seconds += durationvalue } } } diff --git a/tablizer.1 b/tablizer.1 index 4ea873f..eaead61 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -154,7 +154,7 @@ tablizer \- Manipulate tabular output of other programs \& \-H, \-\-no\-headers Disable headers display \& \-s, \-\-separator string Custom field separator \& \-k, \-\-sort\-by int Sort by column (default: 1) -\& \-z, \-\-fuzzy Use fuzzy seach [experimental] +\& \-z, \-\-fuzzy Use fuzzy search [experimental] \& \-F, \-\-filter field=reg Filter given field with regex, can be used multiple times \& \& Output Flags (mutually exclusive): @@ -298,7 +298,7 @@ Example for a case insensitive search: \& kubectl get pods \-A | tablizer "(?i)account" .Ve .PP -You can use the experimental fuzzy seach feature by providing the +You can use the experimental fuzzy search feature by providing the option \fB\-z\fR, in which case the pattern is regarded as a fuzzy search term, not a regexp. .PP diff --git a/tablizer.pod b/tablizer.pod index 79884cd..44a5180 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -15,7 +15,7 @@ tablizer - Manipulate tabular output of other programs -H, --no-headers Disable headers display -s, --separator string Custom field separator -k, --sort-by int Sort by column (default: 1) - -z, --fuzzy Use fuzzy seach [experimental] + -z, --fuzzy Use fuzzy search [experimental] -F, --filter field=reg Filter given field with regex, can be used multiple times Output Flags (mutually exclusive): @@ -156,7 +156,7 @@ Example for a case insensitive search: kubectl get pods -A | tablizer "(?i)account" -You can use the experimental fuzzy seach feature by providing the +You can use the experimental fuzzy search feature by providing the option B<-z>, in which case the pattern is regarded as a fuzzy search term, not a regexp. From 9e2e45715edaa716708e4dd7b4aa95026d3cc853 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 7 May 2024 18:00:57 +0200 Subject: [PATCH 12/13] added -F docs --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 91a5714..351241e 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,17 @@ NAME(1) READY(2) STATUS(3) RESTARTS(4) AGE(5) repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m ``` +Sometimes a filter regex is to broad and you wish to filter only on a +particular column. This is possible using `-F`: +``` +% kubectl get pods | tablizer -n -Fname=2 +NAME READY STATUS RESTARTS AGE +repldepl-7bcd8d5b64-q2bf4 1/1 Running 1 (69m ago) 5h26m +``` + +Here we filtered the `NAME` column for `2`, which would have matched +otherwise on all rows. + There are more output modes like org-mode (orgtbl) and markdown. ## Demo From 473feff4515259783ec89e6e6956f2d2f30cfafe Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 7 May 2024 18:01:12 +0200 Subject: [PATCH 13/13] refactored and un-go-criticed --- Makefile | 3 +- cfg/config.go | 1 + cfg/config_test.go | 12 ++--- cmd/root.go | 1 + lib/common.go | 4 +- lib/filter.go | 48 ++++++++++++++++++++ lib/filter_test.go | 2 - lib/helpers.go | 107 ++++++++++++++++++++++++++------------------ lib/helpers_test.go | 12 ++--- lib/io.go | 22 +++++---- lib/lisp.go | 55 ++++++++++++++--------- lib/lisplib.go | 18 +++++--- lib/parser.go | 44 +++--------------- lib/parser_test.go | 36 +++++++-------- lib/printer.go | 17 ++++--- lib/printer_test.go | 36 +++++++-------- lib/sort.go | 5 +++ lib/sort_test.go | 27 ++++++----- 18 files changed, 262 insertions(+), 188 deletions(-) diff --git a/Makefile b/Makefile index a59b7da..95535c0 100644 --- a/Makefile +++ b/Makefile @@ -95,6 +95,7 @@ goupdate: lint: golangci-lint run +# keep til ireturn lint-full: - golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn + golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn,stylecheck,testpackage,mirror,nestif,revive,goerr113,gomnd gocritic check -enableAll *.go diff --git a/cfg/config.go b/cfg/config.go index 51d8347..f6e891e 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -216,6 +216,7 @@ func (conf *Config) DetermineColormode() { // Return true if current terminal is interactive func isTerminal(f *os.File) bool { o, _ := f.Stat() + return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice } diff --git a/cfg/config_test.go b/cfg/config_test.go index 3bf2732..84a477f 100644 --- a/cfg/config_test.go +++ b/cfg/config_test.go @@ -38,14 +38,14 @@ func TestPrepareModeFlags(t *testing.T) { } // FIXME: use a map for easier printing - for _, tt := range tests { - testname := fmt.Sprintf("PrepareModeFlags-expect-%d", tt.expect) + for _, testdata := range tests { + testname := fmt.Sprintf("PrepareModeFlags-expect-%d", testdata.expect) t.Run(testname, func(t *testing.T) { - c := Config{} + conf := Config{} - c.PrepareModeFlags(tt.flag) - if c.OutputMode != tt.expect { - t.Errorf("got: %d, expect: %d", c.OutputMode, tt.expect) + conf.PrepareModeFlags(testdata.flag) + if conf.OutputMode != testdata.expect { + t.Errorf("got: %d, expect: %d", conf.OutputMode, testdata.expect) } }) } diff --git a/cmd/root.go b/cmd/root.go index 3bf3c8d..38cf2ed 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,6 +34,7 @@ func man() { man := exec.Command("less", "-") var buffer bytes.Buffer + buffer.Write([]byte(manpage)) man.Stdout = os.Stdout diff --git a/lib/common.go b/lib/common.go index 0c9b915..6a6c929 100644 --- a/lib/common.go +++ b/lib/common.go @@ -26,11 +26,11 @@ type Tabdata struct { } func (data *Tabdata) CloneEmpty() Tabdata { - new := Tabdata{ + newdata := Tabdata{ maxwidthHeader: data.maxwidthHeader, columns: data.columns, headers: data.headers, } - return new + return newdata } diff --git a/lib/filter.go b/lib/filter.go index 1d6f7a5..2d35113 100644 --- a/lib/filter.go +++ b/lib/filter.go @@ -18,6 +18,9 @@ along with this program. If not, see . package lib import ( + "bufio" + "fmt" + "io" "strings" "github.com/lithammer/fuzzysearch/fuzzy" @@ -61,6 +64,7 @@ func FilterByFields(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { if !conf.Filters[strings.ToLower(header)].MatchString(row[idx]) { // there IS a filter, but it doesn't match keep = false + break } } @@ -74,9 +78,53 @@ func FilterByFields(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { return newdata, true, nil } +/* generic map.Exists(key) */ func Exists[K comparable, V any](m map[K]V, v K) bool { if _, ok := m[v]; ok { return true } + return false } + +func FilterByPattern(conf cfg.Config, input io.Reader) (io.Reader, error) { + if conf.Pattern == "" { + return input, nil + } + + scanner := bufio.NewScanner(input) + lines := []string{} + hadFirst := false + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if hadFirst { + // don't match 1st line, it's the header + if conf.Pattern != "" && matchPattern(conf, line) == conf.InvertMatch { + // by default -v is false, so if a line does NOT + // match the pattern, we will ignore it. However, + // if the user specified -v, the matching is inverted, + // so we ignore all lines, which DO match. + continue + } + + // apply user defined lisp filters, if any + accept, err := RunFilterHooks(conf, line) + if err != nil { + return input, fmt.Errorf("failed to apply filter hook: %w", err) + } + + if !accept { + // IF there are filter hook[s] and IF one of them + // returns false on the current line, reject it + continue + } + } + + lines = append(lines, line) + + hadFirst = true + } + + return strings.NewReader(strings.Join(lines, "\n")), nil +} diff --git a/lib/filter_test.go b/lib/filter_test.go index 227a714..fc646cb 100644 --- a/lib/filter_test.go +++ b/lib/filter_test.go @@ -65,7 +65,6 @@ func TestMatchPattern(t *testing.T) { } }) } - } func TestFilterByFields(t *testing.T) { @@ -160,5 +159,4 @@ func TestFilterByFields(t *testing.T) { } }) } - } diff --git a/lib/helpers.go b/lib/helpers.go index ad3d841..2f04ad2 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -36,57 +36,63 @@ func contains(s []int, e int) bool { return true } } + return false } // parse columns list given with -c, modifies config.UseColumns based // on eventually given regex func PrepareColumns(conf *cfg.Config, data *Tabdata) error { - if len(conf.Columns) > 0 { - for _, use := range strings.Split(conf.Columns, ",") { - if len(use) == 0 { - msg := fmt.Sprintf("Could not parse columns list %s: empty column", conf.Columns) + if conf.Columns == "" { + return nil + } + + for _, use := range strings.Split(conf.Columns, ",") { + if len(use) == 0 { + return fmt.Errorf("could not parse columns list %s: empty column", conf.Columns) + } + + usenum, err := strconv.Atoi(use) + if err != nil { + // might be a regexp + colPattern, err := regexp.Compile(use) + if err != nil { + msg := fmt.Sprintf("Could not parse columns list %s: %v", conf.Columns, err) + return errors.New(msg) } - usenum, err := strconv.Atoi(use) - if err != nil { - // might be a regexp - colPattern, err := regexp.Compile(use) - if err != nil { - msg := fmt.Sprintf("Could not parse columns list %s: %v", conf.Columns, err) - return errors.New(msg) + // find matching header fields + for i, head := range data.headers { + if colPattern.MatchString(head) { + conf.UseColumns = append(conf.UseColumns, i+1) } - - // find matching header fields - for i, head := range data.headers { - if colPattern.MatchString(head) { - conf.UseColumns = append(conf.UseColumns, i+1) - } - - } - } else { - // we digress from go best practises here, because if - // a colum spec is not a number, we process them above - // inside the err handler for atoi(). so only add the - // number, if it's really just a number. - conf.UseColumns = append(conf.UseColumns, usenum) } + } else { + // we digress from go best practises here, because if + // a colum spec is not a number, we process them above + // inside the err handler for atoi(). so only add the + // number, if it's really just a number. + conf.UseColumns = append(conf.UseColumns, usenum) } - - // deduplicate: put all values into a map (value gets map key) - // thereby removing duplicates, extract keys into new slice - // and sort it - imap := make(map[int]int, len(conf.UseColumns)) - for _, i := range conf.UseColumns { - imap[i] = 0 - } - conf.UseColumns = nil - for k := range imap { - conf.UseColumns = append(conf.UseColumns, k) - } - sort.Ints(conf.UseColumns) } + + // deduplicate: put all values into a map (value gets map key) + // thereby removing duplicates, extract keys into new slice + // and sort it + imap := make(map[int]int, len(conf.UseColumns)) + for _, i := range conf.UseColumns { + imap[i] = 0 + } + + conf.UseColumns = nil + + for k := range imap { + conf.UseColumns = append(conf.UseColumns, k) + } + + sort.Ints(conf.UseColumns) + return nil } @@ -96,7 +102,8 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) { maxwidth := 0 // start from scratch, so we only look at displayed column widths for idx, head := range data.headers { - headlen := 0 + var headlen int + if len(conf.Columns) > 0 { // -c specified if !contains(conf.UseColumns, idx+1) { @@ -104,6 +111,7 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) { continue } } + if conf.NoNumbering { numberedHeaders = append(numberedHeaders, head) headlen = len(head) @@ -117,7 +125,9 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) { maxwidth = headlen } } + data.headers = numberedHeaders + if data.maxwidthHeader != maxwidth && maxwidth > 0 { data.maxwidthHeader = maxwidth } @@ -127,9 +137,12 @@ func numberizeAndReduceHeaders(conf cfg.Config, data *Tabdata) { func reduceColumns(conf cfg.Config, data *Tabdata) { if len(conf.Columns) > 0 { reducedEntries := [][]string{} + var reducedEntry []string + for _, entry := range data.entries { reducedEntry = nil + for i, value := range entry { if !contains(conf.UseColumns, i+1) { continue @@ -137,22 +150,26 @@ func reduceColumns(conf cfg.Config, data *Tabdata) { reducedEntry = append(reducedEntry, value) } + reducedEntries = append(reducedEntries, reducedEntry) } + data.entries = reducedEntries } } +// FIXME: remove this when we only use Tablewriter and strip in ParseFile()! 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)) + var fixedrow = make([]string, len(row)) + + for idx, cell := range row { + fixedrow[idx] = strings.TrimSpace(cell) } return fixedrow } +// FIXME: refactor this beast! func colorizeData(conf cfg.Config, output string) string { switch { case conf.UseHighlight && color.IsConsole(os.Stdout): @@ -180,17 +197,21 @@ func colorizeData(conf cfg.Config, output string) string { } else { line = conf.NoHighlightStyle.Sprint(line) } + highlight = !highlight colorized += line + "\n" } return colorized + case len(conf.Pattern) > 0 && !conf.NoColor && color.IsConsole(os.Stdout): r := regexp.MustCompile("(" + conf.Pattern + ")") + return r.ReplaceAllStringFunc(output, func(in string) string { return conf.ColorStyle.Sprint(in) }) + default: return output } diff --git a/lib/helpers_test.go b/lib/helpers_test.go index f079d8f..8013b44 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -72,18 +72,18 @@ func TestPrepareColumns(t *testing.T) { {"[a-z,4,5", []int{4, 5}, true}, // invalid regexp } - for _, tt := range tests { - testname := fmt.Sprintf("PrepareColumns-%s-%t", tt.input, tt.wanterror) + for _, testdata := range tests { + testname := fmt.Sprintf("PrepareColumns-%s-%t", testdata.input, testdata.wanterror) t.Run(testname, func(t *testing.T) { - conf := cfg.Config{Columns: tt.input} + conf := cfg.Config{Columns: testdata.input} err := PrepareColumns(&conf, &data) if err != nil { - if !tt.wanterror { + if !testdata.wanterror { t.Errorf("got error: %v", err) } } else { - if !reflect.DeepEqual(conf.UseColumns, tt.exp) { - t.Errorf("got: %v, expected: %v", conf.UseColumns, tt.exp) + if !reflect.DeepEqual(conf.UseColumns, testdata.exp) { + t.Errorf("got: %v, expected: %v", conf.UseColumns, testdata.exp) } } }) diff --git a/lib/io.go b/lib/io.go index 5e184ca..90e98a5 100644 --- a/lib/io.go +++ b/lib/io.go @@ -19,12 +19,15 @@ package lib import ( "errors" + "fmt" "io" "os" "github.com/tlinden/tablizer/cfg" ) +const RWRR = 0755 + func ProcessFiles(conf *cfg.Config, args []string) error { fds, pattern, err := determineIO(conf, args) @@ -54,25 +57,29 @@ func ProcessFiles(conf *cfg.Config, args []string) error { } func determineIO(conf *cfg.Config, args []string) ([]io.Reader, string, error) { + var filehandles []io.Reader + var pattern string - var fds []io.Reader + var haveio bool stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { // we're reading from STDIN, which takes precedence over file args - fds = append(fds, os.Stdin) + filehandles = append(filehandles, os.Stdin) + if len(args) > 0 { // ignore any args > 1 pattern = args[0] conf.Pattern = args[0] // used for colorization by printData() } + haveio = true } else if len(args) > 0 { // threre were args left, take a look if args[0] == "-" { // in traditional unix programs a dash denotes STDIN (forced) - fds = append(fds, os.Stdin) + filehandles = append(filehandles, os.Stdin) haveio = true } else { if _, err := os.Stat(args[0]); err != nil { @@ -86,14 +93,13 @@ func determineIO(conf *cfg.Config, args []string) ([]io.Reader, string, error) { if len(args) > 0 { // consider any other args as files for _, file := range args { - - fd, err := os.OpenFile(file, os.O_RDONLY, 0755) + filehandle, err := os.OpenFile(file, os.O_RDONLY, RWRR) if err != nil { - return nil, "", err + return nil, "", fmt.Errorf("failed to read input file %s: %w", file, err) } - fds = append(fds, fd) + filehandles = append(filehandles, filehandle) haveio = true } } @@ -104,5 +110,5 @@ func determineIO(conf *cfg.Config, args []string) ([]io.Reader, string, error) { return nil, "", errors.New("no file specified and nothing to read on stdin") } - return fds, pattern, nil + return filehandles, pattern, nil } diff --git a/lib/lisp.go b/lib/lisp.go index e13c292..f576a3b 100644 --- a/lib/lisp.go +++ b/lib/lisp.go @@ -45,12 +45,14 @@ func AddHook(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) return zygo.SexpNull, errors.New("argument of %add-hook should be: %hook-name %your-function") } - switch t := args[0].(type) { + switch sexptype := args[0].(type) { case *zygo.SexpSymbol: - if !HookExists(t.Name()) { - return zygo.SexpNull, errors.New("Unknown hook " + t.Name()) + if !HookExists(sexptype.Name()) { + return zygo.SexpNull, errors.New("Unknown hook " + sexptype.Name()) } - hookname = t.Name() + + hookname = sexptype.Name() + default: return zygo.SexpNull, errors.New("hook name must be a symbol ") } @@ -63,6 +65,7 @@ func AddHook(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) } else { Hooks[hookname] = append(Hooks[hookname], sexptype) } + default: return zygo.SexpNull, errors.New("hook function must be a symbol ") } @@ -90,7 +93,7 @@ func LoadAndEvalFile(env *zygo.Zlisp, path string) error { if strings.HasSuffix(path, `.zy`) { code, err := os.ReadFile(path) if err != nil { - return err + return fmt.Errorf("failed to read lisp file %s: %w", path, err) } // FIXME: check what res (_ here) could be and mean @@ -131,7 +134,8 @@ func SetupLisp(conf *cfg.Config) error { // load all lisp file in load dir dir, err := os.ReadDir(conf.LispLoadPath) if err != nil { - return err + return fmt.Errorf("failed to read lisp dir %s: %w", + conf.LispLoadPath, err) } for _, entry := range dir { @@ -147,6 +151,7 @@ func SetupLisp(conf *cfg.Config) error { RegisterLib(env) conf.Lisp = env + return nil } @@ -165,17 +170,19 @@ skipped. func RunFilterHooks(conf cfg.Config, line string) (bool, error) { for _, hook := range Hooks["filter"] { var result bool + conf.Lisp.Clear() + res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s `%s`)", hook.Name(), line)) if err != nil { - return false, err + return false, fmt.Errorf("failed to evaluate hook loader: %w", err) } - switch t := res.(type) { + switch sexptype := res.(type) { case *zygo.SexpBool: - result = t.Val + result = sexptype.Val default: - return false, errors.New("filter hook shall return BOOL!") + return false, fmt.Errorf("filter hook shall return bool") } if !result { @@ -206,6 +213,7 @@ versa afterwards. */ func RunProcessHooks(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { var userdata Tabdata + lisplist := []zygo.Sexp{} if len(Hooks["process"]) == 0 { @@ -223,7 +231,7 @@ func RunProcessHooks(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { for idx, cell := range row { err := entry.HashSet(&zygo.SexpStr{S: data.headers[idx]}, &zygo.SexpStr{S: cell}) if err != nil { - return userdata, false, err + return userdata, false, fmt.Errorf("failed to convert to lisp data: %w", err) } } @@ -235,27 +243,29 @@ func RunProcessHooks(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { // execute the actual hook hook := Hooks["process"][0] - var result bool + conf.Lisp.Clear() + var result bool + res, err := conf.Lisp.EvalString(fmt.Sprintf("(%s data)", hook.Name())) if err != nil { - return userdata, false, err + return userdata, false, fmt.Errorf("failed to eval lisp loader: %w", err) } // we expect (bool, array(hash)) as return from the function - switch t := res.(type) { + switch sexptype := res.(type) { case *zygo.SexpPair: - switch th := t.Head.(type) { + switch th := sexptype.Head.(type) { case *zygo.SexpBool: result = th.Val default: return userdata, false, errors.New("xpect (bool, array(hash)) as return value") } - switch tt := t.Tail.(type) { + switch sexptailtype := sexptype.Tail.(type) { case *zygo.SexpArray: - lisplist = tt.Val + lisplist = sexptailtype.Val default: return userdata, false, errors.New("expect (bool, array(hash)) as return value ") } @@ -275,14 +285,17 @@ func RunProcessHooks(conf cfg.Config, data Tabdata) (Tabdata, bool, error) { switch hash := item.(type) { case *zygo.SexpHash: for _, header := range data.headers { - entry, err := hash.HashGetDefault(conf.Lisp, &zygo.SexpStr{S: header}, &zygo.SexpStr{S: ""}) + entry, err := hash.HashGetDefault( + conf.Lisp, + &zygo.SexpStr{S: header}, + &zygo.SexpStr{S: ""}) if err != nil { - return userdata, false, err + return userdata, false, fmt.Errorf("failed to get lisp hash entry: %w", err) } - switch t := entry.(type) { + switch sexptype := entry.(type) { case *zygo.SexpStr: - row = append(row, t.S) + row = append(row, sexptype.S) default: return userdata, false, errors.New("hsh values should be string ") } diff --git a/lib/lisplib.go b/lib/lisplib.go index 21c5ee2..11b763e 100644 --- a/lib/lisplib.go +++ b/lib/lisplib.go @@ -19,6 +19,7 @@ package lib import ( "errors" + "fmt" "regexp" "strconv" @@ -31,29 +32,29 @@ func Splice2SexpList(list []string) zygo.Sexp { for _, item := range list { slist = append(slist, &zygo.SexpStr{S: item}) } + return zygo.MakeList(slist) } func StringReSplit(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, error) { if len(args) < 2 { - return zygo.SexpNull, errors.New("expecting 2 arguments!") + return zygo.SexpNull, errors.New("expecting 2 arguments") } - var separator string - var input string + var separator, input string switch t := args[0].(type) { case *zygo.SexpStr: input = t.S default: - return zygo.SexpNull, errors.New("second argument must be a string!") + return zygo.SexpNull, errors.New("second argument must be a string") } switch t := args[1].(type) { case *zygo.SexpStr: separator = t.S default: - return zygo.SexpNull, errors.New("first argument must be a string!") + return zygo.SexpNull, errors.New("first argument must be a string") } sep := regexp.MustCompile(separator) @@ -67,12 +68,15 @@ func String2Int(env *zygo.Zlisp, name string, args []zygo.Sexp) (zygo.Sexp, erro switch t := args[0].(type) { case *zygo.SexpStr: num, err := strconv.Atoi(t.S) + if err != nil { - return zygo.SexpNull, err + return zygo.SexpNull, fmt.Errorf("failed to convert string to number: %w", err) } + number = num + default: - return zygo.SexpNull, errors.New("argument must be a string!") + return zygo.SexpNull, errors.New("argument must be a string") } return &zygo.SexpInt{Val: int64(number)}, nil diff --git a/lib/parser.go b/lib/parser.go index eec0a59..dfaf62b 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -44,41 +44,12 @@ func Parse(conf cfg.Config, input io.Reader) (Tabdata, error) { Parse CSV input. */ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) { - var content = input data := Tabdata{} - if len(conf.Pattern) > 0 { - scanner := bufio.NewScanner(input) - lines := []string{} - hadFirst := false - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if hadFirst { - // don't match 1st line, it's the header - if conf.Pattern != "" && matchPattern(conf, line) == conf.InvertMatch { - // by default -v is false, so if a line does NOT - // match the pattern, we will ignore it. However, - // if the user specified -v, the matching is inverted, - // so we ignore all lines, which DO match. - continue - } - - // apply user defined lisp filters, if any - accept, err := RunFilterHooks(conf, line) - if err != nil { - return data, fmt.Errorf("failed to apply filter hook: %w", err) - } - - if !accept { - // IF there are filter hook[s] and IF one of them - // returns false on the current line, reject it - continue - } - } - lines = append(lines, line) - hadFirst = true - } - content = strings.NewReader(strings.Join(lines, "\n")) + // apply pattern, if any + content, err := FilterByPattern(conf, input) + if err != nil { + return data, err } csvreader := csv.NewReader(content) @@ -111,6 +82,7 @@ func parseCSV(conf cfg.Config, input io.Reader) (Tabdata, error) { if err != nil { return data, fmt.Errorf("failed to apply filter hook: %w", err) } + if changed { data = userdata } @@ -144,10 +116,6 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { // process all header fields for _, part := range parts { - // if Debug { - // fmt.Printf("Part: <%s>\n", string(line[beg:part[0]])) - //} - // register widest header field headerlen := len(part) if headerlen > data.maxwidthHeader { @@ -211,6 +179,7 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { if err != nil { return data, fmt.Errorf("failed to filter fields: %w", err) } + if changed { data = filtereddata } @@ -220,6 +189,7 @@ func parseTabular(conf cfg.Config, input io.Reader) (Tabdata, error) { if err != nil { return data, fmt.Errorf("failed to apply filter hook: %w", err) } + if changed { data = userdata } diff --git a/lib/parser_test.go b/lib/parser_test.go index 23aba28..e382f59 100644 --- a/lib/parser_test.go +++ b/lib/parser_test.go @@ -62,12 +62,12 @@ func TestParser(t *testing.T) { }, } - for _, in := range input { - testname := fmt.Sprintf("parse-%s", in.name) + for _, testdata := range input { + testname := fmt.Sprintf("parse-%s", testdata.name) t.Run(testname, func(t *testing.T) { - readFd := strings.NewReader(strings.TrimSpace(in.text)) - c := cfg.Config{Separator: in.separator} - gotdata, err := Parse(c, readFd) + readFd := strings.NewReader(strings.TrimSpace(testdata.text)) + conf := cfg.Config{Separator: testdata.separator} + gotdata, err := Parse(conf, readFd) if err != nil { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) @@ -104,28 +104,28 @@ func TestParserPatternmatching(t *testing.T) { }, } - for _, in := range input { - for _, tt := range tests { + for _, inputdata := range input { + for _, testdata := range tests { testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t", - in.name, tt.pattern, tt.invert) + inputdata.name, testdata.pattern, testdata.invert) t.Run(testname, func(t *testing.T) { - conf := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, - Separator: in.separator} + conf := cfg.Config{InvertMatch: testdata.invert, Pattern: testdata.pattern, + Separator: inputdata.separator} - _ = conf.PreparePattern(tt.pattern) + _ = conf.PreparePattern(testdata.pattern) - readFd := strings.NewReader(strings.TrimSpace(in.text)) + readFd := strings.NewReader(strings.TrimSpace(inputdata.text)) gotdata, err := Parse(conf, readFd) if err != nil { - if !tt.want { + if !testdata.want { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) } } else { - if !reflect.DeepEqual(tt.entries, gotdata.entries) { + if !reflect.DeepEqual(testdata.entries, gotdata.entries) { t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n", - tt.pattern, tt.invert, tt.entries, gotdata.entries) + testdata.pattern, testdata.invert, testdata.entries, gotdata.entries) } } }) @@ -152,8 +152,8 @@ asd igig 19191 EDD 1 X` readFd := strings.NewReader(strings.TrimSpace(table)) - c := cfg.Config{Separator: cfg.DefaultSeparator} - gotdata, err := Parse(c, readFd) + conf := cfg.Config{Separator: cfg.DefaultSeparator} + gotdata, err := Parse(conf, readFd) if err != nil { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) @@ -161,6 +161,6 @@ asd igig if !reflect.DeepEqual(data, gotdata) { t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", - c.Separator, data, gotdata) + conf.Separator, data, gotdata) } } diff --git a/lib/printer.go b/lib/printer.go index 418aaf6..7adcd00 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -33,8 +33,6 @@ import ( ) func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { - // some output preparations: - // add numbers to headers and remove this we're not interested in numberizeAndReduceHeaders(conf, data) @@ -48,7 +46,7 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { case cfg.Extended: printExtendedData(writer, conf, data) case cfg.ASCII: - printAsciiData(writer, conf, data) + printASCIIData(writer, conf, data) case cfg.Orgtbl: printOrgmodeData(writer, conf, data) case cfg.Markdown: @@ -60,9 +58,8 @@ func printData(writer io.Writer, conf cfg.Config, data *Tabdata) { case cfg.CSV: printCSVData(writer, data) default: - printAsciiData(writer, conf, data) + printASCIIData(writer, conf, data) } - } func output(writer io.Writer, str string) { @@ -131,13 +128,14 @@ func printMarkdownData(writer io.Writer, conf cfg.Config, data *Tabdata) { /* Simple ASCII table without any borders etc, just like the input we expect */ -func printAsciiData(writer io.Writer, conf cfg.Config, data *Tabdata) { +func printASCIIData(writer io.Writer, conf cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) if !conf.NoHeaders { table.SetHeader(data.headers) } + table.AppendBulk(data.entries) table.SetAutoWrapText(false) @@ -169,6 +167,7 @@ func printExtendedData(writer io.Writer, conf cfg.Config, data *Tabdata) { // needed for data output format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) out := "" + if len(data.entries) > 0 { for _, entry := range data.entries { for i, value := range entry { @@ -187,14 +186,17 @@ Shell output, ready to be eval'd. Just like FreeBSD stat(1) */ func printShellData(writer io.Writer, data *Tabdata) { out := "" + if len(data.entries) > 0 { for _, entry := range data.entries { shentries := []string{} + for idx, value := range entry { shentries = append(shentries, fmt.Sprintf("%s=\"%s\"", data.headers[idx], value)) } - out += fmt.Sprint(strings.Join(shentries, " ")) + "\n" + + out += strings.Join(shentries, " ") + "\n" } } @@ -214,6 +216,7 @@ func printYamlData(writer io.Writer, data *Tabdata) { for idx, entry := range entry { style := yaml.TaggedStyle + _, err := strconv.Atoi(entry) if err != nil { style = yaml.DoubleQuotedStyle diff --git a/lib/printer_test.go b/lib/printer_test.go index 008543a..73ced90 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -250,39 +250,39 @@ DURATION(2) WHEN(4) } func TestPrinter(t *testing.T) { - for _, tt := range tests { + for _, testdata := range tests { testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%d-usecolumns-%s", - tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr) + testdata.column, testdata.desc, testdata.sortby, testdata.mode, testdata.usecolstr) t.Run(testname, func(t *testing.T) { // replaces os.Stdout, but we ignore it - var w bytes.Buffer + var writer bytes.Buffer // cmd flags - c := cfg.Config{ - SortByColumn: tt.column, - SortDescending: tt.desc, - SortMode: tt.sortby, - OutputMode: tt.mode, - NoNumbering: tt.nonum, - UseColumns: tt.usecol, + conf := cfg.Config{ + SortByColumn: testdata.column, + SortDescending: testdata.desc, + SortMode: testdata.sortby, + OutputMode: testdata.mode, + NoNumbering: testdata.nonum, + UseColumns: testdata.usecol, NoColor: true, } - c.ApplyDefaults() + conf.ApplyDefaults() // the test checks the len! - if len(tt.usecol) > 0 { - c.Columns = "yes" + if len(testdata.usecol) > 0 { + conf.Columns = "yes" } else { - c.Columns = "" + conf.Columns = "" } - testdata := newData() - exp := strings.TrimSpace(tt.expect) + data := newData() + exp := strings.TrimSpace(testdata.expect) - printData(&w, c, &testdata) + printData(&writer, conf, &data) - got := strings.TrimSpace(w.String()) + got := strings.TrimSpace(writer.String()) if got != exp { t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s", diff --git a/lib/sort.go b/lib/sort.go index 1a55d80..2d41bf9 100644 --- a/lib/sort.go +++ b/lib/sort.go @@ -63,18 +63,22 @@ func compare(conf *cfg.Config, left string, right string) bool { if err != nil { left = 0 } + right, err := strconv.Atoi(right) if err != nil { right = 0 } + comp = left < right case "duration": left := duration2int(left) right := duration2int(right) + comp = left < right case "time": left, _ := dateparse.ParseAny(left) right, _ := dateparse.ParseAny(right) + comp = left.Unix() < right.Unix() default: comp = left < right @@ -105,6 +109,7 @@ func duration2int(duration string) int { for _, match := range re.FindAllStringSubmatch(duration, -1) { if len(match) == 3 { durationvalue, _ := strconv.Atoi(match[1]) + switch match[2][0] { case 'd': seconds += durationvalue * 86400 diff --git a/lib/sort_test.go b/lib/sort_test.go index b46cce2..4d19cfa 100644 --- a/lib/sort_test.go +++ b/lib/sort_test.go @@ -19,8 +19,9 @@ package lib import ( "fmt" - "github.com/tlinden/tablizer/cfg" "testing" + + "github.com/tlinden/tablizer/cfg" ) func TestDuration2Seconds(t *testing.T) { @@ -36,12 +37,12 @@ func TestDuration2Seconds(t *testing.T) { {"19t77X what?4s", 4}, } - for _, tt := range tests { - testname := fmt.Sprintf("duration-%s", tt.dur) + for _, testdata := range tests { + testname := fmt.Sprintf("duration-%s", testdata.dur) t.Run(testname, func(t *testing.T) { - seconds := duration2int(tt.dur) - if seconds != tt.expect { - t.Errorf("got %d, want %d", seconds, tt.expect) + seconds := duration2int(testdata.dur) + if seconds != testdata.expect { + t.Errorf("got %d, want %d", seconds, testdata.expect) } }) } @@ -66,13 +67,15 @@ func TestCompare(t *testing.T) { {"time", "12/24/2022", "1/1/1970", true, true}, } - for _, tt := range tests { - testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t", tt.mode, tt.a, tt.b, tt.desc) + for _, testdata := range tests { + testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t", + testdata.mode, testdata.a, testdata.b, testdata.desc) + t.Run(testname, func(t *testing.T) { - c := cfg.Config{SortMode: tt.mode, SortDescending: tt.desc} - got := compare(&c, tt.a, tt.b) - if got != tt.want { - t.Errorf("got %t, want %t", got, tt.want) + c := cfg.Config{SortMode: testdata.mode, SortDescending: testdata.desc} + got := compare(&c, testdata.a, testdata.b) + if got != testdata.want { + t.Errorf("got %t, want %t", got, testdata.want) } }) }