continued refactoring, added more tests, better error handling

This commit is contained in:
2022-10-02 14:22:31 +02:00
parent 66c4b68036
commit e6723a6951
9 changed files with 244 additions and 41 deletions

1
TODO
View File

@@ -3,4 +3,3 @@ Add a mode like FreeBSD stat(1):
stat -s dead.letter 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 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()

View File

@@ -33,9 +33,12 @@ var rootCmd = &cobra.Command{
return nil return nil
} }
lib.PrepareColumns() err := lib.PrepareColumns()
if err != nil {
return err
}
err := lib.PrepareModeFlags() err = lib.PrepareModeFlags()
if err != nil { if err != nil {
return err return err
} }
@@ -58,11 +61,14 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator") rootCmd.PersistentFlags().StringVarP(&lib.Separator, "separator", "s", "", "Custom field separator")
rootCmd.PersistentFlags().StringVarP(&lib.Columns, "columns", "c", "", "Only show the speficied columns (separated by ,)") 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.OutflagExtended, "extended", "X", false, "Enable extended output")
rootCmd.PersistentFlags().BoolVarP(&lib.OutflagMarkdown, "markdown", "M", false, "Enable markdown table 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.OutflagOrgtable, "orgtbl", "O", false, "Enable org-mode table output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl") 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 // 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)") rootCmd.PersistentFlags().StringVarP(&lib.OutputMode, "output", "o", "", "Output mode - one of: orgtbl, markdown, extended, ascii(default)")

View File

@@ -40,16 +40,18 @@ func contains(s []int, e int) bool {
return false return false
} }
func PrepareColumns() { func PrepareColumns() error {
if len(Columns) > 0 { if len(Columns) > 0 {
for _, use := range strings.Split(Columns, ",") { for _, use := range strings.Split(Columns, ",") {
usenum, err := strconv.Atoi(use) usenum, err := strconv.Atoi(use)
if err != nil { 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) UseColumns = append(UseColumns, usenum)
} }
} }
return nil
} }
func PrepareModeFlags() error { func PrepareModeFlags() error {

View File

@@ -19,20 +19,22 @@ package lib
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
) )
func TestArrayContains(t *testing.T) { func Testcontains(t *testing.T) {
var tests = []struct { var tests = []struct {
list []int list []int
search int search int
want bool want bool
}{ }{
{[]int{1, 2, 3}, 2, true}, {[]int{1, 2, 3}, 2, true},
{[]int{2, 3, 4}, 5, false},
} }
for _, tt := range tests { 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) { t.Run(testname, func(t *testing.T) {
answer := contains(tt.list, tt.search) answer := contains(tt.list, tt.search)
if answer != tt.want { 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)
}
}
})
}
}

View File

@@ -19,34 +19,48 @@ package lib
import ( import (
"errors" "errors"
"github.com/alecthomas/repr" "io"
"os" "os"
) )
func ProcessFiles(args []string) error { func ProcessFiles(args []string) error {
var pattern string fds, pattern, err := determineIO(args)
havefiles := false
//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 { if len(args) > 0 {
// threre were args left, take a look
if _, err := os.Stat(args[0]); err != nil { 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] pattern = args[0]
args = args[1:] args = args[1:]
} }
if len(args) > 0 { if len(args) > 0 {
// only files
for _, file := range args { 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 { if err != nil {
die(err) return nil, "", err
} }
data := parseFile(fd, pattern) fds = append(fds, fd)
if Debug {
repr.Print(data)
}
printData(data)
} }
havefiles = true havefiles = true
} }
@@ -55,15 +69,11 @@ func ProcessFiles(args []string) error {
if !havefiles { if !havefiles {
stat, _ := os.Stdin.Stat() stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 { if (stat.Mode() & os.ModeCharDevice) == 0 {
data := parseFile(os.Stdin, pattern) fds = append(fds, os.Stdin)
if Debug {
repr.Print(data)
}
printData(data)
} else { } 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
} }

View File

@@ -20,6 +20,7 @@ package lib
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/alecthomas/repr"
"io" "io"
"regexp" "regexp"
"strings" "strings"
@@ -59,7 +60,7 @@ func parseFile(input io.Reader, pattern string) Tabdata {
scanner = bufio.NewScanner(input) scanner = bufio.NewScanner(input)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := strings.TrimSpace(scanner.Text())
values := []string{} values := []string{}
patternR, err := regexp.Compile(pattern) patternR, err := regexp.Compile(pattern)
@@ -109,22 +110,18 @@ func parseFile(input io.Reader, pattern string) Tabdata {
// done // done
hadFirst = true hadFirst = true
} }
// if Debug {
// fmt.Println(data.headerIndices)
// }
} else { } else {
// data processing // data processing
if len(pattern) > 0 { if len(pattern) > 0 {
//fmt.Println(patternR.MatchString(line))
if !patternR.MatchString(line) { if !patternR.MatchString(line) {
continue continue
} }
} }
idx := 0 // we cannot use the header index, because we could exclude columns idx := 0 // we cannot use the header index, because we could exclude columns
for _, index := range data.headerIndices { for _, index := range data.headerIndices {
value := "" value := ""
if index["end"] == 0 { if index["end"] == 0 {
value = string(line[index["beg"]:]) value = string(line[index["beg"]:])
} else { } else {
@@ -159,5 +156,9 @@ func parseFile(input io.Reader, pattern string) Tabdata {
die(scanner.Err()) die(scanner.Err())
} }
if Debug {
repr.Print(data)
}
return data return data
} }

77
lib/parser_test.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

76
lib/printer_test.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -10,12 +10,12 @@ tablizer - Manipulate tabular output of other programs
Flags: Flags:
-c, --columns string Only show the speficied columns (separated by ,) -c, --columns string Only show the speficied columns (separated by ,)
-d, --debug Enable debugging -d, --debug Enable debugging
-X, --extended Enable extended output
-h, --help help for tablizer -h, --help help for tablizer
-M, --markdown Enable markdown table output
-n, --no-numbering Disable header numbering -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) -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 -s, --separator string Custom field separator
-v, --version Print program version -v, --version Print program version
@@ -71,9 +71,14 @@ the original order.
The numbering can be suppressed by using the B<-n> option. 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 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 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<extended mode>. In this mode, each row will be usefull which enables I<extended mode>. In this mode, each row will be
printed vertically, header left, value right, aligned by the field printed vertically, header left, value right, aligned by the field
widths. Here's an example: 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 You can of course still use a regex to reduce the number of rows
displayed. displayed.
Beside normal ascii mode (the default) and extended mode there more Beside normal ascii mode (the default) and extended mode there are
output modes available: B<orgtbl> which prints an Emacs org-mode table more output modes available: B<orgtbl> which prints an Emacs org-mode
and B<markdown> which prints a Markdown table. table and B<markdown> which prints a Markdown table.
Finally the B<-d> option enables debugging output which is mostly
usefull for the developer.
=head1 BUGS =head1 BUGS