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 often, which is not good for a tool intended to be used for many
years. years.
- more features: - more features:
- output table in list mode uses <tab> separator
- better STDIN + pipe support - better STDIN + pipe support
- supports JSON output - supports JSON output
- supports more verbose tabular output - supports more verbose tabular output
@@ -23,6 +24,7 @@ reasons:
- tagging - tagging
- filtering using tags - filtering using tags
- encryption of entries - encryption of entries
- templates for custom output for maximum flexibility
**anydb** can do all the things you can do with skate: **anydb** can do all the things you can do with skate:
@@ -74,8 +76,8 @@ anydb list '[a-z]+\d'
anydb list -o wide anydb list -o wide
KEY TAGS SIZE AGE VALUE KEY TAGS SIZE AGE VALUE
blah important 4 B 7 seconds ago haha blah important 4 B 7 seconds ago haha
foo 3 B 15 seconds ago bar foo 3 B 15 seconds ago bar
猫咪 3 B 3 seconds ago 喵 猫咪 3 B 3 seconds ago 喵
# there are shortcuts as well # there are shortcuts as well
anydb ls -l anydb ls -l
@@ -95,6 +97,19 @@ anydb import -r backup.json
# get command. # get command.
anydb set mypassword -e 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 # it comes with a manpage builtin
anydb man anydb man
``` ```

View File

@@ -13,6 +13,8 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
const MaxValueWidth int = 60
type DB struct { type DB struct {
Debug bool Debug bool
Dbfile string Dbfile string
@@ -27,6 +29,26 @@ type DbEntry struct {
Bin []byte `json:"bin"` Bin []byte `json:"bin"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
Created time.Time `json:"created"` 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 type DbEntries []DbEntry

View File

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

View File

@@ -80,7 +80,7 @@ func Get(conf *cfg.Config) *cobra.Command {
) )
var cmd = &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", Short: "Retrieve value for a key",
Long: `Retrieve value for a key`, Long: `Retrieve value for a key`,
RunE: func(cmd *cobra.Command, args []string) error { 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().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.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, "show")
cmd.Aliases = append(cmd.Aliases, "g") cmd.Aliases = append(cmd.Aliases, "g")
@@ -186,7 +188,7 @@ func Export(conf *cfg.Config) *cobra.Command {
return err 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{ 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", Short: "List database contents",
Long: `List database contents`, Long: `List database contents`,
RunE: func(cmd *cobra.Command, args []string) error { 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.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(&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.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.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
cmd.Aliases = append(cmd.Aliases, "/") cmd.Aliases = append(cmd.Aliases, "/")

View File

@@ -9,7 +9,7 @@ import (
"github.com/tlinden/anydb/cfg" "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) jsonentries, err := json.Marshal(entries)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshall json: %w", err) return fmt.Errorf("failed to marshall json: %w", err)

View File

@@ -1,11 +1,14 @@
package output package output
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"strconv"
"strings" "strings"
tpl "text/template"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
@@ -16,14 +19,12 @@ import (
func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
// FIXME: call sort here // FIXME: call sort here
switch conf.Mode { switch conf.Mode {
case "wide": case "wide", "", "table":
fallthrough
case "":
fallthrough
case "table":
return ListTable(writer, conf, entries) return ListTable(writer, conf, entries)
case "json": case "json":
return ListJson(writer, conf, entries) return ListJson(writer, conf, entries)
case "template":
return ListTemplate(writer, conf, entries)
default: default:
return errors.New("unsupported mode") return errors.New("unsupported mode")
} }
@@ -39,43 +40,67 @@ func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
return nil 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 { func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString) table := tablewriter.NewWriter(tableString)
if !conf.NoHeaders { if !conf.NoHeaders {
if conf.Mode == "wide" { if conf.Mode == "wide" {
table.SetHeader([]string{"KEY", "TAGS", "SIZE", "AGE", "VALUE"}) table.SetHeader([]string{"KEY", "TAGS", "SIZE", "UPDATED", "VALUE"})
} else { } else {
table.SetHeader([]string{"KEY", "VALUE"}) table.SetHeader([]string{"KEY", "VALUE"})
} }
} }
for _, row := range entries { for _, row := range entries {
size := len(row.Value) row.Normalize()
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] + "..."
}
if conf.Mode == "wide" { if conf.Mode == "wide" {
table.Append([]string{ switch conf.NoHumanize {
row.Key, case true:
strings.Join(row.Tags, ","), table.Append([]string{
humanize.Bytes(uint64(size)), row.Key,
//row.Created.Format("02.01.2006T03:04.05"), strings.Join(row.Tags, ","),
humanize.Time(row.Created), strconv.Itoa(row.Size),
row.Value, 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 { } else {
table.Append([]string{row.Key, row.Value}) table.Append([]string{row.Key, row.Value})
} }

View File

@@ -14,38 +14,14 @@ import (
func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error { func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
if attr.File != "" { if attr.File != "" {
fd, err := os.OpenFile(attr.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) WriteFile(writer, conf, attr, entry)
if err != nil {
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
}
defer fd.Close()
if len(entry.Bin) > 0 {
// binary file content
_, err = fd.Write(entry.Bin)
} else {
val := entry.Value
if !strings.HasSuffix(val, "\n") {
// always add a terminal newline
val += "\n"
}
_, err = fd.Write([]byte(val))
}
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", attr.File, err)
}
return nil
} }
isatty := term.IsTerminal(int(os.Stdout.Fd())) isatty := term.IsTerminal(int(os.Stdout.Fd()))
switch conf.Mode { switch conf.Mode {
case "simple": case "simple", "":
fallthrough
case "":
if len(entry.Bin) > 0 { if len(entry.Bin) > 0 {
if isatty { if isatty {
fmt.Println("binary data omitted") fmt.Println("binary data omitted")
@@ -69,6 +45,35 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
fmt.Println(string(jsonentry)) fmt.Println(string(jsonentry))
case "wide": case "wide":
return ListTable(writer, conf, app.DbEntries{*entry}) 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)
}
defer fd.Close()
if len(entry.Bin) > 0 {
// binary file content
_, err = fd.Write(entry.Bin)
} else {
val := entry.Value
if !strings.HasSuffix(val, "\n") {
// always add a terminal newline
val += "\n"
}
_, err = fd.Write([]byte(val))
}
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", attr.File, err)
} }
return nil return nil