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 },