Adding Nested Subcommands¶
GTB's command composition model (since v0.5) makes nested command trees straightforward: every NewCmd<Name> returns *setup.Command, and parents attach children via parent.Register(child...). This guide walks through adding a tool deploy canary command โ first using the generator, then showing the same shape by hand.
The end result is identical either way; the generator simply emits what you would write yourself.
Option A: Use the generator¶
# Scaffold a project (skip if you already have one)
gtb generate skeleton \
--name my-tool \
--repo example/my-tool \
--description "My tool" \
--features init,update,doctor,config \
--path .
# Add a root-level command
gtb generate command --name deploy --short "Deploy stuff"
# Add a nested subcommand under deploy
gtb generate command --name canary --parent deploy --short "Canary deploy"
The generator updates three things in one pass per command:
- Creates
pkg/cmd/<parent>/<name>/cmd.gowith the canonicalNewCmd<Name>constructor returning*setup.Command. - Edits the parent's
cmd.goto add the import and acmd.Register(<pkg>.NewCmd<Name>(props))call. - Updates
.gtb/manifest.yamlsogtb regenerateknows the command tree.
Running go build ./cmd/<tool> after that produces a binary with the full tool deploy canary tree visible in --help.
Option B: Write the constructors by hand¶
1. The leaf command โ canary¶
// pkg/cmd/deploy/canary/cmd.go
package canary
import (
"github.com/spf13/cobra"
"gitlab.com/phpboyscout/go-tool-base/pkg/props"
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func NewCmdCanary(p *props.Props) *setup.Command {
return setup.Wrap("canary", &cobra.Command{
Use: "canary",
Short: "Canary deploy",
RunE: func(cmd *cobra.Command, args []string) error {
p.Logger.Info("running canary deploy")
return nil
},
})
}
2. The parent command โ deploy¶
The parent attaches its children with Register before returning:
// pkg/cmd/deploy/cmd.go
package deploy
import (
"github.com/spf13/cobra"
"github.com/example/my-tool/pkg/cmd/deploy/canary"
"gitlab.com/phpboyscout/go-tool-base/pkg/props"
"gitlab.com/phpboyscout/go-tool-base/pkg/setup"
)
func NewCmdDeploy(p *props.Props) *setup.Command {
cmd := setup.Wrap("deploy", &cobra.Command{
Use: "deploy",
Short: "Deploy stuff",
})
cmd.Register(canary.NewCmdCanary(p))
return cmd
}
Two important details:
cmd := setup.Wrap(...)assigns the composed type tocmdfrom the start. Every later mutation โcmd.Flags(),cmd.MarkFlagsMutuallyExclusive(...),cmd.Register(child)โ flows through the embedded*cobra.Command.- The parent does not thread a feature key into
Register. Each child owns its own feature via its ownsetup.Wrapcall โ siblings can have completely different middleware stacks without the parent knowing.
3. Attach deploy to the root¶
root.NewCmdRoot(p, children...) takes a variadic of *setup.Command and calls Register internally, so the variadic form and the explicit rootCmd.Register(child) form are equivalent.
Verifying the tree compiles and runs¶
go build ./...
./my-tool --help
# ...
# Available Commands:
# deploy Deploy stuff
# ...
./my-tool deploy --help
# Available Commands:
# canary Canary deploy
./my-tool deploy canary --help
# Usage:
# my-tool deploy canary [flags]
If the build fails with cmd.Register undefined (type *cobra.Command has no field or method Register), the parent's cmd variable was typed as *cobra.Command โ likely from an older template. Re-run gtb regenerate or update the constructor to use cmd := setup.Wrap(...) instead of cmd := &cobra.Command{...}.
What the generator does that you should not skip¶
The generator's emission is idempotent: re-running gtb generate command --name canary --parent deploy does not double-register the child, because the AST inserter detects both cmd.Register(...) and legacy setup.AddCommandWithMiddleware(...) shapes and short-circuits. If you wire commands by hand you don't get this for free โ adding the same child twice will compile but produce a runtime "command already added" panic from cobra.
If you mix generated and hand-written commands in the same project:
- Keep hand-written commands in packages the generator does not own (anything not listed in
.gtb/manifest.yaml). - For generated parents, prefer
gtb generate command --parent <name>to add children โ your hand edits inside a generated file will survive only if they sit outside the regions the generator rewrites.
See also¶
- Adding Custom Commands โ full how-to for the basic single-command pattern.
- Command Constructor Pattern โ rationale for
NewCmd*returning*setup.Command. - Command Middleware System โ how
RegisterwrapsRunEwith middleware. gtb generate commandโ flag reference for the generator.- Migration v0.4 to v0.5 โ diff against the previous
AddCommandWithMiddlewarepattern.