From e6723a6951561eaf262d862ee4b39e603983a858 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sun, 2 Oct 2022 14:22:31 +0200 Subject: [PATCH] continued refactoring, added more tests, better error handling --- TODO | 1 - cmd/root.go | 12 +++++-- lib/helpers.go | 6 ++-- lib/helpers_test.go | 34 ++++++++++++++++++-- lib/io.go | 44 ++++++++++++++++---------- lib/parser.go | 13 ++++---- lib/parser_test.go | 77 +++++++++++++++++++++++++++++++++++++++++++++ lib/printer_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++ tablizer.pod | 22 +++++++------ 9 files changed, 244 insertions(+), 41 deletions(-) create mode 100644 lib/parser_test.go create mode 100644 lib/printer_test.go diff --git a/TODO b/TODO index 5b120d5..73b8d16 100644 --- a/TODO +++ b/TODO @@ -3,4 +3,3 @@ Add a mode like FreeBSD stat(1): stat -s dead.letter st_dev=170671546954750497 st_ino=159667 st_mode=0100644 st_nlink=1 st_uid=1001 st_gid=1001 st_rdev=18446744073709551615 st_size=573 st_atime=1661994007 st_mtime=1661961878 st_ctime=1661961878 st_birthtime=1658394900 st_blksize=4096 st_blocks=3 st_flags=2048 -mv UseColumns processing out of process() diff --git a/cmd/root.go b/cmd/root.go index fd0446d..6d047eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,9 +33,12 @@ var rootCmd = &cobra.Command{ return nil } - lib.PrepareColumns() + err := lib.PrepareColumns() + if err != nil { + return err + } - err := lib.PrepareModeFlags() + err = lib.PrepareModeFlags() if err != nil { return err } @@ -58,11 +61,14 @@ func init() { rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator") rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") - // output flags, only 1 allowed + // 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.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl") + rootCmd.Flags().MarkHidden("extended") + rootCmd.Flags().MarkHidden("orgtbl") + rootCmd.Flags().MarkHidden("markdown") // same thing but more common, takes precedence over above group rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, ascii(default)") diff --git a/lib/helpers.go b/lib/helpers.go index 5dab33f..ddda4ee 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -40,16 +40,18 @@ func contains(s []int, e int) bool { return false } -func PrepareColumns() { +func PrepareColumns() error { if len(Columns) > 0 { for _, use := range strings.Split(Columns, ",") { usenum, err := strconv.Atoi(use) if err != nil { - die(err) + msg := fmt.Sprintf("Could not parse columns list %s: %v", Columns, err) + return errors.New(msg) } UseColumns = append(UseColumns, usenum) } } + return nil } func PrepareModeFlags() error { diff --git a/lib/helpers_test.go b/lib/helpers_test.go index 68fa2c3..b02bb9b 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -19,20 +19,22 @@ package lib import ( "fmt" + "reflect" "testing" ) -func TestArrayContains(t *testing.T) { +func Testcontains(t *testing.T) { var tests = []struct { list []int search int want bool }{ {[]int{1, 2, 3}, 2, true}, + {[]int{2, 3, 4}, 5, false}, } for _, tt := range tests { - testname := fmt.Sprintf("%d,%d,%t", tt.list, tt.search, tt.want) + testname := fmt.Sprintf("contains-%d,%d,%t", tt.list, tt.search, tt.want) t.Run(testname, func(t *testing.T) { answer := contains(tt.list, tt.search) if answer != tt.want { @@ -41,3 +43,31 @@ func TestArrayContains(t *testing.T) { }) } } + +func TestPrepareColumns(t *testing.T) { + var tests = []struct { + input string + exp []int + wanterror bool // expect error + }{ + {"1,2,3", []int{1, 2, 3}, false}, + {"1,2,", []int{}, true}, + } + + 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() + 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) + } + } + }) + } +} diff --git a/lib/io.go b/lib/io.go index 3665dbf..4c6a726 100644 --- a/lib/io.go +++ b/lib/io.go @@ -19,34 +19,48 @@ package lib import ( "errors" - "github.com/alecthomas/repr" + "io" "os" ) func ProcessFiles(args []string) error { - var pattern string - havefiles := false + fds, pattern, err := determineIO(args) - //prepareColumns() + if err != nil { + return err + } + + for _, fd := range fds { + printData(parseFile(fd, pattern)) + } + + return nil +} + +func determineIO(args []string) ([]io.Reader, string, error) { + var pattern string + var fds []io.Reader + var havefiles bool if len(args) > 0 { + // threre were args left, take a look 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] args = args[1:] } if len(args) > 0 { + // only files for _, file := range args { fd, err := os.OpenFile(file, os.O_RDONLY, 0755) + if err != nil { - die(err) + return nil, "", err } - data := parseFile(fd, pattern) - if Debug { - repr.Print(data) - } - printData(data) + fds = append(fds, fd) } havefiles = true } @@ -55,15 +69,11 @@ func ProcessFiles(args []string) error { if !havefiles { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) == 0 { - data := parseFile(os.Stdin, pattern) - if Debug { - repr.Print(data) - } - printData(data) + fds = append(fds, os.Stdin) } else { - return 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 nil + return fds, pattern, nil } diff --git a/lib/parser.go b/lib/parser.go index eadac31..0b19e14 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -20,6 +20,7 @@ package lib import ( "bufio" "fmt" + "github.com/alecthomas/repr" "io" "regexp" "strings" @@ -59,7 +60,7 @@ func parseFile(input io.Reader, pattern string) Tabdata { scanner = bufio.NewScanner(input) for scanner.Scan() { - line := scanner.Text() + line := strings.TrimSpace(scanner.Text()) values := []string{} patternR, err := regexp.Compile(pattern) @@ -109,22 +110,18 @@ func parseFile(input io.Reader, pattern string) Tabdata { // done hadFirst = true } - // if Debug { - // fmt.Println(data.headerIndices) - // } } else { // data processing if len(pattern) > 0 { - //fmt.Println(patternR.MatchString(line)) if !patternR.MatchString(line) { continue } } idx := 0 // we cannot use the header index, because we could exclude columns - for _, index := range data.headerIndices { value := "" + if index["end"] == 0 { value = string(line[index["beg"]:]) } else { @@ -159,5 +156,9 @@ func parseFile(input io.Reader, pattern string) Tabdata { die(scanner.Err()) } + if Debug { + repr.Print(data) + } + return data } diff --git a/lib/parser_test.go b/lib/parser_test.go new file mode 100644 index 0000000..19218b3 --- /dev/null +++ b/lib/parser_test.go @@ -0,0 +1,77 @@ +/* +Copyright © 2022 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 ( + "reflect" + "strings" + "testing" +) + +func TestParser(t *testing.T) { + data := Tabdata{ + maxwidthHeader: 5, + maxwidthPerCol: []int{ + 5, + 5, + 8, + }, + columns: 3, + headerIndices: []map[string]int{ + map[string]int{ + "beg": 0, + "end": 6, + }, + map[string]int{ + "end": 13, + "beg": 7, + }, + map[string]int{ + "beg": 14, + "end": 0, + }, + }, + headers: []string{ + "ONE", + "TWO", + "THREE", + }, + entries: [][]string{ + []string{ + "asd", + "igig", + "cxxxncnc", + }, + []string{ + "19191", + "EDD 1", + "X", + }, + }, + } + + table := `ONE TWO THREE +asd igig cxxxncnc +19191 EDD 1 X` + + readFd := strings.NewReader(table) + gotdata := parseFile(readFd, "") + if !reflect.DeepEqual(data, gotdata) { + t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n", data, gotdata) + } +} diff --git a/lib/printer_test.go b/lib/printer_test.go new file mode 100644 index 0000000..5abb75a --- /dev/null +++ b/lib/printer_test.go @@ -0,0 +1,76 @@ +/* +Copyright © 2022 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 ( + "os" + "strings" + "testing" +) + +func TestPrinter(t *testing.T) { + table := `ONE TWO THREE +asd igig cxxxncnc +19191 EDD 1 X` + + expects := map[string]string{ + "ascii": `ONE(1) TWO(2) THREE(3) +asd igig cxxxncnc +19191 EDD 1 X`, + "orgtbl": `|--------+--------+----------| +| ONE(1) | TWO(2) | THREE(3) | +|--------+--------+----------| +| asd | igig | cxxxncnc | +| 19191 | EDD 1 | X | +|--------+--------+----------|`, + "markdown": `| ONE(1) | TWO(2) | THREE(3) | +|--------|--------|----------| +| asd | igig | cxxxncnc | +| 19191 | EDD 1 | X |`, + } + + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + origStdout := os.Stdout + os.Stdout = w + + for mode, expect := range expects { + OutputMode = mode + fd := strings.NewReader(table) + data := parseFile(fd, "") + printData(data) + + buf := make([]byte, 1024) + n, err := r.Read(buf) + if err != nil { + t.Fatal(err) + } + buf = buf[:n] + output := strings.TrimSpace(string(buf)) + + if output != expect { + t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", mode, output, expect, len(output), len(expect)) + } + } + + // Restore + os.Stdout = origStdout + +} diff --git a/tablizer.pod b/tablizer.pod index bb11ee0..4808812 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -10,12 +10,12 @@ tablizer - Manipulate tabular output of other programs Flags: -c, --columns string Only show the speficied columns (separated by ,) -d, --debug Enable debugging - -X, --extended Enable extended output -h, --help help for tablizer - -M, --markdown Enable markdown table output -n, --no-numbering Disable header numbering - -O, --orgtbl Enable org-mode table output -o, --output string Output mode - one of: orgtbl, markdown, extended, ascii(default) + -X, --extended Enable extended output + -M, --markdown Enable markdown table output + -O, --orgtbl Enable org-mode table output -s, --separator string Custom field separator -v, --version Print program version @@ -71,9 +71,14 @@ the original order. The numbering can be suppressed by using the B<-n> option. +Finally the B<-d> option enables debugging output which is mostly +usefull for the developer. + +?head2 OUTPUT MODES + There might be cases when the tabular output of a program is way too large for your current terminal but you still need to see every -column. In such cases the B<-X> option (or B<-o extended> can be +column. In such cases the B<-o extended> or B<-X> option can be usefull which enables I. In this mode, each row will be printed vertically, header left, value right, aligned by the field widths. Here's an example: @@ -88,12 +93,9 @@ widths. Here's an example: You can of course still use a regex to reduce the number of rows displayed. -Beside normal ascii mode (the default) and extended mode there more -output modes available: B which prints an Emacs org-mode table -and B which prints a Markdown table. - -Finally the B<-d> option enables debugging output which is mostly -usefull for the developer. +Beside normal ascii mode (the default) and extended mode there are +more output modes available: B which prints an Emacs org-mode +table and B which prints a Markdown table. =head1 BUGS