CLI: Export reports as CSV/TSV for use in spreadsheets #2247 #2252 #2169

This commit is contained in:
Michael Mayer 2022-04-17 12:30:33 +02:00
parent 73be4df8f8
commit 182bc09d87
11 changed files with 198 additions and 91 deletions

View file

@ -12,14 +12,9 @@ import (
// ShowConfigCommand configures the command name, flags, and action.
var ShowConfigCommand = cli.Command{
Name: "config",
Usage: "Shows global config option names and values",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "md, m",
Usage: "render Markdown without line breaks",
},
},
Name: "config",
Usage: "Shows global config option names and values",
Flags: report.CliFlags,
Action: showConfigAction,
}
@ -30,7 +25,9 @@ func showConfigAction(ctx *cli.Context) error {
rows, cols := conf.Report()
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
result, err := report.Render(rows, cols, report.CliFormat(ctx))
return nil
fmt.Println(result)
return err
}

View file

@ -12,14 +12,9 @@ import (
// ShowFiltersCommand configures the command name, flags, and action.
var ShowFiltersCommand = cli.Command{
Name: "filters",
Usage: "Displays a search filter overview with examples",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "md, m",
Usage: "render Markdown without line breaks",
},
},
Name: "filters",
Usage: "Displays a search filter overview with examples",
Flags: report.CliFlags,
Action: showFiltersAction,
}
@ -35,7 +30,9 @@ func showFiltersAction(ctx *cli.Context) error {
}
})
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
result, err := report.Render(rows, cols, report.CliFormat(ctx))
return nil
fmt.Println(result)
return err
}

View file

@ -15,23 +15,20 @@ var ShowFormatsCommand = cli.Command{
Name: "formats",
Aliases: []string{"filetypes"},
Usage: "Lists supported media and sidecar file formats",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "short, s",
Usage: "hide format descriptions",
},
cli.BoolFlag{
Name: "md, m",
Usage: "render Markdown without line breaks",
},
},
Flags: append(report.CliFlags, cli.BoolFlag{
Name: "short, s",
Usage: "hide links to documentation",
}),
Action: showFormatsAction,
}
// showFormatsAction lists supported media and sidecar file formats.
func showFormatsAction(ctx *cli.Context) error {
rows, cols := media.Report(fs.Extensions.Types(true), !ctx.Bool("short"), true, true)
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
result, err := report.Render(rows, cols, report.CliFormat(ctx))
fmt.Println(result)
return err
}

View file

@ -4,10 +4,9 @@ import (
"fmt"
"sort"
"github.com/photoprism/photoprism/internal/meta"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/pkg/report"
)
@ -16,16 +15,10 @@ var ShowTagsCommand = cli.Command{
Name: "tags",
Aliases: []string{"metadata"},
Usage: "Shows an overview of the supported metadata tags",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "short, s",
Usage: "hide links to documentation",
},
cli.BoolFlag{
Name: "md, m",
Usage: "render Markdown without line breaks",
},
},
Flags: append(report.CliFlags, cli.BoolFlag{
Name: "short, s",
Usage: "hide links to documentation",
}),
Action: showTagsAction,
}
@ -42,14 +35,21 @@ func showTagsAction(ctx *cli.Context) error {
}
})
// Show table with the supported metadata tags.
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
// Output overview of supported metadata tags.
format := report.CliFormat(ctx)
result, err := report.Render(rows, cols, format)
// Show documentation links for those who want to delve deeper.
if !ctx.Bool("short") {
fmt.Printf("## Metadata Tags by Namespace ##\n\n")
fmt.Println(report.Table(meta.Docs, []string{"Namespace", "Documentation"}, ctx.Bool("md")))
fmt.Println(result)
if err != nil || ctx.Bool("short") || format == report.TSV {
return err
}
return nil
// Documentation links for those who want to delve deeper.
result, err = report.Render(meta.Docs, []string{"Namespace", "Documentation"}, format)
fmt.Printf("## Metadata Tags by Namespace ##\n\n")
fmt.Println(result)
return err
}

31
pkg/report/cli.go Normal file
View file

@ -0,0 +1,31 @@
package report
import "github.com/urfave/cli"
func CliFormat(ctx *cli.Context) Format {
switch {
case ctx.Bool("md"), ctx.Bool("markdown"):
return Markdown
case ctx.Bool("tsv"):
return TSV
case ctx.Bool("csv"):
return CSV
default:
return Default
}
}
var CliFlags = []cli.Flag{
cli.BoolFlag{
Name: "md, m",
Usage: "format as machine-readable Markdown",
},
cli.BoolFlag{
Name: "csv, c",
Usage: "export as semicolon separated values",
},
cli.BoolFlag{
Name: "tsv, t",
Usage: "export as tab separated values",
},
}

26
pkg/report/csv.go Normal file
View file

@ -0,0 +1,26 @@
package report
import (
"bytes"
"encoding/csv"
)
// CsvExport returns the report as character separated values.
func CsvExport(rows [][]string, cols []string, sep rune) (string, error) {
buf := &bytes.Buffer{}
writer := csv.NewWriter(buf)
if sep > 0 {
writer.Comma = sep
}
err := writer.Write(cols)
if err != nil {
return "", err
}
err = writer.WriteAll(rows)
return buf.String(), nil
}

10
pkg/report/format.go Normal file
View file

@ -0,0 +1,10 @@
package report
type Format string
const (
Default = ""
Markdown = "markdown"
TSV = "tsv"
CSV = "csv"
)

View file

@ -7,17 +7,11 @@ import (
"github.com/olekukonko/tablewriter"
)
// Table returns a text-formatted table, optionally as valid Markdown,
// MarkdownTable returns a text-formatted table with caption, optionally as valid Markdown,
// so the output can be pasted into the docs.
func Table(rows [][]string, cols []string, markDown bool) string {
return TableWithCaption(rows, cols, "", markDown)
}
// TableWithCaption returns a text-formatted table with caption, optionally as valid Markdown,
// so the output can be pasted into the docs.
func TableWithCaption(rows [][]string, cols []string, caption string, markDown bool) string {
func MarkdownTable(rows [][]string, cols []string, caption string, valid bool) string {
// Escape Markdown.
if markDown {
if valid {
for i := range rows {
for j := range rows[i] {
if strings.ContainsRune(rows[i][j], '|') {
@ -33,8 +27,8 @@ func TableWithCaption(rows [][]string, cols []string, caption string, markDown b
borders := tablewriter.Border{
Left: true,
Right: true,
Top: !markDown,
Bottom: !markDown,
Top: !valid,
Bottom: !valid,
}
// Render.
@ -45,7 +39,7 @@ func TableWithCaption(rows [][]string, cols []string, caption string, markDown b
table.SetCaption(true, caption)
}
table.SetAutoWrapText(!markDown)
table.SetAutoWrapText(!valid)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(borders)

24
pkg/report/render.go Normal file
View file

@ -0,0 +1,24 @@
package report
import (
"fmt"
"github.com/photoprism/photoprism/pkg/clean"
)
// Render returns a text-formatted table, optionally as valid Markdown,
// so the output can be pasted into the docs.
func Render(rows [][]string, cols []string, format Format) (string, error) {
switch format {
case CSV:
return CsvExport(rows, cols, ';')
case TSV:
return CsvExport(rows, cols, '\t')
case Markdown:
return MarkdownTable(rows, cols, "", true), nil
case Default:
return MarkdownTable(rows, cols, "", false), nil
default:
return "", fmt.Errorf("invalid format %s", clean.Log(string(format)))
}
}

59
pkg/report/render_test.go Normal file
View file

@ -0,0 +1,59 @@
package report
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTable(t *testing.T) {
cols := []string{"Col1", "Col2"}
rows := [][]string{
{"foo", "bar" + strings.Repeat(", abc", 30)},
{"bar", "b & a | z"}}
t.Run("DefaultTable", func(t *testing.T) {
result, err := Render(rows, cols, Default)
if err != nil {
t.Fatal(err)
}
assert.Contains(t, result, "| bar | b & a | z |")
})
t.Run("MarkdownTable", func(t *testing.T) {
result, err := Render(rows, cols, Markdown)
if err != nil {
t.Fatal(err)
}
// fmt.Println(result)
assert.Contains(t, result, "| bar | b & a \\| z")
})
t.Run("CsvExport", func(t *testing.T) {
result, err := Render(rows, cols, CSV)
if err != nil {
t.Fatal(err)
}
expected := "Col1;Col2\nfoo;bar, abc, abc, abc, abc, abc, abc," +
" abc, abc, abc, abc, abc, abc, abc, abc, abc," +
" abc, abc, abc, abc, abc, abc, abc, abc, abc," +
" abc, abc, abc, abc, abc, abc\nbar;b & a \\| z\n"
assert.Equal(t, expected, result)
})
t.Run("TsvExport", func(t *testing.T) {
result, err := Render(rows, cols, TSV)
if err != nil {
t.Fatal(err)
}
assert.Contains(t, result, "Col1\tCol2\nfoo\tbar, abc, abc")
})
t.Run("Invalid", func(t *testing.T) {
_, err := Render(rows, cols, Format("invalid"))
if err == nil {
t.Fatal("error expected")
}
})
}

View file

@ -1,28 +0,0 @@
package report
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTable(t *testing.T) {
t.Run("Standard", func(t *testing.T) {
cols := []string{"Col1", "Col2"}
rows := [][]string{
{"foo", "bar" + strings.Repeat(", abc", 30)},
{"bar", "b & a | z"}}
result := Table(rows, cols, false)
assert.Contains(t, result, "| bar | b & a | z |")
})
t.Run("Markdown", func(t *testing.T) {
cols := []string{"Col1", "Col2"}
rows := [][]string{
{"foo", "bar" + strings.Repeat(", abc", 30)},
{"bar", "b & a | z"}}
result := Table(rows, cols, true)
// fmt.Println(result)
assert.Contains(t, result, "| bar | b & a \\| z")
})
}