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
Y bool
A bool
C bool
}
// used for switching printers
@@ -69,6 +70,7 @@ const (
Markdown
Shell
Yaml
CSV
Ascii
)
@@ -130,6 +132,8 @@ func (conf *Config) PrepareModeFlags(flag Modeflag) {
conf.OutputMode = Shell
case flag.Y:
conf.OutputMode = Yaml
case flag.C:
conf.OutputMode = CSV
default:
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.PrepareModeFlags(modeflag)
conf.PrepareSortFlags(sortmode)
conf.ApplyDefaults()
// actual execution starts here
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.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.PersistentFlags().BoolVarP(&modeflag.C, "csv", "C", false, "Enable CSV output")
rootCmd.MarkFlagsMutuallyExclusive("extended", "markdown", "orgtbl", "shell", "yaml", "csv")
rootCmd.SetUsageTemplate(strings.TrimSpace(usage) + "\n")

View File

@@ -22,6 +22,7 @@ SYNOPSIS
-O, --orgtbl Enable org-mode table output
-S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive):
@@ -186,8 +187,8 @@ DESCRIPTION
Beside normal ascii mode (the default) and extended mode there are more
output modes available: orgtbl which prints an Emacs org-mode table and
markdown which prints a Markdown table and yaml, which prints yaml
encoding.
markdown which prints a Markdown table, yaml, which prints yaml encoding
and CSV mode, which prints a comma separated value file.
ENVIRONMENT VARIABLES
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
-S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
Sort Mode Flags (mutually exclusive):

View File

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

View File

@@ -48,11 +48,6 @@ func TestContains(t *testing.T) {
func TestPrepareColumns(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5,
5,
8,
},
columns: 3,
headers: []string{
"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 {
scanner := bufio.NewScanner(input)
lines := []string{}
hadFirst := false
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if hadFirst {
// don't match 1st line, it's the header
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,
@@ -53,21 +56,24 @@ func parseCSV(c cfg.Config, input io.Reader, pattern string) (Tabdata, error) {
// so we ignore all lines, which DO match.
continue
}
}
lines = append(lines, line)
hadFirst = true
}
content = strings.NewReader(strings.Join(lines, "\n"))
}
fmt.Println(content)
csvreader := csv.NewReader(content)
csvreader.Comma = rune(c.Separator[0])
records, err := csvreader.ReadAll()
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 {
data.headers = records[0]
data.columns = len(records)
for _, head := range data.headers {
// 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
values := []string{}
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 {
// fmt.Printf("<%s> ", value)
// }

View File

@@ -25,32 +25,47 @@ import (
"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) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 8,
},
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"asd", "igig", "cxxxncnc",
},
{
"19191", "EDD 1", "X",
},
{"asd", "igig", "cxxxncnc"},
{"19191", "EDD 1", "X"},
},
}
table := `ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`
readFd := strings.NewReader(table)
c := cfg.Config{Separator: cfg.DefaultSeparator}
for _, in := range input {
testname := fmt.Sprintf("parse-%s", in.name)
t.Run(testname, func(t *testing.T) {
readFd := strings.NewReader(strings.TrimSpace(in.text))
c := cfg.Config{Separator: in.separator}
gotdata, err := parseFile(c, readFd, "")
if err != nil {
@@ -58,7 +73,10 @@ asd igig cxxxncnc
}
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{
{
"asd", "igig", "cxxxncnc",
},
{"asd", "igig", "cxxxncnc"},
},
pattern: "ig",
invert: false,
},
{
entries: [][]string{
{
"19191", "EDD 1", "X",
},
{"19191", "EDD 1", "X"},
},
pattern: "ig",
invert: true,
},
{
entries: [][]string{
{
"asd", "igig", "cxxxncnc",
},
{"asd", "igig", "cxxxncnc"},
},
pattern: "[a-z",
want: true,
},
}
table := `ONE TWO THREE
asd igig cxxxncnc
19191 EDD 1 X`
for _, in := range input {
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) {
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)
if err != nil {
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 {
if !reflect.DeepEqual(tt.entries, gotdata.entries) {
@@ -122,33 +134,28 @@ asd igig cxxxncnc
}
})
}
}
}
func TestParserIncompleteRows(t *testing.T) {
data := Tabdata{
maxwidthHeader: 5,
maxwidthPerCol: []int{
5, 5, 1,
},
columns: 3,
headers: []string{
"ONE", "TWO", "THREE",
},
entries: [][]string{
{
"asd", "igig", "",
},
{
"19191", "EDD 1", "X",
},
{"asd", "igig", ""},
{"19191", "EDD 1", "X"},
},
}
table := `ONE TWO THREE
table := `
ONE TWO THREE
asd igig
19191 EDD 1 X`
readFd := strings.NewReader(table)
readFd := strings.NewReader(strings.TrimSpace(table))
c := cfg.Config{Separator: cfg.DefaultSeparator}
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
import (
"encoding/csv"
"fmt"
"github.com/gookit/color"
"github.com/olekukonko/tablewriter"
@@ -55,6 +56,8 @@ func printData(w io.Writer, c cfg.Config, data *Tabdata) {
printShellData(w, c, data)
case cfg.Yaml:
printYamlData(w, c, data)
case cfg.CSV:
printCSVData(w, c, data)
default:
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)
}
@@ -225,3 +228,23 @@ func printYamlData(w io.Writer, c cfg.Config, data *Tabdata) {
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 {
return Tabdata{
maxwidthHeader: 8,
maxwidthPerCol: []int{
5,
9,
3,
26,
},
columns: 4,
headers: []string{
"NAME",
@@ -86,6 +80,15 @@ NAME(1) DURATION(2) COUNT(3) WHEN(4)
beta 1d10h5m1s 33 3/1/2014
alpha 4h35m 170 2013-Feb-03
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",
@@ -265,6 +268,8 @@ func TestPrinter(t *testing.T) {
NoColor: true,
}
c.ApplyDefaults()
// the test checks the len!
if len(tt.usecol) > 0 {
c.Columns = "yes"

View File

@@ -133,7 +133,7 @@
.\" ========================================================================
.\"
.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
.\" way too many mistakes in technical documents.
.if n .ad l
@@ -160,6 +160,7 @@ tablizer \- Manipulate tabular output of other programs
\& \-O, \-\-orgtbl Enable org\-mode table output
\& \-S, \-\-shell Enable shell evaluable ouput
\& \-Y, \-\-yaml Enable yaml output
\& \-C, \-\-csv Enable CSV output
\& \-A, \-\-ascii Default output mode, ascii tabular
\&
\& Sort Mode Flags (mutually exclusive):
@@ -353,8 +354,9 @@ You can use this in an eval loop.
.PP
Beside normal ascii mode (the default) and extended mode there are
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
prints yaml encoding.
table and \fBmarkdown\fR which prints a Markdown table, \fByaml\fR, which
prints yaml encoding and \s-1CSV\s0 mode, which prints a comma separated
value file.
.SS "\s-1ENVIRONMENT VARIABLES\s0"
.IX Subsection "ENVIRONMENT VARIABLES"
\&\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
-S, --shell Enable shell evaluable ouput
-Y, --yaml Enable yaml output
-C, --csv Enable CSV output
-A, --ascii Default output mode, ascii tabular
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
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
prints yaml encoding.
table and B<markdown> which prints a Markdown table, B<yaml>, which
prints yaml encoding and CSV mode, which prints a comma separated
value file.
=head2 ENVIRONMENT VARIABLES