Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow subcommands declaration/addition in subdirectories (child packages) of cmd/ #1197

Closed
chibby0ne opened this issue Aug 20, 2020 · 3 comments

Comments

@chibby0ne
Copy link

Currently cobra does not fully supports having all the subcommands declared in the same package as the root (top level) command. On the contrary, full support is only available if you have the strict directory layout of declaring all the subcommands in the same package (same directory).
But having subcommands declared in subdirectories of cmd lends itself naturally to a cleaner code structure specially when the subcommands have subcommands and the nesting level is > 2, due to namespacing/encapsulation of the variables used as flags.

Currently cobra still "works" in the nested subdirectories (nested packages) case, but the Usage and Available Commands sections of the root command are empty or not there at all.

As a minimal example in this directory structure:

.
├── cmd
│   ├── bar
│   │   └── bar.go
│   ├── bor
│   │   └── bor.go
│   └── root.go
├── go.mod
├── go.sum
└── main.go

Where root.go:

package cmd

import (
    "github.com/spf13/cobra"
    "github.com/chibby0ne/cobra_subdirectory_structure/cmd/bar"
    "github.com/chibby0ne/cobra_subdirectory_structure/cmd/bor"
)

var RootCmd = &cobra.Command{
    Use:   "foo",
    Short: "A foo cli",
}

func init() {
    RootCmd.AddCommand(bor.Cmd)
    RootCmd.AddCommand(bar.Cmd)
}

And bor.go (same code as bar.go, but with s/bor/bar')

package bor

import (
    "fmt"
    "github.com/spf13/cobra"
)

var Cmd = &cobra.Command{
    Use:   "bor",
    Short: "bor subcommands",
}

var versionCmd = &cobra.Command{
    Use: "version",
    Short: "version of bor api",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("%v api version: 0.1.0\n", Cmd.Use)
    },
}

When executing the binary with no arguments, instead of seeing something like this (using the recomended directory layout):

A foo cli

Usage:
  foo [command]

Available Commands:
  bar         bar subcommands
  bor         bor subcommands
  help        Help about any command

Flags:
  -h, --help   help for foo

Use "foo [command] --help" for more information about a command.

We get this:

A foo cli

Usage:

Flags:
  -h, --help   help for foo

Additional help topics:
  foo bar     bar subcommands
  foo bor     bor subcommands

which is less than ideal.

Is there any chance we can be a bit more flexible in the directory structure supported?

I noticed the closed issue #641 and in fact decide to follow the recommendation followed there in: #641 (comment) and in doing so I noticed the current state (cobra@1.0.0)

FYI the project mentioned in the suggestion (https://github.com/ohsu-comp-bio/funnel/) it is able to show the Usage and Available commands, but it uses cobra@0.0.5. 😢

@bukowa
Copy link

bukowa commented Aug 21, 2020

Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}

cobra/command.go

Lines 1351 to 1367 in 8cfa4b4

// IsAvailableCommand determines if a command is available as a non-help command
// (this includes all non deprecated/hidden commands).
func (c *Command) IsAvailableCommand() bool {
if len(c.Deprecated) != 0 || c.Hidden {
return false
}
if c.HasParent() && c.Parent().helpCommand == c {
return false
}
if c.Runnable() || c.HasAvailableSubCommands() {
return true
}
return false
}

cobra/command.go

Lines 1341 to 1344 in 8cfa4b4

// Runnable determines if the command is itself runnable.
func (c *Command) Runnable() bool {
return c.Run != nil || c.RunE != nil
}

So this

package main

import (
	"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
	cmd := &cobra.Command{Use: "app"}
	cmd2 := &cobra.Command{Use: "add", Run: func(cmd *cobra.Command, args []string) {}}
	cmd3 := &cobra.Command{Use: "delete"}
	cmd4 := &cobra.Command{Use: "list"}
	cmd.AddCommand(cmd2, cmd3, cmd4)
	return cmd
}

func main() {
	NewCmd().Execute()
}
Usage:
  app [command]

Available Commands:
  add
  help        Help about any command

Flags:
  -h, --help   help for app

Additional help topics:
  app delete
  app list

Use "app [command] --help" for more information about a command.

But this

package main

import (
	"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
	cmd := &cobra.Command{Use: "app"}
	cmd2 := &cobra.Command{Use: "add"}
	cmd3 := &cobra.Command{Use: "delete"}
	cmd4 := &cobra.Command{Use: "list"}
	cmd.AddCommand(cmd2, cmd3, cmd4)
	return cmd
}

func main() {
	NewCmd().Execute()
}
Usage:

Flags:
  -h, --help   help for app

Additional help topics:
  app add
  app delete
  app list

@chibby0ne
Copy link
Author

Oh I see now, even though your example is not exactly the same directory structure.
In my snippet I forgot to add versionCmd as subcommand to Cmd in an init() function in bor.go and bar.go.

In general, having at least one of command in the chain of commands from the root commands with a Run field non-nil, makes it show that subcomand thread in the available commands section.

Thanks a lot @bukowa

@chibby0ne
Copy link
Author

Just a false positive, closing the issue now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants