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.