├── .gitignore ├── LICENSE ├── README.md ├── _example └── example.csv ├── csviewer.go ├── go.mod ├── go.sum ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ryosuke Yabuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csviewer 2 | 3 | csviewer is command line csv viewer. 4 | 5 | 6 | # Install 7 | 8 | ``` 9 | go get github.com/Konboi/csviewer 10 | ``` 11 | ### From File 12 | 13 | using `p` or `path` option 14 | 15 | ``` 16 | csviewer -p _example/example.csv 17 | ``` 18 | 19 | ### From stdin 20 | 21 | ``` 22 | cat _example/example.csv | csviewer 23 | ``` 24 | ## Display Option 25 | 26 | ### Default 27 | 28 | ``` 29 | $ csviewer -p _example/example.csv 30 | +----+------+----------------+-----------+--------+ 31 | | ID | NAME | MAIL | PHONE | ADRESS | 32 | +----+------+----------------+-----------+--------+ 33 | | 1 | a | aaaa@hoge.fuga | 123456 | 111111 | 34 | | 2 | b | bbb@hoge.fuga | 12345 | | 35 | | 3 | c | | | 22222 | 36 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 37 | +----+------+----------------+-----------+--------+ 38 | ``` 39 | 40 | ### Columns option 41 | 42 | set display columns. 43 | 44 | ``` 45 | $ csviewer -p _example/example.csv -c id,mail,name 46 | +----+----------------+------+ 47 | | ID | MAIL | NAME | 48 | +----+----------------+------+ 49 | | 1 | aaaa@hoge.fuga | a | 50 | | 2 | bbb@hoge.fuga | b | 51 | | 3 | | c | 52 | | 5 | ddd@fuga.hgoe | d | 53 | +----+----------------+------+ 54 | ``` 55 | 56 | ### Limit Option 57 | 58 | set display rows num. 59 | 60 | ``` 61 | $ csviewer -p _example/example.csv -l 2 62 | +----+------+----------------+--------+--------+ 63 | | ID | NAME | MAIL | PHONE | ADRESS | 64 | +----+------+----------------+--------+--------+ 65 | | 1 | a | aaaa@hoge.fuga | 123456 | 111111 | 66 | | 2 | b | bbb@hoge.fuga | 12345 | | 67 | +----+------+----------------+--------+--------+ 68 | ``` 69 | 70 | ### Filter Option 71 | 72 | set display condition. 73 | 74 | ``` 75 | $ ./csviewer -p _example/example.csv -f "id > 2" 76 | +----+------+---------------+-----------+--------+ 77 | | ID | NAME | MAIL | PHONE | ADRESS | 78 | +----+------+---------------+-----------+--------+ 79 | | 3 | c | | | 22222 | 80 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 81 | +----+------+---------------+-----------+--------+ 82 | ``` 83 | 84 | ``` 85 | $ csviewer -p _example/example.csv -f 'phone > 12345' 86 | +----+------+----------------+-----------+--------+ 87 | | ID | NAME | MAIL | PHONE | ADRESS | 88 | +----+------+----------------+-----------+--------+ 89 | | 1 | a | aaaa@hoge.fuga | 123456 | 111111 | 90 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 91 | ``` 92 | 93 | ### Multiple Filter Option 94 | 95 | #### And 96 | 97 | ``` 98 | $ ./csviewer -p _example/example.csv -f "id > 2 && id <= 10" 99 | +----+------+-----------------+-----------+--------+ 100 | | ID | NAME | MAIL | PHONE | ADRESS | 101 | +----+------+-----------------+-----------+--------+ 102 | | 3 | c | | | 22222 | 103 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 104 | | 10 | e | eeeee@fuga.hgoe | 654321 | | 105 | +----+------+-----------------+-----------+--------+ 106 | ``` 107 | 108 | ``` 109 | $ ./csviewer -p _example/example.csv -f "id > 2" -f "id <= 10" 110 | +----+------+-----------------+-----------+--------+ 111 | | ID | NAME | MAIL | PHONE | ADRESS | 112 | +----+------+-----------------+-----------+--------+ 113 | | 3 | c | | | 22222 | 114 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 115 | | 10 | e | eeeee@fuga.hgoe | 654321 | | 116 | +----+------+-----------------+-----------+--------+ 117 | ``` 118 | 119 | #### Or 120 | 121 | 122 | ``` 123 | $ ./csviewer -p _example/example.csv -f "name == 'c'" -f "name == 'd'" -or 124 | +----+------+---------------+-----------+--------+ 125 | | ID | NAME | MAIL | PHONE | ADRESS | 126 | +----+------+---------------+-----------+--------+ 127 | | 3 | c | | | 22222 | 128 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 129 | +----+------+---------------+-----------+--------+ 130 | ``` 131 | 132 | ``` 133 | $ ./csviewer -p _example/example.csv -f "name == 'c' || name == 'd'" 134 | +----+------+---------------+-----------+--------+ 135 | | ID | NAME | MAIL | PHONE | ADRESS | 136 | +----+------+---------------+-----------+--------+ 137 | | 3 | c | | | 22222 | 138 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 139 | +----+------+---------------+-----------+--------+ 140 | ``` 141 | 142 | ### Sort Option 143 | 144 | ``` 145 | $ csviewer -p _example/example.csv -s 'phone asc' 146 | +-----+-------+-----------------+-----------+--------+ 147 | | ID | NAME | MAIL | PHONE | ADRESS | 148 | +-----+-------+-----------------+-----------+--------+ 149 | | 3 | c | | | 22222 | 150 | | 2 | b | bbb@hoge.fuga | 12345 | | 151 | | 1 | a | aaaa@hoge.fuga | 123456 | 111111 | 152 | | 10 | e | eeeee@fuga.hgoe | 654321 | | 153 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 154 | | 222 | asdfg | asdfg@fuga.hgoe | 987654321 | | 155 | +-----+-------+-----------------+-----------+--------+ 156 | ``` 157 | 158 | ``` 159 | $ csviewer -p _example/example.csv -s 'mail desc' 160 | +-----+-------+-----------------+-----------+--------+ 161 | | ID | NAME | MAIL | PHONE | ADRESS | 162 | +-----+-------+-----------------+-----------+--------+ 163 | | 10 | e | eeeee@fuga.hgoe | 654321 | | 164 | | 5 | d | ddd@fuga.hgoe | 123456789 | | 165 | | 2 | b | bbb@hoge.fuga | 12345 | | 166 | | 222 | asdfg | asdfg@fuga.hgoe | 987654321 | | 167 | | 1 | a | aaaa@hoge.fuga | 123456 | 111111 | 168 | | 3 | c | | | 22222 | 169 | +-----+-------+-----------------+-----------+--------+ 170 | ``` 171 | 172 | # Usage 173 | 174 | 175 | ``` 176 | $ csviewer --help 177 | Usage of csviewer: 178 | -c string 179 | print specify columns 180 | -columns string 181 | print specify columns 182 | -f value 183 | filter 184 | -filter value 185 | filter 186 | -l int 187 | set max display rows num 188 | -limit int 189 | set max display rows num 190 | -p string 191 | set csv file path 192 | -path string 193 | set csv file path 194 | -s string 195 | sort by set value 196 | ex) id desc/ hoge_id asc 197 | -sort string 198 | sort by set value 199 | ex) id desc/ hoge_id asc 200 | ``` 201 | 202 | # TODO 203 | 204 | - [x] Order option 205 | - [x] Set multi filters in one column 206 | -------------------------------------------------------------------------------- /_example/example.csv: -------------------------------------------------------------------------------- 1 | id,name,mail,phone,adress 2 | 1,a,aaaa@hoge.fuga,123456,111111 3 | 2,b,bbb@hoge.fuga,12345, 4 | 3,c,,,22222 5 | 5,d,ddd@fuga.hgoe,123456789, 6 | 10,e,eeeee@fuga.hgoe,654321, 7 | 222,asdfg,asdfg@fuga.hgoe,987654321, -------------------------------------------------------------------------------- /csviewer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/Knetic/govaluate" 12 | "github.com/olekukonko/tablewriter" 13 | ) 14 | 15 | type Csviewer struct { 16 | columns []string 17 | rows [][]string 18 | printInfo printInfo 19 | rowsMap []map[string]interface{} 20 | filters []string 21 | limit int 22 | isFiltersOr bool 23 | } 24 | 25 | type printInfo struct { 26 | printColumns []string 27 | printColumnIndex []int 28 | } 29 | 30 | type funcFilter func(map[string]interface{}) bool 31 | 32 | type sortOption struct { 33 | column string 34 | sortType string 35 | } 36 | 37 | func newSortData(data map[int]string, sortType string) *sortData { 38 | d := &sortData{ 39 | data: data, 40 | sortType: strings.ToUpper(sortType), 41 | } 42 | 43 | for k := range data { 44 | d.index = append(d.index, k) 45 | } 46 | 47 | sort.Ints(d.index) 48 | 49 | return d 50 | } 51 | 52 | type sortData struct { 53 | data map[int]string 54 | index []int 55 | sortType string 56 | } 57 | 58 | func (sd *sortData) Len() int { 59 | return len(sd.data) 60 | } 61 | 62 | func (sd *sortData) Less(i, j int) bool { 63 | iData := sd.data[i] 64 | jData := sd.data[j] 65 | 66 | iVal, errI := strconv.Atoi(iData) 67 | jVal, errJ := strconv.Atoi(jData) 68 | if errI != nil || errJ != nil { 69 | // iData and jData: string 70 | if errI != nil && errJ != nil { 71 | sorted := sort.StringsAreSorted([]string{iData, jData}) 72 | 73 | if sd.sortType == "DESC" { 74 | sorted = !sorted 75 | } 76 | 77 | return sorted 78 | } 79 | // iData is number but jData is maybe empty 80 | if errI == nil && errJ != nil { 81 | jVal = 0 82 | } 83 | // jData is number but iData is maybe empty 84 | if errI != nil && errJ == nil { 85 | iVal = 0 86 | } 87 | } 88 | 89 | if sd.sortType == "DESC" { 90 | return iVal > jVal 91 | } 92 | 93 | return iVal < jVal // ASC 94 | } 95 | 96 | func (sd *sortData) Swap(i, j int) { 97 | sd.index[i], sd.index[j] = sd.index[j], sd.index[i] 98 | sd.data[i], sd.data[j] = sd.data[j], sd.data[i] 99 | } 100 | 101 | func newCsviwer(columns []string, rows [][]string, printColumns string, filters []string, limit int, isFiltersOr bool) *Csviewer { 102 | return &Csviewer{ 103 | columns: columns, 104 | rows: rows, 105 | printInfo: parsePrintColumns(columns, printColumns), 106 | rowsMap: sliceToMap(columns, rows), 107 | filters: filters, 108 | limit: limit, 109 | isFiltersOr: isFiltersOr, 110 | } 111 | } 112 | 113 | func sliceToMap(columns []string, rows [][]string) []map[string]interface{} { 114 | // id,name,email 115 | // 1, foo, foo@email.com 116 | // 2, fuga, fuga@email.com 117 | // ↓ 118 | // {"id": "1", "name", "foo", "email": "foo@email.com"}{"id": "2", "name", "fuga", "email": "fuga@email.com"} 119 | data := make([]map[string]interface{}, 0, len(rows)) 120 | for _, row := range rows { 121 | rowMap := make(map[string]interface{}) 122 | var val interface{} 123 | for i, column := range columns { 124 | tmpval, err := strconv.ParseFloat(row[i], 64) 125 | if err == nil { 126 | val = tmpval 127 | } else { 128 | val = row[i] 129 | } 130 | 131 | rowMap[column] = val 132 | } 133 | data = append(data, rowMap) 134 | } 135 | 136 | return data 137 | } 138 | 139 | func parsePrintColumns(columns []string, showColumns string) printInfo { 140 | index := make([]int, 0) 141 | prints := make([]string, 0) 142 | 143 | if showColumns == "" { 144 | for i, _ := range columns { 145 | index = append(index, i) 146 | } 147 | return printInfo{columns, index} 148 | } 149 | 150 | for _, c := range strings.Split(strings.TrimSpace(showColumns), ",") { 151 | for i, column := range columns { 152 | if c == column { 153 | prints = append(prints, column) 154 | index = append(index, i) 155 | break 156 | } 157 | } 158 | } 159 | 160 | return printInfo{prints, index} 161 | } 162 | 163 | func (v *Csviewer) Print(opt *sortOption) { 164 | var printRows [][]string 165 | sortIndexAndValue := make(map[int]string) 166 | var sortData *sortData 167 | 168 | count := 0 169 | for i, rowMap := range v.rowsMap { 170 | if v.filter(rowMap) { 171 | var row []string 172 | for _, j := range v.printInfo.printColumnIndex { 173 | row = append(row, fmt.Sprint(v.rows[i][j])) 174 | 175 | if opt != nil && opt.column == v.printInfo.printColumns[j] { 176 | sortIndexAndValue[i] = fmt.Sprint(v.rows[i][j]) 177 | } 178 | } 179 | printRows = append(printRows, row) 180 | count++ 181 | } 182 | 183 | if 0 < v.limit && v.limit <= count { 184 | break 185 | } 186 | } 187 | 188 | if opt != nil { 189 | sortData = newSortData(sortIndexAndValue, opt.sortType) 190 | sort.Sort(sortData) 191 | } 192 | 193 | t := tablewriter.NewWriter(os.Stdout) 194 | t.SetHeader(v.printInfo.printColumns) 195 | if opt != nil { 196 | rows := [][]string{} 197 | for _, i := range sortData.index { 198 | rows = append(rows, printRows[i]) 199 | } 200 | t.AppendBulk(rows) 201 | } else { 202 | t.AppendBulk(printRows) 203 | } 204 | t.Render() 205 | } 206 | 207 | func (v *Csviewer) filter(rowMap map[string]interface{}) bool { 208 | if len(v.filters) == 0 { 209 | return true 210 | } 211 | 212 | values := make(map[string]interface{}, 0) 213 | 214 | for key, val := range rowMap { 215 | values[key] = val 216 | } 217 | 218 | filters := make([]string, 0) 219 | 220 | for _, f := range v.filters { 221 | filters = append(filters, f) 222 | } 223 | 224 | op := " && " 225 | if v.isFiltersOr { 226 | op = " || " 227 | } 228 | 229 | expression, err := govaluate.NewEvaluableExpression(strings.Join(filters, op)) 230 | if err != nil { 231 | log.Fatal(err) 232 | } 233 | result, err := expression.Evaluate(values) 234 | if err != nil { 235 | log.Fatal(err) 236 | } 237 | 238 | return result.(bool) 239 | } 240 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Konboi/csviewer 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible 7 | github.com/olekukonko/tablewriter v0.0.4 8 | github.com/soh335/sliceflag v0.0.0-20160923061056-d2d28a5acab8 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= 2 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 3 | github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 4 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 5 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 6 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 7 | github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4 h1:Mm4XQCBICntJzH8fKglsRuEiFUJYnTnM4BBFvpP5BWs= 8 | github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 9 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 10 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 11 | github.com/soh335/sliceflag v0.0.0-20160923061056-d2d28a5acab8 h1:Rj9gnP5z34qqbzRWW/NPFAkUi2fSG0efbp6iV23n1Ck= 12 | github.com/soh335/sliceflag v0.0.0-20160923061056-d2d28a5acab8/go.mod h1:d7KDaisiM/NC1Ofz8GkK7K4+5dZcpOauAfGO8+mpXpc= 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/soh335/sliceflag" 13 | ) 14 | 15 | func main() { 16 | var path, printColumns, sort string 17 | var limit int 18 | var filters []string 19 | var isFiltersOr bool 20 | 21 | flag.StringVar(&path, "path", "", "set csv file path") 22 | flag.StringVar(&path, "p", "", "set csv file path") 23 | flag.IntVar(&limit, "limit", 0, "set max display rows num") 24 | flag.IntVar(&limit, "l", 0, "set max display rows num") 25 | flag.StringVar(&printColumns, "columns", "", "print specify columns") 26 | flag.StringVar(&printColumns, "c", "", "print specify columns") 27 | flag.StringVar(&sort, "sort", "", "sort by set value\nex) id desc/ hoge_id asc") 28 | flag.StringVar(&sort, "s", "", "sort by set value\nex) id desc/ hoge_id asc") 29 | sliceflag.StringVar(flag.CommandLine, &filters, "f", []string{}, "filter") 30 | sliceflag.StringVar(flag.CommandLine, &filters, "filter", []string{}, "filter") 31 | flag.BoolVar(&isFiltersOr, "or", false, "filter logical operator") 32 | flag.Parse() 33 | 34 | d, err := loadData(path) 35 | if err != nil { 36 | log.Fatal("error load data", err.Error()) 37 | } 38 | 39 | columns, rows, err := convertData(d) 40 | if err != nil { 41 | log.Fatal("error convet data", err.Error()) 42 | } 43 | 44 | viewer := newCsviwer(columns, rows, printColumns, filters, limit, isFiltersOr) 45 | viewer.Print(parseSort(sort)) 46 | } 47 | 48 | func loadData(path string) (io.Reader, error) { 49 | if path != "" { 50 | return os.Open(path) 51 | } 52 | 53 | stdin := os.Stdin 54 | stats, err := os.Stdin.Stat() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | if stats.Size() > 0 { 60 | return stdin, nil 61 | } 62 | 63 | return nil, fmt.Errorf("data is emptry") 64 | } 65 | 66 | func convertData(data io.Reader) ([]string, [][]string, error) { 67 | c := csv.NewReader(data) 68 | 69 | column, err := c.Read() 70 | if err != nil { 71 | return []string{}, [][]string{}, err 72 | } 73 | var rows [][]string 74 | for { 75 | row, err := c.Read() 76 | if err == io.EOF { 77 | break 78 | } 79 | if err != nil { 80 | return []string{}, [][]string{}, err 81 | } 82 | rows = append(rows, row) 83 | } 84 | 85 | return column, rows, nil 86 | } 87 | 88 | func parseSort(sort string) *sortOption { 89 | _sort := strings.ToUpper(sort) 90 | if !strings.Contains(_sort, "ASC") && !strings.Contains(_sort, "DESC") { 91 | return nil 92 | } 93 | 94 | _sorts := strings.Split(sort, " ") 95 | if len(_sorts) != 2 { 96 | return nil 97 | } 98 | 99 | for i, s := range _sorts { 100 | _sorts[i] = strings.TrimSpace(s) 101 | } 102 | 103 | return &sortOption{ 104 | column: _sorts[0], 105 | sortType: strings.ToUpper(_sorts[1]), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | import "reflect" 5 | 6 | func Test_parseSort(t *testing.T) { 7 | tests := []struct { 8 | Input string 9 | Output *sortOption 10 | }{ 11 | { 12 | Input: "id desc", 13 | Output: &sortOption{ 14 | column: "id", 15 | sortType: "DESC", 16 | }, 17 | }, 18 | { 19 | Input: "fuga_id desc", 20 | Output: &sortOption{ 21 | column: "fuga_id", 22 | sortType: "DESC", 23 | }, 24 | }, 25 | { 26 | Input: "foo asc", 27 | Output: &sortOption{ 28 | column: "foo", 29 | sortType: "ASC", 30 | }, 31 | }, 32 | { 33 | Input: "fuga_id", 34 | Output: nil, 35 | }, 36 | { 37 | Input: "fuga_id hoge", 38 | Output: nil, 39 | }, 40 | } 41 | 42 | for _, test := range tests { 43 | if !reflect.DeepEqual(parseSort(test.Input), test.Output) { 44 | t.Fatal("error invalid result") 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------