diff --git a/cfg/config.go b/cfg/config.go index a569016..d14808d 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -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 + } +} diff --git a/cmd/root.go b/cmd/root.go index d152ea3..bcf7c51 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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") diff --git a/cmd/tablizer.go b/cmd/tablizer.go index 0eaf850..4d3ae27 100644 --- a/cmd/tablizer.go +++ b/cmd/tablizer.go @@ -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): diff --git a/lib/common.go b/lib/common.go index a72d138..5e6e920 100644 --- a/lib/common.go +++ b/lib/common.go @@ -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 diff --git a/lib/helpers_test.go b/lib/helpers_test.go index 2308239..430514d 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -48,12 +48,7 @@ func TestContains(t *testing.T) { func TestPrepareColumns(t *testing.T) { data := Tabdata{ maxwidthHeader: 5, - maxwidthPerCol: []int{ - 5, - 5, - 8, - }, - columns: 3, + columns: 3, headers: []string{ "ONE", "TWO", "THREE", }, diff --git a/lib/parser.go b/lib/parser.go index f44c37f..cda33a7 100644 --- a/lib/parser.go +++ b/lib/parser.go @@ -44,30 +44,36 @@ 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 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, - // so we ignore all lines, which DO match. - continue + 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, + // if the user specified -v, the matching is inverted, + // 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) // } diff --git a/lib/parser_test.go b/lib/parser_test.go index 154f5a0..2243ecf 100644 --- a/lib/parser_test.go +++ b/lib/parser_test.go @@ -25,40 +25,58 @@ 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, + 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` + 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, "") - readFd := strings.NewReader(table) - 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 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", c.Separator, data, gotdata) + if !reflect.DeepEqual(data, gotdata) { + t.Errorf("Parser returned invalid data\nExp: %+v\nGot: %+v\n", + data, gotdata) + } + }) } } @@ -71,84 +89,73 @@ 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-%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: in.separator} - for _, tt := range tests { - testname := fmt.Sprintf("parse-with-pattern-%s-inverted-%t", tt.pattern, tt.invert) - t.Run(testname, func(t *testing.T) { - c := cfg.Config{InvertMatch: tt.invert, Pattern: tt.pattern, Separator: cfg.DefaultSeparator} + readFd := strings.NewReader(strings.TrimSpace(in.text)) + gotdata, err := parseFile(c, readFd, tt.pattern) - readFd := strings.NewReader(table) - 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) + if err != nil { + if !tt.want { + t.Errorf("Parser returned error: %s\nData processed so far: %+v", + err, gotdata) + } + } else { + if !reflect.DeepEqual(tt.entries, gotdata.entries) { + t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n", + tt.pattern, tt.invert, tt.entries, gotdata.entries) + } } - } else { - if !reflect.DeepEqual(tt.entries, gotdata.entries) { - t.Errorf("Parser returned invalid data (pattern: %s, invert: %t)\nExp: %+v\nGot: %+v\n", - tt.pattern, tt.invert, tt.entries, gotdata.entries) - } - } - }) + }) + } } } func TestParserIncompleteRows(t *testing.T) { data := Tabdata{ maxwidthHeader: 5, - maxwidthPerCol: []int{ - 5, 5, 1, - }, - columns: 3, + 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, "") diff --git a/lib/printer.go b/lib/printer.go index 20c6587..06a619a 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -18,6 +18,7 @@ along with this program. If not, see . 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) + } +} diff --git a/lib/printer_test.go b/lib/printer_test.go index 5f0efd0..6419e25 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -29,13 +29,7 @@ import ( func newData() Tabdata { return Tabdata{ maxwidthHeader: 8, - maxwidthPerCol: []int{ - 5, - 9, - 3, - 26, - }, - columns: 4, + columns: 4, headers: []string{ "NAME", "DURATION", @@ -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" diff --git a/tablizer.1 b/tablizer.1 index 4ced80c..b8d3b91 100644 --- a/tablizer.1 +++ b/tablizer.1 @@ -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 diff --git a/tablizer.pod b/tablizer.pod index ee73053..312dd1d 100644 --- a/tablizer.pod +++ b/tablizer.pod @@ -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 which prints an Emacs org-mode -table and B which prints a Markdown table and B, which -prints yaml encoding. +table and B which prints a Markdown table, B, which +prints yaml encoding and CSV mode, which prints a comma separated +value file. =head2 ENVIRONMENT VARIABLES