diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 6071725ba1..3f2cc7c95c 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -177,6 +177,10 @@ "ImportPath": "github.com/mitchellh/mapstructure", "Rev": "2caf8efc93669b6c43e0441cdc6aed17546c96f3" }, + { + "ImportPath": "github.com/olekukonko/tablewriter", + "Rev": "a5eefc286b03d5560735698ef36c83728a6ae560" + }, { "ImportPath": "github.com/prometheus/client_golang/prometheus", "Comment": "0.7.0-53-g449ccef", diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/.travis.yml b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/.travis.yml new file mode 100644 index 0000000000..354b7f8b24 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/.travis.yml @@ -0,0 +1,8 @@ +language: go + +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - tip diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/LICENCE.md b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/LICENCE.md new file mode 100644 index 0000000000..1fd8484253 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/LICENCE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 by Oleku Konko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/README.md b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/README.md new file mode 100644 index 0000000000..4ee44356d2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/README.md @@ -0,0 +1,141 @@ +ASCII Table Writer +========= + +[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) [![Total views](https://sourcegraph.com/api/repos/github.com/olekukonko/tablewriter/counters/views.png)](https://sourcegraph.com/github.com/olekukonko/tablewriter) + +Generate ASCII table on the fly ... Installation is simple as + + go get github.com/olekukonko/tablewriter + + +#### Features +- Automatic Padding +- Support Multiple Lines +- Supports Alignment +- Support Custom Separators +- Automatic Alignment of numbers & percentage +- Write directly to http , file etc via `io.Writer` +- Read directly from CSV file +- Optional row line via `SetRowLine` +- Normalise table header +- Make CSV Headers optional +- Enable or disable table border +- Set custom footer support + + +#### Example 1 - Basic +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +##### Output 1 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +``` + +#### Example 2 - Without Border / Footer / Bulk Append +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 2 +``` + + DATE | DESCRIPTION | CV2 | AMOUNT ++----------+--------------------------+-------+---------+ + 1/1/2014 | Domain name | 2233 | $10.98 + 1/1/2014 | January Hosting | 2233 | $54.95 + 1/4/2014 | February Hosting | 2233 | $51.00 + 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 ++----------+--------------------------+-------+---------+ + TOTAL | $146 93 + +-------+---------+ + +``` + + +#### Example 3 - CSV +```go +table, _ := tablewriter.NewCSV(os.Stdout, "test_info.csv", true) +table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment +table.Render() +``` + +##### Output 3 +``` ++----------+--------------+------+-----+---------+----------------+ +| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA | ++----------+--------------+------+-----+---------+----------------+ +| user_id | smallint(5) | NO | PRI | NULL | auto_increment | +| username | varchar(10) | NO | | NULL | | +| password | varchar(100) | NO | | NULL | | ++----------+--------------+------+-----+---------+----------------+ +``` + +#### Example 4 - Custom Separator +```go +table, _ := tablewriter.NewCSV(os.Stdout, "test.csv", true) +table.SetRowLine(true) // Enable row line + +// Change table lines +table.SetCenterSeparator("*") +table.SetColumnSeparator("‡") +table.SetRowSeparator("-") + +table.SetAlignment(tablewriter.ALIGN_LEFT) +table.Render() +``` + +##### Output 4 +``` +*------------*-----------*---------* +╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪ +*------------*-----------*---------* +╪ John ╪ Barry ╪ 123456 ╪ +*------------*-----------*---------* +╪ Kathy ╪ Smith ╪ 687987 ╪ +*------------*-----------*---------* +╪ Bob ╪ McCornick ╪ 3979870 ╪ +*------------*-----------*---------* +``` + +#### TODO +- ~~Import Directly from CSV~~ - `done` +- ~~Support for `SetFooter`~~ - `done` +- ~~Support for `SetBorder`~~ - `done` +- ~~Support table with uneven rows~~ - `done` +- Support custom alignment +- General Improvement & Optimisation +- `NewHTML` Parse table from HTML diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv.go b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv.go new file mode 100644 index 0000000000..98878303bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv.go @@ -0,0 +1,52 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "encoding/csv" + "io" + "os" +) + +// Start A new table by importing from a CSV file +// Takes io.Writer and csv File name +func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) { + file, err := os.Open(fileName) + if err != nil { + return &Table{}, err + } + defer file.Close() + csvReader := csv.NewReader(file) + t, err := NewCSVReader(writer, csvReader, hasHeader) + return t, err +} + +// Start a New Table Writer with csv.Reader +// This enables customisation such as reader.Comma = ';' +// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94 +func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) { + t := NewWriter(writer) + if hasHeader { + // Read the first row + headers, err := csvReader.Read() + if err != nil { + return &Table{}, err + } + t.SetHeader(headers) + } + for { + record, err := csvReader.Read() + if err == io.EOF { + break + } else if err != nil { + return &Table{}, err + } + t.Append(record) + } + return t, nil +} diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/README.md b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/README.md new file mode 100644 index 0000000000..6cf5628ab6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/README.md @@ -0,0 +1,43 @@ +ASCII Table Writer Tool +========= + +Generate ASCII table on the fly via command line ... Installation is simple as + +#### Get Tool + + go get github.com/olekukonko/tablewriter/csv2table + +#### Install Tool + + go install github.com/olekukonko/tablewriter/csv2table + + +#### Usage + + csv2table -f test.csv + +#### Support for Piping + + cat test.csv | csv2table -p=true + +#### Output + +``` ++------------+-----------+---------+ +| FIRST NAME | LAST NAME | SSN | ++------------+-----------+---------+ +| John | Barry | 123456 | +| Kathy | Smith | 687987 | +| Bob | McCornick | 3979870 | ++------------+-----------+---------+ +``` + +#### Another Piping with Header set to `false` + + echo dance,with,me | csv2table -p=true -h=false + +#### Output + + +-------+------+-----+ + | dance | with | me | + +-------+------+-----+ diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/csv2table.go b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/csv2table.go new file mode 100644 index 0000000000..411f6535c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/csv2table.go @@ -0,0 +1,84 @@ +package main + +import ( + "encoding/csv" + "flag" + "fmt" + "github.com/olekukonko/tablewriter" + "io" + "os" + "unicode/utf8" +) + +var ( + fileName = flag.String("f", "", "Set file with eg. sample.csv") + delimiter = flag.String("d", ",", "Set CSV File delimiter eg. ,|;|\t ") + header = flag.Bool("h", true, "Set header options eg. true|false ") + align = flag.String("a", "none", "Set aligmement with eg. none|left|right|centre") + pipe = flag.Bool("p", false, "Suport for Piping from STDIN") + border = flag.Bool("b", true, "Enable / disable table border") +) + +func main() { + flag.Parse() + fmt.Println() + if *pipe || hasArg("-p") { + process(os.Stdin) + } else { + if *fileName == "" { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + fmt.Println() + os.Exit(1) + } + processFile() + } + fmt.Println() +} + +func hasArg(name string) bool { + for _ , v := range os.Args { + if name == v { + return true + } + } + return false +} +func processFile() { + r, err := os.Open(*fileName) + if err != nil { + exit(err) + } + defer r.Close() + process(r) +} +func process(r io.Reader) { + csvReader := csv.NewReader(r) + rune, size := utf8.DecodeRuneInString(*delimiter) + if size == 0 { + rune = ',' + } + csvReader.Comma = rune + + table, err := tablewriter.NewCSVReader(os.Stdout, csvReader, *header) + + if err != nil { + exit(err) + } + + switch *align { + case "left": + table.SetAlignment(tablewriter.ALIGN_LEFT) + case "right": + table.SetAlignment(tablewriter.ALIGN_RIGHT) + case "center": + table.SetAlignment(tablewriter.ALIGN_CENTRE) + } + table.SetBorder(*border) + table.Render() +} + +func exit(err error) { + fmt.Fprintf(os.Stderr, "#Error : %s", err) + os.Exit(1) +} diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/table.go b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/table.go new file mode 100644 index 0000000000..4550746c16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/table.go @@ -0,0 +1,472 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +// Create & Generate text based table +package tablewriter + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +const ( + MAX_ROW_WIDTH = 30 +) + +const ( + CENTRE = "+" + ROW = "-" + COLUMN = "|" + SPACE = " " +) + +const ( + ALIGN_DEFAULT = iota + ALIGN_CENTRE + ALIGN_RIGHT + ALIGN_LEFT +) + +var ( + decimal = regexp.MustCompile(`^\d*\.?\d*$`) + percent = regexp.MustCompile(`^\d*\.?\d*$%$`) +) + +type Table struct { + out io.Writer + rows [][]string + lines [][][]string + cs map[int]int + rs map[int]int + headers []string + footers []string + autoFmt bool + autoWrap bool + mW int + pCenter string + pRow string + pColumn string + tColumn int + tRow int + align int + rowLine bool + border bool + colSize int +} + +// Start New Table +// Take io.Writer Directly +func NewWriter(writer io.Writer) *Table { + t := &Table{ + out: writer, + rows: [][]string{}, + lines: [][][]string{}, + cs: make(map[int]int), + rs: make(map[int]int), + headers: []string{}, + footers: []string{}, + autoFmt: true, + autoWrap: true, + mW: MAX_ROW_WIDTH, + pCenter: CENTRE, + pRow: ROW, + pColumn: COLUMN, + tColumn: -1, + tRow: -1, + align: ALIGN_DEFAULT, + rowLine: false, + border: true, + colSize: -1} + return t +} + +// Render table output +func (t Table) Render() { + if t.border { + t.printLine(true) + } + t.printHeading() + t.printRows() + + if !t.rowLine && t.border { + t.printLine(true) + } + t.printFooter() + +} + +// Set table header +func (t *Table) SetHeader(keys []string) { + t.colSize = len(keys) + for i, v := range keys { + t.parseDimension(v, i, -1) + t.headers = append(t.headers, v) + } +} + +// Set table Footer +func (t *Table) SetFooter(keys []string) { + //t.colSize = len(keys) + for i, v := range keys { + t.parseDimension(v, i, -1) + t.footers = append(t.footers, v) + } +} + +// Turn header autoformatting on/off. Default is on (true). +func (t *Table) SetAutoFormatHeaders(auto bool) { + t.autoFmt = auto +} + +// Turn automatic multiline text adjustment on/off. Default is on (true). +func (t *Table) SetAutoWrapText(auto bool) { + t.autoWrap = auto +} + +// Set the Default column width +func (t *Table) SetColWidth(width int) { + t.mW = width +} + +// Set the Column Separator +func (t *Table) SetColumnSeparator(sep string) { + t.pColumn = sep +} + +// Set the Row Separator +func (t *Table) SetRowSeparator(sep string) { + t.pRow = sep +} + +// Set the center Separator +func (t *Table) SetCenterSeparator(sep string) { + t.pCenter = sep +} + +// Set Table Alignment +func (t *Table) SetAlignment(align int) { + t.align = align +} + +// Set Row Line +// This would enable / disable a line on each row of the table +func (t *Table) SetRowLine(line bool) { + t.rowLine = line +} + +// Set Table Border +// This would enable / disable line around the table +func (t *Table) SetBorder(border bool) { + t.border = border +} + +// Append row to table +func (t *Table) Append(row []string) error { + rowSize := len(t.headers) + if rowSize > t.colSize { + t.colSize = rowSize + } + + n := len(t.lines) + line := [][]string{} + for i, v := range row { + + // Detect string width + // Detect String height + // Break strings into words + out := t.parseDimension(v, i, n) + + // Append broken words + line = append(line, out) + } + t.lines = append(t.lines, line) + return nil +} + +// Allow Support for Bulk Append +// Eliminates repeated for loops +func (t *Table) AppendBulk(rows [][]string) (err error) { + for _, row := range rows { + err = t.Append(row) + if err != nil { + return err + } + } + return nil +} + +// Print line based on row width +func (t Table) printLine(nl bool) { + fmt.Fprint(t.out, t.pCenter) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.pCenter) + } + if nl { + fmt.Fprintln(t.out) + } +} + +// Print heading information +func (t Table) printHeading() { + // Check if headers is available + if len(t.headers) < 1 { + return + } + + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) + + // Identify last column + end := len(t.cs) - 1 + + // Print Heading column + for i := 0; i <= end; i++ { + v := t.cs[i] + h := t.headers[i] + if t.autoFmt { + h = Title(h) + } + pad := ConditionString((i == end && !t.border), SPACE, t.pColumn) + fmt.Fprintf(t.out, " %s %s", + Pad(h, SPACE, v), + pad) + } + // Next line + fmt.Fprintln(t.out) + t.printLine(true) +} + +// Print heading information +func (t Table) printFooter() { + // Check if headers is available + if len(t.footers) < 1 { + return + } + + // Only print line if border is not set + if !t.border { + t.printLine(true) + } + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) + + // Identify last column + end := len(t.cs) - 1 + + // Print Heading column + for i := 0; i <= end; i++ { + v := t.cs[i] + f := t.footers[i] + if t.autoFmt { + f = Title(f) + } + pad := ConditionString((i == end && !t.border), SPACE, t.pColumn) + + if len(t.footers[i]) == 0 { + pad = SPACE + } + fmt.Fprintf(t.out, " %s %s", + Pad(f, SPACE, v), + pad) + } + // Next line + fmt.Fprintln(t.out) + //t.printLine(true) + + hasPrinted := false + + for i := 0; i <= end; i++ { + v := t.cs[i] + pad := t.pRow + center := t.pCenter + length := len(t.footers[i]) + + if length > 0 { + hasPrinted = true + } + + // Set center to be space if length is 0 + if length == 0 && !t.border { + center = SPACE + } + + // Print first junction + if i == 0 { + fmt.Fprint(t.out, center) + } + + // Pad With space of length is 0 + if length == 0 { + pad = SPACE + } + // Ignore left space of it has printed before + if hasPrinted || t.border { + pad = t.pRow + center = t.pCenter + } + + // Change Center start position + if center == SPACE { + if i < end && len(t.footers[i+1]) != 0 { + center = t.pCenter + } + } + + // Print the footer + fmt.Fprintf(t.out, "%s%s%s%s", + pad, + strings.Repeat(string(pad), v), + pad, + center) + + } + + fmt.Fprintln(t.out) + +} + +func (t Table) printRows() { + for i, lines := range t.lines { + t.printRow(lines, i) + } + +} + +// Print Row Information +// Adjust column alignment based on type + +func (t Table) printRow(columns [][]string, colKey int) { + // Get Maximum Height + max := t.rs[colKey] + total := len(columns) + + // TODO Fix uneven col size + // if total < t.colSize { + // for n := t.colSize - total; n < t.colSize ; n++ { + // columns = append(columns, []string{SPACE}) + // t.cs[n] = t.mW + // } + //} + + // Pad Each Height + // pads := []int{} + pads := []int{} + + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + //fmt.Println(max, "\n") + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + fmt.Fprint(t.out, ConditionString((!t.border && y == 0), SPACE, t.pColumn)) + + fmt.Fprintf(t.out, SPACE) + str := columns[y][x] + + // This would print alignment + // Default alignment would use multiple configuration + switch t.align { + case ALIGN_CENTRE: // + fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + + // TODO Custom alignment per column + //if max == 1 || pads[y] > 0 { + // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + //} else { + // fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + //} + + } + } + fmt.Fprintf(t.out, SPACE) + } + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.border, t.pColumn, SPACE)) + fmt.Fprintln(t.out) + } + + if t.rowLine { + t.printLine(true) + } + +} + +func (t *Table) parseDimension(str string, colKey, rowKey int) []string { + var ( + raw []string + max int + ) + w := DisplayWidth(str) + // Calculate Width + // Check if with is grater than maximum width + if w > t.mW { + w = t.mW + } + + // Check if width exists + v, ok := t.cs[colKey] + if !ok || v < w || v == 0 { + t.cs[colKey] = w + } + + if rowKey == -1 { + return raw + } + // Calculate Height + if t.autoWrap { + raw, _ = WrapString(str, t.cs[colKey]) + } else { + raw = getLines(str) + } + + for _, line := range raw { + if w := DisplayWidth(line); w > max { + max = w + } + } + + // Make sure the with is the same length as maximum word + // Important for cases where the width is smaller than maxu word + if max > t.cs[colKey] { + t.cs[colKey] = max + } + + h := len(raw) + v, ok = t.rs[rowKey] + + if !ok || v < h || v == 0 { + t.rs[rowKey] = h + } + //fmt.Printf("Raw %+v %d\n", raw, len(raw)) + return raw +} diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test.csv b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test.csv new file mode 100644 index 0000000000..1609327e93 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test.csv @@ -0,0 +1,4 @@ +first_name,last_name,ssn +John,Barry,123456 +Kathy,Smith,687987 +Bob,McCornick,3979870 \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test_info.csv b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test_info.csv new file mode 100644 index 0000000000..e4c40e983a --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/test_info.csv @@ -0,0 +1,4 @@ +Field,Type,Null,Key,Default,Extra +user_id,smallint(5),NO,PRI,NULL,auto_increment +username,varchar(10),NO,,NULL, +password,varchar(100),NO,,NULL, \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/util.go b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/util.go new file mode 100644 index 0000000000..62894e006c --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/util.go @@ -0,0 +1,81 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "regexp" + "strings" + "unicode/utf8" +) + +var ( + ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]") +) + +func DisplayWidth(str string) int { + tmp := ansi.ReplaceAllLiteralString(str, "") + tmp_rune := []rune(tmp) + count := 0 + for _, v := range tmp_rune { + if v > 128 { + count++ + } + } + return utf8.RuneCountInString(tmp) + count +} + +// Simple Condition for string +// Returns value based on condition +func ConditionString(cond bool, valid, inValid string) string { + if cond { + return valid + } + return inValid +} + +// Format Table Header +// Replace _ , . and spaces +func Title(name string) string { + name = strings.Replace(name, "_", " ", -1) + name = strings.Replace(name, ".", " ", -1) + name = strings.TrimSpace(name) + return strings.ToUpper(name) +} + +// Pad String +// Attempts to play string in the center +func Pad(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + gapLeft := int(math.Ceil(float64(gap / 2))) + gapRight := gap - gapLeft + return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight) + } + return s +} + +// Pad String Right position +// This would pace string at the left side fo the screen +func PadRight(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return s + strings.Repeat(string(pad), gap) + } + return s +} + +// Pad String Left position +// This would pace string at the right side fo the screen +func PadLeft(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return strings.Repeat(string(pad), gap) + s + } + return s +} diff --git a/Godeps/_workspace/src/github.com/olekukonko/tablewriter/wrap.go b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/wrap.go new file mode 100644 index 0000000000..f3747d9f32 --- /dev/null +++ b/Godeps/_workspace/src/github.com/olekukonko/tablewriter/wrap.go @@ -0,0 +1,103 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "strings" + "unicode/utf8" +) + +var ( + nl = "\n" + sp = " " +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapString(s string, lim int) ([]string, int) { + words := strings.Split(strings.Replace(strings.TrimSpace(s), nl, sp, -1), sp) + var lines []string + max := 0 + for _, v := range words { + max = len(v) + if max > lim { + lim = max + } + } + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, strings.Join(line, sp)) + } + return lines, lim +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, +// WrapString will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each rune as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words []string, spc, lim, pen int) [][]string { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = utf8.RuneCountInString(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + utf8.RuneCountInString(words[j]) + } + } + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + var lines [][]string + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} + +// getLines decomposes a multiline string into a slice of strings. +func getLines(s string) []string { + var lines []string + + for _, line := range strings.Split(strings.TrimSpace(s), nl) { + lines = append(lines, line) + } + return lines +} diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index ce3166c561..6046a58520 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -13,7 +13,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "sort" "strings" "testing" @@ -162,23 +161,56 @@ func splitLines(chunk string) []string { return results } -// List keys, parses the output, and returns the keys as an array of root key -// IDs and an array of signing key IDs -func GetKeys(t *testing.T, tempDir string) ([]string, []string) { +// List keys, parses the output, and returns the unique key IDs as an array +// of root key IDs and an array of signing key IDs. Output expected looks like: +// ROLE GUN KEY ID LOCATION +// ---------------------------------------------------------------- +// root 8bd63a896398b558ac... file (.../private) +// snapshot repo e9e9425cd9a85fc7a5... file (.../private) +// targets repo f5b84e2d92708c5acb... file (.../private) +func getUniqueKeys(t *testing.T, tempDir string) ([]string, []string) { output, err := runCommand(t, tempDir, "key", "list") assert.NoError(t, err) - - parts := strings.Split(output, "# Signing keys:") - assert.Len(t, parts, 2) - - fixed := make([][]string, 2) - for i, part := range parts { - fixed[i] = splitLines( - strings.TrimPrefix(strings.TrimSpace(part), "# Root keys:")) - sort.Strings(fixed[i]) + lines := splitLines(output) + if len(lines) == 1 && lines[0] == "No signing keys found." { + return []string{}, []string{} + } + if len(lines) < 3 { // 2 lines of header, at least 1 line with keys + t.Logf("This output is not what is expected by the test:\n%s", output) } - return fixed[0], fixed[1] + var ( + rootMap = make(map[string]bool) + nonrootMap = make(map[string]bool) + root []string + nonroot []string + ) + // first two lines are header + for _, line := range lines[2:] { + parts := strings.Fields(line) + var ( + placeToGo map[string]bool + keyID string + ) + if strings.TrimSpace(parts[0]) == "root" { + // no gun, so there are only 3 fields + placeToGo, keyID = rootMap, parts[1] + } else { + // gun comes between role and key ID + placeToGo, keyID = nonrootMap, parts[2] + } + // keys are 32-chars long (32 byte shasum, hex-encoded) + assert.Len(t, keyID, 64) + placeToGo[keyID] = true + } + for k := range rootMap { + root = append(root, k) + } + for k := range nonrootMap { + nonroot = append(nonroot, k) + } + + return root, nonroot } // List keys, parses the output, and asserts something about the number of root @@ -186,24 +218,19 @@ func GetKeys(t *testing.T, tempDir string) ([]string, []string) { func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int, rootOnDisk bool) ([]string, []string) { - uniqueKeys := make(map[string]struct{}) - root, signing := GetKeys(t, tempDir) + root, signing := getUniqueKeys(t, tempDir) + assert.Len(t, root, numRoot) assert.Len(t, signing, numSigning) - for i, rootKeyLine := range root { - keyID := strings.Split(rootKeyLine, "-")[0] - keyID = strings.TrimSpace(keyID) - root[i] = keyID - uniqueKeys[keyID] = struct{}{} + for _, rootKeyID := range root { _, err := os.Stat(filepath.Join( - tempDir, "private", "root_keys", keyID+"_root.key")) + tempDir, "private", "root_keys", rootKeyID+"_root.key")) // os.IsExist checks to see if the error is because a file already // exist, and hence doesn't actually the right funciton to use here assert.Equal(t, rootOnDisk, !os.IsNotExist(err)) // this function is declared is in the build-tagged setup files - verifyRootKeyOnHardware(t, keyID) + verifyRootKeyOnHardware(t, rootKeyID) } - assert.Len(t, uniqueKeys, numRoot) return root, signing } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 0d1d5e07e0..aab55651ba 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -2,6 +2,8 @@ package main import ( "archive/zip" + "fmt" + "io" "os" "path/filepath" "sort" @@ -13,6 +15,7 @@ import ( "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -88,6 +91,110 @@ var cmdKeyImportRoot = &cobra.Command{ Run: keysImportRoot, } +func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string { + if len(str) <= maxWidth { + return str + } + if leftTruncate { + return fmt.Sprintf("...%s", str[len(str)-(maxWidth-3):]) + } + return fmt.Sprintf("%s...", str[:maxWidth-3]) +} + +const ( + maxGUNWidth = 25 + maxLocWidth = 40 +) + +type keyInfo struct { + gun string // assumption that this is "" if role is root + role string + keyID string + location string +} + +// We want to sort by gun, then by role, then by keyID, then by location +// In the case of a root role, then there is no GUN, and a root role comes +// first. +type keyInfoSorter []keyInfo + +func (k keyInfoSorter) Len() int { return len(k) } +func (k keyInfoSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } +func (k keyInfoSorter) Less(i, j int) bool { + // special-case role + if k[i].role != k[j].role { + if k[i].role == data.CanonicalRootRole { + return true + } + if k[j].role == data.CanonicalRootRole { + return false + } + // otherwise, neither of them are root, they're just different, so + // go with the traditional sort order. + } + + // sort order is GUN, role, keyID, location. + orderedI := []string{k[i].gun, k[i].role, k[i].keyID, k[i].location} + orderedJ := []string{k[j].gun, k[j].role, k[j].keyID, k[j].location} + + for x := 0; x < 4; x++ { + switch { + case orderedI[x] < orderedJ[x]: + return true + case orderedI[x] > orderedJ[x]: + return false + } + // continue on and evalulate the next item + } + // this shouldn't happen - that means two values are exactly equal + return false +} + +// Given a list of KeyStores in order of listing preference, pretty-prints the +// root keys and then the signing keys. +func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) { + var info []keyInfo + + for _, store := range keyStores { + for keyPath, role := range store.ListKeys() { + gun := "" + if role != data.CanonicalRootRole { + gun = filepath.Dir(keyPath) + } + info = append(info, keyInfo{ + role: role, + location: store.Name(), + gun: gun, + keyID: filepath.Base(keyPath), + }) + } + } + if len(info) == 0 { + writer.Write([]byte("No signing keys found.\n")) + return + } + + sort.Stable(keyInfoSorter(info)) + + table := tablewriter.NewWriter(writer) + table.SetHeader([]string{"ROLE", "GUN", "KEY ID", "LOCATION"}) + table.SetBorder(false) + table.SetColumnSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("-") + table.SetAutoWrapText(false) + + for _, oneKeyInfo := range info { + table.Append([]string{ + oneKeyInfo.role, + truncateWithEllipsis(oneKeyInfo.gun, maxGUNWidth, true), + oneKeyInfo.keyID, + truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true), + }) + } + table.Render() +} + func keysList(cmd *cobra.Command, args []string) { if len(args) > 0 { cmd.Usage() @@ -97,42 +204,9 @@ func keysList(cmd *cobra.Command, args []string) { parseConfig() stores := getKeyStores(cmd, mainViper.GetString("trust_dir"), retriever, true) - - keys := make(map[trustmanager.KeyStore]map[string]string) - for _, store := range stores { - keys[store] = store.ListKeys() - } - cmd.Println("") - cmd.Println("# Root keys: ") - for store, keysMap := range keys { - for k, v := range keysMap { - if v == "root" { - cmd.Println(k, "-", store.Name()) - } - } - } - + prettyPrintKeys(stores, cmd.Out()) cmd.Println("") - cmd.Println("# Signing keys: ") - - // Get a list of all the keys - for store, keysMap := range keys { - var sortedKeys []string - for k := range keysMap { - sortedKeys = append(sortedKeys, k) - } - - // Sort the list of all the keys - sort.Strings(sortedKeys) - - // Print a sorted list of the key/role - for _, k := range sortedKeys { - if keysMap[k] != "root" { - printKey(cmd, k, keysMap[k], store.Name()) - } - } - } } func keysGenerateRootKey(cmd *cobra.Command, args []string) { diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go new file mode 100644 index 0000000000..64a0abb43c --- /dev/null +++ b/cmd/notary/keys_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "crypto/rand" + "fmt" + "io/ioutil" + "reflect" + "sort" + "strings" + "testing" + + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/stretchr/testify/assert" +) + +func TestTruncateWithEllipsis(t *testing.T) { + digits := "1234567890" + // do not truncate + assert.Equal(t, truncateWithEllipsis(digits, 10, true), digits) + assert.Equal(t, truncateWithEllipsis(digits, 10, false), digits) + assert.Equal(t, truncateWithEllipsis(digits, 11, true), digits) + assert.Equal(t, truncateWithEllipsis(digits, 11, false), digits) + + // left and right truncate + assert.Equal(t, truncateWithEllipsis(digits, 8, true), "...67890") + assert.Equal(t, truncateWithEllipsis(digits, 8, false), "12345...") +} + +func TestKeyInfoSorter(t *testing.T) { + expected := []keyInfo{ + {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "i"}, + {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "j"}, + {role: data.CanonicalRootRole, gun: "", keyID: "z", location: "z"}, + {role: "a", gun: "a", keyID: "a", location: "y"}, + {role: "b", gun: "a", keyID: "a", location: "y"}, + {role: "b", gun: "a", keyID: "b", location: "y"}, + {role: "b", gun: "a", keyID: "b", location: "z"}, + {role: "a", gun: "b", keyID: "a", location: "z"}, + } + jumbled := make([]keyInfo, len(expected)) + // randomish indices + for j, e := range []int{3, 6, 1, 4, 0, 7, 5, 2} { + jumbled[j] = expected[e] + } + + sort.Sort(keyInfoSorter(jumbled)) + assert.True(t, reflect.DeepEqual(expected, jumbled), + fmt.Sprintf("Expected %v, Got %v", expected, jumbled)) +} + +type otherMemoryStore struct { + trustmanager.KeyMemoryStore +} + +func (l *otherMemoryStore) Name() string { + return strings.Repeat("z", 70) +} + +// Given a list of key stores, the keys should be pretty-printed with their +// roles, locations, IDs, and guns first in sorted order in the key store +func TestPrettyPrintRootAndSigningKeys(t *testing.T) { + ret := passphrase.ConstantRetriever("pass") + keyStores := []trustmanager.KeyStore{ + trustmanager.NewKeyMemoryStore(ret), + &otherMemoryStore{KeyMemoryStore: *trustmanager.NewKeyMemoryStore(ret)}, + } + + longNameShortened := "..." + strings.Repeat("z", 37) + + // just use the same key for testing + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + + root := data.CanonicalRootRole + + // add keys to the key stores + err = keyStores[0].AddKey(key.ID(), root, key) + assert.NoError(t, err) + + err = keyStores[1].AddKey(key.ID(), root, key) + assert.NoError(t, err) + + err = keyStores[0].AddKey(strings.Repeat("a/", 30)+key.ID(), "targets", key) + assert.NoError(t, err) + + err = keyStores[1].AddKey("short/gun/"+key.ID(), "snapshot", key) + assert.NoError(t, err) + + expected := [][]string{ + {root, key.ID(), keyStores[0].Name()}, + {root, key.ID(), longNameShortened}, + {"targets", "..." + strings.Repeat("/a", 11), key.ID(), keyStores[0].Name()}, + {"snapshot", "short/gun", key.ID(), longNameShortened}, + } + + var b bytes.Buffer + prettyPrintKeys(keyStores, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, len(expected)+2) + + // starts with headers + assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), + []string{"ROLE", "GUN", "KEY", "ID", "LOCATION"})) + assert.Equal(t, "----", lines[1][:4]) + + for i, line := range lines[2:] { + // we are purposely not putting spaces in test data so easier to split + splitted := strings.Fields(line) + for j, v := range splitted { + assert.Equal(t, expected[i][j], strings.TrimSpace(v)) + } + } +} + +// If there are no keys in any of the key stores, a message that there are no +// signing keys should be displayed. +func TestPrettyPrintZeroKeys(t *testing.T) { + ret := passphrase.ConstantRetriever("pass") + emptyKeyStore := trustmanager.NewKeyMemoryStore(ret) + + var b bytes.Buffer + prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, 1) + assert.Equal(t, "No signing keys found.", lines[0]) +}