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