From d94868132d6fa09ecf377f68670a5dcac59f3a9b Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 20 Dec 2024 11:53:09 +0100 Subject: [PATCH] added -N flag, added -m template support to get and list commands --- README.md | 19 +++++++++-- app/db.go | 22 ++++++++++++ cfg/config.go | 20 ++++++----- cmd/maincommands.go | 12 ++++--- output/export.go | 2 +- output/list.go | 81 +++++++++++++++++++++++++++++---------------- output/single.go | 57 ++++++++++++++++--------------- 7 files changed, 143 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index f47518c..02751f3 100644 --- a/README.md +++ b/README.md @@ -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 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: @@ -74,8 +76,8 @@ anydb list '[a-z]+\d' anydb list -o wide KEY TAGS SIZE AGE VALUE blah important 4 B 7 seconds ago haha -foo 3 B 15 seconds ago bar -猫咪 3 B 3 seconds ago 喵 +foo 3 B 15 seconds ago bar +猫咪 3 B 3 seconds ago 喵 # there are shortcuts as well anydb ls -l @@ -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 ``` diff --git a/app/db.go b/app/db.go index f4a4473..94ab9e3 100644 --- a/app/db.go +++ b/app/db.go @@ -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 = "" + } + + if len(entry.Bin) > 0 { + entry.Value = "" + entry.Size = len(entry.Bin) + } + + if len(entry.Value) > MaxValueWidth { + entry.Value = entry.Value[0:MaxValueWidth] + "..." + } } type DbEntries []DbEntry diff --git a/cfg/config.go b/cfg/config.go index 73fd8c3..339bf02 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -2,15 +2,17 @@ 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 - Mode string // wide, table, yaml, json - NoHeaders bool - Encrypt bool - DB *app.DB - File string - Tags []string + Debug bool + Dbfile string + Template string + Mode string // wide, table, yaml, json + NoHeaders bool + NoHumanize bool + Encrypt bool + DB *app.DB + File string + Tags []string } diff --git a/cmd/maincommands.go b/cmd/maincommands.go index b5339a1..7ff95c4 100644 --- a/cmd/maincommands.go +++ b/cmd/maincommands.go @@ -80,7 +80,7 @@ func Get(conf *cfg.Config) *cobra.Command { ) var cmd = &cobra.Command{ - Use: "get [-o ] [-m ] [-n]", + Use: "get [-o ] [-m ] [-n -N] [-T ]", 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 ] [-o ] []", + Use: "list [] [-t ] [-m ] [-n -N] [-T ]", 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, "/") diff --git a/output/export.go b/output/export.go index c707ce3..7099f81 100644 --- a/output/export.go +++ b/output/export.go @@ -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) diff --git a/output/list.go b/output/list.go index d251475..b664360 100644 --- a/output/list.go +++ b/output/list.go @@ -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 = "" - } - - if len(row.Bin) > 0 { - row.Value = "" - size = len(row.Bin) - } - - if len(row.Value) > 60 { - row.Value = row.Value[0:60] + "..." - } + row.Normalize() if conf.Mode == "wide" { - table.Append([]string{ - row.Key, - strings.Join(row.Tags, ","), - humanize.Bytes(uint64(size)), - //row.Created.Format("02.01.2006T03:04.05"), - humanize.Time(row.Created), - row.Value, - }) + switch conf.NoHumanize { + case true: + table.Append([]string{ + row.Key, + strings.Join(row.Tags, ","), + 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}) } diff --git a/output/single.go b/output/single.go index 61cda29..ba5f5ef 100644 --- a/output/single.go +++ b/output/single.go @@ -14,38 +14,14 @@ import ( func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error { if attr.File != "" { - 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) - } + WriteFile(writer, conf, attr, entry) - return nil } isatty := term.IsTerminal(int(os.Stdout.Fd())) switch conf.Mode { - case "simple": - fallthrough - case "": + case "simple", "": if len(entry.Bin) > 0 { if isatty { 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)) 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) + } + 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