Search code examples
gogo-cobra

How to put cobra sub commands sources into separate folders


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 :

  • Create a command directory inside the cmd one
  • Move the command.go file inside the command directory
  • Change the package clause from the command.go file to command
  • Do the same for 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?


Solution

  • 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.

    1. 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'
      
    2. 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.

    3. 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)
      }
      
    4. 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.