Help output depends on the order AddCommand is called (CommandPathPadding is snapshotted, never recomputed)
Two identical command trees render different help text depending only on the order in which AddCommand was called. Building bottom-up — construct a subcommand fully, then attach it to the root — produces misaligned columns.
Observed on cobra v1.10.2, go1.24, linux/amd64.
Steps to reproduce
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func build(topFirst bool) {
root := &cobra.Command{Use: "app", Run: func(*cobra.Command, []string) {}}
sub := &cobra.Command{Use: "remote", Short: "remote ops", Run: func(*cobra.Command, []string) {}}
// help topics (no Run) render under "Additional help topics", padded with CommandPathPadding
t1 := &cobra.Command{Use: "authentication", Short: "how auth works"}
t2 := &cobra.Command{Use: "tls", Short: "certificate setup"}
if topFirst {
root.AddCommand(sub)
sub.AddCommand(t1)
sub.AddCommand(t2)
} else {
sub.AddCommand(t1)
sub.AddCommand(t2)
root.AddCommand(sub)
}
_ = sub.Help()
}
func main() {
fmt.Println("--- root.AddCommand(sub) first ---")
build(true)
fmt.Println("--- sub.AddCommand(topics) first ---")
build(false)
}
Current behaviour
--- root.AddCommand(sub) first ---
Additional help topics:
app remote authentication how auth works
app remote tls certificate setup
--- sub.AddCommand(topics) first ---
Additional help topics:
app remote authentication how auth works
app remote tls certificate setup <- misaligned
Expected behaviour
Both orders describe the same tree, so both should render identically (the first form).
Cause
AddCommand computes the child's path length eagerly and caches it on the parent:
commandPathLen := len(x.CommandPath())
if commandPathLen > c.commandsMaxCommandPathLen {
c.commandsMaxCommandPathLen = commandPathLen
}
CommandPath() walks up through parent, so its value depends on how much of the tree is attached at that moment. In sub.AddCommand(t1) before sub has a parent, t1.CommandPath() is "remote authentication" (21) rather than "app remote authentication" (25). Attaching sub to root afterwards never invalidates the cached value, so sub.commandsMaxCommandPathLen stays 4 short forever.
leaf.CommandPathPadding() // 21 built bottom-up, 25 built top-down
leaf.NamePadding() // 14 either way
leaf.UsagePadding() // 25 either way
Only CommandPathPadding is affected, because Name() and Use are local to the command while CommandPath() is the only one of the three that depends on ancestry.
RemoveCommand already recomputes all three lengths from scratch, which suggests the cache is intended to stay accurate.
Notes
- Bottom-up construction is the more natural pattern (build a subcommand in its own package, export it, attach it), so the broken order is the common one.
- This is separate from the hidden/deprecated-command padding reports in #2431 and #2426 — those are about which commands are counted, this is about when the length is computed.
- A fix could recompute on attach, or compute the max lazily at render time. Happy to open a PR once you've decided which shape you'd prefer.
Source: spf13/cobra