Skip to content

Commit be8a7b0

Browse files
committed
Merge pull request #1 from Clever/birthday
first commit
2 parents 0089969 + 3bafc97 commit be8a7b0

File tree

12 files changed

+379
-3
lines changed

12 files changed

+379
-3
lines changed

.drone.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
image: bradrydzewski/go:1.3
2+
script:
3+
- make test
4+
notify:
5+
email:
6+
recipients:
7+
8+
hipchat:
9+
room: Clever-Dev-CI
10+
token: {{hipchat_token}}
11+
on_started: true
12+
on_success: true
13+
on_failure: true
14+
publish:
15+
github:
16+
branch: master
17+
script:
18+
- make release
19+
artifacts:
20+
- release
21+
tag: v$(cat VERSION)
22+
token: {{github_token}}
23+
user: Clever
24+
repo: csvlint

Makefile

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
SHELL := /bin/bash
2+
PKG = github.com/Clever/csvlint
3+
PKGS = $(PKG)
4+
VERSION := $(shell cat VERSION)
5+
EXECUTABLE := csvlint
6+
BUILDS := \
7+
build/$(EXECUTABLE)-v$(VERSION)-darwin-amd64 \
8+
build/$(EXECUTABLE)-v$(VERSION)-linux-amd64 \
9+
build/$(EXECUTABLE)-v$(VERSION)-windows-amd64
10+
COMPRESSED_BUILDS := $(BUILDS:%=%.tar.gz)
11+
RELEASE_ARTIFACTS := $(COMPRESSED_BUILDS:build/%=release/%)
12+
13+
.PHONY: test golint
14+
15+
golint:
16+
@go get github.com/golang/lint/golint
17+
18+
test: $(PKGS)
19+
20+
$(PKGS): golint
21+
@go get -d -t $@
22+
@gofmt -w=true $(GOPATH)/src/$@*/**.go
23+
ifneq ($(NOLINT),1)
24+
@echo "LINTING..."
25+
@PATH=$(PATH):$(GOPATH)/bin golint $(GOPATH)/src/$@*/**.go
26+
@echo ""
27+
endif
28+
ifeq ($(COVERAGE),1)
29+
@go test -cover -coverprofile=$(GOPATH)/src/$@/c.out $@ -test.v
30+
@go tool cover -html=$(GOPATH)/src/$@/c.out
31+
else
32+
@echo "TESTING..."
33+
@go test $@ -test.v
34+
endif
35+
36+
run:
37+
@go run cmd/csvlint/main.go
38+
39+
build/$(EXECUTABLE)-v$(VERSION)-darwin-amd64:
40+
GOARCH=amd64 GOOS=darwin go build -o "$@/$(EXECUTABLE)" $(PKG)/cmd/csvlint
41+
build/$(EXECUTABLE)-v$(VERSION)-linux-amd64:
42+
GOARCH=amd64 GOOS=linux go build -o "$@/$(EXECUTABLE)" $(PKG)/cmd/csvlint
43+
build/$(EXECUTABLE)-v$(VERSION)-windows-amd64:
44+
GOARCH=amd64 GOOS=windows go build -o "$@/$(EXECUTABLE).exe" $(PKG)/cmd/csvlint
45+
build: $(BUILDS)
46+
%.tar.gz: %
47+
tar -C `dirname $<` -zcvf "$<.tar.gz" `basename $<`
48+
$(RELEASE_ARTIFACTS): release/% : build/%
49+
mkdir -p release
50+
cp $< $@
51+
release: $(RELEASE_ARTIFACTS)
52+
53+
clean:
54+
rm -rf build release

README.md

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,63 @@
1-
csv-checker
2-
===========
1+
# csvlint
32

4-
command line utility that takes in a CSV and reports if it's a valid CSV, reporting any errors and their line numbers if not
3+
`csvlint` is a library and command-line utility for linting CSV files according to [RFC 4180](http://tools.ietf.org/html/rfc4180).
4+
5+
It assumes that your CSV file has an initial header row.
6+
7+
Everything in this README file refers to the command-line utility.
8+
For information about the library, see [godoc](http://godoc.org/github.com/Clever/csvlint).
9+
10+
## Installing
11+
12+
Standalone executables for multiple platforms are available via [Github Releases](https://github.com/Clever/csvlint/releases).
13+
14+
You can also compile from source:
15+
16+
```shell
17+
go get github.com/Clever/csvlint/cmd/csvlint
18+
```
19+
20+
## Usage
21+
22+
`csvlint [options] /path/to/csv/file`
23+
24+
### Options
25+
26+
* delimiter: the field delimiter to default with
27+
* default: comma
28+
* valid options: comma, tab
29+
* if you want anything else, you're probably doing CSVs wrong
30+
* lazyquotes: allow a quote to appear in an unquoted field and a non-doubled quote to appear in a quoted field. _WARNING: your file may pass linting, but not parse in the way you would expect_
31+
32+
### Examples
33+
34+
```shell
35+
$ csvlint bad_quote.csv
36+
Record #1 has error: bare " in non-quoted-field
37+
38+
unable to parse any further
39+
40+
$ csvlint --lazyquotes bad_quote.csv
41+
file is valid
42+
43+
$ csvlint mult_long_columns.csv
44+
Record #2 has error: wrong number of fields in line
45+
Record #4 has error: wrong number of fields in line
46+
47+
$ csvlint --delimiter=tab mult_long_columns_tabs.csv
48+
Record #2 has error: wrong number of fields in line
49+
Record #4 has error: wrong number of fields in line
50+
51+
$ csvlint one_long_column.csv
52+
Record #2 has error: wrong number of fields in line
53+
54+
$ csvlint perfect.csv
55+
file is valid
56+
```
57+
58+
### Exit codes
59+
60+
`csvlint` uses three different exit codes to mean different things:
61+
* 0 - the file is valid
62+
* 1 - couldn't parse the entire file
63+
* 2 - could parse the file, but there were lint failures

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

cmd/csvlint/main.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"github.com/Clever/csvlint"
7+
"os"
8+
)
9+
10+
func printHelpAndExit(code int) {
11+
flag.PrintDefaults()
12+
os.Exit(code)
13+
}
14+
15+
func main() {
16+
delimiter := flag.String("delimiter", "comma", "field delimiter in the file. options: comma, tab")
17+
lazyquotes := flag.Bool("lazyquotes", false, "try to parse improperly escaped quotes")
18+
help := flag.Bool("help", false, "print help and exit")
19+
flag.Parse()
20+
21+
if *help {
22+
printHelpAndExit(0)
23+
}
24+
25+
var comma rune
26+
switch *delimiter {
27+
case "comma":
28+
comma = ','
29+
case "tab":
30+
comma = '\t'
31+
default:
32+
fmt.Printf("unrecognized delimiter '%s'\n\n", *delimiter)
33+
printHelpAndExit(1)
34+
}
35+
36+
if len(flag.Args()) != 1 {
37+
fmt.Println("csvlint accepts a single filepath as an argument\n")
38+
printHelpAndExit(1)
39+
}
40+
41+
f, err := os.Open(flag.Args()[0])
42+
if err != nil {
43+
if os.IsNotExist(err) {
44+
fmt.Printf("file '%s' does not exist\n", flag.Args()[0])
45+
os.Exit(1)
46+
} else {
47+
panic(err)
48+
}
49+
}
50+
defer f.Close()
51+
52+
invalids, halted, err := csvlint.Validate(f, comma, *lazyquotes)
53+
if err != nil {
54+
panic(err)
55+
}
56+
if len(invalids) == 0 {
57+
fmt.Println("file is valid")
58+
os.Exit(0)
59+
}
60+
for _, invalid := range invalids {
61+
fmt.Println(invalid.Error())
62+
}
63+
if halted {
64+
fmt.Println("\nunable to parse any further")
65+
os.Exit(1)
66+
}
67+
os.Exit(2)
68+
}

linter.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package csvlint
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"io"
7+
)
8+
9+
// CSVError returns information about an invalid record in a CSV file
10+
type CSVError struct {
11+
// Record is the invalid record. This will be nil when we were unable to parse a record.
12+
Record []string
13+
// Num is the record number of this record.
14+
Num int
15+
err error
16+
}
17+
18+
// Error implements the error interface
19+
func (e CSVError) Error() string {
20+
return fmt.Sprintf("Record #%d has error: %s", e.Num, e.err.Error())
21+
}
22+
23+
// Validate tests whether or not a CSV lints according to RFC 4180.
24+
// The lazyquotes option will attempt to parse lines that aren't quoted properly.
25+
func Validate(reader io.Reader, delimiter rune, lazyquotes bool) ([]CSVError, bool, error) {
26+
r := csv.NewReader(reader)
27+
r.TrailingComma = true
28+
r.FieldsPerRecord = -1
29+
r.LazyQuotes = lazyquotes
30+
r.Comma = delimiter
31+
32+
var header []string
33+
errors := []CSVError{}
34+
records := 0
35+
for {
36+
record, err := r.Read()
37+
if header != nil {
38+
records++
39+
}
40+
if err != nil {
41+
if err == io.EOF {
42+
break
43+
}
44+
parsedErr, ok := err.(*csv.ParseError)
45+
if !ok {
46+
return errors, true, err
47+
}
48+
errors = append(errors, CSVError{
49+
Record: nil,
50+
Num: records,
51+
err: parsedErr.Err,
52+
})
53+
return errors, true, nil
54+
}
55+
if header == nil {
56+
header = record
57+
continue
58+
} else if len(record) != len(header) {
59+
errors = append(errors, CSVError{
60+
Record: record,
61+
Num: records,
62+
err: csv.ErrFieldCount,
63+
})
64+
}
65+
}
66+
return errors, false, nil
67+
}

linter_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package csvlint
2+
3+
import (
4+
"encoding/csv"
5+
"github.com/stretchr/testify/assert"
6+
"os"
7+
"testing"
8+
)
9+
10+
var validationTable = []struct {
11+
file string
12+
err error
13+
invalids []CSVError
14+
comma rune
15+
halted bool
16+
}{
17+
{file: "./test_data/perfect.csv", err: nil, invalids: []CSVError{}},
18+
{file: "./test_data/one_long_column.csv", err: nil, invalids: []CSVError{{
19+
Record: []string{"d", "e", "f", "g"},
20+
err: csv.ErrFieldCount,
21+
Num: 2,
22+
}}},
23+
{file: "./test_data/mult_long_columns.csv", err: nil, invalids: []CSVError{
24+
{
25+
Record: []string{"d", "e", "f", "g"},
26+
err: csv.ErrFieldCount,
27+
Num: 2,
28+
}, {
29+
Record: []string{"k", "l", "m", "n"},
30+
err: csv.ErrFieldCount,
31+
Num: 4,
32+
}},
33+
},
34+
{file: "./test_data/mult_long_columns_tabs.csv", err: nil, comma: '\t', invalids: []CSVError{
35+
{
36+
Record: []string{"d", "e", "f", "g"},
37+
err: csv.ErrFieldCount,
38+
Num: 2,
39+
}, {
40+
Record: []string{"k", "l", "m", "n"},
41+
err: csv.ErrFieldCount,
42+
Num: 4,
43+
}},
44+
},
45+
}
46+
47+
func TestTable(t *testing.T) {
48+
for _, test := range validationTable {
49+
f, err := os.Open(test.file)
50+
assert.Nil(t, err)
51+
defer f.Close()
52+
comma := test.comma
53+
if test.comma == 0 {
54+
comma = ','
55+
}
56+
invalids, halted, err := Validate(f, comma, false)
57+
assert.Equal(t, test.err, err)
58+
assert.Equal(t, halted, test.halted)
59+
assert.Equal(t, test.invalids, invalids)
60+
}
61+
}
62+
63+
var errTable = []struct {
64+
err error
65+
message string
66+
}{
67+
{
68+
err: CSVError{Record: []string{"a", "b", "c"}, Num: 3, err: csv.ErrFieldCount},
69+
message: "Record #3 has error: wrong number of fields in line",
70+
},
71+
{
72+
err: CSVError{Record: []string{"d", "e", "f"}, Num: 1, err: csv.ErrBareQuote},
73+
message: `Record #1 has error: bare " in non-quoted-field`,
74+
},
75+
}
76+
77+
func TestErrors(t *testing.T) {
78+
for _, test := range errTable {
79+
assert.Equal(t, test.err.Error(), test.message)
80+
}
81+
}

test_data/bad_quote.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
field1,field2,field3
2+
john "the rock" smith,a,b
3+
c,d,e

test_data/mult_long_columns.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
field1,field2,field3
2+
a,b,c
3+
d,e,f,g
4+
h,i,j
5+
k,l,m,n
6+
o,p,q
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
field1 field2 field3
2+
a b c
3+
d e f g
4+
h i j
5+
k l m n
6+
o p q

0 commit comments

Comments
 (0)