added CSV output mode, enhanced parser tests

This commit is contained in:
2022-10-23 16:57:30 +02:00
parent b5c802403b
commit 138ae51936
11 changed files with 148 additions and 104 deletions

View File

@@ -60,6 +60,7 @@ type Modeflag struct {
S bool S bool
Y bool Y bool
A bool A bool
C bool
} }
// used for switching printers // used for switching printers
@@ -69,6 +70,7 @@ const (
Markdown Markdown
Shell Shell
Yaml Yaml
CSV
Ascii Ascii
) )
@@ -130,6 +132,8 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) {
conf.OutputMode = Shell conf.OutputMode = Shell
case flag.Y: case flag.Y:
conf.OutputMode = Yaml conf.OutputMode = Yaml
case flag.C:
conf.OutputMode = CSV
default: default:
conf.OutputMode = Ascii conf.OutputMode = Ascii
} }
@@ -152,3 +156,10 @@ func (c *Config) CheckEnv() {
} }
} }
} }
func (c *Config) ApplyDefaults() {
// mode specific defaults
if c.OutputMode == Yaml || c.OutputMode == CSV {
c.NoNumbering = true
}
}

View File

@@ -73,6 +73,7 @@ func Execute() {
conf.CheckEnv() conf.CheckEnv()
conf.PrepareModeFlags(modeflag) conf.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode) conf.PrepareSortFlags(sortmode)
conf.ApplyDefaults()
// actual execution starts here // actual execution starts here
return lib.ProcessFiles(conf, args) return lib.ProcessFiles(conf, args)
@@ -105,7 +106,8 @@ func Execute() {
rootCmd.PersistentFlags().BoolVarP(&modeflag.O, "orgtbl", "O", false, "Enable org-mode 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.S, "shell", "S", false, "Enable shell mode output")
rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output") rootCmd.PersistentFlags().BoolVarP(&modeflag.Y, "yaml", "Y", false, "Enable yaml output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml") rootCmd.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, "Enable CSV output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv")
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n") rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")

View File

@@ -22,6 +22,7 @@ SYNOPSIS
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable ouput -S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
@@ -186,8 +187,8 @@ DESCRIPTION
Beside normal ascii mode (the default) and extended mode there are more Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table and yaml, which prints yaml markdown which prints a Markdown table, yaml, which prints yaml encoding
encoding. and CSV mode, which prints a comma separated value file.
ENVIRONMENT VARIABLES ENVIRONMENT VARIABLES
tablizer supports certain environment variables which use can use to tablizer supports certain environment variables which use can use to
@@ -241,6 +242,7 @@ Output Flags (mutually exclusive):
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable ouput -S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):

View File

@@ -20,7 +20,6 @@ package lib
// contains a whole parsed table // contains a whole parsed table
type Tabdata struct { type Tabdata struct {
maxwidthHeader int // longest header maxwidthHeader int // longest header
maxwidthPerCol []int // max width per column
columns int // count columns int // count
headers []string // [ "ID", "NAME", ...] headers []string // [ "ID", "NAME", ...]
entries [][]string entries [][]string

View File

@@ -48,11 +48,6 @@ func TestContains(t *testing.T) {
func TestPrepareColumns(t *testing.T) { func TestPrepareColumns(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",

View File

@@ -44,8 +44,11 @@ func parseCSV(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
if len(pattern) > 0 { if len(pattern) > 0 {
scanner := bufio.NewScanner(input) scanner := bufio.NewScanner(input)
lines := []string{} lines := []string{}
hadFirst := false
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
if hadFirst {
// don't match 1st line, it's the header
if patternR.MatchString(line) == c.InvertMatch { if patternR.MatchString(line) == c.InvertMatch {
// by default -v is false, so if a line does NOT // by default -v is false, so if a line does NOT
// match the pattern, we will ignore it. However, // match the pattern, we will ignore it. However,
@@ -53,21 +56,24 @@ func parseCSV(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
// so we ignore all lines, which DO match. // so we ignore all lines, which DO match.
continue continue
} }
}
lines = append(lines, line) lines = append(lines, line)
hadFirst = true
} }
content = strings.NewReader(strings.Join(lines, "\n")) content = strings.NewReader(strings.Join(lines, "\n"))
} }
fmt.Println(content)
csvreader := csv.NewReader(content) csvreader := csv.NewReader(content)
csvreader.Comma = rune(c.Separator[0]) csvreader.Comma = rune(c.Separator[0])
records, err := csvreader.ReadAll() records, err := csvreader.ReadAll()
if err != nil { if err != nil {
return data, errors.Unwrap(fmt.Errorf("Could not parse CSV input: %w", pattern, err)) return data, errors.Unwrap(fmt.Errorf("Could not parse CSV input: %w", err))
} }
if len(records) >= 1 { if len(records) >= 1 {
data.headers = records[0] data.headers = records[0]
data.columns = len(records)
for _, head := range data.headers { for _, head := range data.headers {
// register widest header field // register widest header field
@@ -149,16 +155,6 @@ func parseFile(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
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
values := []string{} values := []string{}
for _, part := range parts { for _, part := range parts {
width := len(strings.TrimSpace(part))
if len(data.maxwidthPerCol)-1 < idx {
data.maxwidthPerCol = append(data.maxwidthPerCol, width)
} else {
if width > data.maxwidthPerCol[idx] {
data.maxwidthPerCol[idx] = width
}
}
// if Debug { // if Debug {
// fmt.Printf("<%s> ", value) // fmt.Printf("<%s> ", value)
// } // }

View File

@@ -25,32 +25,47 @@ import (
"testing" "testing"
) )
var input = []struct {
name string
text string
separator string
}{
{
name: "tabular-data",
separator: cfg.DefaultSeparator,
text: `
ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`,
},
{
name: "csv-data",
separator: ",",
text: `
ONE,TWO,THREE
asd,igig,cxxxncnc
19191,"EDD 1",X`,
},
}
func TestParser(t *testing.T) { func TestParser(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 8,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",
}, },
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", "cxxxncnc"},
"asd", "igig", "cxxxncnc", {"19191", "EDD 1", "X"},
},
{
"19191", "EDD 1", "X",
},
}, },
} }
table := `ONE TWO THREE for _, in := range input {
asd igig cxxxncnc testname := fmt.Sprintf("parse-%s", in.name)
19191 EDD 1 X` t.Run(testname, func(t *testing.T) {
readFd := strings.NewReader(strings.TrimSpace(in.text))
readFd := strings.NewReader(table) c := cfg.Config{Separator: in.separator}
c := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := parseFile(c, readFd, "") gotdata, err := parseFile(c, readFd, "")
if err != nil { if err != nil {
@@ -58,7 +73,10 @@ asd igig cxxxncnc
} }
if !reflect.DeepEqual(data, gotdata) { if !reflect.DeepEqual(data, gotdata) {
t.Errorf("Parser returned invalid data, Regex: %s\nExp: %+v\nGot: %+v\n", c.Separator, data, gotdata) t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n",
data, gotdata)
}
})
} }
} }
@@ -71,48 +89,42 @@ func TestParserPatternmatching(t *testing.T) {
}{ }{
{ {
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", "cxxxncnc"},
"asd", "igig", "cxxxncnc",
},
}, },
pattern: "ig", pattern: "ig",
invert: false, invert: false,
}, },
{ {
entries: [][]string{ entries: [][]string{
{ {"19191", "EDD 1", "X"},
"19191", "EDD 1", "X",
},
}, },
pattern: "ig", pattern: "ig",
invert: true, invert: true,
}, },
{ {
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", "cxxxncnc"},
"asd", "igig", "cxxxncnc",
},
}, },
pattern: "[a-z", pattern: "[a-z",
want: true, want: true,
}, },
} }
table := `ONE TWO THREE for _, in := range input {
asd igig cxxxncnc
19191 EDD 1 X`
for _, tt := range tests { for _, tt := range tests {
testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert) testname := fmt.Sprintf("parse-%s-with-pattern-%s-inverted-%t",
in.name, tt.pattern, tt.invert)
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: cfg.DefaultSeparator} c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern,
Separator: in.separator}
readFd := strings.NewReader(table) readFd := strings.NewReader(strings.TrimSpace(in.text))
gotdata, err := parseFile(c, readFd, tt.pattern) gotdata, err := parseFile(c, readFd, tt.pattern)
if err != nil { if err != nil {
if !tt.want { if !tt.want {
t.Errorf("Parser returned error: %s\nData processed so far: %+v", err, gotdata) t.Errorf("Parser returned error: %s\nData processed so far: %+v",
err, gotdata)
} }
} else { } else {
if !reflect.DeepEqual(tt.entries, gotdata.entries) { if !reflect.DeepEqual(tt.entries, gotdata.entries) {
@@ -123,32 +135,27 @@ asd igig cxxxncnc
}) })
} }
} }
}
func TestParserIncompleteRows(t *testing.T) { func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{ data := Tabdata{
maxwidthHeader: 5, maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 1,
},
columns: 3, columns: 3,
headers: []string{ headers: []string{
"ONE", "TWO", "THREE", "ONE", "TWO", "THREE",
}, },
entries: [][]string{ entries: [][]string{
{ {"asd", "igig", ""},
"asd", "igig", "", {"19191", "EDD 1", "X"},
},
{
"19191", "EDD 1", "X",
},
}, },
} }
table := `ONE TWO THREE table := `
ONE TWO THREE
asd igig asd igig
19191 EDD 1 X` 19191 EDD 1 X`
readFd := strings.NewReader(table) readFd := strings.NewReader(strings.TrimSpace(table))
c := cfg.Config{Separator: cfg.DefaultSeparator} c := cfg.Config{Separator: cfg.DefaultSeparator}
gotdata, err := parseFile(c, readFd, "") gotdata, err := parseFile(c, readFd, "")

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib package lib
import ( import (
"encoding/csv"
"fmt" "fmt"
"github.com/gookit/color" "github.com/gookit/color"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
@@ -55,6 +56,8 @@ func printData(w io.Writer, c cfg.Config, data *Tabdata) {
printShellData(w, c, data) printShellData(w, c, data)
case cfg.Yaml: case cfg.Yaml:
printYamlData(w, c, data) printYamlData(w, c, data)
case cfg.CSV:
printCSVData(w, c, data)
default: default:
printAsciiData(w, c, data) printAsciiData(w, c, data)
} }
@@ -186,7 +189,7 @@ func printShellData(w io.Writer, c cfg.Config, data *Tabdata) {
} }
} }
// no colrization here // no colorization here
output(w, out) output(w, out)
} }
@@ -225,3 +228,23 @@ func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) {
output(w, string(yamlstr)) output(w, string(yamlstr))
} }
func printCSVData(w io.Writer, c cfg.Config, data *Tabdata) {
csvout := csv.NewWriter(w)
if err := csvout.Write(data.headers); err != nil {
log.Fatalln("error writing record to csv:", err)
}
for _, entry := range data.entries {
if err := csvout.Write(entry); err != nil {
log.Fatalln("error writing record to csv:", err)
}
}
csvout.Flush()
if err := csvout.Error(); err != nil {
log.Fatal(err)
}
}

View File

@@ -29,12 +29,6 @@ import (
func newData() Tabdata { func newData() Tabdata {
return Tabdata{ return Tabdata{
maxwidthHeader: 8, maxwidthHeader: 8,
maxwidthPerCol: []int{
5,
9,
3,
26,
},
columns: 4, columns: 4,
headers: []string{ headers: []string{
"NAME", "NAME",
@@ -86,6 +80,15 @@ NAME(1) DURATION(2) COUNT(3) WHEN(4)
beta 1d10h5m1s 33 3/1/2014 beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03 alpha 4h35m 170 2013-Feb-03
ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`,
},
{
mode: cfg.CSV,
name: "csv",
expect: `
NAME,DURATION,COUNT,WHEN
beta,1d10h5m1s,33,3/1/2014
alpha,4h35m,170,2013-Feb-03
ceta,33d12h,9,06/Jan/2008 15:04:05 -0700`,
}, },
{ {
name: "default", name: "default",
@@ -265,6 +268,8 @@ func TestPrinter(t *testing.T) {
NoColor: true, NoColor: true,
} }
c.ApplyDefaults()
// the test checks the len! // the test checks the len!
if len(tt.usecol) > 0 { if len(tt.usecol) > 0 {
c.Columns = "yes" c.Columns = "yes"

View File

@@ -133,7 +133,7 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "TABLIZER 1" .IX Title "TABLIZER 1"
.TH TABLIZER 1 "2022-10-22" "1" "User Commands" .TH TABLIZER 1 "2022-10-23" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .if n .ad l
@@ -160,6 +160,7 @@ tablizer \- Manipulate tabular output of other programs
\& \-O, \-\-orgtbl Enable org\-mode table output \& \-O, \-\-orgtbl Enable org\-mode table output
\& \-S, \-\-shell Enable shell evaluable ouput \& \-S, \-\-shell Enable shell evaluable ouput
\& \-Y, \-\-yaml Enable yaml output \& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular \& \-A, \-\-ascii Default output mode, ascii tabular
\& \&
\& Sort Mode Flags (mutually exclusive): \& Sort Mode Flags (mutually exclusive):
@@ -353,8 +354,9 @@ You can use this in an eval loop.
.PP .PP
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: \fBorgtbl\fR which prints an Emacs org-mode more output modes available: \fBorgtbl\fR which prints an Emacs org-mode
table and \fBmarkdown\fR which prints a Markdown table and \fByaml\fR, which table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
prints yaml encoding. prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
value file.
.SS "\s-1ENVIRONMENT VARIABLES\s0" .SS "\s-1ENVIRONMENT VARIABLES\s0"
.IX Subsection "ENVIRONMENT VARIABLES" .IX Subsection "ENVIRONMENT VARIABLES"
\&\fBtablizer\fR supports certain environment variables which use can use \&\fBtablizer\fR supports certain environment variables which use can use

View File

@@ -21,6 +21,7 @@ tablizer - Manipulate tabular output of other programs
-O, --orgtbl Enable org-mode table output -O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable ouput -S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output -Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular -A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive): Sort Mode Flags (mutually exclusive):
@@ -206,8 +207,9 @@ You can use this in an eval loop.
Beside normal ascii mode (the default) and extended mode there are Beside normal ascii mode (the default) and extended mode there are
more output modes available: B<orgtbl> which prints an Emacs org-mode more output modes available: B<orgtbl> which prints an Emacs org-mode
table and B<markdown> which prints a Markdown table and B<yaml>, which table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding. prints yaml encoding and CSV mode, which prints a comma separated
value file.
=head2 ENVIRONMENT VARIABLES =head2 ENVIRONMENT VARIABLES