├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── codecov.yml ├── go.mod ├── go.sum ├── simplecobra.go └── simplecobra_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bep] -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | permissions: 6 | contents: read 7 | name: Test 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | go-version: [1.23.x, 1.24.x] 13 | platform: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Install staticcheck 21 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 22 | shell: bash 23 | - name: Install golint 24 | run: go install golang.org/x/lint/golint@latest 25 | shell: bash 26 | - name: Update PATH 27 | run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH 28 | shell: bash 29 | - name: Checkout code 30 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | - name: Fmt 32 | if: matrix.platform != 'windows-latest' # :( 33 | run: "diff <(gofmt -d .) <(printf '')" 34 | shell: bash 35 | - name: Vet 36 | run: go vet ./... 37 | - name: Staticcheck 38 | run: staticcheck ./... 39 | - name: Lint 40 | run: golint ./... 41 | - name: Test 42 | run: go test -race ./... -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic 43 | - name: Upload coverage 44 | if: success() && matrix.platform == 'ubuntu-latest' 45 | run: | 46 | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time step 47 | curl -Os https://uploader.codecov.io/latest/linux/codecov 48 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 49 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 50 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 51 | shasum -a 256 -c codecov.SHA256SUM 52 | chmod +x codecov 53 | ./codecov 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bjørn Erik Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests on Linux, MacOS and Windows](https://github.com/bep/simplecobra/workflows/Test/badge.svg)](https://github.com/bep/simplecobra/actions?query=workflow:Test) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/bep/simplecobra)](https://goreportcard.com/report/github.com/bep/simplecobra) 3 | [![codecov](https://codecov.io/gh/bep/simplecobra/branch/master/graph/badge.svg)](https://codecov.io/gh/bep/simplecobra) 4 | [![GoDoc](https://godoc.org/github.com/bep/simplecobra?status.svg)](https://godoc.org/github.com/bep/simplecobra) 5 | 6 | So, [Cobra](https://github.com/spf13/cobra) is a Go CLI library with a feature set that's hard to resist for bigger applications (autocompletion, docs and man pages auto generation etc.). But it's also complex to use beyond the simplest of applications. This package was built to help rewriting [Hugo's](https://github.com/gohugoio/hugo) commands package to something that's easier to understand and maintain. 7 | 8 | I welcome suggestions to improve/simplify this further, but the core idea is that the command graph gets built in one go with a tree of struct pointers implementing a simple `Commander` interface: 9 | 10 | ```go 11 | // Commander is the interface that must be implemented by all commands. 12 | type Commander interface { 13 | // The name of the command. 14 | Name() string 15 | 16 | // Init is called when the cobra command is created. 17 | // This is where the flags, short and long description etc. can be added. 18 | Init(*Commandeer) error 19 | 20 | // PreRun called on all ancestors and the executing command itself, before execution, starting from the root. 21 | // This is the place to evaluate flags and set up the this Commandeer. 22 | // The runner Commandeer holds the currently running command, which will be PreRun last. 23 | PreRun(this, runner *Commandeer) error 24 | 25 | // The command execution. 26 | Run(ctx context.Context, cd *Commandeer, args []string) error 27 | 28 | // Commands returns the sub commands, if any. 29 | Commands() []Commander 30 | } 31 | ``` 32 | 33 | The `Init` method allows for flag compilation, referencing the parent and root etc. If needed, the full Cobra command is still available. 34 | 35 | There's a runnable [example](https://pkg.go.dev/github.com/bep/simplecobra#example-package) in the documentation, but the gist of it is: 36 | 37 | ```go 38 | func main() { 39 | rootCmd := &rootCommand{name: "root", 40 | commands: []simplecobra.Commander{ 41 | &lvl1Command{name: "foo"}, 42 | &lvl1Command{name: "bar", 43 | commands: []simplecobra.Commander{ 44 | &lvl2Command{name: "baz"}, 45 | }, 46 | }, 47 | }, 48 | } 49 | 50 | x, err := simplecobra.New(rootCmd) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | cd, err := x.Execute(context.Background(), []string{"bar", "baz", "--localFlagName", "baz_local", "--persistentFlagName", "baz_persistent"}) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | // These are wired up in Init(). 60 | lvl2 := cd.Command.(*lvl2Command) 61 | lvl1 := lvl2.parentCmd 62 | root := lvl1.rootCmd 63 | 64 | fmt.Printf("Executed %s.%s.%s with localFlagName %s and and persistentFlagName %s.\n", root.name, lvl1.name, lvl2.name, lvl2.localFlagName, root.persistentFlagName) 65 | } 66 | ``` 67 | 68 | 69 | ## Differences to Cobra 70 | 71 | You have access to the `*cobra.Command` pointer so there's not much you cannot do with this project compared to the more low-level Cobra, but there's one small, but important difference: 72 | 73 | Cobra only treats the first level of misspelled commands as an `unknown command` with "Did you mean this?" suggestions, see [see this issue](https://github.com/spf13/cobra/pull/1500) for more context. The reason for this is the ambiguity between sub-command names and command arguments, but that is throwing away a very useful feature for a not very good reason. We recently rewrote [Hugo's CLI](https://github.com/gohugoio/hugo) using this package, and found only one sub command that needed to be adjusted to avoid this ambiguity. 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0.5% 7 | patch: off 8 | 9 | comment: 10 | require_changes: true 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bep/simplecobra 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/frankban/quicktest v1.14.6 7 | github.com/spf13/cobra v1.8.1 8 | ) 9 | 10 | require ( 11 | github.com/google/go-cmp v0.5.9 // indirect 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/kr/pretty v0.3.1 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/rogpeppe/go-internal v1.9.0 // indirect 16 | github.com/spf13/pflag v1.0.5 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 4 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 5 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 6 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 8 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 14 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 15 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 16 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 17 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 18 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 19 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 20 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /simplecobra.go: -------------------------------------------------------------------------------- 1 | package simplecobra 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // Commander is the interface that must be implemented by all commands. 13 | type Commander interface { 14 | // The name of the command. 15 | Name() string 16 | 17 | // Init is called when the cobra command is created. 18 | // This is where the flags, short and long description etc. can be added. 19 | Init(*Commandeer) error 20 | 21 | // PreRun called on all ancestors and the executing command itself, before execution, starting from the root. 22 | // This is the place to evaluate flags and set up the this Commandeer. 23 | // The runner Commandeer holds the currently running command, which will be PreRun last. 24 | PreRun(this, runner *Commandeer) error 25 | 26 | // The command execution. 27 | Run(ctx context.Context, cd *Commandeer, args []string) error 28 | 29 | // Commands returns the sub commands, if any. 30 | Commands() []Commander 31 | } 32 | 33 | // New creates a new Executer from the command tree in Commander. 34 | func New(rootCmd Commander) (*Exec, error) { 35 | rootCd := &Commandeer{ 36 | Command: rootCmd, 37 | } 38 | rootCd.Root = rootCd 39 | 40 | // Add all commands recursively. 41 | var addCommands func(cd *Commandeer, cmd Commander) 42 | addCommands = func(cd *Commandeer, cmd Commander) { 43 | cd2 := &Commandeer{ 44 | Root: rootCd, 45 | Parent: cd, 46 | Command: cmd, 47 | } 48 | cd.commandeers = append(cd.commandeers, cd2) 49 | for _, c := range cmd.Commands() { 50 | addCommands(cd2, c) 51 | } 52 | 53 | } 54 | 55 | for _, cmd := range rootCmd.Commands() { 56 | addCommands(rootCd, cmd) 57 | } 58 | 59 | if err := rootCd.compile(); err != nil { 60 | return nil, err 61 | } 62 | 63 | return &Exec{c: rootCd}, nil 64 | 65 | } 66 | 67 | // Commandeer holds the state of a command and its subcommands. 68 | type Commandeer struct { 69 | Command Commander 70 | CobraCommand *cobra.Command 71 | 72 | Root *Commandeer 73 | Parent *Commandeer 74 | commandeers []*Commandeer 75 | } 76 | 77 | func (c *Commandeer) init() error { 78 | 79 | // Collect all ancestors including self. 80 | var ancestors []*Commandeer 81 | { 82 | cd := c 83 | for cd != nil { 84 | ancestors = append(ancestors, cd) 85 | cd = cd.Parent 86 | } 87 | } 88 | 89 | // Init all of them starting from the root. 90 | for i := len(ancestors) - 1; i >= 0; i-- { 91 | cd := ancestors[i] 92 | if err := cd.Command.PreRun(cd, c); err != nil { 93 | return err 94 | } 95 | } 96 | 97 | return nil 98 | 99 | } 100 | 101 | type runErr struct { 102 | error 103 | } 104 | 105 | func (c *Commandeer) compile() error { 106 | useCommandFlagsArgs := "[command] [flags]" 107 | if len(c.commandeers) == 0 { 108 | useCommandFlagsArgs = "[flags] [args]" 109 | } 110 | c.CobraCommand = &cobra.Command{ 111 | Use: fmt.Sprintf("%s %s", c.Command.Name(), useCommandFlagsArgs), 112 | RunE: func(cmd *cobra.Command, args []string) error { 113 | if err := c.Command.Run(cmd.Context(), c, args); err != nil { 114 | return &runErr{error: err} 115 | } 116 | return nil 117 | }, 118 | PreRunE: func(cmd *cobra.Command, args []string) error { 119 | return c.init() 120 | }, 121 | SilenceErrors: true, 122 | SilenceUsage: true, 123 | SuggestionsMinimumDistance: 2, 124 | } 125 | 126 | // This is where the flags, short and long description etc. are added 127 | if err := c.Command.Init(c); err != nil { 128 | return err 129 | } 130 | 131 | // Add commands recursively. 132 | for _, cc := range c.commandeers { 133 | if err := cc.compile(); err != nil { 134 | return err 135 | } 136 | c.CobraCommand.AddCommand(cc.CobraCommand) 137 | } 138 | 139 | return nil 140 | } 141 | 142 | // Exec provides methods to execute the command tree. 143 | type Exec struct { 144 | c *Commandeer 145 | } 146 | 147 | // Execute executes the command tree starting from the root command. 148 | // The args are usually filled with os.Args[1:]. 149 | func (r *Exec) Execute(ctx context.Context, args []string) (*Commandeer, error) { 150 | if args == nil { 151 | // Cobra falls back to os.Args[1:] if args is nil. 152 | args = []string{} 153 | } 154 | r.c.CobraCommand.SetArgs(args) 155 | cobraCommand, err := r.c.CobraCommand.ExecuteContextC(ctx) 156 | var cd *Commandeer 157 | if cobraCommand != nil { 158 | if err == nil { 159 | err = checkArgs(cobraCommand, args) 160 | } 161 | 162 | // Find the commandeer that was executed. 163 | var find func(*cobra.Command, *Commandeer) *Commandeer 164 | find = func(what *cobra.Command, in *Commandeer) *Commandeer { 165 | if in.CobraCommand == what { 166 | return in 167 | } 168 | for _, in2 := range in.commandeers { 169 | if found := find(what, in2); found != nil { 170 | return found 171 | } 172 | } 173 | return nil 174 | } 175 | cd = find(cobraCommand, r.c) 176 | } 177 | 178 | return cd, wrapErr(err) 179 | } 180 | 181 | // CommandError is returned when a command fails because of a user error (unknown command, invalid flag etc.). 182 | // All other errors comes from the execution of the command. 183 | type CommandError struct { 184 | Err error 185 | } 186 | 187 | // Error implements error. 188 | func (e *CommandError) Error() string { 189 | return fmt.Sprintf("command error: %v", e.Err) 190 | } 191 | 192 | // Is reports whether e is of type *CommandError. 193 | func (*CommandError) Is(e error) bool { 194 | _, ok := e.(*CommandError) 195 | return ok 196 | } 197 | 198 | // IsCommandError reports whether any error in err's tree matches CommandError. 199 | func IsCommandError(err error) bool { 200 | return errors.Is(err, &CommandError{}) 201 | } 202 | 203 | func wrapErr(err error) error { 204 | if err == nil { 205 | return nil 206 | } 207 | 208 | if rerr, ok := err.(*runErr); ok { 209 | return rerr.error 210 | } 211 | 212 | // All other errors are coming from Cobra. 213 | return &CommandError{Err: err} 214 | } 215 | 216 | // Cobra only does suggestions for the root command. 217 | // See https://github.com/spf13/cobra/pull/1500 218 | func checkArgs(cmd *cobra.Command, args []string) error { 219 | // no subcommand, always take args. 220 | if !cmd.HasSubCommands() { 221 | return nil 222 | } 223 | 224 | var commandName string 225 | for _, arg := range args { 226 | if strings.HasPrefix(arg, "-") { 227 | break 228 | } 229 | commandName = arg 230 | } 231 | 232 | if commandName == "" || cmd.Name() == commandName { 233 | return nil 234 | } 235 | 236 | // Also check the aliases. 237 | if cmd.HasAlias(commandName) { 238 | return nil 239 | } 240 | 241 | return fmt.Errorf("unknown command %q for %q%s", args[1], cmd.CommandPath(), findSuggestions(cmd, commandName)) 242 | } 243 | 244 | func findSuggestions(cmd *cobra.Command, arg string) string { 245 | if cmd.DisableSuggestions { 246 | return "" 247 | } 248 | suggestionsString := "" 249 | if suggestions := cmd.SuggestionsFor(arg); len(suggestions) > 0 { 250 | suggestionsString += "\n\nDid you mean this?\n" 251 | for _, s := range suggestions { 252 | suggestionsString += fmt.Sprintf("\t%v\n", s) 253 | } 254 | } 255 | return suggestionsString 256 | } 257 | -------------------------------------------------------------------------------- /simplecobra_test.go: -------------------------------------------------------------------------------- 1 | package simplecobra_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "testing" 10 | 11 | "github.com/bep/simplecobra" 12 | qt "github.com/frankban/quicktest" 13 | ) 14 | 15 | func testCommands() *rootCommand { 16 | return &rootCommand{name: "root", 17 | commands: []simplecobra.Commander{ 18 | &lvl1Command{name: "foo"}, 19 | &lvl1Command{name: "bar", 20 | commands: []simplecobra.Commander{ 21 | &lvl2Command{name: "baz"}, 22 | }, 23 | }, 24 | }, 25 | } 26 | 27 | } 28 | 29 | func TestSimpleCobra(t *testing.T) { 30 | c := qt.New(t) 31 | 32 | rootCmd := testCommands() 33 | 34 | x, err := simplecobra.New(rootCmd) 35 | c.Assert(err, qt.IsNil) 36 | // This can be anything, just used to make sure the same context is passed all the way. 37 | type key string 38 | ctx := context.WithValue(context.Background(), key("foo"), "bar") 39 | // Execute the root command. 40 | args := []string{"--localFlagName", "root_local", "--persistentFlagName", "root_persistent"} 41 | cd, err := x.Execute(ctx, args) 42 | c.Assert(err, qt.IsNil) 43 | c.Assert(cd.Command.Name(), qt.Equals, "root") 44 | tc := cd.Command.(*rootCommand) 45 | c.Assert(tc, qt.Equals, rootCmd) 46 | c.Assert(tc.ctx, qt.Equals, ctx) 47 | c.Assert(tc.localFlagName, qt.Equals, "root_local") 48 | c.Assert(tc.persistentFlagName, qt.Equals, "root_persistent") 49 | c.Assert(tc.persistentFlagNameC, qt.Equals, "root_persistent_rootCommand_compiled") 50 | c.Assert(tc.localFlagNameC, qt.Equals, "root_local_rootCommand_compiled") 51 | c.Assert(tc.initRunner, qt.Equals, cd) 52 | c.Assert(tc.initThis, qt.Equals, cd) 53 | 54 | // Execute a level 1 command. 55 | // This may not be very realistic, but it works. The common use case for a CLI app is to run one command and then exit. 56 | args = []string{"bar", "--localFlagName", "bar_local", "--persistentFlagName", "bar_persistent"} 57 | ctx = context.WithValue(context.Background(), key("bar"), "baz") 58 | cd2, err := x.Execute(ctx, args) 59 | c.Assert(err, qt.IsNil) 60 | c.Assert(cd2.Command.Name(), qt.Equals, "bar") 61 | tc2 := cd2.Command.(*lvl1Command) 62 | c.Assert(tc2.rootCmd, qt.Equals, rootCmd) 63 | c.Assert(tc2.ctx, qt.Equals, ctx) 64 | c.Assert(tc2.localFlagName, qt.Equals, "bar_local") 65 | c.Assert(tc2.localFlagNameC, qt.Equals, "bar_local_lvl1Command_compiled") 66 | c.Assert(tc.persistentFlagName, qt.Equals, "bar_persistent") 67 | c.Assert(tc.persistentFlagNameC, qt.Equals, "bar_persistent_rootCommand_compiled") 68 | c.Assert(tc2.rootCmd.initRunner, qt.Equals, cd2) 69 | c.Assert(tc2.rootCmd.initThis, qt.Equals, cd2.Root) 70 | 71 | // Execute a level 2 command. 72 | args = []string{"bar", "baz", "--persistentFlagName", "baz_persistent"} 73 | ctx = context.WithValue(context.Background(), key("baz"), "qux") 74 | cd3, err := x.Execute(ctx, args) 75 | c.Assert(err, qt.IsNil) 76 | c.Assert(cd3.Command.Name(), qt.Equals, "baz") 77 | tc3 := cd3.Command.(*lvl2Command) 78 | c.Assert(tc3.rootCmd, qt.Equals, rootCmd) 79 | c.Assert(tc3.parentCmd, qt.Equals, tc2) 80 | c.Assert(tc3.ctx, qt.Equals, ctx) 81 | c.Assert(tc3.rootCmd.initRunner, qt.Equals, cd3) 82 | c.Assert(tc3.rootCmd.initThis, qt.Equals, cd3.Root) 83 | 84 | } 85 | 86 | func TestAliases(t *testing.T) { 87 | c := qt.New(t) 88 | 89 | rootCmd := &rootCommand{name: "root", 90 | commands: []simplecobra.Commander{ 91 | &lvl1Command{name: "foo", aliases: []string{"f"}, 92 | commands: []simplecobra.Commander{ 93 | &lvl2Command{name: "bar"}, 94 | }, 95 | }, 96 | }, 97 | } 98 | 99 | x, err := simplecobra.New(rootCmd) 100 | c.Assert(err, qt.IsNil) 101 | args := []string{"f"} 102 | _, err = x.Execute(context.Background(), args) 103 | c.Assert(err, qt.IsNil) 104 | } 105 | 106 | func TestInitAncestorsOnly(t *testing.T) { 107 | c := qt.New(t) 108 | 109 | rootCmd := testCommands() 110 | x, err := simplecobra.New(rootCmd) 111 | c.Assert(err, qt.IsNil) 112 | args := []string{"bar", "baz", "--persistentFlagName", "baz_persistent"} 113 | cd3, err := x.Execute(context.Background(), args) 114 | c.Assert(err, qt.IsNil) 115 | c.Assert(cd3.Command.Name(), qt.Equals, "baz") 116 | c.Assert(rootCmd.isInit, qt.IsTrue) 117 | c.Assert(rootCmd.commands[0].(*lvl1Command).isInit, qt.IsFalse) 118 | c.Assert(rootCmd.commands[1].(*lvl1Command).isInit, qt.IsTrue) 119 | c.Assert(cd3.Command.(*lvl2Command).isInit, qt.IsTrue) 120 | } 121 | 122 | func TestErrors(t *testing.T) { 123 | c := qt.New(t) 124 | 125 | c.Run("unknown similar command", func(c *qt.C) { 126 | x, err := simplecobra.New(testCommands()) 127 | c.Assert(err, qt.IsNil) 128 | _, err = x.Execute(context.Background(), []string{"fooo"}) 129 | c.Assert(err, qt.Not(qt.IsNil)) 130 | c.Assert(err.Error(), qt.Contains, "unknown command \"fooo\"") 131 | c.Assert(err.Error(), qt.Contains, "Did you mean this?") 132 | c.Assert(simplecobra.IsCommandError(err), qt.Equals, true) 133 | }) 134 | 135 | c.Run("unknown similar sub command", func(c *qt.C) { 136 | x, err := simplecobra.New(testCommands()) 137 | c.Assert(err, qt.IsNil) 138 | _, err = x.Execute(context.Background(), []string{"bar", "bazz"}) 139 | c.Assert(err, qt.Not(qt.IsNil)) 140 | c.Assert(err.Error(), qt.Contains, "unknown") 141 | c.Assert(err.Error(), qt.Contains, "Did you mean this?") 142 | c.Assert(simplecobra.IsCommandError(err), qt.Equals, true) 143 | }) 144 | 145 | c.Run("disable suggestions", func(c *qt.C) { 146 | r := &rootCommand{name: "root", 147 | commands: []simplecobra.Commander{ 148 | &lvl1Command{name: "foo", disableSuggestions: true, 149 | commands: []simplecobra.Commander{ 150 | &lvl2Command{name: "bar"}, 151 | }, 152 | }, 153 | }, 154 | } 155 | x, err := simplecobra.New(r) 156 | c.Assert(err, qt.IsNil) 157 | _, err = x.Execute(context.Background(), []string{"foo", "bars"}) 158 | c.Assert(err, qt.Not(qt.IsNil)) 159 | c.Assert(err.Error(), qt.Contains, `command error: unknown command "bars" for "root foo"`) 160 | c.Assert(err.Error(), qt.Not(qt.Contains), "Did you mean this?") 161 | }) 162 | 163 | c.Run("unknown flag", func(c *qt.C) { 164 | x, err := simplecobra.New(testCommands()) 165 | c.Assert(err, qt.IsNil) 166 | _, err = x.Execute(context.Background(), []string{"bar", "--unknown"}) 167 | c.Assert(err, qt.Not(qt.IsNil)) 168 | c.Assert(err.Error(), qt.Contains, "unknown") 169 | c.Assert(simplecobra.IsCommandError(err), qt.Equals, true) 170 | }) 171 | 172 | c.Run("fail New in root command", func(c *qt.C) { 173 | r := &rootCommand{name: "root", failWithCobraCommand: true, 174 | commands: []simplecobra.Commander{ 175 | &lvl1Command{name: "foo"}, 176 | }, 177 | } 178 | _, err := simplecobra.New(r) 179 | c.Assert(err, qt.IsNotNil) 180 | }) 181 | 182 | c.Run("fail New in sub command", func(c *qt.C) { 183 | r := &rootCommand{name: "root", 184 | commands: []simplecobra.Commander{ 185 | &lvl1Command{name: "foo", failWithCobraCommand: true}, 186 | }, 187 | } 188 | _, err := simplecobra.New(r) 189 | c.Assert(err, qt.IsNotNil) 190 | }) 191 | 192 | c.Run("fail run root command", func(c *qt.C) { 193 | r := &rootCommand{name: "root", failRun: true, 194 | commands: []simplecobra.Commander{ 195 | &lvl1Command{name: "foo"}, 196 | }, 197 | } 198 | x, err := simplecobra.New(r) 199 | c.Assert(err, qt.IsNil) 200 | _, err = x.Execute(context.Background(), nil) 201 | c.Assert(err, qt.IsNotNil) 202 | c.Assert(err.Error(), qt.Equals, "failRun") 203 | }) 204 | 205 | c.Run("fail init sub command", func(c *qt.C) { 206 | r := &rootCommand{name: "root", 207 | commands: []simplecobra.Commander{ 208 | &lvl1Command{name: "foo", failInit: true}, 209 | }, 210 | } 211 | x, err := simplecobra.New(r) 212 | c.Assert(err, qt.IsNil) 213 | _, err = x.Execute(context.Background(), []string{"foo"}) 214 | c.Assert(err, qt.IsNotNil) 215 | 216 | }) 217 | 218 | } 219 | 220 | func TestIsCommandError(t *testing.T) { 221 | c := qt.New(t) 222 | cerr := &simplecobra.CommandError{Err: errors.New("foo")} 223 | c.Assert(simplecobra.IsCommandError(os.ErrNotExist), qt.Equals, false) 224 | c.Assert(simplecobra.IsCommandError(nil), qt.Equals, false) 225 | c.Assert(simplecobra.IsCommandError(errors.New("foo")), qt.Equals, false) 226 | c.Assert(simplecobra.IsCommandError(cerr), qt.Equals, true) 227 | c.Assert(simplecobra.IsCommandError(fmt.Errorf("foo: %w", cerr)), qt.Equals, true) 228 | 229 | } 230 | 231 | func Example() { 232 | rootCmd := &rootCommand{name: "root", 233 | commands: []simplecobra.Commander{ 234 | &lvl1Command{name: "foo"}, 235 | &lvl1Command{name: "bar", 236 | commands: []simplecobra.Commander{ 237 | &lvl2Command{name: "baz"}, 238 | }, 239 | }, 240 | }, 241 | } 242 | 243 | x, err := simplecobra.New(rootCmd) 244 | if err != nil { 245 | log.Fatal(err) 246 | } 247 | cd, err := x.Execute(context.Background(), []string{"bar", "baz", "--localFlagName", "baz_local", "--persistentFlagName", "baz_persistent"}) 248 | if err != nil { 249 | log.Fatal(err) 250 | } 251 | 252 | // These are wired up in Init(). 253 | lvl2 := cd.Command.(*lvl2Command) 254 | lvl1 := lvl2.parentCmd 255 | root := lvl1.rootCmd 256 | 257 | fmt.Printf("Executed %s.%s.%s with localFlagName %s and and persistentFlagName %s.\n", root.name, lvl1.name, lvl2.name, lvl2.localFlagName, root.persistentFlagName) 258 | // Output: Executed root.bar.baz with localFlagName baz_local and and persistentFlagName baz_persistent. 259 | 260 | } 261 | 262 | type rootCommand struct { 263 | name string 264 | isInit bool 265 | 266 | // Flags 267 | persistentFlagName string 268 | localFlagName string 269 | 270 | // Compiled flags. 271 | persistentFlagNameC string 272 | localFlagNameC string 273 | 274 | // For testing. 275 | ctx context.Context 276 | initThis *simplecobra.Commandeer 277 | initRunner *simplecobra.Commandeer 278 | failWithCobraCommand bool 279 | failRun bool 280 | 281 | // Sub commands. 282 | commands []simplecobra.Commander 283 | } 284 | 285 | func (c *rootCommand) Commands() []simplecobra.Commander { 286 | return c.commands 287 | } 288 | 289 | func (c *rootCommand) PreRun(this, runner *simplecobra.Commandeer) error { 290 | c.isInit = true 291 | c.persistentFlagNameC = c.persistentFlagName + "_rootCommand_compiled" 292 | c.localFlagNameC = c.localFlagName + "_rootCommand_compiled" 293 | c.initThis = this 294 | c.initRunner = runner 295 | return nil 296 | } 297 | 298 | func (c *rootCommand) Name() string { 299 | return c.name 300 | } 301 | 302 | func (c *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { 303 | if c.failRun { 304 | return errors.New("failRun") 305 | } 306 | c.ctx = ctx 307 | return nil 308 | } 309 | 310 | func (c *rootCommand) Init(cd *simplecobra.Commandeer) error { 311 | if c.failWithCobraCommand { 312 | return errors.New("failWithCobraCommand") 313 | } 314 | cmd := cd.CobraCommand 315 | localFlags := cmd.Flags() 316 | persistentFlags := cmd.PersistentFlags() 317 | 318 | localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName") 319 | persistentFlags.StringVar(&c.persistentFlagName, "persistentFlagName", "", "set persistentFlagName") 320 | 321 | return nil 322 | } 323 | 324 | type lvl1Command struct { 325 | name string 326 | isInit bool 327 | 328 | aliases []string 329 | 330 | localFlagName string 331 | localFlagNameC string 332 | 333 | failInit bool 334 | failWithCobraCommand bool 335 | disableSuggestions bool 336 | 337 | rootCmd *rootCommand 338 | 339 | commands []simplecobra.Commander 340 | 341 | ctx context.Context 342 | } 343 | 344 | func (c *lvl1Command) Commands() []simplecobra.Commander { 345 | return c.commands 346 | } 347 | 348 | func (c *lvl1Command) PreRun(this, runner *simplecobra.Commandeer) error { 349 | if c.failInit { 350 | return fmt.Errorf("failInit") 351 | } 352 | c.isInit = true 353 | c.localFlagNameC = c.localFlagName + "_lvl1Command_compiled" 354 | c.rootCmd = this.Root.Command.(*rootCommand) 355 | return nil 356 | } 357 | 358 | func (c *lvl1Command) Name() string { 359 | return c.name 360 | } 361 | 362 | func (c *lvl1Command) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { 363 | c.ctx = ctx 364 | return nil 365 | } 366 | 367 | func (c *lvl1Command) Init(cd *simplecobra.Commandeer) error { 368 | if c.failWithCobraCommand { 369 | return errors.New("failWithCobraCommand") 370 | } 371 | cmd := cd.CobraCommand 372 | cmd.DisableSuggestions = c.disableSuggestions 373 | cmd.Aliases = c.aliases 374 | localFlags := cmd.Flags() 375 | localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName for lvl1Command") 376 | return nil 377 | } 378 | 379 | type lvl2Command struct { 380 | name string 381 | isInit bool 382 | localFlagName string 383 | 384 | ctx context.Context 385 | rootCmd *rootCommand 386 | parentCmd *lvl1Command 387 | } 388 | 389 | func (c *lvl2Command) Commands() []simplecobra.Commander { 390 | return nil 391 | } 392 | 393 | func (c *lvl2Command) PreRun(this, runner *simplecobra.Commandeer) error { 394 | c.isInit = true 395 | c.rootCmd = this.Root.Command.(*rootCommand) 396 | c.parentCmd = this.Parent.Command.(*lvl1Command) 397 | return nil 398 | } 399 | 400 | func (c *lvl2Command) Name() string { 401 | return c.name 402 | } 403 | 404 | func (c *lvl2Command) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { 405 | c.ctx = ctx 406 | return nil 407 | } 408 | 409 | func (c *lvl2Command) Init(cd *simplecobra.Commandeer) error { 410 | cmd := cd.CobraCommand 411 | localFlags := cmd.Flags() 412 | localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName for lvl2Command") 413 | return nil 414 | } 415 | --------------------------------------------------------------------------------