From 1e36c148ff54633d38fd03d81c6668510cab6090 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Wed, 19 Oct 2022 12:44:19 +0200 Subject: [PATCH] get rid of global variables, makes testing way easier --- cmd/root.go | 112 ++++++++++++++++++++++++-------------------- lib/common.go | 68 --------------------------- lib/helpers.go | 100 ++++++++++----------------------------- lib/helpers_test.go | 30 ++++-------- lib/io.go | 37 +++++++++------ lib/parser.go | 9 ++-- lib/parser_test.go | 17 +++---- lib/printer.go | 47 ++++++++++--------- lib/printer_test.go | 24 +++++----- lib/sort.go | 17 ++++--- lib/sort_test.go | 6 +-- 11 files changed, 184 insertions(+), 283 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e2d3421..96bd1f9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,14 +20,13 @@ import ( "bytes" "fmt" "github.com/spf13/cobra" + "github.com/tlinden/tablizer/cfg" "github.com/tlinden/tablizer/lib" "log" "os" "os/exec" ) -var ShowManual = false - func man() { man := exec.Command("less", "-") @@ -45,62 +44,66 @@ func man() { } } -var rootCmd = &cobra.Command{ - Use: "tablizer [regex] [file, ...]", - Short: "[Re-]tabularize tabular data", - Long: `Manipulate tabular output of other programs`, - RunE: func(cmd *cobra.Command, args []string) error { - if lib.ShowVersion { - fmt.Printf("This is tablizer version %s\n", lib.VERSION) - return nil - } - - if ShowManual { - man() - return nil - } - - err := lib.PrepareModeFlags() - if err != nil { - return err - } - - lib.PrepareSortFlags() - - return lib.ProcessFiles(args) - }, -} - func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} + var ( + conf cfg.Config + ShowManual bool + Outputmode string + ShowVersion bool + modeflag cfg.Modeflag + sortmode cfg.Sortmode + ) -func init() { - rootCmd.PersistentFlags().BoolVarP(&lib.Debug, "debug", "d", false, "Enable debugging") - rootCmd.PersistentFlags().BoolVarP(&lib.NoNumbering, "no-numbering", "n", false, "Disable header numbering") - rootCmd.PersistentFlags().BoolVarP(&lib.NoColor, "no-color", "N", false, "Disable pattern highlighting") - rootCmd.PersistentFlags().BoolVarP(&lib.ShowVersion, "version", "V", false, "Print program version") - rootCmd.PersistentFlags().BoolVarP(&lib.InvertMatch, "invert-match", "v", false, "select non-matching rows") + var rootCmd = &cobra.Command{ + Use: "tablizer [regex] [file, ...]", + Short: "[Re-]tabularize tabular data", + Long: `Manipulate tabular output of other programs`, + RunE: func(cmd *cobra.Command, args []string) error { + if ShowVersion { + fmt.Println(cfg.Getversion()) + } + + if ShowManual { + man() + return nil + } + + // prepare flags + err := conf.PrepareModeFlags(modeflag, Outputmode) + if err != nil { + return err + } + + conf.PrepareSortFlags(sortmode) + + // actual execution starts here + return lib.ProcessFiles(conf, args) + }, + } + + // 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.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().StringVarP(&lib.Separator, "separator", "s", lib.DefaultSeparator, "Custom field separator") - rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") + 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(&lib.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)") - rootCmd.PersistentFlags().BoolVarP(&lib.SortDescending, "sort-desc", "D", false, "Sort in descending order (default: ascending)") - rootCmd.PersistentFlags().BoolVarP(&lib.SortNumeric, "sort-numeric", "i", false, "sort according to string numerical value") - rootCmd.PersistentFlags().BoolVarP(&lib.SortTime, "sort-time", "t", false, "sort according to time string") - rootCmd.PersistentFlags().BoolVarP(&lib.SortAge, "sort-age", "a", false, "sort according to age (duration) string") + rootCmd.PersistentFlags().IntVarP(&conf.SortByColumn, "sort-by", "k", 0, "Sort by column (default: 1)") + 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") // output flags, only 1 allowed, hidden, since just short cuts - rootCmd.PersistentFlags().BoolVarP(&lib.OutflagExtended, "extended", "X", false, "Enable extended output") - rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table output") - rootCmd.PersistentFlags().BoolVarP(&lib.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output") - rootCmd.PersistentFlags().BoolVarP(&lib.OutflagShell, "shell", "S", false, "Enable shell mode output") - rootCmd.PersistentFlags().BoolVarP(&lib.OutflagYaml, "yaml", "Y", false, "Enable yaml output") + 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.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml") rootCmd.Flags().MarkHidden("extended") rootCmd.Flags().MarkHidden("orgtbl") @@ -109,5 +112,10 @@ func init() { rootCmd.Flags().MarkHidden("yaml") // same thing but more common, takes precedence over above group - rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)") + rootCmd.PersistentFlags().StringVarP(&Outputmode, "output", "o", "ascii", "Output mode - one of: orgtbl, markdown, extended, shell, ascii(default)") + + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } } diff --git a/lib/common.go b/lib/common.go index ee510aa..a72d138 100644 --- a/lib/common.go +++ b/lib/common.go @@ -17,74 +17,6 @@ along with this program. If not, see . package lib -import ( - "github.com/gookit/color" -) - -var ( - // command line flags - Debug bool - XtendedOut bool - NoNumbering bool - ShowVersion bool - Columns string - UseColumns []int - DefaultSeparator string = `(\s\s+|\t)` - Separator string = `(\s\s+|\t)` - OutflagExtended bool - OutflagMarkdown bool - OutflagOrgtable bool - OutflagShell bool - OutflagYaml bool - OutputMode string - InvertMatch bool - Pattern string - - /* - FIXME: make configurable somehow, config file or ENV - see https://github.com/gookit/color will be set by - io.ProcessFiles() according to currently supported - color mode. - */ - MatchFG string - MatchBG string - NoColor bool - - // colors to be used per supported color mode - Colors = map[color.Level]map[string]string{ - color.Level16: { - "bg": "green", "fg": "black", - }, - color.Level256: { - "bg": "lightGreen", "fg": "black", - }, - color.LevelRgb: { - // FIXME: maybe use something nicer - "bg": "lightGreen", "fg": "black", - }, - } - - // used for validation - validOutputmodes = "(orgtbl|markdown|extended|ascii|yaml)" - - // main program version - Version = "v1.0.11" - - // generated version string, used by -v contains lib.Version on - // main branch, and lib.Version-$branch-$lastcommit-$date on - // development branch - VERSION string - - // sorting - SortByColumn int - SortDescending bool - - SortNumeric bool - SortTime bool - SortAge bool - SortMode string -) - // contains a whole parsed table type Tabdata struct { maxwidthHeader int // longest header diff --git a/lib/helpers.go b/lib/helpers.go index 4c7ab5c..79757c3 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "github.com/gookit/color" + "github.com/tlinden/tablizer/cfg" "os" "regexp" "sort" @@ -37,13 +38,13 @@ func contains(s []int, e int) bool { return false } -// parse columns list given with -c -func PrepareColumns(data *Tabdata) error { - UseColumns = nil - if len(Columns) > 0 { - for _, use := range strings.Split(Columns, ",") { +// 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, ",") { if len(use) == 0 { - msg := fmt.Sprintf("Could not parse columns list %s: empty column", Columns) + msg := fmt.Sprintf("Could not parse columns list %s: empty column", c.Columns) return errors.New(msg) } @@ -52,14 +53,14 @@ func PrepareColumns(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", Columns, err) + msg := fmt.Sprintf("Could not parse columns list %s: %v", c.Columns, err) return errors.New(msg) } // find matching header fields for i, head := range data.headers { if colPattern.MatchString(head) { - UseColumns = append(UseColumns, i+1) + c.UseColumns = append(c.UseColumns, i+1) } } @@ -68,41 +69,41 @@ func PrepareColumns(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. - UseColumns = append(UseColumns, usenum) + c.UseColumns = append(c.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(UseColumns)) - for _, i := range UseColumns { + imap := make(map[int]int, len(c.UseColumns)) + for _, i := range c.UseColumns { imap[i] = 0 } - UseColumns = nil + c.UseColumns = nil for k := range imap { - UseColumns = append(UseColumns, k) + c.UseColumns = append(c.UseColumns, k) } - sort.Ints(UseColumns) + sort.Ints(c.UseColumns) } return nil } // prepare headers: add numbers to headers -func numberizeAndReduceHeaders(data *Tabdata) { +func numberizeAndReduceHeaders(c 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 { headlen := 0 - if len(Columns) > 0 { + if len(c.Columns) > 0 { // -c specified - if !contains(UseColumns, i+1) { + if !contains(c.UseColumns, i+1) { // ignore this one continue } } - if NoNumbering { + if c.NoNumbering { numberedHeaders = append(numberedHeaders, head) headlen = len(head) } else { @@ -122,14 +123,14 @@ func numberizeAndReduceHeaders(data *Tabdata) { } // exclude columns, if any -func reduceColumns(data *Tabdata) { - if len(Columns) > 0 { +func reduceColumns(c cfg.Config, data *Tabdata) { + if len(c.Columns) > 0 { reducedEntries := [][]string{} var reducedEntry []string for _, entry := range data.entries { reducedEntry = nil for i, value := range entry { - if !contains(UseColumns, i+1) { + if !contains(c.UseColumns, i+1) { continue } @@ -141,55 +142,6 @@ func reduceColumns(data *Tabdata) { } } -func PrepareModeFlags() error { - if len(OutputMode) == 0 { - // associate short flags like -X with mode selector - switch { - case OutflagExtended: - OutputMode = "extended" - case OutflagMarkdown: - OutputMode = "markdown" - case OutflagOrgtable: - OutputMode = "orgtbl" - case OutflagShell: - OutputMode = "shell" - NoNumbering = true - case OutflagYaml: - OutputMode = "yaml" - NoNumbering = true - default: - OutputMode = "ascii" - } - } else { - r, err := regexp.Compile(validOutputmodes) - - if err != nil { - return errors.New("Failed to validate output mode spec!") - } - - match := r.MatchString(OutputMode) - - if !match { - return errors.New("Invalid output mode!") - } - } - - return nil -} - -func PrepareSortFlags() { - switch { - case SortNumeric: - SortMode = "numeric" - case SortAge: - SortMode = "duration" - case SortTime: - SortMode = "time" - default: - SortMode = "string" - } -} - func trimRow(row []string) []string { // FIXME: remove this when we only use Tablewriter and strip in ParseFile()! var fixedrow []string @@ -200,10 +152,10 @@ func trimRow(row []string) []string { return fixedrow } -func colorizeData(output string) string { - if len(Pattern) > 0 && !NoColor && color.IsConsole(os.Stdout) { - r := regexp.MustCompile("(" + Pattern + ")") - return r.ReplaceAllString(output, "$1") +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.ReplaceAllString(output, "$1") } else { return output } diff --git a/lib/helpers_test.go b/lib/helpers_test.go index 795c00c..68d135e 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -19,6 +19,7 @@ package lib import ( "fmt" + "github.com/tlinden/tablizer/cfg" "reflect" "testing" ) @@ -62,6 +63,7 @@ func TestPrepareColumns(t *testing.T) { }, }, } + var tests = []struct { input string exp []int @@ -77,15 +79,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) { - Columns = tt.input - err := PrepareColumns(&data) + c := cfg.Config{Columns: tt.input} + err := PrepareColumns(&c, &data) if err != nil { if !tt.wanterror { t.Errorf("got error: %v", err) } } else { - if !reflect.DeepEqual(UseColumns, tt.exp) { - t.Errorf("got: %v, expected: %v", UseColumns, tt.exp) + if !reflect.DeepEqual(c.UseColumns, tt.exp) { + t.Errorf("got: %v, expected: %v", c.UseColumns, tt.exp) } } }) @@ -117,22 +119,17 @@ func TestReduceColumns(t *testing.T) { input := [][]string{{"a", "b", "c"}} - Columns = "y" // used as a flag with len(Columns)... - for _, tt := range tests { testname := fmt.Sprintf("reduce-columns-by-%+v", tt.columns) t.Run(testname, func(t *testing.T) { - UseColumns = tt.columns + c := cfg.Config{Columns: "x", UseColumns: tt.columns} data := Tabdata{entries: input} - reduceColumns(&data) + reduceColumns(c, &data) if !reflect.DeepEqual(data.entries, tt.expect) { t.Errorf("reduceColumns returned invalid data:\ngot: %+v\nexp: %+v", data.entries, tt.expect) } }) } - - Columns = "" // reset for other tests - UseColumns = nil } func TestNumberizeHeaders(t *testing.T) { @@ -150,23 +147,16 @@ func TestNumberizeHeaders(t *testing.T) { {[]string{"ONE", "TWO"}, []int{1, 2}, true}, } - Columns = "1" // so the len test succeeds, since we don't parse - for _, tt := range tests { testname := fmt.Sprintf("numberize-headers-columns-%+v-nonum-%t", tt.columns, tt.nonum) t.Run(testname, func(t *testing.T) { - UseColumns = tt.columns - NoNumbering = tt.nonum + c := cfg.Config{Columns: "x", UseColumns: tt.columns, NoNumbering: tt.nonum} usedata := data - numberizeAndReduceHeaders(&usedata) + numberizeAndReduceHeaders(c, &usedata) if !reflect.DeepEqual(usedata.headers, tt.expect) { t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v", usedata.headers, tt.expect) } }) } - - Columns = "" - UseColumns = nil - NoNumbering = false } diff --git a/lib/io.go b/lib/io.go index ae37052..6e1645c 100644 --- a/lib/io.go +++ b/lib/io.go @@ -20,43 +20,50 @@ package lib import ( "errors" "github.com/gookit/color" + "github.com/tlinden/tablizer/cfg" "io" "os" ) -func ProcessFiles(args []string) error { - fds, pattern, err := determineIO(args) - - if !isTerminal(os.Stdout) { - color.Disable() - } else { - level := color.TermColorLevel() - MatchFG = Colors[level]["fg"] - MatchBG = Colors[level]["bg"] - } +func ProcessFiles(c cfg.Config, args []string) error { + fds, pattern, err := determineIO(&c, args) if err != nil { return err } + determineColormode(&c) + for _, fd := range fds { - data, err := parseFile(fd, pattern) + data, err := parseFile(c, fd, pattern) if err != nil { return err } - err = PrepareColumns(&data) + err = PrepareColumns(&c, &data) if err != nil { return err } - printData(os.Stdout, &data) + printData(os.Stdout, c, &data) } return nil } -func determineIO(args []string) ([]io.Reader, string, error) { +// find supported color mode, modifies config based on constants +func determineColormode(c *cfg.Config) { + if !isTerminal(os.Stdout) { + color.Disable() + } else { + level := color.TermColorLevel() + colors := cfg.Colors() + c.MatchFG = colors[level]["fg"] + c.MatchBG = colors[level]["bg"] + } +} + +func determineIO(c *cfg.Config, args []string) ([]io.Reader, string, error) { var pattern string var fds []io.Reader var havefiles bool @@ -67,7 +74,7 @@ func determineIO(args []string) ([]io.Reader, string, error) { // first one is not a file, consider it as regexp and // shift arg list pattern = args[0] - Pattern = args[0] // FIXME + c.Pattern = args[0] // used for colorization by printData() args = args[1:] } diff --git a/lib/parser.go b/lib/parser.go index 2b9c0e0..0e2ca96 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "github.com/alecthomas/repr" + "github.com/tlinden/tablizer/cfg" "io" "regexp" "strings" @@ -30,13 +31,13 @@ import ( /* Parse tabular input. */ -func parseFile(input io.Reader, pattern string) (Tabdata, error) { +func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) { data := Tabdata{} var scanner *bufio.Scanner hadFirst := false - separate := regexp.MustCompile(Separator) + separate := regexp.MustCompile(c.Separator) patternR, err := regexp.Compile(pattern) if err != nil { return data, errors.Unwrap(fmt.Errorf("Regexp pattern %s is invalid: %w", pattern, err)) @@ -76,7 +77,7 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) { } else { // data processing if len(pattern) > 0 { - if patternR.MatchString(line) == InvertMatch { + if patternR.MatchString(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, @@ -119,7 +120,7 @@ func parseFile(input io.Reader, pattern string) (Tabdata, error) { return data, errors.Unwrap(fmt.Errorf("Failed to read from io.Reader: %w", scanner.Err())) } - if Debug { + if c.Debug { repr.Print(data) } diff --git a/lib/parser_test.go b/lib/parser_test.go index 995a725..332c52f 100644 --- a/lib/parser_test.go +++ b/lib/parser_test.go @@ -19,6 +19,7 @@ package lib import ( "fmt" + "github.com/tlinden/tablizer/cfg" "reflect" "strings" "testing" @@ -49,15 +50,15 @@ asd igig cxxxncnc 19191 EDD 1 X` readFd := strings.NewReader(table) - gotdata, err := parseFile(readFd, "") - Separator = DefaultSeparator + c := cfg.Config{Separator: cfg.DefaultSeparator} + gotdata, err := parseFile(c, readFd, "") if err != nil { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) } if !reflect.DeepEqual(data, gotdata) { - t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", Separator, data, gotdata) + t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", c.Separator, data, gotdata) } } @@ -94,10 +95,10 @@ asd igig cxxxncnc for _, tt := range tests { testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert) t.Run(testname, func(t *testing.T) { - InvertMatch = tt.invert + c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: cfg.DefaultSeparator} readFd := strings.NewReader(table) - gotdata, err := parseFile(readFd, tt.pattern) + gotdata, err := parseFile(c, readFd, tt.pattern) if err != nil { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) @@ -136,8 +137,8 @@ asd igig 19191 EDD 1 X` readFd := strings.NewReader(table) - gotdata, err := parseFile(readFd, "") - Separator = DefaultSeparator + c := cfg.Config{Separator: cfg.DefaultSeparator} + gotdata, err := parseFile(c, readFd, "") if err != nil { t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) @@ -145,6 +146,6 @@ asd igig if !reflect.DeepEqual(data, gotdata) { t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", - Separator, data, gotdata) + c.Separator, data, gotdata) } } diff --git a/lib/printer.go b/lib/printer.go index 5cc5899..f0ff4d6 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/gookit/color" "github.com/olekukonko/tablewriter" + "github.com/tlinden/tablizer/cfg" "gopkg.in/yaml.v3" "io" "log" @@ -29,33 +30,33 @@ import ( "strings" ) -func printData(w io.Writer, data *Tabdata) { +func printData(w io.Writer, c cfg.Config, data *Tabdata) { // some output preparations: // add numbers to headers and remove this we're not interested in - numberizeAndReduceHeaders(data) + numberizeAndReduceHeaders(c, data) // remove unwanted columns, if any - reduceColumns(data) + reduceColumns(c, data) // sort the data - sortTable(data, SortByColumn) + sortTable(c, data) - switch OutputMode { + switch c.OutputMode { case "extended": - printExtendedData(w, data) + printExtendedData(w, c, data) case "ascii": - printAsciiData(w, data) + printAsciiData(w, c, data) case "orgtbl": - printOrgmodeData(w, data) + printOrgmodeData(w, c, data) case "markdown": - printMarkdownData(w, data) + printMarkdownData(w, c, data) case "shell": - printShellData(w, data) + printShellData(w, c, data) case "yaml": - printYamlData(w, data) + printYamlData(w, c, data) default: - printAsciiData(w, data) + printAsciiData(w, c, data) } } @@ -66,7 +67,7 @@ func output(w io.Writer, str string) { /* Emacs org-mode compatible table (also orgtbl-mode) */ -func printOrgmodeData(w io.Writer, data *Tabdata) { +func printOrgmodeData(w io.Writer, c cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -93,7 +94,7 @@ func printOrgmodeData(w io.Writer, data *Tabdata) { rightR := regexp.MustCompile("\\+(?m)$") output(w, color.Sprint( - colorizeData( + colorizeData(c, rightR.ReplaceAllString( leftR.ReplaceAllString(tableString.String(), "|"), "|")))) } @@ -101,7 +102,7 @@ func printOrgmodeData(w io.Writer, data *Tabdata) { /* Markdown table */ -func printMarkdownData(w io.Writer, data *Tabdata) { +func printMarkdownData(w io.Writer, c cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -115,13 +116,13 @@ func printMarkdownData(w io.Writer, data *Tabdata) { table.SetCenterSeparator("|") table.Render() - output(w, color.Sprint(colorizeData(tableString.String()))) + output(w, color.Sprint(colorizeData(c, tableString.String()))) } /* Simple ASCII table without any borders etc, just like the input we expect */ -func printAsciiData(w io.Writer, data *Tabdata) { +func printAsciiData(w io.Writer, c cfg.Config, data *Tabdata) { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -145,13 +146,13 @@ func printAsciiData(w io.Writer, data *Tabdata) { table.SetNoWhiteSpace(true) table.Render() - output(w, color.Sprint(colorizeData(tableString.String()))) + output(w, color.Sprint(colorizeData(c, tableString.String()))) } /* We simulate the \x command of psql (the PostgreSQL client) */ -func printExtendedData(w io.Writer, data *Tabdata) { +func printExtendedData(w io.Writer, c cfg.Config, data *Tabdata) { // needed for data output format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) out := "" @@ -165,13 +166,13 @@ func printExtendedData(w io.Writer, data *Tabdata) { } } - output(w, out) + output(w, colorizeData(c, out)) } /* Shell output, ready to be eval'd. Just like FreeBSD stat(1) */ -func printShellData(w io.Writer, data *Tabdata) { +func printShellData(w io.Writer, c cfg.Config, data *Tabdata) { out := "" if len(data.entries) > 0 { for _, entry := range data.entries { @@ -183,10 +184,12 @@ func printShellData(w io.Writer, data *Tabdata) { out += fmt.Sprint(strings.Join(shentries, " ")) + "\n" } } + + // no colrization here output(w, out) } -func printYamlData(w io.Writer, data *Tabdata) { +func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) { type D struct { Entries []map[string]interface{} `yaml:"entries"` } diff --git a/lib/printer_test.go b/lib/printer_test.go index b132bb4..0f1c6f6 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -21,6 +21,7 @@ import ( "bytes" "fmt" //"github.com/alecthomas/repr" + "github.com/tlinden/tablizer/cfg" "strings" "testing" ) @@ -246,8 +247,6 @@ DURATION(2) WHEN(4) } func TestPrinter(t *testing.T) { - NoColor = true - for _, tt := range tests { testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%s-usecolumns-%s", tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr) @@ -256,24 +255,27 @@ func TestPrinter(t *testing.T) { var w bytes.Buffer // cmd flags - SortByColumn = tt.column - SortDescending = tt.desc - SortMode = tt.sortby - OutputMode = tt.mode - NoNumbering = tt.nonum - UseColumns = tt.usecol + c := cfg.Config{ + SortByColumn: tt.column, + SortDescending: tt.desc, + SortMode: tt.sortby, + OutputMode: tt.mode, + NoNumbering: tt.nonum, + UseColumns: tt.usecol, + NoColor: true, + } // the test checks the len! if len(tt.usecol) > 0 { - Columns = "yes" + c.Columns = "yes" } else { - Columns = "" + c.Columns = "" } testdata := newData() exp := strings.TrimSpace(tt.expect) - printData(&w, &testdata) + printData(&w, c, &testdata) got := strings.TrimSpace(w.String()) diff --git a/lib/sort.go b/lib/sort.go index 90e4836..37dfa79 100644 --- a/lib/sort.go +++ b/lib/sort.go @@ -19,17 +19,21 @@ package lib import ( "github.com/araddon/dateparse" + "github.com/tlinden/tablizer/cfg" "regexp" "sort" "strconv" ) -func sortTable(data *Tabdata, col int) { - if SortByColumn <= 0 { +func sortTable(c cfg.Config, data *Tabdata) { + if c.SortByColumn <= 0 { // no sorting wanted return } + // slightly modified here to match internal array indicies + col := c.SortByColumn + col-- // ui starts counting by 1, but use 0 internally // sanity checks @@ -44,14 +48,15 @@ func sortTable(data *Tabdata, col int) { // actual sorting sort.SliceStable(data.entries, func(i, j int) bool { - return compare(data.entries[i][col], data.entries[j][col]) + return compare(&c, data.entries[i][col], data.entries[j][col]) }) } -func compare(a string, b string) bool { +// 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 { var comp bool - switch SortMode { + switch c.SortMode { case "numeric": left, err := strconv.Atoi(a) if err != nil { @@ -74,7 +79,7 @@ func compare(a string, b string) bool { comp = a < b } - if SortDescending { + if c.SortDescending { comp = !comp } diff --git a/lib/sort_test.go b/lib/sort_test.go index c784872..b46cce2 100644 --- a/lib/sort_test.go +++ b/lib/sort_test.go @@ -19,6 +19,7 @@ package lib import ( "fmt" + "github.com/tlinden/tablizer/cfg" "testing" ) @@ -68,9 +69,8 @@ func TestCompare(t *testing.T) { for _, tt := range tests { testname := fmt.Sprintf("compare-mode-%s-a-%s-b-%s-desc-%t", tt.mode, tt.a, tt.b, tt.desc) t.Run(testname, func(t *testing.T) { - SortMode = tt.mode - SortDescending = tt.desc - got := compare(tt.a, tt.b) + 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) }