diff --git a/cli/configssh.go b/cli/configssh.go index 6ea03abd3f903..bc4ea9541e11c 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -141,15 +141,16 @@ func configSSH() *cobra.Command { Annotations: workspaceCommand, Use: "config-ssh", Short: "Populate your SSH config with Host entries for all of your workspaces", - Example: ` - - You can use -o (or --ssh-option) so set SSH options to be used for all your - workspaces. - - ` + cliui.Styles.Code.Render("$ coder config-ssh -o ForwardAgent=yes") + ` - - - You can use --dry-run (or -n) to see the changes that would be made. - - ` + cliui.Styles.Code.Render("$ coder config-ssh --dry-run"), + Example: formatExamples( + example{ + Description: "You can use -o (or --ssh-option) so set SSH options to be used for all your workspaces", + Command: "coder config-ssh -o ForwardAgent=yes", + }, + example{ + Description: "You can use --dry-run (or -n) to see the changes that would be made", + Command: "coder config-ssh --dry-run", + }, + ), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/dotfiles.go b/cli/dotfiles.go index 79110a3d6e1c6..11c24af1710fc 100644 --- a/cli/dotfiles.go +++ b/cli/dotfiles.go @@ -18,14 +18,17 @@ import ( ) func dotfiles() *cobra.Command { - var ( - symlinkDir string - ) + var symlinkDir string cmd := &cobra.Command{ - Use: "dotfiles [git_repo_url]", - Args: cobra.ExactArgs(1), - Short: "Check out and install a dotfiles repository.", - Example: "coder dotfiles [-y] git@github.com:example/dotfiles.git", + Use: "dotfiles [git_repo_url]", + Args: cobra.ExactArgs(1), + Short: "Check out and install a dotfiles repository.", + Example: formatExamples( + example{ + Description: "Check out and install a dotfiles repository without prompts", + Command: "coder dotfiles --yes git@github.com:example/dotfiles.git", + }, + ), RunE: func(cmd *cobra.Command, args []string) error { var ( dotfilesRepoDir = "dotfiles" diff --git a/cli/parameters.go b/cli/parameters.go index cc5607247012b..5d69cb51d33e8 100644 --- a/cli/parameters.go +++ b/cli/parameters.go @@ -10,9 +10,13 @@ import ( func parameters() *cobra.Command { cmd := &cobra.Command{ - Short: "List parameters for a given scope", - Example: "coder parameters list workspace my-workspace", - Use: "parameters", + Short: "List parameters for a given scope", + Example: formatExamples( + example{ + Command: "coder parameters list workspace my-workspace", + }, + ), + Use: "parameters", // Currently hidden as this shows parameter values, not parameter // schemes. Until we have a good way to distinguish the two, it's better // not to add confusion or lock ourselves into a certain api. diff --git a/cli/portforward.go b/cli/portforward.go index bbbda6923d8ee..a604aad220a2f 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -32,28 +32,28 @@ func portForward() *cobra.Command { Short: "Forward one or more ports from the local machine to the remote workspace", Aliases: []string{"tunnel"}, Args: cobra.ExactArgs(1), - Example: ` - - Port forward a single TCP port from 1234 in the workspace to port 5678 on - your local machine - - ` + cliui.Styles.Code.Render("$ coder port-forward --tcp 5678:1234") + ` - - - Port forward a single UDP port from port 9000 to port 9000 on your local - machine - - ` + cliui.Styles.Code.Render("$ coder port-forward --udp 9000") + ` - - - Forward a Unix socket in the workspace to a local Unix socket - - ` + cliui.Styles.Code.Render("$ coder port-forward --unix ./local.sock:~/remote.sock") + ` - - - Forward a Unix socket in the workspace to a local TCP port - - ` + cliui.Styles.Code.Render("$ coder port-forward --unix 8080:~/remote.sock") + ` - - - Port forward multiple TCP ports and a UDP port - - ` + cliui.Styles.Code.Render("$ coder port-forward --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53"), + Example: formatExamples( + example{ + Description: "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine", + Command: "coder port-forward --tcp 5678:1234", + }, + example{ + Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine", + Command: "coder port-forward --udp 9000", + }, + example{ + Description: "Forward a Unix socket in the workspace to a local Unix socket", + Command: "coder port-forward --unix ./local.sock:~/remote.sock", + }, + example{ + Description: "Forward a Unix socket in the workspace to a local TCP port", + Command: "coder port-forward --unix 8080:~/remote.sock", + }, + example{ + Description: "Port forward multiple TCP ports and a UDP port", + Command: "coder port-forward --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53", + }, + ), RunE: func(cmd *cobra.Command, args []string) error { specs, err := parsePortForwards(tcpForwards, udpForwards, unixForwards) if err != nil { diff --git a/cli/root.go b/cli/root.go index c89b0e299ff97..e81cc652a9fe8 100644 --- a/cli/root.go +++ b/cli/root.go @@ -101,12 +101,16 @@ func Root() *cobra.Command { _, _ = fmt.Fprintln(cmd.ErrOrStderr()) } }, - - Example: ` Start a Coder server. - ` + cliui.Styles.Code.Render("$ coder server") + ` - - Get started by creating a template from an example. - ` + cliui.Styles.Code.Render("$ coder templates init"), + Example: formatExamples( + example{ + Description: "Start a Coder server", + Command: "coder server", + }, + example{ + Description: "Get started by creating a template from an example", + Command: "coder templates init", + }, + ), } cmd.AddCommand( @@ -158,9 +162,8 @@ func Root() *cobra.Command { // versionCmd prints the coder version func versionCmd() *cobra.Command { return &cobra.Command{ - Use: "version", - Short: "Show coder version", - Example: "coder version", + Use: "version", + Short: "Show coder version", RunE: func(cmd *cobra.Command, args []string) error { var str strings.Builder _, _ = str.WriteString(fmt.Sprintf("Coder %s", buildinfo.Version())) @@ -370,6 +373,34 @@ Use "{{.CommandPath}} [command] --help" for more information about a command. {{end}}` } +// example represents a standard example for command usage, to be used +// with formatExamples. +type example struct { + Description string + Command string +} + +// formatExamples formats the exampels as width wrapped bulletpoint +// descriptions with the command underneath. +func formatExamples(examples ...example) string { + wrap := cliui.Styles.Wrap.Copy() + wrap.PaddingLeft(4) + var sb strings.Builder + for i, e := range examples { + if len(e.Description) > 0 { + _, _ = sb.WriteString(" - " + wrap.Render(e.Description + ":")[4:] + "\n\n ") + } + // We add 1 space here because `cliui.Styles.Code` adds an extra + // space. This makes the code block align at an even 2 or 6 + // spaces for symmetry. + _, _ = sb.WriteString(" " + cliui.Styles.Code.Render(fmt.Sprintf("$ %s", e.Command))) + if i < len(examples)-1 { + _, _ = sb.WriteString("\n\n") + } + } + return sb.String() +} + // FormatCobraError colorizes and adds "--help" docs to cobra commands. func FormatCobraError(err error, cmd *cobra.Command) string { helpErrMsg := fmt.Sprintf("Run '%s --help' for usage.", cmd.CommandPath()) diff --git a/cli/root_internal_test.go b/cli/root_internal_test.go new file mode 100644 index 0000000000000..ab472ba53d28a --- /dev/null +++ b/cli/root_internal_test.go @@ -0,0 +1,66 @@ +package cli + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_formatExamples(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + examples []example + wantMatches []string + }{ + { + name: "No examples", + examples: nil, + wantMatches: nil, + }, + { + name: "Output examples", + examples: []example{ + { + Description: "Hello world", + Command: "echo hello", + }, + { + Description: "Bye bye", + Command: "echo bye", + }, + }, + wantMatches: []string{ + "Hello world", "echo hello", + "Bye bye", "echo bye", + }, + }, + { + name: "No description outputs commands", + examples: []example{ + { + Command: "echo hello", + }, + }, + wantMatches: []string{ + "echo hello", + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := formatExamples(tt.examples...) + if len(tt.wantMatches) == 0 { + require.Empty(t, got) + } else { + for _, want := range tt.wantMatches { + require.Contains(t, got, want) + } + } + }) + } +} diff --git a/cli/root_test.go b/cli/root_test.go index 48298267a02db..f920401c9651c 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -4,10 +4,9 @@ import ( "bytes" "testing" - "github.com/coder/coder/buildinfo" - "github.com/stretchr/testify/require" + "github.com/coder/coder/buildinfo" "github.com/coder/coder/cli" "github.com/coder/coder/cli/clitest" ) diff --git a/cli/schedule.go b/cli/schedule.go index 4c8a3a6be67c4..1cd7cc65edca9 100644 --- a/cli/schedule.go +++ b/cli/schedule.go @@ -103,10 +103,15 @@ func scheduleStart() *cobra.Command { cmd := &cobra.Command{ Annotations: workspaceCommand, Use: "start { [day-of-week] [location] | manual }", - Example: `start my-workspace 9:30AM Mon-Fri Europe/Dublin`, - Short: "Edit workspace start schedule", - Long: scheduleStartDescriptionLong, - Args: cobra.RangeArgs(2, 4), + Example: formatExamples( + example{ + Description: "Set the workspace to start at 9:30am (in Dublin) from Monday to Friday", + Command: "coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin", + }, + ), + Short: "Edit workspace start schedule", + Long: scheduleStartDescriptionLong, + Args: cobra.RangeArgs(2, 4), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { @@ -151,9 +156,13 @@ func scheduleStop() *cobra.Command { Annotations: workspaceCommand, Args: cobra.ExactArgs(2), Use: "stop { | manual }", - Example: `stop my-workspace 2h30m`, - Short: "Edit workspace stop schedule", - Long: scheduleStopDescriptionLong, + Example: formatExamples( + example{ + Command: "coder schedule stop my-workspace 2h30m", + }, + ), + Short: "Edit workspace stop schedule", + Long: scheduleStopDescriptionLong, RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { @@ -194,9 +203,13 @@ func scheduleOverride() *cobra.Command { Args: cobra.ExactArgs(2), Annotations: workspaceCommand, Use: "override-stop ", - Example: "override-stop my-workspace 90m", - Short: "Edit stop time of active workspace", - Long: scheduleOverrideDescriptionLong, + Example: formatExamples( + example{ + Command: "coder schedule override-stop my-workspace 90m", + }, + ), + Short: "Edit stop time of active workspace", + Long: scheduleOverrideDescriptionLong, RunE: func(cmd *cobra.Command, args []string) error { overrideDuration, err := parseDuration(args[1]) if err != nil { diff --git a/cli/templates.go b/cli/templates.go index 9d51fe6e74439..7ffb1bdfb21b4 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -16,18 +16,20 @@ func templates() *cobra.Command { Use: "templates", Short: "Create, manage, and deploy templates", Aliases: []string{"template"}, - Example: ` - - Create a template for developers to create workspaces - - ` + cliui.Styles.Code.Render("$ coder templates create") + ` - - - Make changes to your template, and plan the changes - - ` + cliui.Styles.Code.Render("$ coder templates plan ") + ` - - - Update the template. Your developers can update their workspaces - - ` + cliui.Styles.Code.Render("$ coder templates update "), + Example: formatExamples( + example{ + Description: "Create a template for developers to create workspaces", + Command: "coder templates create", + }, + example{ + Description: "Make changes to your template, and plan the changes", + Command: "coder templates plan my-template", + }, + example{ + Description: "Update the template. Your developers can update their workspaces", + Command: "coder templates update my-template", + }, + ), } cmd.AddCommand( templateCreate(), diff --git a/cli/userlist.go b/cli/userlist.go index 58325702cda3d..23325611d47fb 100644 --- a/cli/userlist.go +++ b/cli/userlist.go @@ -9,9 +9,7 @@ import ( ) func userList() *cobra.Command { - var ( - columns []string - ) + var columns []string cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, @@ -35,14 +33,16 @@ func userList() *cobra.Command { } func userSingle() *cobra.Command { - var ( - columns []string - ) + var columns []string cmd := &cobra.Command{ - Use: "show ", - Short: "Show a single user. Use 'me' to indicate the currently authenticated user.", - Example: "coder users show me", - Args: cobra.ExactArgs(1), + Use: "show ", + Short: "Show a single user. Use 'me' to indicate the currently authenticated user.", + Example: formatExamples( + example{ + Command: "coder users show me", + }, + ), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/userstatus.go b/cli/userstatus.go index 3c908539a7704..4596053a14359 100644 --- a/cli/userstatus.go +++ b/cli/userstatus.go @@ -31,15 +31,17 @@ func createUserStatusCommand(sdkStatus codersdk.UserStatus) *cobra.Command { panic(fmt.Sprintf("%s is not supported", sdkStatus)) } - var ( - columns []string - ) + var columns []string cmd := &cobra.Command{ Use: fmt.Sprintf("%s ", verb), Short: short, Args: cobra.ExactArgs(1), Aliases: aliases, - Example: fmt.Sprintf("coder users %s example_user", verb), + Example: formatExamples( + example{ + Command: fmt.Sprintf("coder users %s example_user", verb), + }, + ), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/wireguardtunnel.go b/cli/wireguardtunnel.go index 6a44f17d9ec58..4d062ded65d8b 100644 --- a/cli/wireguardtunnel.go +++ b/cli/wireguardtunnel.go @@ -37,21 +37,20 @@ func wireguardPortForward() *cobra.Command { Args: cobra.ExactArgs(1), // Hide all wireguard commands for now while we test! Hidden: true, - Example: ` - - Port forward a single TCP port from 1234 in the workspace to port 5678 on - your local machine - - ` + cliui.Styles.Code.Render("$ coder port-forward --tcp 5678:1234") + ` - - - Port forward a single UDP port from port 9000 to port 9000 on your local - machine - - ` + cliui.Styles.Code.Render("$ coder port-forward --udp 9000") + ` - - - Port forward multiple TCP ports and a UDP port - - ` + cliui.Styles.Code.Render("$ coder port-forward --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53") + ` -`, + Example: formatExamples( + example{ + Description: "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine", + Command: "coder wireguard-port-forward --tcp 5678:1234", + }, + example{ + Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine", + Command: "coder wireguard-port-forward --udp 9000", + }, + example{ + Description: "Port forward multiple TCP ports and a UDP port", + Command: "coder wireguard-port-forward --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53", + }, + ), RunE: func(cmd *cobra.Command, args []string) error { specs, err := parsePortForwards(tcpForwards, nil, nil) if err != nil {