mirror of https://github.com/docker/docs.git
Merge pull request #279 from docker/pretty-print
Pretty print output of `notary key list`.
This commit is contained in:
commit
daa844079f
|
@ -177,6 +177,10 @@
|
||||||
"ImportPath": "github.com/mitchellh/mapstructure",
|
"ImportPath": "github.com/mitchellh/mapstructure",
|
||||||
"Rev": "2caf8efc93669b6c43e0441cdc6aed17546c96f3"
|
"Rev": "2caf8efc93669b6c43e0441cdc6aed17546c96f3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/olekukonko/tablewriter",
|
||||||
|
"Rev": "a5eefc286b03d5560735698ef36c83728a6ae560"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/prometheus/client_golang/prometheus",
|
"ImportPath": "github.com/prometheus/client_golang/prometheus",
|
||||||
"Comment": "0.7.0-53-g449ccef",
|
"Comment": "0.7.0-53-g449ccef",
|
||||||
|
|
8
Godeps/_workspace/src/github.com/olekukonko/tablewriter/.travis.yml
generated
vendored
Normal file
8
Godeps/_workspace/src/github.com/olekukonko/tablewriter/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.1
|
||||||
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- tip
|
19
Godeps/_workspace/src/github.com/olekukonko/tablewriter/LICENCE.md
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/olekukonko/tablewriter/LICENCE.md
generated
vendored
Normal file
|
@ -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.
|
141
Godeps/_workspace/src/github.com/olekukonko/tablewriter/README.md
generated
vendored
Normal file
141
Godeps/_workspace/src/github.com/olekukonko/tablewriter/README.md
generated
vendored
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
ASCII Table Writer
|
||||||
|
=========
|
||||||
|
|
||||||
|
[](https://travis-ci.org/olekukonko/tablewriter) [](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
|
|
@ -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
|
||||||
|
}
|
43
Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/README.md
generated
vendored
Normal file
43
Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/README.md
generated
vendored
Normal file
|
@ -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 |
|
||||||
|
+-------+------+-----+
|
84
Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/csv2table.go
generated
vendored
Normal file
84
Godeps/_workspace/src/github.com/olekukonko/tablewriter/csv2table/csv2table.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
first_name,last_name,ssn
|
||||||
|
John,Barry,123456
|
||||||
|
Kathy,Smith,687987
|
||||||
|
Bob,McCornick,3979870
|
|
4
Godeps/_workspace/src/github.com/olekukonko/tablewriter/test_info.csv
generated
vendored
Normal file
4
Godeps/_workspace/src/github.com/olekukonko/tablewriter/test_info.csv
generated
vendored
Normal file
|
@ -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,
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -162,23 +161,56 @@ func splitLines(chunk string) []string {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// List keys, parses the output, and returns the keys as an array of root key
|
// List keys, parses the output, and returns the unique key IDs as an array
|
||||||
// IDs and an array of signing key IDs
|
// of root key IDs and an array of signing key IDs. Output expected looks like:
|
||||||
func GetKeys(t *testing.T, tempDir string) ([]string, []string) {
|
// 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")
|
output, err := runCommand(t, tempDir, "key", "list")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
lines := splitLines(output)
|
||||||
parts := strings.Split(output, "# Signing keys:")
|
if len(lines) == 1 && lines[0] == "No signing keys found." {
|
||||||
assert.Len(t, parts, 2)
|
return []string{}, []string{}
|
||||||
|
}
|
||||||
fixed := make([][]string, 2)
|
if len(lines) < 3 { // 2 lines of header, at least 1 line with keys
|
||||||
for i, part := range parts {
|
t.Logf("This output is not what is expected by the test:\n%s", output)
|
||||||
fixed[i] = splitLines(
|
|
||||||
strings.TrimPrefix(strings.TrimSpace(part), "# Root keys:"))
|
|
||||||
sort.Strings(fixed[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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,
|
func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int,
|
||||||
rootOnDisk bool) ([]string, []string) {
|
rootOnDisk bool) ([]string, []string) {
|
||||||
|
|
||||||
uniqueKeys := make(map[string]struct{})
|
root, signing := getUniqueKeys(t, tempDir)
|
||||||
root, signing := GetKeys(t, tempDir)
|
assert.Len(t, root, numRoot)
|
||||||
assert.Len(t, signing, numSigning)
|
assert.Len(t, signing, numSigning)
|
||||||
for i, rootKeyLine := range root {
|
for _, rootKeyID := range root {
|
||||||
keyID := strings.Split(rootKeyLine, "-")[0]
|
|
||||||
keyID = strings.TrimSpace(keyID)
|
|
||||||
root[i] = keyID
|
|
||||||
uniqueKeys[keyID] = struct{}{}
|
|
||||||
_, err := os.Stat(filepath.Join(
|
_, 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
|
// 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
|
// exist, and hence doesn't actually the right funciton to use here
|
||||||
assert.Equal(t, rootOnDisk, !os.IsNotExist(err))
|
assert.Equal(t, rootOnDisk, !os.IsNotExist(err))
|
||||||
|
|
||||||
// this function is declared is in the build-tagged setup files
|
// 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
|
return root, signing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -13,6 +15,7 @@ import (
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
|
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,6 +91,110 @@ var cmdKeyImportRoot = &cobra.Command{
|
||||||
Run: keysImportRoot,
|
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) {
|
func keysList(cmd *cobra.Command, args []string) {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
cmd.Usage()
|
cmd.Usage()
|
||||||
|
@ -97,42 +204,9 @@ func keysList(cmd *cobra.Command, args []string) {
|
||||||
parseConfig()
|
parseConfig()
|
||||||
|
|
||||||
stores := getKeyStores(cmd, mainViper.GetString("trust_dir"), retriever, true)
|
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("")
|
||||||
cmd.Println("# Root keys: ")
|
prettyPrintKeys(stores, cmd.Out())
|
||||||
for store, keysMap := range keys {
|
|
||||||
for k, v := range keysMap {
|
|
||||||
if v == "root" {
|
|
||||||
cmd.Println(k, "-", store.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("")
|
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) {
|
func keysGenerateRootKey(cmd *cobra.Command, args []string) {
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
Loading…
Reference in New Issue