Skip to content

Help output depends on the order AddCommand is called (CommandPathPadding is snapshotted, never recomputed) #2463

Description

@earfman

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions