Previously we were printing a table for a single item which, for one
was unnecessary, but this also make the output of a backup more
compact.
Before:
```
Logging to file: /home/meain/.cache/corso/logs/2023-10-20T10-21-23Z.log
Connecting to M365 done
Connecting to repository 1s done
Backing Up ∙ Teams Testing (meain)
Discovering items to backup 27s done
Libraries (TeamsTestingmeain) done (found 0 items)
Libraries (TeamsTestingmeain-Shared0) done (found 0 items)
Libraries (TeamsTestingmeain-Private2) done (found 0 items)
Messages 7s done (found 6 channels)
Backing up data 7s done
Backup complete ∙ 7b40dd40-f808-4d57-8e39-b4553e48dc5d
ID Bytes Uploaded Items Uploaded Items Skipped Errors
7b40dd40-f808-4d57-8e39-b4553e48dc5d 0 B 0 0 0
Completed Backups:
ID Started At Duration Status Resource Owner
7b40dd40-f808-4d57-8e39-b4553e48dc5d 2023-10-20T10:21:32Z 36.632747912s Completed Teams Testing (meain)
```
After:
```
Connecting to M365 done
Connecting to repository 1s done
Backing Up ∙ Teams Testing (meain)
Discovering items to backup 31s done
Libraries (TeamsTestingmeain) done (found 0 items)
Libraries (TeamsTestingmeain-Shared0) done (found 0 items)
Libraries (TeamsTestingmeain-Private2) done (found 0 items)
Messages 9s done (found 6 channels)
Backing up data 7s done
Backup complete ∙ ffb2f619-1cb7-4a11-b3e2-7300aa513c6a
Bytes Uploaded: 0 B | Items Uploaded: 0 | Items Skipped: 0 | Errors: 0
Completed Backups:
ID ID Started At Duration Status Resource Owner
ffb2f619-1cb7-4a11-b3e2-7300aa513c6a 2023-10-20T10:23:35Z 40.096203016s Completed Teams Testing (meain)
```
---
#### Does this PR need a docs update or release note?
- [x] ✅ Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ] ⛔ No
#### Type of change
<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup
#### Issue(s)
<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>
#### Test Plan
<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x] ⚡ Unit test
- [ ] 💚 E2E
355 lines
9.4 KiB
Go
355 lines
9.4 KiB
Go
package print
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/tidwall/pretty"
|
|
"github.com/tomlazar/table"
|
|
|
|
"github.com/alcionai/corso/src/internal/common/color"
|
|
"github.com/alcionai/corso/src/internal/observe"
|
|
)
|
|
|
|
var (
|
|
outputAsJSON bool
|
|
outputAsJSONDebug bool
|
|
outputVerbose bool
|
|
)
|
|
|
|
type rootCmdCtx struct{}
|
|
|
|
// Adds a root cobra command to the context.
|
|
// Used to amend output controls like SilenceUsage or to retrieve
|
|
// the command's output writer.
|
|
func SetRootCmd(ctx context.Context, root *cobra.Command) context.Context {
|
|
return context.WithValue(ctx, rootCmdCtx{}, root)
|
|
}
|
|
|
|
// Gets the root cobra command from the context.
|
|
// If no command is found, returns a new, blank command.
|
|
func getRootCmd(ctx context.Context) *cobra.Command {
|
|
if ctx == nil {
|
|
return &cobra.Command{}
|
|
}
|
|
|
|
cmdIface := ctx.Value(rootCmdCtx{})
|
|
cmd, ok := cmdIface.(*cobra.Command)
|
|
|
|
if cmd == nil || !ok {
|
|
return &cobra.Command{}
|
|
}
|
|
|
|
return cmd
|
|
}
|
|
|
|
// adds the persistent flag --output to the provided command.
|
|
func AddOutputFlag(cmd *cobra.Command) {
|
|
fs := cmd.PersistentFlags()
|
|
fs.BoolVar(&outputAsJSON, "json", false, "output data in JSON format")
|
|
fs.BoolVar(&outputAsJSONDebug, "json-debug", false, "output all internal and debugging data in JSON format")
|
|
cobra.CheckErr(fs.MarkHidden("json-debug"))
|
|
fs.BoolVar(&outputVerbose, "verbose", false, "don't hide additional information")
|
|
}
|
|
|
|
// DisplayJSONFormat returns true if the printer plans to output as json.
|
|
func DisplayJSONFormat() bool {
|
|
return outputAsJSON || outputAsJSONDebug
|
|
}
|
|
|
|
// DisplayVerbose returns true if verbose output is enabled
|
|
func DisplayVerbose() bool {
|
|
return outputVerbose
|
|
}
|
|
|
|
// StderrWriter returns the stderr writer used in the root
|
|
// cmd. Returns nil if no root command is seeded.
|
|
func StderrWriter(ctx context.Context) io.Writer {
|
|
return getRootCmd(ctx).ErrOrStderr()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
// Exported interface
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
|
|
// Only tells the CLI to only display this error, preventing the usage
|
|
// (ie, help) menu from displaying as well.
|
|
func Only(ctx context.Context, e error) error {
|
|
getRootCmd(ctx).SilenceUsage = true
|
|
return e
|
|
}
|
|
|
|
// Err prints the params to cobra's error writer (stdErr by default)
|
|
// if s is nil, prints nothing.
|
|
func Err(ctx context.Context, s ...any) {
|
|
cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr())
|
|
|
|
s = append([]any{"Error:"}, s...)
|
|
|
|
out(ctx, cw, s...)
|
|
}
|
|
|
|
// Errf prints the params to cobra's error writer (stdErr by default)
|
|
// if s is nil, prints nothing.
|
|
// You should ideally be using SimpleError or OperationError.
|
|
func Errf(ctx context.Context, tmpl string, s ...any) {
|
|
cw := color.NewColorableWriter(color.Red, getRootCmd(ctx).ErrOrStderr())
|
|
tmpl = "Error: " + tmpl
|
|
outf(ctx, cw, tmpl, s...)
|
|
}
|
|
|
|
// Out prints the params to cobra's output writer (stdOut by default)
|
|
// if s is nil, prints nothing.
|
|
func Out(ctx context.Context, s ...any) {
|
|
out(ctx, getRootCmd(ctx).OutOrStdout(), s...)
|
|
}
|
|
|
|
// Out prints the formatted strings to cobra's output writer (stdOut by default)
|
|
// if t is empty, prints nothing.
|
|
func Outf(ctx context.Context, t string, s ...any) {
|
|
outf(ctx, getRootCmd(ctx).OutOrStdout(), t, s...)
|
|
}
|
|
|
|
// Info prints the params to cobra's error writer (stdErr by default)
|
|
// if s is nil, prints nothing.
|
|
func Info(ctx context.Context, s ...any) {
|
|
out(ctx, getRootCmd(ctx).ErrOrStderr(), s...)
|
|
}
|
|
|
|
// Info prints the formatted strings to cobra's error writer (stdErr by default)
|
|
// if t is empty, prints nothing.
|
|
func Infof(ctx context.Context, t string, s ...any) {
|
|
outf(ctx, getRootCmd(ctx).ErrOrStderr(), t, s...)
|
|
}
|
|
|
|
// Pretty prettifies and prints the value.
|
|
func Pretty(ctx context.Context, a any) {
|
|
if a == nil {
|
|
Err(ctx, "<nil>")
|
|
return
|
|
}
|
|
|
|
printPrettyJSON(getRootCmd(ctx).ErrOrStderr(), a)
|
|
}
|
|
|
|
// PrettyJSON prettifies and prints the value.
|
|
func PrettyJSON(ctx context.Context, p minimumPrintabler) {
|
|
if p == nil {
|
|
Err(ctx, "<nil>")
|
|
return
|
|
}
|
|
|
|
outputJSON(getRootCmd(ctx).ErrOrStderr(), p, outputAsJSONDebug)
|
|
}
|
|
|
|
// out is the testable core of exported print funcs
|
|
func out(ctx context.Context, w io.Writer, s ...any) {
|
|
if len(s) == 0 {
|
|
return
|
|
}
|
|
|
|
// observe bars needs to be flushed before printing
|
|
observe.Flush(ctx)
|
|
|
|
fmt.Fprint(w, s...)
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
// outf is the testable core of exported print funcs
|
|
func outf(ctx context.Context, w io.Writer, t string, s ...any) {
|
|
if len(t) == 0 {
|
|
return
|
|
}
|
|
|
|
// observe bars needs to be flushed before printing
|
|
observe.Flush(ctx)
|
|
|
|
fmt.Fprintf(w, t, s...)
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
// Output control for backup list/details
|
|
// ---------------------------------------------------------------------------------------------------------
|
|
|
|
type Printable interface {
|
|
minimumPrintabler
|
|
// should list the property names of the values surfaced in Values()
|
|
Headers(skipID bool) []string
|
|
// list of values for tabular or csv formatting
|
|
// if the backing data is nil or otherwise missing,
|
|
// values should provide an empty string as opposed to skipping entries
|
|
Values(skipID bool) []string
|
|
}
|
|
|
|
type minimumPrintabler interface {
|
|
// reduces the struct to a minimized format for easier human consumption
|
|
MinimumPrintable() any
|
|
}
|
|
|
|
// Item prints the printable, according to the caller's requested format.
|
|
func Item(ctx context.Context, p Printable) {
|
|
printItem(getRootCmd(ctx).OutOrStdout(), p)
|
|
}
|
|
|
|
// print prints the printable items,
|
|
// according to the caller's requested format.
|
|
func printItem(w io.Writer, p Printable) {
|
|
if outputAsJSON || outputAsJSONDebug {
|
|
outputJSON(w, p, outputAsJSONDebug)
|
|
return
|
|
}
|
|
|
|
outputTable(w, []Printable{p})
|
|
}
|
|
|
|
// ItemProperties prints the printable either as in a single line or a json
|
|
// The difference between this and Item is that this one does not print the ID
|
|
func ItemProperties(ctx context.Context, p Printable) {
|
|
printItemProperties(getRootCmd(ctx).OutOrStdout(), p)
|
|
}
|
|
|
|
// print prints the printable items,
|
|
// according to the caller's requested format.
|
|
func printItemProperties(w io.Writer, p Printable) {
|
|
if outputAsJSON || outputAsJSONDebug {
|
|
outputJSON(w, p, outputAsJSONDebug)
|
|
return
|
|
}
|
|
|
|
outputOneLine(w, []Printable{p})
|
|
}
|
|
|
|
// All prints the slice of printable items,
|
|
// according to the caller's requested format.
|
|
func All(ctx context.Context, ps ...Printable) {
|
|
printAll(getRootCmd(ctx).OutOrStdout(), ps)
|
|
}
|
|
|
|
// printAll prints the slice of printable items,
|
|
// according to the caller's requested format.
|
|
func printAll(w io.Writer, ps []Printable) {
|
|
if len(ps) == 0 {
|
|
return
|
|
}
|
|
|
|
if outputAsJSON || outputAsJSONDebug {
|
|
outputJSONArr(w, ps, outputAsJSONDebug)
|
|
return
|
|
}
|
|
|
|
outputTable(w, ps)
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// Tabular
|
|
// ------------------------------------------------------------------------------------------
|
|
|
|
// Table writes the printables in a tabular format. Takes headers from
|
|
// the 0th printable only.
|
|
func Table(ctx context.Context, ps []Printable) {
|
|
outputTable(getRootCmd(ctx).OutOrStdout(), ps)
|
|
}
|
|
|
|
// output to stdout the list of printable structs in a table
|
|
func outputTable(w io.Writer, ps []Printable) {
|
|
t := table.Table{
|
|
Headers: ps[0].Headers(false),
|
|
Rows: [][]string{},
|
|
}
|
|
|
|
for _, p := range ps {
|
|
t.Rows = append(t.Rows, p.Values(false))
|
|
}
|
|
|
|
_ = t.WriteTable(
|
|
w,
|
|
&table.Config{
|
|
ShowIndex: false,
|
|
Color: false,
|
|
AlternateColors: false,
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------
|
|
// JSON
|
|
// ------------------------------------------------------------------------------------------
|
|
|
|
func outputJSON(w io.Writer, p minimumPrintabler, debug bool) {
|
|
if debug {
|
|
printJSON(w, p)
|
|
return
|
|
}
|
|
|
|
if debug {
|
|
printJSON(w, p)
|
|
} else {
|
|
printJSON(w, p.MinimumPrintable())
|
|
}
|
|
}
|
|
|
|
func outputJSONArr(w io.Writer, ps []Printable, debug bool) {
|
|
sl := make([]any, 0, len(ps))
|
|
|
|
for _, p := range ps {
|
|
if debug {
|
|
sl = append(sl, p)
|
|
} else {
|
|
sl = append(sl, p.MinimumPrintable())
|
|
}
|
|
}
|
|
|
|
printJSON(w, sl)
|
|
}
|
|
|
|
// output to stdout the list of printable structs as json.
|
|
func printJSON(w io.Writer, a any) {
|
|
bs, err := json.Marshal(a)
|
|
if err != nil {
|
|
fmt.Fprintf(w, "error formatting results to json: %v\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(w, string(pretty.Pretty(bs)))
|
|
}
|
|
|
|
// output to stdout the list of printable structs as prettified json.
|
|
func printPrettyJSON(w io.Writer, a any) {
|
|
bs, err := json.MarshalIndent(a, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintf(w, "error formatting results to json: %v\n", err)
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(w, string(pretty.Pretty(bs)))
|
|
}
|
|
|
|
// -------------------------------------------------------------------------------------------
|
|
// One line
|
|
// -------------------------------------------------------------------------------------------
|
|
|
|
// Output in the following format:
|
|
// Bytes Uploaded: 401 kB | Items Uploaded: 59 | Items Skipped: 0 | Errors: 0
|
|
func outputOneLine(w io.Writer, ps []Printable) {
|
|
headers := ps[0].Headers(true)
|
|
rows := [][]string{}
|
|
|
|
for _, p := range ps {
|
|
rows = append(rows, p.Values(true))
|
|
}
|
|
|
|
printables := []string{}
|
|
|
|
for _, row := range rows {
|
|
for i, col := range row {
|
|
printables = append(printables, fmt.Sprintf("%s: %s", headers[i], col))
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(w, strings.Join(printables, " | "))
|
|
}
|