From 54f6683761f7152abd0f8189743646d0d1f0bcd4 Mon Sep 17 00:00:00 2001 From: onozaty Date: Sat, 4 Dec 2021 18:40:01 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[head]=20head=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E5=AE=9F=E8=A3=85=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/head.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + go.mod | 1 + go.sum | 4 +++ 4 files changed, 93 insertions(+) create mode 100644 cmd/head.go diff --git a/cmd/head.go b/cmd/head.go new file mode 100644 index 0000000..ea168ad --- /dev/null +++ b/cmd/head.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/olekukonko/tablewriter" + "github.com/onozaty/csvt/csv" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func newHeadCmd() *cobra.Command { + + headCmd := &cobra.Command{ + Use: "head", + Short: "Show head few rows", + RunE: func(cmd *cobra.Command, args []string) error { + + format, err := getFlagBaseCsvFormat(cmd.Flags()) + if err != nil { + return err + } + + inputPath, _ := cmd.Flags().GetString("input") + number, _ := cmd.Flags().GetInt("number") + + // 表示件数は1以上 + if number <= 0 { + return fmt.Errorf("number must be greater than or equal to 1") + } + + // 引数の解析に成功した時点で、エラーが起きてもUsageは表示しない + cmd.SilenceUsage = true + + return runHead( + format, + inputPath, + number, + cmd.OutOrStdout()) + }, + } + + headCmd.Flags().StringP("input", "i", "", "Input CSV file path.") + headCmd.MarkFlagRequired("input") + headCmd.Flags().IntP("number", "n", 10, "The number of records to show. If not specified, it will be the first 10 rows.") + + return headCmd +} + +func runHead(format csv.Format, inputPath string, number int, writer io.Writer) error { + + reader, close, err := setupInput(inputPath, format) + if err != nil { + return err + } + defer close() + + return head(reader, number, writer) +} + +func head(reader csv.CsvReader, number int, writer io.Writer) error { + + columnNames, err := reader.Read() + if err != nil { + return errors.Wrap(err, "failed to read the input CSV file") + } + + table := tablewriter.NewWriter(writer) + table.SetAutoFormatHeaders(false) + table.SetHeader(columnNames) + + for i := 0; i < number; i++ { + row, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return errors.Wrap(err, "failed to read the input CSV file") + } + + table.Append(row) + } + + table.Render() + return nil +} diff --git a/cmd/root.go b/cmd/root.go index d80228d..62443a9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,6 +43,7 @@ func newRootCmd() *cobra.Command { rootCmd.AddCommand(newAddCmd()) rootCmd.AddCommand(newSortCmd()) rootCmd.AddCommand(newSplitCmd()) + rootCmd.AddCommand(newHeadCmd()) for _, c := range rootCmd.Commands() { // フラグ以外は受け付けないように diff --git a/go.mod b/go.mod index ddcff57..9ac88e9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/boltdb/bolt v1.3.1 + github.com/olekukonko/tablewriter v0.0.5 github.com/onozaty/go-customcsv v1.0.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index 43e96d0..2dd11bb 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -121,6 +123,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onozaty/go-customcsv v1.0.0 h1:UTqXPtFAS1BZdPYwap1ypcE65cREhg3UKWqkZ9Wv6DA= github.com/onozaty/go-customcsv v1.0.0/go.mod h1:c5W8hV70qtNo6oK6mTjRw7GvvmenKP3SiYEh9uNL+4E= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= From a3b08cfddb86ef5347fb7f131cf251f845739646 Mon Sep 17 00:00:00 2001 From: onozaty Date: Sat, 4 Dec 2021 23:03:31 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[head]=20head=E3=82=B3=E3=83=9E=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/head.go | 1 + cmd/head_test.go | 212 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 cmd/head_test.go diff --git a/cmd/head.go b/cmd/head.go index ea168ad..7d3d9cf 100644 --- a/cmd/head.go +++ b/cmd/head.go @@ -68,6 +68,7 @@ func head(reader csv.CsvReader, number int, writer io.Writer) error { table := tablewriter.NewWriter(writer) table.SetAutoFormatHeaders(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetHeader(columnNames) for i := 0; i < number; i++ { diff --git a/cmd/head_test.go b/cmd/head_test.go new file mode 100644 index 0000000..4fb30b9 --- /dev/null +++ b/cmd/head_test.go @@ -0,0 +1,212 @@ +package cmd + +import ( + "bytes" + "os" + "testing" +) + +func TestHeadCmd(t *testing.T) { + + s := `ID,Name,Company ID +1,Yamada,1 +2,Ichikawa, +3,"Hanako, Sato",3 +4,Otani,3 +5,,2 +6,Suzuki,1 +7,Jane,2 +8,Michel,3 +9,1,3 +10,Ken,3 +11,Suzuki,2 +12,Z, +` + f := createTempFile(t, s) + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + }) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + + err := rootCmd.Execute() + if err != nil { + t.Fatal("failed test\n", err) + } + + result := buf.String() + + except := `+----+--------------+------------+ +| ID | Name | Company ID | ++----+--------------+------------+ +| 1 | Yamada | 1 | +| 2 | Ichikawa | | +| 3 | Hanako, Sato | 3 | +| 4 | Otani | 3 | +| 5 | | 2 | +| 6 | Suzuki | 1 | +| 7 | Jane | 2 | +| 8 | Michel | 3 | +| 9 | 1 | 3 | +| 10 | Ken | 3 | ++----+--------------+------------+ +` + if result != except { + t.Fatal("failed test\n", result) + } +} + +func TestHeadCmd_number(t *testing.T) { + + s := `ID,Name,Company ID +1,Yamada,1 +2,Ichikawa, +3,"Hanako, Sato",3 +4,Otani,3 +5,,2 +` + f := createTempFile(t, s) + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + "-n", "3", + }) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + + err := rootCmd.Execute() + if err != nil { + t.Fatal("failed test\n", err) + } + + result := buf.String() + + except := `+----+--------------+------------+ +| ID | Name | Company ID | ++----+--------------+------------+ +| 1 | Yamada | 1 | +| 2 | Ichikawa | | +| 3 | Hanako, Sato | 3 | ++----+--------------+------------+ +` + if result != except { + t.Fatal("failed test\n", result) + } +} + +func TestHeadCmd_less(t *testing.T) { + + s := `ID,Name,Company ID +1,Yamada,1 +2,Ichikawa, +` + f := createTempFile(t, s) + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + }) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + + err := rootCmd.Execute() + if err != nil { + t.Fatal("failed test\n", err) + } + + result := buf.String() + + except := `+----+----------+------------+ +| ID | Name | Company ID | ++----+----------+------------+ +| 1 | Yamada | 1 | +| 2 | Ichikawa | | ++----+----------+------------+ +` + if result != except { + t.Fatal("failed test\n", result) + } +} + +func TestHeadCmd_format(t *testing.T) { + + s := "col1,col2;1,a;2,b;" + + f := createTempFile(t, s) + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + "--sep", ";", + }) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + + err := rootCmd.Execute() + if err != nil { + t.Fatal("failed test\n", err) + } + + result := buf.String() + + except := `+------+------+ +| col1 | col2 | ++------+------+ +| 1 | a | +| 2 | b | ++------+------+ +` + if result != except { + t.Fatal("failed test\n", result) + } +} + +func TestHeadCmd_invalidNumber(t *testing.T) { + + f := createTempFile(t, "") + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + "-n", "0", + }) + + err := rootCmd.Execute() + if err == nil || err.Error() != "number must be greater than or equal to 1" { + t.Fatal("failed test\n", err) + } +} + +func TestHeadCmd_empty(t *testing.T) { + + f := createTempFile(t, "") + defer os.Remove(f) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "head", + "-i", f, + }) + + err := rootCmd.Execute() + if err == nil || err.Error() != "failed to read the input CSV file: EOF" { + t.Fatal("failed test\n", err) + } +} From da0fa47a7632912d726fc158eccb869d8bc8a887 Mon Sep 17 00:00:00 2001 From: onozaty Date: Sun, 5 Dec 2021 22:26:58 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[slice]=20=E3=83=95=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E3=81=AE=E8=AA=AC=E6=98=8E=E3=81=A7=E3=82=BF=E3=83=96=E3=81=8C?= =?UTF-8?q?=E5=85=A5=E3=81=A3=E3=81=A6=E3=81=97=E3=81=BE=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=84=E3=81=9F=E7=AE=87=E6=89=80=E3=82=92=E3=82=B9=E3=83=9A?= =?UTF-8?q?=E3=83=BC=E3=82=B9=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/slice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/slice.go b/cmd/slice.go index 5d486cb..23c4429 100644 --- a/cmd/slice.go +++ b/cmd/slice.go @@ -51,7 +51,7 @@ func newSliceCmd() *cobra.Command { sliceCmd.Flags().StringP("input", "i", "", "Input CSV file path.") sliceCmd.MarkFlagRequired("input") - sliceCmd.Flags().IntP("start", "s", 1, "The number of the starting row. If not specified, it will be the first row.") + sliceCmd.Flags().IntP("start", "s", 1, "The number of the starting row. If not specified, it will be the first row.") sliceCmd.Flags().IntP("end", "e", math.MaxInt32, "The number of the end row. If not specified, it will be the last row.") sliceCmd.Flags().StringP("output", "o", "", "Output CSV file path.") sliceCmd.MarkFlagRequired("output") From 8e391158d3e457036a7ee902136ca9a2ff1105a1 Mon Sep 17 00:00:00 2001 From: onozaty Date: Sun, 5 Dec 2021 22:28:41 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[head]=20=E3=83=98=E3=83=83=E3=83=80?= =?UTF-8?q?=E3=82=82=E5=8F=B3=E5=AF=84=E3=81=9B=E3=81=AB=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/head.go | 3 ++- cmd/head_test.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/head.go b/cmd/head.go index 7d3d9cf..f66f28c 100644 --- a/cmd/head.go +++ b/cmd/head.go @@ -43,7 +43,7 @@ func newHeadCmd() *cobra.Command { headCmd.Flags().StringP("input", "i", "", "Input CSV file path.") headCmd.MarkFlagRequired("input") - headCmd.Flags().IntP("number", "n", 10, "The number of records to show. If not specified, it will be the first 10 rows.") + headCmd.Flags().IntP("number", "n", 10, "The number of records to show. If not specified, it will be the first 10 rows.") return headCmd } @@ -68,6 +68,7 @@ func head(reader csv.CsvReader, number int, writer io.Writer) error { table := tablewriter.NewWriter(writer) table.SetAutoFormatHeaders(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetHeader(columnNames) diff --git a/cmd/head_test.go b/cmd/head_test.go index 4fb30b9..a7663b6 100644 --- a/cmd/head_test.go +++ b/cmd/head_test.go @@ -42,7 +42,7 @@ func TestHeadCmd(t *testing.T) { result := buf.String() except := `+----+--------------+------------+ -| ID | Name | Company ID | +| ID | Name | Company ID | +----+--------------+------------+ | 1 | Yamada | 1 | | 2 | Ichikawa | | @@ -91,7 +91,7 @@ func TestHeadCmd_number(t *testing.T) { result := buf.String() except := `+----+--------------+------------+ -| ID | Name | Company ID | +| ID | Name | Company ID | +----+--------------+------------+ | 1 | Yamada | 1 | | 2 | Ichikawa | | @@ -129,7 +129,7 @@ func TestHeadCmd_less(t *testing.T) { result := buf.String() except := `+----+----------+------------+ -| ID | Name | Company ID | +| ID | Name | Company ID | +----+----------+------------+ | 1 | Yamada | 1 | | 2 | Ichikawa | | From aa9fe0a729a98ef7cac9a9c6362d41a406aa485a Mon Sep 17 00:00:00 2001 From: onozaty Date: Sun, 5 Dec 2021 22:29:06 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[head]=20README=E3=81=AB=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/README.md b/README.md index cfa48dc..ab66fa2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ * [count](#count) Count the number of records. * [exclude](#exclude) Exclude rows by included in another CSV file. * [filter](#filter) Filter rows by condition. +* [head](#head) Show head few rows. * [header](#header) Show header. * [include](#include) Filter rows by included in another CSV file. * [join](#join) Join CSV files. @@ -454,6 +455,76 @@ Please refer to the following for the syntax of regular expressions. * https://pkg.go.dev/regexp/syntax +## head + +Show the first few rows. +It will be shown in table format. + +### Usage + +``` +csvt head -i INPUT [-n NUMBER] +``` + +``` +Usage: + csvt head [flags] + +Flags: + -i, --input string Input CSV file path. + -n, --number int The number of records to show. If not specified, it will be the first 10 rows. (default 10) + -h, --help help for head +``` + +### Example + +The contents of `input.csv`. + +``` +UserID,Name,Age +1,"Taro, Yamada",10 +2,Hanako,21 +3,Smith,30 +4,Jun,22 +5,Kevin,10 +6,Bob, +7,Jackson,51 +8,Harry,22 +9,Olivia,32 +10,Aiko,35 +11,Kaede,9 +12,Sakura,12 +13,Momoka,16 +``` + +``` +$ csvt head -i input.csv ++--------+--------------+-----+ +| UserID | Name | Age | ++--------+--------------+-----+ +| 1 | Taro, Yamada | 10 | +| 2 | Hanako | 21 | +| 3 | Smith | 30 | +| 4 | Jun | 22 | +| 5 | Kevin | 10 | +| 6 | Bob | | +| 7 | Jackson | 51 | +| 8 | Harry | 22 | +| 9 | Olivia | 32 | +| 10 | Aiko | 35 | ++--------+--------------+-----+ +``` + +``` +$ csvt head -i input.csv -n 2 ++--------+--------------+-----+ +| UserID | Name | Age | ++--------+--------------+-----+ +| 1 | Taro, Yamada | 10 | +| 2 | Hanako | 21 | ++--------+--------------+-----+ +``` + ## header Show the header of CSV file.