Skip to content

Commit 00d468b

Browse files
authored
feat(cli): add --output={text,json} to version cmd (#7010)
* feat(cliui): add TextFormat * feat(cli): add --format={text,json} to version cmd
1 parent 9c4ccd7 commit 00d468b

File tree

7 files changed

+192
-33
lines changed

7 files changed

+192
-33
lines changed

cli/cliui/output.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cliui
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"reflect"
78
"strings"
89

@@ -171,3 +172,23 @@ func (jsonFormat) Format(_ context.Context, data any) (string, error) {
171172

172173
return string(outBytes), nil
173174
}
175+
176+
type textFormat struct{}
177+
178+
var _ OutputFormat = textFormat{}
179+
180+
// TextFormat is a formatter that just outputs unstructured text.
181+
// It uses fmt.Sprintf under the hood.
182+
func TextFormat() OutputFormat {
183+
return textFormat{}
184+
}
185+
186+
func (textFormat) ID() string {
187+
return "text"
188+
}
189+
190+
func (textFormat) AttachOptions(_ *clibase.OptionSet) {}
191+
192+
func (textFormat) Format(_ context.Context, data any) (string, error) {
193+
return fmt.Sprintf("%s", data), nil
194+
}

cli/cliui/output_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func Test_OutputFormatter(t *testing.T) {
5050
require.Panics(t, func() {
5151
cliui.NewOutputFormatter(cliui.JSONFormat())
5252
})
53+
require.NotPanics(t, func() {
54+
cliui.NewOutputFormatter(cliui.JSONFormat(), cliui.TextFormat())
55+
})
5356
})
5457

5558
t.Run("NoMissingFormatID", func(t *testing.T) {

cli/root.go

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
8282
r.templates(),
8383
r.users(),
8484
r.tokens(),
85-
r.version(),
85+
r.version(defaultVersionInfo),
8686

8787
// Workspace Commands
8888
r.configSSH(),
@@ -370,36 +370,6 @@ func LoggerFromContext(ctx context.Context) (slog.Logger, bool) {
370370
return l, ok
371371
}
372372

373-
// version prints the coder version
374-
func (*RootCmd) version() *clibase.Cmd {
375-
return &clibase.Cmd{
376-
Use: "version",
377-
Short: "Show coder version",
378-
Handler: func(inv *clibase.Invocation) error {
379-
var str strings.Builder
380-
_, _ = str.WriteString("Coder ")
381-
if buildinfo.IsAGPL() {
382-
_, _ = str.WriteString("(AGPL) ")
383-
}
384-
_, _ = str.WriteString(buildinfo.Version())
385-
buildTime, valid := buildinfo.Time()
386-
if valid {
387-
_, _ = str.WriteString(" " + buildTime.Format(time.UnixDate))
388-
}
389-
_, _ = str.WriteString("\r\n" + buildinfo.ExternalURL() + "\r\n\r\n")
390-
391-
if buildinfo.IsSlim() {
392-
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.\n", cliui.Styles.Code.Render("server")))
393-
} else {
394-
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.\n", cliui.Styles.Code.Render("server")))
395-
}
396-
397-
_, _ = fmt.Fprint(inv.Stdout, str.String())
398-
return nil
399-
},
400-
}
401-
}
402-
403373
func isTest() bool {
404374
return flag.Lookup("test.v") != nil
405375
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
Usage: coder version
1+
Usage: coder version [flags]
22

33
Show coder version
44

5+
Options
6+
-o, --output string (default: text)
7+
Output format. Available formats: text, json.
8+
59
---
610
Run `coder --help` for a list of global options.

cli/version.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/coder/coder/buildinfo"
9+
"github.com/coder/coder/cli/clibase"
10+
"github.com/coder/coder/cli/cliui"
11+
)
12+
13+
// versionInfo wraps the stuff we get from buildinfo so that it's
14+
// easier to emit in different formats.
15+
type versionInfo struct {
16+
Version string `json:"version"`
17+
BuildTime time.Time `json:"build_time"`
18+
ExternalURL string `json:"external_url"`
19+
Slim bool `json:"slim"`
20+
AGPL bool `json:"agpl"`
21+
}
22+
23+
// String() implements Stringer
24+
func (vi versionInfo) String() string {
25+
var str strings.Builder
26+
_, _ = str.WriteString("Coder ")
27+
if vi.AGPL {
28+
_, _ = str.WriteString("(AGPL) ")
29+
}
30+
_, _ = str.WriteString(vi.Version)
31+
32+
if !vi.BuildTime.IsZero() {
33+
_, _ = str.WriteString(" " + vi.BuildTime.Format(time.UnixDate))
34+
}
35+
_, _ = str.WriteString("\r\n" + vi.ExternalURL + "\r\n\r\n")
36+
37+
if vi.Slim {
38+
_, _ = str.WriteString(fmt.Sprintf("Slim build of Coder, does not support the %s subcommand.", cliui.Styles.Code.Render("server")))
39+
} else {
40+
_, _ = str.WriteString(fmt.Sprintf("Full build of Coder, supports the %s subcommand.", cliui.Styles.Code.Render("server")))
41+
}
42+
return str.String()
43+
}
44+
45+
func defaultVersionInfo() *versionInfo {
46+
buildTime, _ := buildinfo.Time()
47+
return &versionInfo{
48+
Version: buildinfo.Version(),
49+
BuildTime: buildTime,
50+
ExternalURL: buildinfo.ExternalURL(),
51+
Slim: buildinfo.IsSlim(),
52+
AGPL: buildinfo.IsAGPL(),
53+
}
54+
}
55+
56+
// version prints the coder version
57+
func (*RootCmd) version(versionInfo func() *versionInfo) *clibase.Cmd {
58+
var (
59+
formatter = cliui.NewOutputFormatter(
60+
cliui.TextFormat(),
61+
cliui.JSONFormat(),
62+
)
63+
vi = versionInfo()
64+
)
65+
66+
cmd := &clibase.Cmd{
67+
Use: "version",
68+
Short: "Show coder version",
69+
Options: clibase.OptionSet{},
70+
Handler: func(inv *clibase.Invocation) error {
71+
out, err := formatter.Format(inv.Context(), vi)
72+
if err != nil {
73+
return err
74+
}
75+
76+
_, err = fmt.Fprintln(inv.Stdout, out)
77+
return err
78+
},
79+
}
80+
81+
formatter.AttachOptions(&cmd.Options)
82+
83+
return cmd
84+
}

cli/version_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/cli/clitest"
12+
"github.com/coder/coder/testutil"
13+
)
14+
15+
func TestVersion(t *testing.T) {
16+
t.Parallel()
17+
expectedText := `Coder v0.0.0-devel
18+
https://github.com/coder/coder
19+
20+
Full build of Coder, supports the server subcommand.
21+
`
22+
expectedJSON := `{
23+
"version": "v0.0.0-devel",
24+
"build_time": "0001-01-01T00:00:00Z",
25+
"external_url": "https://github.com/coder/coder",
26+
"slim": false,
27+
"agpl": false
28+
}
29+
`
30+
for _, tt := range []struct {
31+
Name string
32+
Args []string
33+
Expected string
34+
}{
35+
{
36+
Name: "Defaults to human-readable output",
37+
Args: []string{"version"},
38+
Expected: expectedText,
39+
},
40+
{
41+
Name: "JSON output",
42+
Args: []string{"version", "--output=json"},
43+
Expected: expectedJSON,
44+
},
45+
{
46+
Name: "Text output",
47+
Args: []string{"version", "--output=text"},
48+
Expected: expectedText,
49+
},
50+
} {
51+
tt := tt
52+
t.Run(tt.Name, func(t *testing.T) {
53+
t.Parallel()
54+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
55+
t.Cleanup(cancel)
56+
inv, _ := clitest.New(t, tt.Args...)
57+
buf := new(bytes.Buffer)
58+
inv.Stdout = buf
59+
err := inv.WithContext(ctx).Run()
60+
require.NoError(t, err)
61+
actual := buf.String()
62+
actual = strings.ReplaceAll(actual, "\r\n", "\n")
63+
require.Equal(t, tt.Expected, actual)
64+
})
65+
}
66+
}

docs/cli/version.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,16 @@ Show coder version
77
## Usage
88

99
```console
10-
coder version
10+
coder version [flags]
1111
```
12+
13+
## Options
14+
15+
### -o, --output
16+
17+
| | |
18+
| ------- | ------------------- |
19+
| Type | <code>string</code> |
20+
| Default | <code>text</code> |
21+
22+
Output format. Available formats: text, json.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy