Skip to content

Commit 176dc37

Browse files
authored
Commandline package initial code (#1)
Signed-off-by: Krzysztof Suszyński <[email protected]>
1 parent 00594d9 commit 176dc37

File tree

11 files changed

+365
-0
lines changed

11 files changed

+365
-0
lines changed

.github/workflows/go.yml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Go
2+
3+
on:
4+
push:
5+
branches: [ 'main', 'release-*' ]
6+
pull_request:
7+
types: [opened, synchronize, reopened]
8+
9+
jobs:
10+
11+
build:
12+
name: Test
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
go-version:
17+
- '1.18'
18+
steps:
19+
20+
- name: Set up Go ${{ matrix.go-version }}
21+
uses: actions/setup-go@v2
22+
with:
23+
go-version: ${{ matrix.go-version }}
24+
id: go
25+
26+
- name: Check out code into the Go module directory
27+
uses: actions/checkout@v2
28+
29+
- name: Test
30+
run: go run gotest.tools/[email protected] --format testname --
31+
-race -count=1 -short ./...
32+
env:
33+
FORCE_COLOR: true

.github/workflows/knative-style.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: Code Style
2+
3+
on:
4+
pull_request:
5+
branches: [ 'main', 'release-*' ]
6+
7+
jobs:
8+
9+
style:
10+
uses: knative/actions/.github/workflows/style.yaml@main

go.mod

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module github.com/wavesoftware/go-commandline
2+
3+
go 1.18
4+
5+
require (
6+
github.com/spf13/cobra v1.5.0
7+
github.com/wavesoftware/go-retcode v1.0.0
8+
gotest.tools/v3 v3.3.0
9+
)
10+
11+
require (
12+
github.com/google/go-cmp v0.5.5 // indirect
13+
github.com/inconshreveable/mousetrap v1.0.0 // indirect
14+
github.com/spf13/pflag v1.0.5 // indirect
15+
)

go.sum

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
3+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4+
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
5+
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
6+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
7+
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
8+
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
9+
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
10+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
11+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
12+
github.com/wavesoftware/go-retcode v1.0.0 h1:Z53+VpIHMvRMtjS6jPScdihbAN1ks3lIJ5Mj32gCpno=
13+
github.com/wavesoftware/go-retcode v1.0.0/go.mod h1:BLqIIXhB/PQ+izkkRGfSQgu95BDtMmUBuvTJ/gkSWVM=
14+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
15+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
16+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
17+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
18+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
19+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
20+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
21+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
22+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
23+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
24+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
25+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
26+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
27+
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
29+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
30+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
31+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
32+
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
33+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
34+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
35+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
36+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
37+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
38+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
39+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
40+
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
41+
gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=

test/cmd/main.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package main
2+
3+
import (
4+
"github.com/wavesoftware/go-commandline"
5+
"github.com/wavesoftware/go-commandline/test/internal/cli"
6+
)
7+
8+
func main() {
9+
commandline.New(new(cli.App)).ExecuteOrDie(cli.Opts...)
10+
}

test/cmd/main_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
main "github.com/wavesoftware/go-commandline/test/cmd"
8+
"github.com/wavesoftware/go-commandline/test/internal/cli"
9+
"gotest.tools/v3/assert"
10+
11+
"github.com/wavesoftware/go-commandline"
12+
)
13+
14+
func TestTheMain(t *testing.T) {
15+
s := capture(func() {
16+
main.Main()
17+
})
18+
assert.Equal(t, 0, s.exitCode)
19+
assert.Equal(t, "Hello, world!\n", s.out.String())
20+
}
21+
22+
type state struct {
23+
exitCode int
24+
out bytes.Buffer
25+
}
26+
27+
func (s *state) opts() []commandline.Option {
28+
return []commandline.Option{
29+
commandline.WithOutput(&s.out),
30+
commandline.WithExit(func(code int) {
31+
s.exitCode = code
32+
}),
33+
}
34+
}
35+
36+
func capture(fn func()) state {
37+
var s state
38+
withOpts(fn, s.opts())
39+
return s
40+
}
41+
42+
func withOpts(fn func(), opts []commandline.Option) {
43+
keep := cli.Opts
44+
defer func() {
45+
cli.Opts = keep
46+
}()
47+
cli.Opts = opts
48+
fn()
49+
}

test/cmd/themain_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package main
2+
3+
// Main is the entrypoint for the main package.
4+
func Main() {
5+
main()
6+
}

test/internal/cli/app.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cli
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/wavesoftware/go-commandline"
6+
)
7+
8+
// Opts is the list of commandline options to pass to the main function.
9+
var Opts []commandline.Option
10+
11+
type App struct{}
12+
13+
func (a App) Command() *cobra.Command {
14+
return &cobra.Command{
15+
Use: "example",
16+
Run: func(cmd *cobra.Command, args []string) {
17+
cmd.Println("Hello, world!")
18+
},
19+
}
20+
}

test/internal/cli/app_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/wavesoftware/go-commandline/test/internal/cli"
8+
"gotest.tools/v3/assert"
9+
)
10+
11+
func TestAppCommand(t *testing.T) {
12+
app := new(cli.App)
13+
cmd := app.Command()
14+
var buf bytes.Buffer
15+
cmd.SetOut(&buf)
16+
err := cmd.Execute()
17+
assert.NilError(t, err)
18+
assert.Equal(t, "Hello, world!\n", buf.String())
19+
}

types.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package commandline
2+
3+
import (
4+
"errors"
5+
"io"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/wavesoftware/go-retcode"
10+
)
11+
12+
// ErrNoRootCommand is returned when no root command is provided.
13+
var ErrNoRootCommand = errors.New("no root command provided")
14+
15+
// App represents a command line application.
16+
type App struct {
17+
CobraProvider
18+
Exit func(code int)
19+
root *cobra.Command
20+
}
21+
22+
// CobraProvider is used to provide a Cobra command.
23+
type CobraProvider interface {
24+
Command() *cobra.Command
25+
}
26+
27+
// Option is used to configure an App.
28+
type Option func(*App)
29+
30+
// New creates a new App from CobraProvider.
31+
func New(cp CobraProvider) *App {
32+
return &App{
33+
CobraProvider: cp,
34+
Exit: os.Exit,
35+
}
36+
}
37+
38+
// ExecuteOrDie will execute the application or perform os.Exit in case of error.
39+
func (a *App) ExecuteOrDie(options ...Option) {
40+
if err := a.Execute(options...); err != nil {
41+
a.Exit(retcode.Calc(err))
42+
}
43+
}
44+
45+
// WithArgs creates an option which sets args.
46+
func WithArgs(args ...string) Option {
47+
return func(app *App) {
48+
app.root.SetArgs(args)
49+
}
50+
}
51+
52+
// WithInput creates an option witch sets os.Stdin.
53+
func WithInput(in io.Reader) Option {
54+
return func(app *App) {
55+
app.root.SetIn(in)
56+
}
57+
}
58+
59+
// WithOutput creates an option witch sets os.Stdout and os.Stderr.
60+
func WithOutput(out io.Writer) Option {
61+
return func(app *App) {
62+
app.root.SetOut(out)
63+
app.root.SetErr(out)
64+
}
65+
}
66+
67+
// WithExit creates an option which sets the exit function.
68+
func WithExit(fn func(code int)) Option {
69+
return func(app *App) {
70+
app.Exit = fn
71+
}
72+
}
73+
74+
// Execute will execute the application with the provided options and return
75+
// error if any.
76+
func (a *App) Execute(options ...Option) error {
77+
if err := a.init(); err != nil {
78+
return err
79+
}
80+
for _, config := range options {
81+
config(a)
82+
}
83+
// cobra.Command should pass our own errors, no need to wrap them.
84+
return a.root.Execute() //nolint:wrapcheck
85+
}
86+
87+
func (a *App) init() error {
88+
if a.Exit == nil {
89+
a.Exit = os.Exit
90+
}
91+
if a.CobraProvider == nil {
92+
return ErrNoRootCommand
93+
}
94+
a.root = a.Command()
95+
if a.root == nil {
96+
return ErrNoRootCommand
97+
}
98+
return nil
99+
}

types_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package commandline_test
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io/ioutil"
7+
"testing"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/wavesoftware/go-commandline"
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func TestExecuteOrDie(t *testing.T) {
15+
var buf bytes.Buffer
16+
var retcode int
17+
commandline.New(new(testApp)).ExecuteOrDie(
18+
commandline.WithOutput(&buf),
19+
commandline.WithInput(bytes.NewBufferString("Input")),
20+
commandline.WithArgs("arg1", "arg2"),
21+
commandline.WithExit(func(code int) {
22+
retcode = code
23+
}),
24+
)
25+
assert.Equal(t, `example Input: ["arg1" "arg2"]`, buf.String())
26+
assert.Equal(t, 133, retcode)
27+
}
28+
29+
func TestExit(t *testing.T) {
30+
app := commandline.App{CobraProvider: nil}
31+
err := app.Execute()
32+
assert.ErrorIs(t, err, commandline.ErrNoRootCommand)
33+
34+
app = commandline.App{CobraProvider: nilApp{}}
35+
err = app.Execute()
36+
assert.ErrorIs(t, err, commandline.ErrNoRootCommand)
37+
}
38+
39+
var errExample = errors.New("example error")
40+
41+
type testApp struct{}
42+
43+
func (t testApp) Command() *cobra.Command {
44+
return &cobra.Command{
45+
Use: "example",
46+
SilenceUsage: true,
47+
SilenceErrors: true,
48+
RunE: func(cmd *cobra.Command, args []string) error {
49+
in, err := ioutil.ReadAll(cmd.InOrStdin())
50+
if err != nil {
51+
return err
52+
}
53+
cmd.Printf("%s %s: %q", cmd.Use, in, args)
54+
return errExample
55+
},
56+
}
57+
}
58+
59+
type nilApp struct{}
60+
61+
func (n nilApp) Command() *cobra.Command {
62+
return nil
63+
}

0 commit comments

Comments
 (0)