diff --git a/cmd/gitql/query.go b/cmd/gitql/query.go index 19914a1e2..7e78ffd76 100644 --- a/cmd/gitql/query.go +++ b/cmd/gitql/query.go @@ -9,17 +9,18 @@ import ( "github.com/gitql/gitql" gitqlgit "github.com/gitql/gitql/git" + "github.com/gitql/gitql/internal/format" "github.com/gitql/gitql/sql" - "github.com/olekukonko/tablewriter" "gopkg.in/src-d/go-git.v4" ) type CmdQuery struct { cmd - Path string `short:"p" long:"path" description:"Path where the git repository is located"` - Args struct { + Path string `short:"p" long:"path" description:"Path where the git repository is located"` + Format string `short:"f" long:"format" default:"pretty" description:"Ouptut format. Formats supported: pretty, csv, json."` + Args struct { SQL string `positional-arg-name:"sql" required:"true" description:"SQL query to execute"` } `positional-args:"yes"` @@ -86,34 +87,39 @@ func (c *CmdQuery) executeQuery() error { return err } - c.printQuery(schema, iter) - - return nil + return c.printQuery(schema, iter) } -func (c *CmdQuery) printQuery(schema sql.Schema, iter sql.RowIter) { - w := tablewriter.NewWriter(os.Stdout) +func (c *CmdQuery) printQuery(schema sql.Schema, iter sql.RowIter) error { + f, err := format.NewFormat(c.Format, os.Stdout) + if err != nil { + return err + } + headers := []string{} for _, f := range schema { headers = append(headers, f.Name) } - w.SetHeader(headers) + + if err := f.WriteHeader(headers); err != nil { + return err + } + for { row, err := iter.Next() if err == io.EOF { break } if err != nil { - fmt.Printf("Error: %v\n", err) - return + return err } - rowStrings := []string{} - for _, v := range row.Fields() { - rowStrings = append(rowStrings, fmt.Sprintf("%v", v)) + + if err := f.Write(row.Fields()); err != nil { + return err } - w.Append(rowStrings) } - w.Render() + + return f.Close() } func findDotGitFolder(path string) (string, error) { diff --git a/internal/format/common.go b/internal/format/common.go new file mode 100644 index 000000000..36692f0ab --- /dev/null +++ b/internal/format/common.go @@ -0,0 +1,25 @@ +package format + +import ( + "fmt" + "io" +) + +type Format interface { + WriteHeader(headers []string) error + Write(line []interface{}) error + Close() error +} + +func NewFormat(id string, w io.Writer) (Format, error) { + switch id { + case "pretty": + return NewPrettyFormat(w), nil + case "csv": + return NewCsvFormat(w), nil + case "json": + return NewJsonFormat(w), nil + default: + return nil, fmt.Errorf("format not supported: %v", id) + } +} diff --git a/internal/format/common_test.go b/internal/format/common_test.go new file mode 100644 index 000000000..9dc4f48c9 --- /dev/null +++ b/internal/format/common_test.go @@ -0,0 +1,49 @@ +package format + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewFormat_InvalidId(t *testing.T) { + assert := assert.New(t) + + f, err := NewFormat("INVALID", bytes.NewBuffer(nil)) + assert.Nil(f) + assert.NotNil(err) +} + +func testNewFormat(id string, t *testing.T) { + assert := assert.New(t) + + w := bytes.NewBuffer(nil) + f, err := NewFormat(id, w) + assert.Nil(err) + assert.NotNil(f) +} + +func testFormat(fs *formatSpec, writer *bytes.Buffer, t *testing.T) { + assert := assert.New(t) + + err := fs.Format.WriteHeader(fs.Headers) + assert.Nil(err) + for _, l := range fs.Lines { + err := fs.Format.Write(l) + assert.Nil(err) + } + err = fs.Format.Close() + assert.Nil(err) + + assert.Equal(fs.Result, writer.String()) + + writer.Reset() +} + +type formatSpec struct { + Headers []string + Lines [][]interface{} + Format Format + Result string +} diff --git a/internal/format/csv.go b/internal/format/csv.go new file mode 100644 index 000000000..55fe43df5 --- /dev/null +++ b/internal/format/csv.go @@ -0,0 +1,36 @@ +package format + +import ( + "encoding/csv" + "fmt" + "io" +) + +type CsvFormat struct { + cw *csv.Writer +} + +func NewCsvFormat(w io.Writer) *CsvFormat { + return &CsvFormat{ + cw: csv.NewWriter(w), + } +} + +func (cf *CsvFormat) WriteHeader(headers []string) error { + return cf.cw.Write(headers) +} + +func (cf *CsvFormat) Write(line []interface{}) error { + rowStrings := []string{} + for _, v := range line { + rowStrings = append(rowStrings, fmt.Sprintf("%v", v)) + } + + return cf.cw.Write(rowStrings) +} + +func (cf *CsvFormat) Close() error { + cf.cw.Flush() + + return nil +} diff --git a/internal/format/csv_test.go b/internal/format/csv_test.go new file mode 100644 index 000000000..20b5c65b5 --- /dev/null +++ b/internal/format/csv_test.go @@ -0,0 +1,24 @@ +package format + +import ( + "bytes" + "testing" +) + +func TestNewCsvFormat(t *testing.T) { + w := bytes.NewBuffer(nil) + testFormat(&formatSpec{ + Format: NewCsvFormat(w), + Result: "a,b,c\na1,b1,c1\n", + Headers: []string{"a", "b", "c"}, + Lines: [][]interface{}{ + []interface{}{ + "a1", "b1", "c1", + }, + }, + }, w, t) +} + +func TestNewFormat_Csv(t *testing.T) { + testNewFormat("csv", t) +} diff --git a/internal/format/json.go b/internal/format/json.go new file mode 100644 index 000000000..702705eec --- /dev/null +++ b/internal/format/json.go @@ -0,0 +1,36 @@ +package format + +import ( + "encoding/json" + "io" +) + +type JsonFormat struct { + je *json.Encoder + keys []string +} + +func NewJsonFormat(w io.Writer) *JsonFormat { + return &JsonFormat{ + je: json.NewEncoder(w), + } +} + +func (cf *JsonFormat) WriteHeader(headers []string) error { + cf.keys = headers + + return nil +} + +func (cf *JsonFormat) Write(line []interface{}) error { + j := make(map[string]interface{}) + for i, k := range cf.keys { + j[k] = line[i] + } + + return cf.je.Encode(j) +} + +func (cf *JsonFormat) Close() error { + return nil +} diff --git a/internal/format/json_test.go b/internal/format/json_test.go new file mode 100644 index 000000000..70689a8af --- /dev/null +++ b/internal/format/json_test.go @@ -0,0 +1,25 @@ +package format + +import ( + "bytes" + "testing" +) + +func TestNewJsonFormat(t *testing.T) { + w := bytes.NewBuffer(nil) + testFormat(&formatSpec{ + Format: NewJsonFormat(w), + Result: "{\"a\":\"a1\",\"b\":\"b1\",\"c\":\"c1\"}\n", + Headers: []string{"a", "b", "c"}, + Lines: [][]interface{}{ + []interface{}{ + "a1", "b1", "c1", + }, + }, + }, w, t) +} + + +func TestNewFormat_Json(t *testing.T) { + testNewFormat("json", t) +} \ No newline at end of file diff --git a/internal/format/pretty.go b/internal/format/pretty.go new file mode 100644 index 000000000..bc828cefb --- /dev/null +++ b/internal/format/pretty.go @@ -0,0 +1,40 @@ +package format + +import ( + "fmt" + "io" + + "github.com/olekukonko/tablewriter" +) + +type PrettyFormat struct { + tw *tablewriter.Table +} + +func NewPrettyFormat(w io.Writer) *PrettyFormat { + return &PrettyFormat{ + tw: tablewriter.NewWriter(w), + } +} + +func (pf *PrettyFormat) WriteHeader(headers []string) error { + pf.tw.SetHeader(headers) + + return nil +} + +func (pf *PrettyFormat) Write(line []interface{}) error { + rowStrings := []string{} + for _, v := range line { + rowStrings = append(rowStrings, fmt.Sprintf("%v", v)) + } + pf.tw.Append(rowStrings) + + return nil +} + +func (pf *PrettyFormat) Close() error { + pf.tw.Render() + + return nil +} diff --git a/internal/format/pretty_test.go b/internal/format/pretty_test.go new file mode 100644 index 000000000..7650d69ec --- /dev/null +++ b/internal/format/pretty_test.go @@ -0,0 +1,26 @@ +package format + +import ( + "bytes" + "testing" +) + +func TestNewPrettyFormat(t *testing.T) { + w := bytes.NewBuffer(nil) + testFormat(&formatSpec{ + Format: NewPrettyFormat(w), + Result: "+----+----+----+\n| A | B | C |" + + "\n+----+----+----+\n| a1 | b1 | c1 |" + + "\n+----+----+----+\n", + Headers: []string{"a", "b", "c"}, + Lines: [][]interface{}{ + []interface{}{ + "a1", "b1", "c1", + }, + }, + }, w, t) +} + +func TestNewFormat_Pretty(t *testing.T) { + testNewFormat("pretty", t) +}