feat(cli/cliui): output empty string for empty table (#20967)

This changes makes it so that we output the empty string for Format
when there is no data. It turns out there are many places in the code
where we have such handling, but in a way that would break the JSON
formatter (since we'd output nothing on stdout or text rather than
`[]`/`null`).
This commit is contained in:
Mathias Fredriksson
2025-12-03 11:32:59 +02:00
committed by GitHub
parent 3c05cb6255
commit c750695d83
23 changed files with 116 additions and 49 deletions
+3
View File
@@ -106,6 +106,9 @@ var _ OutputFormat = &tableFormat{}
//
// defaultColumns is optional and specifies the default columns to display. If
// not specified, all columns are displayed by default.
//
// If the data is empty, an empty string is returned. Callers should check for
// this and provide an appropriate message to the user.
func TableFormat(out any, defaultColumns []string) OutputFormat {
v := reflect.Indirect(reflect.ValueOf(out))
if v.Kind() != reflect.Slice {
+6
View File
@@ -180,6 +180,12 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
func renderTable(out any, sort string, headers table.Row, filterColumns []string) (string, error) {
v := reflect.Indirect(reflect.ValueOf(out))
// Return empty string for empty data. Callers should check for this
// and provide an appropriate message to the user.
if v.Kind() == reflect.Slice && v.Len() == 0 {
return "", nil
}
headers = filterHeaders(headers, filterColumns)
columnConfigs := createColumnConfigs(headers, filterColumns)
+9
View File
@@ -472,6 +472,15 @@ alice 1
require.NoError(t, err)
compareTables(t, expected, out)
})
t.Run("Empty", func(t *testing.T) {
t.Parallel()
var in []tableTest4
out, err := cliui.DisplayTable(in, "", nil)
require.NoError(t, err)
require.Empty(t, out)
})
}
// compareTables normalizes the incoming table lines
+6 -6
View File
@@ -139,7 +139,12 @@ func (r *RootCmd) list() *serpent.Command {
return err
}
if len(res) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() {
out, err := formatter.Format(inv.Context(), res)
if err != nil {
return err
}
if out == "" {
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n")
_, _ = fmt.Fprintln(inv.Stderr)
_, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder create <name>"))
@@ -147,11 +152,6 @@ func (r *RootCmd) list() *serpent.Command {
return nil
}
out, err := formatter.Format(inv.Context(), res)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5
View File
@@ -170,6 +170,11 @@ func (r *RootCmd) listOrganizationMembers(orgContext *OrganizationContext) *serp
return err
}
if out == "" {
cliui.Infof(inv.Stderr, "No organization members found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5
View File
@@ -92,6 +92,11 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen
return err
}
if out == "" {
cliui.Infof(inv.Stderr, "No organization roles found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5
View File
@@ -110,6 +110,11 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command {
return xerrors.Errorf("display provisioner daemons: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No provisioner jobs found.")
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
+5 -5
View File
@@ -74,11 +74,6 @@ func (r *RootCmd) provisionerList() *serpent.Command {
return xerrors.Errorf("list provisioner daemons: %w", err)
}
if len(daemons) == 0 {
_, _ = fmt.Fprintln(inv.Stdout, "No provisioner daemons found")
return nil
}
var rows []provisionerDaemonRow
for _, daemon := range daemons {
rows = append(rows, provisionerDaemonRow{
@@ -92,6 +87,11 @@ func (r *RootCmd) provisionerList() *serpent.Command {
return xerrors.Errorf("display provisioner daemons: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No provisioner daemons found.")
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
+5
View File
@@ -129,6 +129,11 @@ func (r *RootCmd) scheduleShow() *serpent.Command {
return err
}
if out == "" {
cliui.Infof(inv.Stderr, "No schedules found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+4 -6
View File
@@ -157,12 +157,6 @@ func (r *RootCmd) taskList() *serpent.Command {
return nil
}
// If no rows and not JSON, show a friendly message.
if len(tasks) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() {
_, _ = fmt.Fprintln(inv.Stderr, "No tasks found.")
return nil
}
rows := make([]taskListRow, len(tasks))
now := time.Now()
for i := range tasks {
@@ -173,6 +167,10 @@ func (r *RootCmd) taskList() *serpent.Command {
if err != nil {
return xerrors.Errorf("format tasks: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No tasks found.")
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
},
+5
View File
@@ -59,6 +59,11 @@ func (r *RootCmd) taskLogs() *serpent.Command {
return xerrors.Errorf("format task logs: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No task logs found.")
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
},
+6 -6
View File
@@ -30,18 +30,18 @@ func (r *RootCmd) templateList() *serpent.Command {
return err
}
if len(templates) == 0 {
_, _ = fmt.Fprintf(inv.Stderr, "%s No templates found! Create one:\n\n", Caret)
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates push <directory>\n"))
return nil
}
rows := templatesToRows(templates...)
out, err := formatter.Format(inv.Context(), rows)
if err != nil {
return err
}
if out == "" {
_, _ = fmt.Fprintf(inv.Stderr, "%s No templates found! Create one:\n\n", Caret)
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates push <directory>\n"))
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+7 -2
View File
@@ -106,7 +106,7 @@ func (r *RootCmd) templatePresetsList() *serpent.Command {
if len(presets) == 0 {
cliui.Infof(
inv.Stdout,
"No presets found for template %q and template-version %q.\n", template.Name, version.Name,
"No presets found for template %q and template-version %q.", template.Name, version.Name,
)
return nil
}
@@ -115,7 +115,7 @@ func (r *RootCmd) templatePresetsList() *serpent.Command {
if formatter.FormatID() == "table" {
cliui.Infof(
inv.Stdout,
"Showing presets for template %q and template version %q.\n", template.Name, version.Name,
"Showing presets for template %q and template version %q.", template.Name, version.Name,
)
}
rows := templatePresetsToRows(presets...)
@@ -124,6 +124,11 @@ func (r *RootCmd) templatePresetsList() *serpent.Command {
return xerrors.Errorf("render table: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No template presets found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5
View File
@@ -121,6 +121,11 @@ func (r *RootCmd) templateVersionsList() *serpent.Command {
return xerrors.Errorf("render table: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No template versions found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5 -7
View File
@@ -246,13 +246,6 @@ func (r *RootCmd) listTokens() *serpent.Command {
return xerrors.Errorf("list tokens: %w", err)
}
if len(tokens) == 0 {
cliui.Infof(
inv.Stdout,
"No tokens found.\n",
)
}
displayTokens = make([]tokenListRow, len(tokens))
for i, token := range tokens {
@@ -264,6 +257,11 @@ func (r *RootCmd) listTokens() *serpent.Command {
return err
}
if out == "" {
cliui.Info(inv.Stderr, "No tokens found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+1
View File
@@ -34,6 +34,7 @@ func TestTokens(t *testing.T) {
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
inv.Stdout = buf
inv.Stderr = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
res := buf.String()
+5
View File
@@ -58,6 +58,11 @@ func (r *RootCmd) userList() *serpent.Command {
return err
}
if out == "" {
cliui.Infof(inv.Stderr, "No users found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+7 -6
View File
@@ -12,6 +12,7 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
@@ -197,7 +198,12 @@ func (r *RootCmd) externalWorkspaceList() *serpent.Command {
return err
}
if len(res) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() {
out, err := formatter.Format(inv.Context(), res)
if err != nil {
return err
}
if out == "" {
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n")
_, _ = fmt.Fprintln(inv.Stderr)
_, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder external-workspaces create <name>"))
@@ -205,11 +211,6 @@ func (r *RootCmd) externalWorkspaceList() *serpent.Command {
return nil
}
out, err := formatter.Format(inv.Context(), res)
if err != nil {
return err
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+6 -6
View File
@@ -43,18 +43,18 @@ func (r *RootCmd) groupList() *serpent.Command {
return xerrors.Errorf("get groups: %w", err)
}
if len(groups) == 0 {
_, _ = fmt.Fprintf(inv.Stderr, "%s No groups found in %s! Create one:\n\n", agpl.Caret, color.HiWhiteString(org.Name))
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder groups create <name>\n"))
return nil
}
rows := groupsToRows(groups...)
out, err := formatter.Format(inv.Context(), rows)
if err != nil {
return xerrors.Errorf("display groups: %w", err)
}
if out == "" {
_, _ = fmt.Fprintf(inv.Stderr, "%s No groups found in %s! Create one:\n\n", agpl.Caret, color.HiWhiteString(org.Name))
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder groups create <name>\n"))
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
},
+5
View File
@@ -166,6 +166,11 @@ func (r *RootCmd) licensesList() *serpent.Command {
return err
}
if out == "" {
cliui.Infof(inv.Stderr, "No licenses found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, out)
return err
},
+5 -5
View File
@@ -126,16 +126,16 @@ func (r *RootCmd) provisionerKeysList() *serpent.Command {
return xerrors.Errorf("list provisioner keys: %w", err)
}
if len(keys) == 0 {
_, _ = fmt.Fprintln(inv.Stdout, "No provisioner keys found")
return nil
}
out, err := formatter.Format(inv.Context(), keys)
if err != nil {
return xerrors.Errorf("display provisioner keys: %w", err)
}
if out == "" {
cliui.Infof(inv.Stderr, "No provisioner keys found.")
return nil
}
_, _ = fmt.Fprintln(inv.Stdout, out)
return nil
+1
View File
@@ -94,6 +94,7 @@ func TestProvisionerKeys(t *testing.T) {
)
pty = ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
clitest.SetupConfig(t, orgAdminClient, conf)
err = inv.WithContext(ctx).Run()
+5
View File
@@ -392,6 +392,11 @@ func (r *RootCmd) listProxies() *serpent.Command {
return err
}
if output == "" {
cliui.Infof(inv.Stderr, "No workspace proxies found.")
return nil
}
_, err = fmt.Fprintln(inv.Stdout, output)
return err
},