feat(list): port over to go-pretty

This commit is contained in:
Lewis Wynne 2025-11-07 13:49:43 +00:00
parent 4ace97bddc
commit 5ba0ff1e31
4 changed files with 101 additions and 75 deletions

View file

@ -24,10 +24,9 @@ package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"text/tabwriter"
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -58,16 +57,10 @@ func list(cmd *cobra.Command, args []string) error {
targetDB = "@" + dbName targetDB = "@" + dbName
} }
delimiter, err := cmd.Flags().GetString("delimiter")
if err != nil {
return err
}
showSecrets, err := cmd.Flags().GetBool("secret") showSecrets, err := cmd.Flags().GetBool("secret")
if err != nil { if err != nil {
return err return err
} }
noKeys, err := cmd.Flags().GetBool("no-keys") noKeys, err := cmd.Flags().GetBool("no-keys")
if err != nil { if err != nil {
return err return err
@ -80,36 +73,45 @@ func list(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
binary, err := cmd.Flags().GetBool("binary") noHeader, err := cmd.Flags().GetBool("no-header")
if err != nil { if err != nil {
return err return err
} }
includeBinary, err := cmd.Flags().GetBool("binary")
if err != nil {
return err
}
format, err := cmd.Flags().GetString("format")
if err != nil {
return err
}
switch format {
case "auto", "table", "tabular", "csv", "html", "markdown", "md":
default:
return fmt.Errorf("unsupported format %q", format)
}
includeKey := !noKeys includeKey := !noKeys
includeValue := !noValues includeValue := !noValues
if !includeKey && !includeValue && !showTTL { if !includeKey && !includeValue && !showTTL {
return fmt.Errorf("no columns selected; disable --no-keys/--no-values or pass --ttl") return fmt.Errorf("no columns selected; disable --no-keys/--no-values or pass --ttl")
} }
prefetchVals := includeValue
columnKinds := selectColumns(includeKey, includeValue, showTTL) columnKinds := selectColumns(includeKey, includeValue, showTTL)
if len(columnKinds) == 0 { if len(columnKinds) == 0 {
return fmt.Errorf("no columns selected; enable key, value, or ttl output") return fmt.Errorf("no columns selected; enable key, value, or ttl output")
} }
delimiterBytes := []byte(delimiter) tw := table.NewWriter()
columnCount := len(columnKinds) tw.SetOutputMirror(cmd.OutOrStdout())
if len(delimiterBytes) > 0 && columnCount > 1 { configureListTable(tw)
columnCount = columnCount*2 - 1
if !noHeader {
header := buildHeaderCells(columnKinds)
tw.AppendHeader(stringSliceToRow(header))
} }
format := buildTabbedFormat(columnCount)
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) placeholder := "[secret: pass --secret to view]"
defer writer.Flush()
placeholder := []byte("**********")
header := insertDelimiters(buildHeaderCells(columnKinds), delimiterBytes)
store.PrintTo(writer, format, false, header...)
trans := TransactionArgs{ trans := TransactionArgs{
key: targetDB, key: targetDB,
@ -118,16 +120,17 @@ func list(cmd *cobra.Command, args []string) error {
transact: func(tx *badger.Txn, k []byte) error { transact: func(tx *badger.Txn, k []byte) error {
opts := badger.DefaultIteratorOptions opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 10 opts.PrefetchSize = 10
opts.PrefetchValues = prefetchVals opts.PrefetchValues = includeValue
it := tx.NewIterator(opts) it := tx.NewIterator(opts)
defer it.Close() defer it.Close()
var valueBuf []byte var valueBuf []byte
for it.Rewind(); it.Valid(); it.Next() { for it.Rewind(); it.Valid(); it.Next() {
item := it.Item() item := it.Item()
key := item.KeyCopy(nil) key := string(item.KeyCopy(nil))
meta := item.UserMeta() meta := item.UserMeta()
isSecret := meta&metaSecret != 0 isSecret := meta&metaSecret != 0
valueBuf = valueBuf[:0]
var valueStr string
if includeValue && (!isSecret || showSecrets) { if includeValue && (!isSecret || showSecrets) {
if err := item.Value(func(v []byte) error { if err := item.Value(func(v []byte) error {
valueBuf = append(valueBuf[:0], v...) valueBuf = append(valueBuf[:0], v...)
@ -135,8 +138,10 @@ func list(cmd *cobra.Command, args []string) error {
}); err != nil { }); err != nil {
return err return err
} }
valueStr = store.FormatBytes(includeBinary, valueBuf)
} }
columns := make([][]byte, 0, len(columnKinds))
columns := make([]string, 0, len(columnKinds))
for _, column := range columnKinds { for _, column := range columnKinds {
switch column { switch column {
case columnKey: case columnKey:
@ -145,29 +150,44 @@ func list(cmd *cobra.Command, args []string) error {
if isSecret && !showSecrets { if isSecret && !showSecrets {
columns = append(columns, placeholder) columns = append(columns, placeholder)
} else { } else {
columns = append(columns, valueBuf) columns = append(columns, valueStr)
} }
case columnTTL: case columnTTL:
columns = append(columns, []byte(formatExpiry(item.ExpiresAt()))) columns = append(columns, formatExpiry(item.ExpiresAt()))
} }
} }
row := insertDelimiters(columns, delimiterBytes)
store.PrintTo(writer, format, binary, row...) tw.AppendRow(stringSliceToRow(columns))
} }
return nil return nil
}, },
} }
return store.Transaction(trans) if err := store.Transaction(trans); err != nil {
return err
}
switch format {
case "auto", "table", "tabular":
tw.Render()
case "csv":
tw.RenderCSV()
case "html":
tw.RenderHTML()
case "markdown", "md":
tw.RenderMarkdown()
}
return nil
} }
func init() { func init() {
listCmd.Flags().BoolP("binary", "b", false, "include binary data in text output") listCmd.Flags().BoolP("binary", "b", false, "include binary data in text output")
listCmd.Flags().StringP("delimiter", "d", "", "string inserted between columns") listCmd.Flags().BoolP("secret", "S", false, "display values marked as secret")
listCmd.Flags().Bool("secret", false, "display values marked as secret")
listCmd.Flags().Bool("no-keys", false, "suppress the key column") listCmd.Flags().Bool("no-keys", false, "suppress the key column")
listCmd.Flags().Bool("no-values", false, "suppress the value column") listCmd.Flags().Bool("no-values", false, "suppress the value column")
listCmd.Flags().Bool("ttl", false, "append a TTL column when entries expire") listCmd.Flags().BoolP("ttl", "t", false, "append a TTL column when entries expire")
listCmd.Flags().Bool("no-header", false, "omit the header rows")
listCmd.Flags().StringP("format", "f", "table", "supports: table, csv, html, markdown")
rootCmd.AddCommand(listCmd) rootCmd.AddCommand(listCmd)
} }
@ -193,46 +213,32 @@ func selectColumns(includeKey, includeValue, showTTL bool) []columnKind {
return columns return columns
} }
func buildTabbedFormat(cols int) string { func buildHeaderCells(columnKinds []columnKind) []string {
if cols <= 0 { labels := make([]string, 0, len(columnKinds))
return "\n"
}
var b strings.Builder
for i := 0; i < cols; i++ {
if i > 0 {
b.WriteByte('\t')
}
b.WriteString("%s")
}
b.WriteByte('\n')
return b.String()
}
func insertDelimiters(columns [][]byte, delimiter []byte) [][]byte {
if len(delimiter) == 0 || len(columns) <= 1 {
return columns
}
out := make([][]byte, 0, len(columns)*2-1)
for i, col := range columns {
out = append(out, col)
if i < len(columns)-1 {
out = append(out, delimiter)
}
}
return out
}
func buildHeaderCells(columnKinds []columnKind) [][]byte {
headers := make([][]byte, 0, len(columnKinds))
for _, column := range columnKinds { for _, column := range columnKinds {
switch column { switch column {
case columnKey: case columnKey:
headers = append(headers, []byte("Key")) labels = append(labels, "Key")
case columnValue: case columnValue:
headers = append(headers, []byte("Value")) labels = append(labels, "Value")
case columnTTL: case columnTTL:
headers = append(headers, []byte("TTL")) labels = append(labels, "TTL")
} }
} }
return headers return labels
}
func stringSliceToRow(values []string) table.Row {
row := make(table.Row, len(values))
for i, val := range values {
row[i] = val
}
return row
}
func configureListTable(tw table.Writer) {
tw.SetStyle(table.StyleColoredBright)
tw.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: 20},
})
} }

View file

@ -98,15 +98,10 @@ func (s *Store) Print(pf string, includeBinary bool, vs ...[]byte) {
} }
func (s *Store) PrintTo(w io.Writer, pf string, includeBinary bool, vs ...[]byte) { func (s *Store) PrintTo(w io.Writer, pf string, includeBinary bool, vs ...[]byte) {
nb := "(omitted binary data)"
fvs := make([]any, 0, len(vs))
tty := term.IsTerminal(int(os.Stdout.Fd())) tty := term.IsTerminal(int(os.Stdout.Fd()))
fvs := make([]any, 0, len(vs))
for _, v := range vs { for _, v := range vs {
if tty && !includeBinary && !utf8.Valid(v) { fvs = append(fvs, s.formatBytes(includeBinary, v))
fvs = append(fvs, nb)
} else {
fvs = append(fvs, string(v))
}
} }
fmt.Fprintf(w, pf, fvs...) fmt.Fprintf(w, pf, fvs...)
if w == os.Stdout && tty && !strings.HasSuffix(pf, "\n") { if w == os.Stdout && tty && !strings.HasSuffix(pf, "\n") {
@ -114,6 +109,18 @@ func (s *Store) PrintTo(w io.Writer, pf string, includeBinary bool, vs ...[]byte
} }
} }
func (s *Store) FormatBytes(includeBinary bool, v []byte) string {
return s.formatBytes(includeBinary, v)
}
func (s *Store) formatBytes(includeBinary bool, v []byte) string {
tty := term.IsTerminal(int(os.Stdout.Fd()))
if tty && !includeBinary && !utf8.Valid(v) {
return "(omitted binary data)"
}
return string(v)
}
func (s *Store) AllStores() ([]string, error) { func (s *Store) AllStores() ([]string, error) {
path, err := s.path() path, err := s.path()
if err != nil { if err != nil {

4
go.mod
View file

@ -12,9 +12,12 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jedib0t/go-pretty/v6 v6.7.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/muesli/go-app-paths v0.2.2 // indirect github.com/muesli/go-app-paths v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect github.com/spf13/pflag v1.0.9 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@ -24,5 +27,6 @@ require (
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
) )

9
go.sum
View file

@ -18,12 +18,19 @@ github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6F
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty/v6 v6.7.0 h1:DanoN1RnjXTwDN+B8yqtixXzXqNBCs2Vxo2ARsnrpsY=
github.com/jedib0t/go-pretty/v6 v6.7.0/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI= github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho= github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
@ -45,6 +52,8 @@ golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=