diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index 6764ca1..63a9119 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -105,15 +105,23 @@ func loadLocaleSources(sources ...LocaleSource) { func RegisteredLocales() []fs.FS { registeredCommandsMu.Lock() defer registeredCommandsMu.Unlock() - return registeredLocales + if len(registeredLocales) == 0 { + return nil + } + out := make([]fs.FS, len(registeredLocales)) + copy(out, registeredLocales) + return out } // RegisteredCommands returns an iterator over the registered command functions. func RegisteredCommands() iter.Seq[CommandRegistration] { return func(yield func(CommandRegistration) bool) { registeredCommandsMu.Lock() - defer registeredCommandsMu.Unlock() - for _, fn := range registeredCommands { + snapshot := make([]CommandRegistration, len(registeredCommands)) + copy(snapshot, registeredCommands) + registeredCommandsMu.Unlock() + + for _, fn := range snapshot { if !yield(fn) { return } @@ -125,10 +133,12 @@ func RegisteredCommands() iter.Seq[CommandRegistration] { // Called by Init() after creating the root command. func attachRegisteredCommands(root *cobra.Command) { registeredCommandsMu.Lock() - defer registeredCommandsMu.Unlock() + snapshot := make([]CommandRegistration, len(registeredCommands)) + copy(snapshot, registeredCommands) + commandsAttached = true + registeredCommandsMu.Unlock() - for _, fn := range registeredCommands { + for _, fn := range snapshot { fn(root) } - commandsAttached = true } diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index d1629b6..65a53d5 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -148,6 +148,26 @@ func TestRegisterCommands_Bad(t *testing.T) { require.NoError(t, err) assert.Equal(t, "late", cmd.Use) }) + + t.Run("nested registration during startup does not deadlock", func(t *testing.T) { + resetGlobals(t) + + RegisterCommands(func(root *cobra.Command) { + root.AddCommand(&cobra.Command{Use: "outer", Short: "Outer"}) + RegisterCommands(func(root *cobra.Command) { + root.AddCommand(&cobra.Command{Use: "inner", Short: "Inner"}) + }) + }) + + err := Init(Options{AppName: "test"}) + require.NoError(t, err) + + for _, name := range []string{"outer", "inner"} { + cmd, _, err := RootCmd().Find([]string{name}) + require.NoError(t, err) + assert.Equal(t, name, cmd.Use) + } + }) } // TestLocaleLoading_Good verifies locale files become available to the active i18n service.