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. diff --git a/cmd/head.go b/cmd/head.go new file mode 100644 index 0000000..f66f28c --- /dev/null +++ b/cmd/head.go @@ -0,0 +1,89 @@ +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.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + 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/head_test.go b/cmd/head_test.go new file mode 100644 index 0000000..a7663b6 --- /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) + } +} 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/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") 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=