From c750695d832f1c8613193638b95f0b5d8f2c7639 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 3 Dec 2025 11:32:59 +0200 Subject: [PATCH] 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`). --- cli/cliui/output.go | 3 +++ cli/cliui/table.go | 6 ++++++ cli/cliui/table_test.go | 9 +++++++++ cli/list.go | 12 ++++++------ cli/organizationmembers.go | 5 +++++ cli/organizationroles.go | 5 +++++ cli/provisionerjobs.go | 5 +++++ cli/provisioners.go | 10 +++++----- cli/schedule.go | 5 +++++ cli/task_list.go | 10 ++++------ cli/task_logs.go | 5 +++++ cli/templatelist.go | 12 ++++++------ cli/templatepresets.go | 9 +++++++-- cli/templateversions.go | 5 +++++ cli/tokens.go | 12 +++++------- cli/tokens_test.go | 1 + cli/userlist.go | 5 +++++ enterprise/cli/externalworkspaces.go | 13 +++++++------ enterprise/cli/grouplist.go | 12 ++++++------ enterprise/cli/licenses.go | 5 +++++ enterprise/cli/provisionerkeys.go | 10 +++++----- enterprise/cli/provisionerkeys_test.go | 1 + enterprise/cli/workspaceproxy.go | 5 +++++ 23 files changed, 116 insertions(+), 49 deletions(-) diff --git a/cli/cliui/output.go b/cli/cliui/output.go index 65f6171c2c..b74587bebd 100644 --- a/cli/cliui/output.go +++ b/cli/cliui/output.go @@ -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 { diff --git a/cli/cliui/table.go b/cli/cliui/table.go index c828548022..78141d3252 100644 --- a/cli/cliui/table.go +++ b/cli/cliui/table.go @@ -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) diff --git a/cli/cliui/table_test.go b/cli/cliui/table_test.go index 424b9c9a7d..f7ac8b2da1 100644 --- a/cli/cliui/table_test.go +++ b/cli/cliui/table_test.go @@ -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 diff --git a/cli/list.go b/cli/list.go index bcd5ae2dc0..8b4c56edbc 100644 --- a/cli/list.go +++ b/cli/list.go @@ -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 ")) @@ -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 }, diff --git a/cli/organizationmembers.go b/cli/organizationmembers.go index 60dca731da..3ff7dd1f0c 100644 --- a/cli/organizationmembers.go +++ b/cli/organizationmembers.go @@ -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 }, diff --git a/cli/organizationroles.go b/cli/organizationroles.go index d6d867c6ee..7046d8a233 100644 --- a/cli/organizationroles.go +++ b/cli/organizationroles.go @@ -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 }, diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index ee29476ef0..e580615361 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -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 diff --git a/cli/provisioners.go b/cli/provisioners.go index 4198809c1f..0b9f333878 100644 --- a/cli/provisioners.go +++ b/cli/provisioners.go @@ -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 diff --git a/cli/schedule.go b/cli/schedule.go index a4b02d6d8b..cf292b7f48 100644 --- a/cli/schedule.go +++ b/cli/schedule.go @@ -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 }, diff --git a/cli/task_list.go b/cli/task_list.go index 1f13c85a05..16c0b31a15 100644 --- a/cli/task_list.go +++ b/cli/task_list.go @@ -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 }, diff --git a/cli/task_logs.go b/cli/task_logs.go index 87e2e8112f..5e71f75bf8 100644 --- a/cli/task_logs.go +++ b/cli/task_logs.go @@ -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 }, diff --git a/cli/templatelist.go b/cli/templatelist.go index feb2809816..bb97ed0aaa 100644 --- a/cli/templatelist.go +++ b/cli/templatelist.go @@ -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 \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 \n")) + return nil + } + _, err = fmt.Fprintln(inv.Stdout, out) return err }, diff --git a/cli/templatepresets.go b/cli/templatepresets.go index 2a2270b44c..e0459871eb 100644 --- a/cli/templatepresets.go +++ b/cli/templatepresets.go @@ -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 }, diff --git a/cli/templateversions.go b/cli/templateversions.go index c1323883eb..5390adb4f5 100644 --- a/cli/templateversions.go +++ b/cli/templateversions.go @@ -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 }, diff --git a/cli/tokens.go b/cli/tokens.go index 9316f5de14..624b91dae2 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -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 }, diff --git a/cli/tokens_test.go b/cli/tokens_test.go index 990516aa9b..1981892b69 100644 --- a/cli/tokens_test.go +++ b/cli/tokens_test.go @@ -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() diff --git a/cli/userlist.go b/cli/userlist.go index 536290e656..c8a6740a93 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -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 }, diff --git a/enterprise/cli/externalworkspaces.go b/enterprise/cli/externalworkspaces.go index 4de11b0092..27d88efa3c 100644 --- a/enterprise/cli/externalworkspaces.go +++ b/enterprise/cli/externalworkspaces.go @@ -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 ")) @@ -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 }, diff --git a/enterprise/cli/grouplist.go b/enterprise/cli/grouplist.go index f038a8f018..f28d6c354d 100644 --- a/enterprise/cli/grouplist.go +++ b/enterprise/cli/grouplist.go @@ -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 \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 \n")) + return nil + } + _, _ = fmt.Fprintln(inv.Stdout, out) return nil }, diff --git a/enterprise/cli/licenses.go b/enterprise/cli/licenses.go index 8dd1a6c162..cd9846cc69 100644 --- a/enterprise/cli/licenses.go +++ b/enterprise/cli/licenses.go @@ -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 }, diff --git a/enterprise/cli/provisionerkeys.go b/enterprise/cli/provisionerkeys.go index 1a09797811..f4f90ac242 100644 --- a/enterprise/cli/provisionerkeys.go +++ b/enterprise/cli/provisionerkeys.go @@ -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 diff --git a/enterprise/cli/provisionerkeys_test.go b/enterprise/cli/provisionerkeys_test.go index 8ca2835a13..53ee012fea 100644 --- a/enterprise/cli/provisionerkeys_test.go +++ b/enterprise/cli/provisionerkeys_test.go @@ -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() diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go index 8738497f9e..d03e10d049 100644 --- a/enterprise/cli/workspaceproxy.go +++ b/enterprise/cli/workspaceproxy.go @@ -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 },