added -N flag, added -m template support to get and list commands

This commit is contained in:
2024-12-20 11:53:09 +01:00
parent ba39e3f8cd
commit d94868132d
7 changed files with 143 additions and 70 deletions

View File

@@ -16,6 +16,7 @@ reasons:
often, which is not good for a tool intended to be used for many
years.
- more features:
- output table in list mode uses <tab> separator
- better STDIN + pipe support
- supports JSON output
- supports more verbose tabular output
@@ -23,6 +24,7 @@ reasons:
- tagging
- filtering using tags
- encryption of entries
- templates for custom output for maximum flexibility
**anydb** can do all the things you can do with skate:
@@ -95,6 +97,19 @@ anydb import -r backup.json
# get command.
anydb set mypassword -e
# using template output mode you can freely design how to print stuff
# here, we print the values in CSV format ONLY if they have some tag
anydb ls -m template -T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created}}{{ end }}"
# or, to simulate skate's -k or -v
anydb ls -m template -T "{{ .Key }}"
anydb ls -m template -T "{{ .Value }}"
# maybe you want to digest the item in a shell script? also
# note, that both the list and get commands support templates
eval $(anydb get foo -m template -T "key='{{ .Key }}' value='{{ .Value }}' ts='{{ .Created}}'")
echo "$key: $value"
# it comes with a manpage builtin
anydb man
```

View File

@@ -13,6 +13,8 @@ import (
bolt "go.etcd.io/bbolt"
)
const MaxValueWidth int = 60
type DB struct {
Debug bool
Dbfile string
@@ -27,6 +29,26 @@ type DbEntry struct {
Bin []byte `json:"bin"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
Size int
}
// Post process an entry for list output.
// Do NOT call it during write processing!
func (entry *DbEntry) Normalize() {
entry.Size = len(entry.Value)
if entry.Encrypted {
entry.Value = "<encrypted-content>"
}
if len(entry.Bin) > 0 {
entry.Value = "<binary-content>"
entry.Size = len(entry.Bin)
}
if len(entry.Value) > MaxValueWidth {
entry.Value = entry.Value[0:MaxValueWidth] + "..."
}
}
type DbEntries []DbEntry

View File

@@ -2,13 +2,15 @@ package cfg
import "github.com/tlinden/anydb/app"
var Version string = "v0.0.2"
var Version string = "v0.0.3"
type Config struct {
Debug bool
Dbfile string
Template string
Mode string // wide, table, yaml, json
NoHeaders bool
NoHumanize bool
Encrypt bool
DB *app.DB
File string

View File

@@ -80,7 +80,7 @@ func Get(conf *cfg.Config) *cobra.Command {
)
var cmd = &cobra.Command{
Use: "get <key> [-o <file>] [-m <mode>] [-n]",
Use: "get <key> [-o <file>] [-m <mode>] [-n -N] [-T <tpl>]",
Short: "Retrieve value for a key",
Long: `Retrieve value for a key`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -124,9 +124,11 @@ func Get(conf *cfg.Config) *cobra.Command {
},
}
cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output to file (ignores -m)")
cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output value to file (ignores -m)")
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (simple|wide|json) (default 'simple')")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
cmd.Aliases = append(cmd.Aliases, "show")
cmd.Aliases = append(cmd.Aliases, "g")
@@ -186,7 +188,7 @@ func Export(conf *cfg.Config) *cobra.Command {
return err
}
return output.WriteFile(&attr, conf, entries)
return output.WriteJSON(&attr, conf, entries)
},
}
@@ -205,7 +207,7 @@ func List(conf *cfg.Config) *cobra.Command {
)
var cmd = &cobra.Command{
Use: "list [-t <tag>] [-o <mode>] [<filter-regex>]",
Use: "list [<filter-regex>] [-t <tag>] [-m <mode>] [-n -N] [-T <tpl>]",
Short: "List database contents",
Long: `List database contents`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -235,8 +237,10 @@ func List(conf *cfg.Config) *cobra.Command {
}
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (table|wide|json), wide is a verbose table. (default 'table')")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
cmd.Aliases = append(cmd.Aliases, "/")

View File

@@ -9,7 +9,7 @@ import (
"github.com/tlinden/anydb/cfg"
)
func WriteFile(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error {
func WriteJSON(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error {
jsonentries, err := json.Marshal(entries)
if err != nil {
return fmt.Errorf("failed to marshall json: %w", err)

View File

@@ -1,11 +1,14 @@
package output
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
tpl "text/template"
"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
@@ -16,14 +19,12 @@ import (
func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
// FIXME: call sort here
switch conf.Mode {
case "wide":
fallthrough
case "":
fallthrough
case "table":
case "wide", "", "table":
return ListTable(writer, conf, entries)
case "json":
return ListJson(writer, conf, entries)
case "template":
return ListTemplate(writer, conf, entries)
default:
return errors.New("unsupported mode")
}
@@ -39,43 +40,67 @@ func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
return nil
}
func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tmpl, err := tpl.New("list").Parse(conf.Template)
if err != nil {
return fmt.Errorf("failed to parse output template: %w", err)
}
buf := bytes.Buffer{}
for _, row := range entries {
row.Normalize()
buf.Reset()
err = tmpl.Execute(&buf, row)
if err != nil {
return fmt.Errorf("failed to execute output template: %w", err)
}
if buf.Len() > 0 {
fmt.Fprintln(writer, buf.String())
}
}
return nil
}
func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !conf.NoHeaders {
if conf.Mode == "wide" {
table.SetHeader([]string{"KEY", "TAGS", "SIZE", "AGE", "VALUE"})
table.SetHeader([]string{"KEY", "TAGS", "SIZE", "UPDATED", "VALUE"})
} else {
table.SetHeader([]string{"KEY", "VALUE"})
}
}
for _, row := range entries {
size := len(row.Value)
if row.Encrypted {
row.Value = "<encrypted-content>"
}
if len(row.Bin) > 0 {
row.Value = "<binary-content>"
size = len(row.Bin)
}
if len(row.Value) > 60 {
row.Value = row.Value[0:60] + "..."
}
row.Normalize()
if conf.Mode == "wide" {
switch conf.NoHumanize {
case true:
table.Append([]string{
row.Key,
strings.Join(row.Tags, ","),
humanize.Bytes(uint64(size)),
strconv.Itoa(row.Size),
row.Created.Format("02.01.2006T03:04.05"),
row.Value,
})
default:
table.Append([]string{
row.Key,
strings.Join(row.Tags, ","),
humanize.Bytes(uint64(row.Size)),
//row.Created.Format("02.01.2006T03:04.05"),
humanize.Time(row.Created),
row.Value,
})
}
} else {
table.Append([]string{row.Key, row.Value})
}

View File

@@ -14,6 +14,45 @@ import (
func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
if attr.File != "" {
WriteFile(writer, conf, attr, entry)
}
isatty := term.IsTerminal(int(os.Stdout.Fd()))
switch conf.Mode {
case "simple", "":
if len(entry.Bin) > 0 {
if isatty {
fmt.Println("binary data omitted")
} else {
os.Stdout.Write(entry.Bin)
}
} else {
fmt.Print(entry.Value)
if !strings.HasSuffix(entry.Value, "\n") {
// always add a terminal newline
fmt.Println()
}
}
case "json":
jsonentry, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshall json: %s", err)
}
fmt.Println(string(jsonentry))
case "wide":
return ListTable(writer, conf, app.DbEntries{*entry})
case "template":
return ListTemplate(writer, conf, app.DbEntries{*entry})
}
return nil
}
func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
fd, err := os.OpenFile(attr.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
@@ -37,39 +76,5 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
return fmt.Errorf("failed to write to file %s: %w", attr.File, err)
}
return nil
}
isatty := term.IsTerminal(int(os.Stdout.Fd()))
switch conf.Mode {
case "simple":
fallthrough
case "":
if len(entry.Bin) > 0 {
if isatty {
fmt.Println("binary data omitted")
} else {
os.Stdout.Write(entry.Bin)
}
} else {
fmt.Print(entry.Value)
if !strings.HasSuffix(entry.Value, "\n") {
// always add a terminal newline
fmt.Println()
}
}
case "json":
jsonentry, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshall json: %s", err)
}
fmt.Println(string(jsonentry))
case "wide":
return ListTable(writer, conf, app.DbEntries{*entry})
}
return nil
}