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
Two identical command trees render different help text depending only on the order in which
AddCommandwas 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
Current behaviour
Expected behaviour
Both orders describe the same tree, so both should render identically (the first form).
Cause
AddCommandcomputes the child's path length eagerly and caches it on the parent:CommandPath()walks up throughparent, so its value depends on how much of the tree is attached at that moment. Insub.AddCommand(t1)beforesubhas a parent,t1.CommandPath()is"remote authentication"(21) rather than"app remote authentication"(25). Attachingsubtorootafterwards never invalidates the cached value, sosub.commandsMaxCommandPathLenstays 4 short forever.Only
CommandPathPaddingis affected, becauseName()andUseare local to the command whileCommandPath()is the only one of the three that depends on ancestry.RemoveCommandalready recomputes all three lengths from scratch, which suggests the cache is intended to stay accurate.Notes