Skip to content

Commit f338d80

Browse files
committed
Refactor CLI to make it testable using testscript
1 parent a4a846a commit f338d80

10 files changed

Lines changed: 234 additions & 172 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ lint: golint govulncheck
175175

176176
golint: SHELL:=/bin/bash
177177
golint:
178-
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m
178+
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --build-tags=integration --timeout=30m
179179

180180
govulncheck:
181181
$Q govulncheck ./...

cmd/step/main.go

Lines changed: 2 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,12 @@
11
package main
22

33
import (
4-
"errors"
5-
"fmt"
6-
"io"
74
"os"
8-
"reflect"
9-
"regexp"
10-
"strings"
11-
"time"
12-
13-
"github.com/urfave/cli"
145

156
"github.com/smallstep/certificates/ca"
16-
"github.com/smallstep/cli-utils/command"
177
"github.com/smallstep/cli-utils/step"
18-
"github.com/smallstep/cli-utils/ui"
19-
"github.com/smallstep/cli-utils/usage"
20-
"go.step.sm/crypto/jose"
21-
"go.step.sm/crypto/pemutil"
22-
23-
"github.com/smallstep/cli/command/version"
24-
"github.com/smallstep/cli/internal/plugin"
25-
"github.com/smallstep/cli/utils"
26-
27-
// Enabled cas interfaces.
28-
_ "github.com/smallstep/certificates/cas/cloudcas"
29-
_ "github.com/smallstep/certificates/cas/softcas"
30-
_ "github.com/smallstep/certificates/cas/stepcas"
318

32-
// Enabled commands
33-
_ "github.com/smallstep/cli/command/api"
34-
_ "github.com/smallstep/cli/command/base64"
35-
_ "github.com/smallstep/cli/command/beta"
36-
_ "github.com/smallstep/cli/command/ca"
37-
_ "github.com/smallstep/cli/command/certificate"
38-
_ "github.com/smallstep/cli/command/completion"
39-
_ "github.com/smallstep/cli/command/context"
40-
_ "github.com/smallstep/cli/command/crl"
41-
_ "github.com/smallstep/cli/command/crypto"
42-
_ "github.com/smallstep/cli/command/fileserver"
43-
_ "github.com/smallstep/cli/command/oauth"
44-
_ "github.com/smallstep/cli/command/path"
45-
_ "github.com/smallstep/cli/command/ssh"
9+
"github.com/smallstep/cli/internal/cmd"
4610
)
4711

4812
// Version is set by an LDFLAG at build time representing the git tag or commit
@@ -59,137 +23,5 @@ func init() {
5923
}
6024

6125
func main() {
62-
// initialize step environment.
63-
if err := step.Init(); err != nil {
64-
fmt.Fprintln(os.Stderr, err.Error())
65-
os.Exit(1)
66-
}
67-
68-
defer panicHandler()
69-
70-
// create new instance of app
71-
app := newApp(os.Stdout, os.Stderr)
72-
73-
if err := app.Run(os.Args); err != nil {
74-
var messenger interface {
75-
Message() string
76-
}
77-
if errors.As(err, &messenger) {
78-
if os.Getenv("STEPDEBUG") == "1" {
79-
fmt.Fprintf(os.Stderr, "%+v\n\n%s", err, messenger.Message())
80-
} else {
81-
fmt.Fprintln(os.Stderr, messenger.Message())
82-
fmt.Fprintln(os.Stderr, "Re-run with STEPDEBUG=1 for more info.")
83-
}
84-
} else {
85-
if os.Getenv("STEPDEBUG") == "1" {
86-
fmt.Fprintf(os.Stderr, "%+v\n", err)
87-
} else {
88-
fmt.Fprintln(os.Stderr, err)
89-
}
90-
}
91-
//nolint:gocritic // ignore exitAfterDefer error because the defer is required for recovery.
92-
os.Exit(1)
93-
}
94-
}
95-
96-
func newApp(stdout, stderr io.Writer) *cli.App {
97-
// Define default file writers and prompters for go.step.sm/crypto
98-
pemutil.WriteFile = utils.WriteFile
99-
pemutil.PromptPassword = func(msg string) ([]byte, error) {
100-
return ui.PromptPassword(msg)
101-
}
102-
jose.PromptPassword = func(msg string) ([]byte, error) {
103-
return ui.PromptPassword(msg)
104-
}
105-
106-
// Override global framework components
107-
cli.VersionPrinter = func(c *cli.Context) {
108-
version.Command(c)
109-
}
110-
cli.AppHelpTemplate = usage.AppHelpTemplate
111-
cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate
112-
cli.CommandHelpTemplate = usage.CommandHelpTemplate
113-
cli.HelpPrinter = usage.HelpPrinter
114-
cli.FlagNamePrefixer = usage.FlagNamePrefixer
115-
cli.FlagStringer = stringifyFlag
116-
117-
// Configure cli app
118-
app := cli.NewApp()
119-
app.Name = "step"
120-
app.HelpName = "step"
121-
app.Usage = "plumbing for distributed systems"
122-
app.Version = step.Version()
123-
app.Commands = command.Retrieve()
124-
app.Flags = append(app.Flags, cli.HelpFlag)
125-
app.EnableBashCompletion = true
126-
app.Copyright = fmt.Sprintf("(c) 2018-%d Smallstep Labs, Inc.", time.Now().Year())
127-
128-
// Flag of custom configuration flag
129-
app.Flags = append(app.Flags, cli.StringFlag{
130-
Name: "config",
131-
Usage: "path to the config file to use for CLI flags",
132-
})
133-
134-
// Action runs on `step` or `step <command>` if the command is not enabled.
135-
app.Action = func(ctx *cli.Context) error {
136-
args := ctx.Args()
137-
if name := args.First(); name != "" {
138-
if file, err := plugin.LookPath(name); err == nil {
139-
return plugin.Run(ctx, file)
140-
}
141-
if u := plugin.GetURL(name); u != "" {
142-
//nolint:staticcheck // this is a top level error - capitalization is ok
143-
return fmt.Errorf("The plugin %q was not found on this system.\nDownload it from %s", name, u)
144-
}
145-
return cli.ShowCommandHelp(ctx, name)
146-
}
147-
return cli.ShowAppHelp(ctx)
148-
}
149-
150-
// All non-successful output should be written to stderr
151-
app.Writer = stdout
152-
app.ErrWriter = stderr
153-
154-
return app
155-
}
156-
157-
func panicHandler() {
158-
if r := recover(); r != nil {
159-
if os.Getenv("STEPDEBUG") == "1" {
160-
fmt.Fprintf(os.Stderr, "%s\n", step.Version())
161-
fmt.Fprintf(os.Stderr, "Release Date: %s\n\n", step.ReleaseDate())
162-
panic(r)
163-
}
164-
165-
fmt.Fprintln(os.Stderr, "Something unexpected happened.")
166-
fmt.Fprintln(os.Stderr, "If you want to help us debug the problem, please run:")
167-
fmt.Fprintf(os.Stderr, "STEPDEBUG=1 %s\n", strings.Join(os.Args, " "))
168-
fmt.Fprintln(os.Stderr, "and send the output to [email protected]")
169-
os.Exit(2)
170-
}
171-
}
172-
173-
func flagValue(f cli.Flag) reflect.Value {
174-
fv := reflect.ValueOf(f)
175-
for fv.Kind() == reflect.Ptr {
176-
fv = reflect.Indirect(fv)
177-
}
178-
return fv
179-
}
180-
181-
var placeholderString = regexp.MustCompile(`<.*?>`)
182-
183-
func stringifyFlag(f cli.Flag) string {
184-
fv := flagValue(f)
185-
usg := fv.FieldByName("Usage").String()
186-
placeholder := placeholderString.FindString(usg)
187-
if placeholder == "" {
188-
switch f.(type) {
189-
case cli.BoolFlag, cli.BoolTFlag:
190-
default:
191-
placeholder = "<value>"
192-
}
193-
}
194-
return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usg
26+
os.Exit(cmd.Run())
19527
}

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ require (
1414
github.com/icrowley/fake v0.0.0-20221112152111-d7b7e2276db2
1515
github.com/manifoldco/promptui v0.9.0
1616
github.com/pkg/errors v0.9.1
17+
<<<<<<< HEAD
1718
github.com/pquerna/otp v1.5.0
19+
=======
20+
github.com/pquerna/otp v1.4.0
21+
github.com/rogpeppe/go-internal v1.13.1
22+
>>>>>>> b69ee0ea (Refactor CLI to make it testable using `testscript`)
1823
github.com/slackhq/nebula v1.9.5
1924
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
2025
github.com/smallstep/certificates v0.28.3
@@ -142,6 +147,7 @@ require (
142147
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
143148
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
144149
google.golang.org/grpc v1.72.2 // indirect
150+
golang.org/x/tools v0.31.0 // indirect
145151
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
146152
gopkg.in/yaml.v3 v3.0.1 // indirect
147153
howett.net/plist v1.0.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
474474
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
475475
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
476476
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
477+
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
478+
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
477479
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
478480
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
479481
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=

integration/script/script_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package script
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/rogpeppe/go-internal/testscript"
8+
9+
"github.com/smallstep/cli/internal/cmd"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
os.Exit(testscript.RunMain(m, map[string]func() int{
14+
"step": cmd.Run,
15+
}))
16+
}
17+
18+
func TestHelp(t *testing.T) {
19+
testscript.Run(t, testscript.Params{
20+
Files: []string{"testdata/help.txtar"},
21+
})
22+
}
23+
24+
func TestCryptoHelp(t *testing.T) {
25+
testscript.Run(t, testscript.Params{
26+
Files: []string{"testdata/crypto/help.txtar"},
27+
})
28+
}
29+
30+
func TestBogusCommand(t *testing.T) {
31+
testscript.Run(t, testscript.Params{
32+
Files: []string{"testdata/bogus.txtar"},
33+
})
34+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
! exec step bogus
2+
stderr 'No help topic for'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exec step help crypto
2+
stdout 'cryptographic primitives that balances completeness and safety'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
exec step --help
2+
stdout 'plumbing for distributed systems'

0 commit comments

Comments
 (0)