Documentation ¶
Overview ¶
Package psv converts arrays of structs or maps into pretty, pipe-separated text tables, and vice-versa.
Example
data := []map[string]string{ {"name":"Joe", "age":"42"}, {"name":"Freddie", "age":"41"}, {"name":"Amy", "age":"don't ask"}, } tbl := table.Marshal(data) tbl.Columns = []string{"age","name"} tbl.Indent = " " tbl.Decorations = []table.Decoration{ {Line:0,Text:`The "who's-who" list of people`}, {Line:1}, {Line:2,Ruler:"+ -"}, {Line:4,Ruler:"|==="}, } fmt.Println(tbl.Encode())
Output
The "who's-who" list of people + --------- + ------- + | age | name | |=====================| | 42 | Joe | | 41 | Freddie | | don't ask | Amy |
The table data and appearance can be set up via any of the following examples:
// convert a string into a table tbl := table.DecodeString(string) // all decorations are retained for re-rendering, e.g. fmt.Println(tbl.Encode()) // convert a string into a data structure // data returned in interface{} table.Unmarshal(string,interface{}) // create a table from scratch (string data only) tbl := table.NewTable() tbl.Indent = "..." tbl.Decorations = []*Decoration{...} tbl.Data = [][]string{...} fmt.Println(tbl.Encode()) // convert a data structure into a table (for rendering) tbl := table.Marshal(interface{}) // optionally modify aspects of the table tbl.Indent = "..." tbl.Decorations = []*Decoration{...} fmt.Println(tbl.Encode())
References ¶
This package focusses on data representation, human readability and the exchange of intentions, which a computer may incidentally be able to make sense of. Mostly, each cell of a table is a simple, single string value, however, with a bit of additional work, the string values may be mapped to slices or maps of slightly more complicated (incl. custom) structures.
The lack of support for every last, complicated, nested structure is intentional.
There a large number of csv, tsv, dsv packages availble on pkg.go.dev, but they all seem to concentrate on "machine readable data exchange". Nevertheless, they served as inspiration for this package as well.
psv always *felt* like a package I should not have to write, but I was unable to find an existing program with suitable features:
- simple to use, suitable for as an editor plugin / shell pipe - align columnated data - while ignoring lines which aren't columnated
The unix tools [column] and [tbl] and go's own encoding/csv package all served as a good basis, but they all follow different goals.
Basic Concepts ¶
Table is the central struct provided by psv. With a Table struct you can then add rows of data as well as Decorations (non-table rows to be interspersed before, between or after the data rows) and additional formatting such as indents.
Creating Tables ¶
Reading Tables ¶
The table parser expects an bufio.Scanner, from which it will extract all rows of data (beginning with a '|' character), while retaining enough information to re-construct the original text it was given.
For convenience, psv.TableFromString() may be used to parse in-situ tables, ideal for testing etc.
e.g.
colors, _ := psv.TableFromString(` | color | rgb | hue | | ----- | --- | --- | | red | f00 | 0 | | green | 0f0 | 120 | | blue | 00f | 240 | `) for _, row := range colors.DataRows()[1:] { // [1:] skips the header row ... }
Index ¶
- type Decoration
- type Indenter
- type Options
- type Ruler
- type Scanner
- type Section
- type Sorter
- type Table
- func (tbl *Table) AnalyseData() (rows, cols int, width []int)
- func (tbl *Table) AppendDataRow(row []string)
- func (tbl *Table) AppendRuler(ruler string)
- func (tbl *Table) AppendText(textLines ...string)
- func (tbl *Table) Clone() *Table
- func (tbl *Table) ColumnNames() []string
- func (tbl *Table) DataRows() [][]string
- func (tbl *Table) DecodeString(input string) error
- func (tbl *Table) Encode() string
- func (tbl *Table) NewSorter() *Sorter
- func (t *Table) Read(in Scanner) error
- func (tbl *Table) Sections() []*Section
- func (tbl *Table) Sort()
- func (tbl *Table) Write(b Writer)
- type Writer
- Bugs
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Decoration ¶
type Decoration struct { Line int // 1-based number of this line's position in the resulting table Indent string // only used to re-construct original indentation outside a table's bounds Text string // un-indented text to be inserted into the table, may be "" Ruler string // ruler specification, see Ruler type }
Decoration specifies a single non-table text line to be positioned at a specific point in the generated table string.
func (*Decoration) Clone ¶ added in v0.1.1
func (d *Decoration) Clone() *Decoration
type Indenter ¶
type Indenter struct {
// contains filtered or unexported fields
}
Indenter is used to track how a table was, and should be, indented.
When decoding tables, the Table's Indenter is used to automatically detect and track the indent of the first line of the table.
When encoding tables, the Table's Indenter provides the desired indentation for each table row.
In addition, the Indenter is also responsible for detecting and reconstructing a consistent 'comment style', if it was provided with a non-whitespace indent pattern.
Example ¶
package main import ( "fmt" "codeberg.org/japh/psv" ) func main() { inputLines := []string{ ``, `hello`, ` hello`, `// hello`, `// // hello`, ` // hello`, } patterns := []struct { pattern string isFinal bool }{ {pattern: ""}, {pattern: ``, isFinal: true}, {pattern: `0`}, {pattern: `-1`}, {pattern: `-0`}, {pattern: `+1`}, {pattern: `1`}, {pattern: `63`}, {pattern: `64`}, {pattern: ` `}, {pattern: `//`}, {pattern: ` //`}, {pattern: ` // `}, {pattern: `> >`}, {pattern: ` > >`}, {pattern: ` > > `}, } PATTERNS: for _, p := range patterns { for _, l := range inputLines { i := psv.NewIndenter() fmt.Printf("indent option: %q\n", p.pattern) if p.pattern != "" || p.isFinal { err := i.SetIndent(p.pattern) if err != nil { fmt.Printf("failed to set indent: %q\n\n", err) continue PATTERNS } } fmt.Printf("indenter: %s\n", i.String()) found := i.FindIndent(l) i.FinalizeIndent() fmt.Printf("input line: %q\n", l) fmt.Printf("detected indent: %q\n", found) fmt.Printf("finalised indent: %q\n", i.Indent()+l[len(found):]) fmt.Println() } fmt.Println("----") } }
Output: indent option: "" indenter: "" (default) input line: "" detected indent: "" finalised indent: "" indent option: "" indenter: "" (default) input line: "hello" detected indent: "" finalised indent: "hello" indent option: "" indenter: "" (default) input line: " hello" detected indent: " " finalised indent: " hello" indent option: "" indenter: "" (default) input line: "// hello" detected indent: "" finalised indent: "// hello" indent option: "" indenter: "" (default) input line: "// // hello" detected indent: "" finalised indent: "// // hello" indent option: "" indenter: "" (default) input line: " // hello" detected indent: " " finalised indent: " // hello" ---- indent option: "" indenter: "" (finalised) input line: "" detected indent: "" finalised indent: "" indent option: "" indenter: "" (finalised) input line: "hello" detected indent: "" finalised indent: "hello" indent option: "" indenter: "" (finalised) input line: " hello" detected indent: " " finalised indent: "hello" indent option: "" indenter: "" (finalised) input line: "// hello" detected indent: "" finalised indent: "// hello" indent option: "" indenter: "" (finalised) input line: "// // hello" detected indent: "" finalised indent: "// // hello" indent option: "" indenter: "" (finalised) input line: " // hello" detected indent: " " finalised indent: "// hello" ---- indent option: "0" indenter: "" (finalised) input line: "" detected indent: "" finalised indent: "" indent option: "0" indenter: "" (finalised) input line: "hello" detected indent: "" finalised indent: "hello" indent option: "0" indenter: "" (finalised) input line: " hello" detected indent: " " finalised indent: "hello" indent option: "0" indenter: "" (finalised) input line: "// hello" detected indent: "" finalised indent: "// hello" indent option: "0" indenter: "" (finalised) input line: "// // hello" detected indent: "" finalised indent: "// // hello" indent option: "0" indenter: "" (finalised) input line: " // hello" detected indent: " " finalised indent: "// hello" ---- indent option: "-1" failed to set indent: "indent must be an unsigned integer <64 or non-numeric string" indent option: "-0" failed to set indent: "indent must be an unsigned integer <64 or non-numeric string" indent option: "+1" failed to set indent: "indent must be an unsigned integer <64 or non-numeric string" indent option: "1" indenter: " " (finalised) input line: "" detected indent: "" finalised indent: " " indent option: "1" indenter: " " (finalised) input line: "hello" detected indent: "" finalised indent: " hello" indent option: "1" indenter: " " (finalised) input line: " hello" detected indent: " " finalised indent: " hello" indent option: "1" indenter: " " (finalised) input line: "// hello" detected indent: "" finalised indent: " // hello" indent option: "1" indenter: " " (finalised) input line: "// // hello" detected indent: "" finalised indent: " // // hello" indent option: "1" indenter: " " (finalised) input line: " // hello" detected indent: " " finalised indent: " // hello" ---- indent option: "63" indenter: " " (finalised) input line: "" detected indent: "" finalised indent: " " indent option: "63" indenter: " " (finalised) input line: "hello" detected indent: "" finalised indent: " hello" indent option: "63" indenter: " " (finalised) input line: " hello" detected indent: " " finalised indent: " hello" indent option: "63" indenter: " " (finalised) input line: "// hello" detected indent: "" finalised indent: " // hello" indent option: "63" indenter: " " (finalised) input line: "// // hello" detected indent: "" finalised indent: " // // hello" indent option: "63" indenter: " " (finalised) input line: " // hello" detected indent: " " finalised indent: " // hello" ---- indent option: "64" failed to set indent: "indent must be an unsigned integer <64 or non-numeric string" indent option: " " indenter: " " (finalised) input line: "" detected indent: "" finalised indent: " " indent option: " " indenter: " " (finalised) input line: "hello" detected indent: "" finalised indent: " hello" indent option: " " indenter: " " (finalised) input line: " hello" detected indent: " " finalised indent: " hello" indent option: " " indenter: " " (finalised) input line: "// hello" detected indent: "" finalised indent: " // hello" indent option: " " indenter: " " (finalised) input line: "// // hello" detected indent: "" finalised indent: " // // hello" indent option: " " indenter: " " (finalised) input line: " // hello" detected indent: " " finalised indent: " // hello" ---- indent option: "//" indenter: "//" (default) input line: "" detected indent: "" finalised indent: "// " indent option: "//" indenter: "//" (default) input line: "hello" detected indent: "" finalised indent: "// hello" indent option: "//" indenter: "//" (default) input line: " hello" detected indent: " " finalised indent: "// hello" indent option: "//" indenter: "//" (default) input line: "// hello" detected indent: "// " finalised indent: "// hello" indent option: "//" indenter: "//" (default) input line: "// // hello" detected indent: "// // " finalised indent: "// // hello" indent option: "//" indenter: "//" (default) input line: " // hello" detected indent: " // " finalised indent: " // hello" ---- indent option: " //" indenter: " //" (finalised) input line: "" detected indent: "" finalised indent: " //" indent option: " //" indenter: " //" (finalised) input line: "hello" detected indent: "" finalised indent: " //hello" indent option: " //" indenter: " //" (finalised) input line: " hello" detected indent: " " finalised indent: " //hello" indent option: " //" indenter: " //" (finalised) input line: "// hello" detected indent: "// " finalised indent: " //hello" indent option: " //" indenter: " //" (finalised) input line: "// // hello" detected indent: "// // " finalised indent: " //hello" indent option: " //" indenter: " //" (finalised) input line: " // hello" detected indent: " // " finalised indent: " //hello" ---- indent option: " // " indenter: " // " (finalised) input line: "" detected indent: "" finalised indent: " // " indent option: " // " indenter: " // " (finalised) input line: "hello" detected indent: "" finalised indent: " // hello" indent option: " // " indenter: " // " (finalised) input line: " hello" detected indent: " " finalised indent: " // hello" indent option: " // " indenter: " // " (finalised) input line: "// hello" detected indent: "// " finalised indent: " // hello" indent option: " // " indenter: " // " (finalised) input line: "// // hello" detected indent: "// // " finalised indent: " // hello" indent option: " // " indenter: " // " (finalised) input line: " // hello" detected indent: " // " finalised indent: " // hello" ---- indent option: "> >" indenter: "> >" (default) input line: "" detected indent: "" finalised indent: "> > " indent option: "> >" indenter: "> >" (default) input line: "hello" detected indent: "" finalised indent: "> > hello" indent option: "> >" indenter: "> >" (default) input line: " hello" detected indent: " " finalised indent: "> > hello" indent option: "> >" indenter: "> >" (default) input line: "// hello" detected indent: "" finalised indent: "> > // hello" indent option: "> >" indenter: "> >" (default) input line: "// // hello" detected indent: "" finalised indent: "> > // // hello" indent option: "> >" indenter: "> >" (default) input line: " // hello" detected indent: " " finalised indent: "> > // hello" ---- indent option: " > >" indenter: " > >" (finalised) input line: "" detected indent: "" finalised indent: " > >" indent option: " > >" indenter: " > >" (finalised) input line: "hello" detected indent: "" finalised indent: " > >hello" indent option: " > >" indenter: " > >" (finalised) input line: " hello" detected indent: " " finalised indent: " > >hello" indent option: " > >" indenter: " > >" (finalised) input line: "// hello" detected indent: "" finalised indent: " > >// hello" indent option: " > >" indenter: " > >" (finalised) input line: "// // hello" detected indent: "" finalised indent: " > >// // hello" indent option: " > >" indenter: " > >" (finalised) input line: " // hello" detected indent: " " finalised indent: " > >// hello" ---- indent option: " > > " indenter: " > > " (finalised) input line: "" detected indent: "" finalised indent: " > > " indent option: " > > " indenter: " > > " (finalised) input line: "hello" detected indent: "" finalised indent: " > > hello" indent option: " > > " indenter: " > > " (finalised) input line: " hello" detected indent: " " finalised indent: " > > hello" indent option: " > > " indenter: " > > " (finalised) input line: "// hello" detected indent: "" finalised indent: " > > // hello" indent option: " > > " indenter: " > > " (finalised) input line: "// // hello" detected indent: "" finalised indent: " > > // // hello" indent option: " > > " indenter: " > > " (finalised) input line: " // hello" detected indent: " " finalised indent: " > > // hello" ----
func (*Indenter) FinalizeIndent ¶
func (i *Indenter) FinalizeIndent()
FinalizeIndent is called while decoding, to indicate that the first table row has been detected.
The most recently detected indent will then be retained and used for table row encoding.
func (*Indenter) FindIndent ¶
FindIndent checks if the beginning of a line of text matches the current indent pattern and returns the indent string.
The returned string is used to then split the line into <indent> and <text>, and may be used to re-construct the original line if it lies before or after the table's rows
func (*Indenter) SetIndent ¶
SetIndent explicitly sets a desired indent to use.
If SetIndent has been called, the Indenter will match indents according to the provided pattern, and will re-create indents using the provided pattern.
Special situations:
the pattern is just whitesspace (including "")
leading whitespace will be removed when decoding
the indent string will be used, as provided, when encoding
the pattern provided is just a number `n`
leading whitespace will be removed when decoding
the encoding indent will be `n` of spaces
the pattern contains non-whitespace characters
any instances of the non-whitespace pattern will be skipped at the beginning of each line
if the pattern has no leading or trailing whitespace
the skipped indent from the first table row will be re-used to indent all table rows
if the pattern has a leading whitespace
the indent string will be used as provided
the whitespace padding between the indent and the start of text will be retained from the first table row
if the pattern has a trailing whitespace
only the whitespace before the first non-whitespace character found will be re-used for indenting table rows
the indent string will then be used as provided
if the pattern has leading and trailing whitespace
then the indent will be used, unmodified, for each table row
Example (Remove_indents) ¶
package main import ( "codeberg.org/japh/psv" ) func main() { testCases := []string{ "", // explicitly request an empty-string indent "0", // explicitly set the indent width to 0 } for _, testIndent := range testCases { tbl := psv.NewTable() tbl.SetIndent(testIndent) tbl.DecodeString(``) } }
Output:
type Options ¶
type Options struct { IndentPattern string // user-provided indent to use IndentIsFinal bool // flag to force the use of Options.Indent (required to force the use of "") Squash bool // squash multiple blank lines into a single blank line when encoding Sort bool // sort data rows, see also: SortColumns and SortSections SortLanguage string // the BCP 47 locale to use for collation rules SortColumns []string // list of column names or numbers to use for sorting SortSections []string // list of section names or numbers to use for sorting }
Options is used by Table to store user specified configuration settings
type Ruler ¶
type Ruler struct {
// contains filtered or unexported fields
}
Ruler represents a horizontal separator to be placed within a table.
Rulers only contain the characters |, :, *, +, - and optional spaces.
When a table is rendered, rulers are generated to fit the column widths of the table.
If the input string contains 4 runes or less, it is interpreted as a format:
e.g. '| -:' |||+-- internal column separator ||+--- horizonal line |+---- padding +----- outer border
If the input string is more than 4 runes, it is assumed to be a previously-generated ruler, and is analysed to determine the format.
If the format of a ruler cannot be determined, the 'border' rune will be 0 and the original input is used. (non-destructive failure)
func NewRuler ¶
NewRuler creates a new ruler based on a format or previously rendered ruler. Note: a returned error is really only a warning! The returned *Ruler is still valid and usable!
type Scanner ¶
Scanner is the interface required to read an input source, 1 line at a time. Subset of bufio.Scanner
type Section ¶ added in v0.1.1
type Section struct {
// contains filtered or unexported fields
}
func (*Section) ColumnNames ¶ added in v0.1.1
type Sorter ¶ added in v0.1.1
type Sorter struct {
// contains filtered or unexported fields
}
func (*Sorter) InSections ¶ added in v0.1.1
type Table ¶
type Table struct { Options // configuration options Data [][]string // 2-Dimensional array of string cells indexed by [row][column] Decorations []*Decoration // an array of decorations to add to the table *Indenter // indent matcher and producer // contains filtered or unexported fields }
Table is a structure used to encapsulate a table of string data, which can be re-rendered with correct indentation and column alignment. The exported fields of Table may be used to customize the rendered result.
func New ¶ added in v0.1.1
func New() *Table
New creates a new, empty table. Use its public fields to setup the data to be printed.
See also: encode_test.go
func TableFromString ¶ added in v0.1.0
TableFromString Creates a new table from a string containing a psv table. This is the recommended way to read in-situ tables. e.g.
tbl, err := psv.FromString(` | 1 | one | | 2 | two | : : `)
func (*Table) AnalyseData ¶
AnalyseData was only created for testing (to allow access to the internal workings of encode()).
Never call this - it provides nothing of use.
func (*Table) AppendDataRow ¶ added in v0.1.1
AppendDataRow adds a single row of data to the end of the table A row is just a []string Rows do not have to have a specific number of columns, all rows are normalised to a consistent width before encoding
func (*Table) AppendRuler ¶ added in v0.1.1
AppendRuler add a ruler decoration line to the end of the table.
Rulers are specified by an up-to 4 character string, with 1 character each for the outer borders, column padding, horizonal lines and internal column separator.
e.g.:
// ,----- outer border // |,---- padding // ||,--- horizonal line // |||,-- internal column separator // |||| tbl.AppendRuler("| -:")
func (*Table) AppendText ¶ added in v0.1.1
AppendText adds any number of text decoration lines to the end of the table.
func (*Table) ColumnNames ¶ added in v0.1.1
func (*Table) DataRows ¶ added in v0.1.1
DataRows returns the [][]string of data in the table. The rows returned are guaranteed to all have the same number of columns
tbl := psv.TableFromString(...).DataRows() for _, row := range tbl { for _, cell := range row { ... } } }
func (*Table) DecodeString ¶ added in v0.1.0
DecodeString extracts data from a block of text containing a PSV table
func (*Table) Encode ¶
Encode formats a Table struct into a multi-line string
Table.Data is converted into a formatted table Table.Decorations are interspersed and indented according to the formatting rules.
Example (Encoding_decorations) ¶
package main import ( "fmt" "codeberg.org/japh/psv" ) func main() { tbl := psv.NewTable() tbl.SetIndent("....") tbl.Decorations = []*psv.Decoration{ // column headers {Line: 2, Ruler: "+ ="}, {Line: 3}, {Line: 4, Text: "Empty rows are kept"}, {Line: 5, Text: "Decorations appear between rows"}, {Line: 7}, {Line: 7, Text: "# Comments are always good"}, {Line: 7, Text: "# repeating line numbers is not good, but works"}, // data... {Line: 12}, {Line: 12, Text: "leading and trailing spaces need extra quotes"}, {Line: 15}, {Line: 15, Text: "Rulers can be placed anywhere"}, {Line: 15, Ruler: "+ -"}, {Line: 20, Text: "and trailing text lines are no problem either"}, } tbl.Data = [][]string{ {"one", "foo", "two", "three"}, // first row are headers only {}, // empty row {"the first", "", "the second", "the third"}, // full row {"more data"}, // partial row {`" "`, `" x "`, `'"'`, `" x"`}, // handling of leading and trailing spaces {"lorem", "", "ipsum", "upsum", "oopsum"}, // extra column! } fmt.Print(tbl.Encode()) }
Output: ....| one | foo | two | three | | ....+ ========= + ===== + ========== + ========= + ====== + ....Empty rows are kept ....Decorations appear between rows ....| | | | | | ....# Comments are always good ....# repeating line numbers is not good, but works ....| the first | | the second | the third | | ....| more data | | | | | ....leading and trailing spaces need extra quotes ....| " " | " x " | '"' | " x" | | ....Rulers can be placed anywhere ....+ --------- + ----- + ---------- + --------- + ------ + ....| lorem | | ipsum | upsum | oopsum | and trailing text lines are no problem either
Notes ¶
Bugs ¶
2022-02-14 it should be possible to just use a slice over the underlying []byte array instead of having to peice the indent and text of a decoration line back together