@marcel91200 / caddy Mirror · read-only / commits / e633d013
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 23:30 UTC

cmd: fix `Commands` function not returning all registered commands (#7059)

Herman Slatman committed Jun 13, 2025 at 01:17 UTC e633d013f64d057f462786ccf4a430cd27817d4d
2 files changed +85 -21
cmd/commands.go
+46 -21
@@ -20,6 +20,7 @@ import (
20 "os"
21 "regexp"
22 "strings"
23 + "sync"
24
25 "github.com/spf13/cobra"
26 "github.com/spf13/cobra/doc"
@@ -80,10 +81,16 @@ type CommandFunc func(Flags) (int, error)
81 // Commands returns a list of commands initialised by
82 // RegisterCommand
83 func Commands() map[string]Command {
84 + commandsMu.RLock()
85 + defer commandsMu.RUnlock()
86 +
87 return commands
88 }
89
86 -var commands = make(map[string]Command)
90 +var (
91 + commandsMu sync.RWMutex
92 + commands = make(map[string]Command)
93 +)
94
95 func init() {
96 RegisterCommand(Command{
@@ -441,7 +448,7 @@ EXPERIMENTAL: May be changed or removed.
448 })
449
450 defaultFactory.Use(func(rootCmd *cobra.Command) {
444 - rootCmd.AddCommand(caddyCmdToCobra(Command{
451 + manpageCommand := Command{
452 Name: "manpage",
453 Usage: "--directory <path>",
454 Short: "Generates the manual pages for Caddy commands",
@@ -471,11 +478,12 @@ argument of --directory. If the directory does not exist, it will be created.
478 return caddy.ExitCodeSuccess, nil
479 })
480 },
474 - }))
481 + }
482
483 // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
477 - rootCmd.AddCommand(&cobra.Command{
478 - Use: "completion [bash|zsh|fish|powershell]",
484 + completionCommand := Command{
485 + Name: "completion",
486 + Usage: "[bash|zsh|fish|powershell]",
487 Short: "Generate completion script",
488 Long: fmt.Sprintf(`To load completions:
489
@@ -516,24 +524,37 @@ argument of --directory. If the directory does not exist, it will be created.
524 PS> %[1]s completion powershell > %[1]s.ps1
525 # and source this file from your PowerShell profile.
526 `, rootCmd.Root().Name()),
519 - DisableFlagsInUseLine: true,
520 - ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
521 - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
522 - RunE: func(cmd *cobra.Command, args []string) error {
523 - switch args[0] {
524 - case "bash":
525 - return cmd.Root().GenBashCompletion(os.Stdout)
526 - case "zsh":
527 - return cmd.Root().GenZshCompletion(os.Stdout)
528 - case "fish":
529 - return cmd.Root().GenFishCompletion(os.Stdout, true)
530 - case "powershell":
531 - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
532 - default:
533 - return fmt.Errorf("unrecognized shell: %s", args[0])
527 + CobraFunc: func(cmd *cobra.Command) {
528 + cmd.DisableFlagsInUseLine = true
529 + cmd.ValidArgs = []string{"bash", "zsh", "fish", "powershell"}
530 + cmd.Args = cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)
531 + cmd.RunE = func(cmd *cobra.Command, args []string) error {
532 + switch args[0] {
533 + case "bash":
534 + return cmd.Root().GenBashCompletion(os.Stdout)
535 + case "zsh":
536 + return cmd.Root().GenZshCompletion(os.Stdout)
537 + case "fish":
538 + return cmd.Root().GenFishCompletion(os.Stdout, true)
539 + case "powershell":
540 + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
541 + default:
542 + return fmt.Errorf("unrecognized shell: %s", args[0])
543 + }
544 }
545 },
536 - })
546 + }
547 +
548 + rootCmd.AddCommand(caddyCmdToCobra(manpageCommand))
549 + rootCmd.AddCommand(caddyCmdToCobra(completionCommand))
550 +
551 + // add manpage and completion commands to the map of
552 + // available commands, because they're not registered
553 + // through RegisterCommand.
554 + commandsMu.Lock()
555 + commands[manpageCommand.Name] = manpageCommand
556 + commands[completionCommand.Name] = completionCommand
557 + commandsMu.Unlock()
558 })
559 }
560
@@ -552,6 +573,9 @@ argument of --directory. If the directory does not exist, it will be created.
573 //
574 // This function should be used in init().
575 func RegisterCommand(cmd Command) {
576 + commandsMu.Lock()
577 + defer commandsMu.Unlock()
578 +
579 if cmd.Name == "" {
580 panic("command name is required")
581 }
@@ -570,6 +594,7 @@ func RegisterCommand(cmd Command) {
594 defaultFactory.Use(func(rootCmd *cobra.Command) {
595 rootCmd.AddCommand(caddyCmdToCobra(cmd))
596 })
597 + commands[cmd.Name] = cmd
598 }
599
600 var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
cmd/commands_test.go new
+39
@@ -0,0 +1,39 @@
1 +package caddycmd
2 +
3 +import (
4 + "maps"
5 + "reflect"
6 + "slices"
7 + "testing"
8 +)
9 +
10 +func TestCommandsAreAvailable(t *testing.T) {
11 + // trigger init, and build the default factory, so that
12 + // all commands from this package are available
13 + cmd := defaultFactory.Build()
14 + if cmd == nil {
15 + t.Fatal("default factory failed to build")
16 + }
17 +
18 + // check that the default factory has 17 commands; it doesn't
19 + // include the commands registered through calls to init in
20 + // other packages
21 + cmds := Commands()
22 + if len(cmds) != 17 {
23 + t.Errorf("expected 17 commands, got %d", len(cmds))
24 + }
25 +
26 + commandNames := slices.Collect(maps.Keys(cmds))
27 + slices.Sort(commandNames)
28 +
29 + expectedCommandNames := []string{
30 + "adapt", "add-package", "build-info", "completion",
31 + "environ", "fmt", "list-modules", "manpage",
32 + "reload", "remove-package", "run", "start",
33 + "stop", "storage", "upgrade", "validate", "version",
34 + }
35 +
36 + if !reflect.DeepEqual(expectedCommandNames, commandNames) {
37 + t.Errorf("expected %v, got %v", expectedCommandNames, commandNames)
38 + }
39 +}