I'm a Go beginner, and I'm trying to create a CLI with Cobra. To bootstrap the project, I used the Cobra Generator, generated a command, a subcommand, and everything works fine.
I now have this type of layout :
cli
├── cmd
│ ├── command.go
│ ├── root.go
│ ├── subcommand.go
├── go.mod
├── go.sum
└── main.go
This doesn't suit me, let's say my project is intended to have lots of commands, or lots of command namespaces, each owned by a different team, it would become very messy and hard to maintain. I would prefer a layout like this :
cli
├── cmd
│ ├── command
│ │ ├── command.go
│ │ └── subcommand.go
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Now, I lack some knowledge about the way packages and imports are done in Go (even after reading the doc here and there), but as far as I understand, a resource can be accessed accross multiple go source files as long as they belong to the same package. But as said in the documentation, "An implementation may require that all source files for a package inhabit the same directory.", so to achieve a layout such as I would like, I would need to have multiple packages, e.g. one for each command namespace, and I think this is fine (or at least, I don't understand what would be wrong with that). So this is what I've tried :
command
directory inside the cmd
onecommand.go
file inside the command
directorypackage
clause from the command.go
file to command
subcommand.go
And this builds fine, but the command is not found(Error: unknown command "command" for "cli"
). I thought it was because I never imported that new command
package, so I imported it in the main.go
file, where the cmd
package is imported.
The build fails, telling me undefined: rootCmd
in the command.go
file. I guess the command
package is unable to see the resources from the cmd
one, so I imported the cmd
package in the command.go
file, and replaced rootCmd
with cmd.rootCmd
(rootCmd
being a variable created in the cli/cmd/root.go
file, part of the Cobra Generator provided files). I really had hope this time, but the result is still the same (undefined: cmd.rootCmd
), and now I'm lost.
Am I trying to do something that is not possible with Cobra?
Am I trying to do something that is possible with Cobra, but not using the Cobra Generator?
Am I trying to do something that should not be done at all (like a bad design from which Go is trying to protect me)?
If not, how should I proceed?
You can't get the layout you want by using the cobra-cli
command, but you can certainly set things up manually. Let's start with the cobra-cli
layou:
$ go mod init example
$ cobra-cli init
And add a couple of commands:
$ cobra-cli add foo
$ cobra-cli add bar
This gets us:
.
├── cmd
│ ├── bar.go
│ ├── foo.go
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
Let's first move the commands into subdirectories so we have the desired layout. That gets us:
.
├── cmd
│ ├── bar
│ │ └── bar.go
│ ├── foo
│ │ └── foo.go
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
Now we need to make some changes to our code so that this works.
Because our subcommands are now in separate packages, we'll need to rename rootCmd
so that it is exportable.
find cmd -name '*.go' -print | xargs sed -i 's/rootCmd/RootCmd/g'
We need each subcommand to be in its own package, so we'll replace package cmd
at the top of cmd/foo/foo.go
with package foo
, and with package bar
in cmd/bar/bar.go
.
We need the subcommands to import the root command so that they can call RootCmd.AddCommand
. That means in cmd/foo/foo.go
and cmd/bar/bar.go
we need to add example/cmd
to our import
section (recall that we named our top-level package example
via the go mod init
command). That means each file will start with:
package foo
import (
"fmt"
"example/cmd"
"github.com/spf13/cobra"
)
As part of this change, we will also need to update the reference to AddCommand
to use an explicit package name:
func init() {
cmd.RootCmd.AddCommand(fooCmd)
}
Lastly, we need to import the subcommands somewhere. Right now we never import the foo
or bar
packages, so the code is effectively invisible (try introducing an egregious syntax error in one of those files -- you'll see that go build
will succeed because those files aren't referenced anywhere).
We can do this in our top level main.go
file:
package main
import (
"example/cmd"
_ "example/cmd/bar"
_ "example/cmd/foo"
)
func main() {
cmd.Execute()
}
Now if we build and run the code, we see that our foo
and bar
commands are available:
$ go build
$ ./example
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
example [command]
Available Commands:
bar A brief description of your command
completion Generate the autocompletion script for the specified shell
foo A brief description of your command
help Help about any command
Flags:
-h, --help help for example
-t, --toggle Help message for toggle
Use "example [command] --help" for more information about a command.