The shape of a Go CLI's command tree - main.go, the root command for each project type, Execute and the exit path, a command file, the Run body, Run against RunE, and subcommand packages. Use when scaffolding a Go CLI, writing or changing cmd/root.go, adding a command file or a subcommand package, wiring setupLogs and --debug, or deciding whether a command returns an error. Triggers on main.go, cmd/root.go, cobra.Command, rootCmd, Execute, AddCommand, cobra.OnInitialize, setupLogs, CompletionOptions, SetHelpCommand, SilenceErrors, SilenceUsage, Run, RunE, cmd/ packages, and AppVersion ldflags injection. Not for flag conventions, positional arguments, or a flag reading stdin.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
The shape of a Go CLI's command tree - main.go, the root command for each project type, Execute and the exit path, a command file, the Run body, Run against RunE, and subcommand packages. Use when scaffolding a Go CLI, writing or changing cmd/root.go, adding a command file or a subcommand package, wiring setupLogs and --debug, or deciding whether a command returns an error. Triggers on main.go, cmd/root.go, cobra.Command, rootCmd, Execute, AddCommand, cobra.OnInitialize, setupLogs, CompletionOptions, SetHelpCommand, SilenceErrors, SilenceUsage, Run, RunE, cmd/ packages, and AppVersion ldflags injection. Not for flag conventions, positional arguments, or a flag reading stdin.
user-invocable
false
Go CLI Structure
How a Cobra command tree is put together: the entry point, the root, the commands hanging off it, and where a command's work goes.
CLI Only projects and Web Only projects use different roots, and a CLI + Web hybrid uses the CLI Only root plus a serve command.
Aspect
CLI Only
Web Only
Imports
zerolog, utils, subcommand packages
zerolog, cobra, subcommand packages
Global flags
--debug
--debug
Logging setup
setupLogs() via cobra.OnInitialize, terminal check through utils
setupLogs() via cobra.OnInitialize, terminal check inline
Output
utils.Print*
zerolog only
Both set Use, Short, Version from the AppVersion ldflag, and CompletionOptions.HiddenDefaultCmd: true.
Entry Point
package main
import"github.com/[GITHUB_USER]/REPO_NAME/cmd"funcmain() {
cmd.Execute()
}
main.go holds nothing else, so no logic ends up in the one function a test cannot call.
Root Command, CLI Only
package cmd
import (
"os""time""github.com/rs/zerolog""github.com/rs/zerolog/log""github.com/spf13/cobra""github.com/[GITHUB_USER]/REPO_NAME/utils"
featureCmd "github.com/[GITHUB_USER]/REPO_NAME/cmd/feature-cmd"
)
var AppVersion = "dev-build"// set at build time via ldflagsvar debugFlag boolvar rootCmd = &cobra.Command{
Use: "appname",
Short: "Brief description of the application",
Version: AppVersion,
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
}
funcExecute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
funcsetupLogs() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.DateTime, NoColor: !utils.StdoutIsTerminal}
log.Logger = zerolog.New(output).With().Timestamp().Logger()
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if debugFlag {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
utils.GlobalDebugFlag = true
}
}
funcinit() {
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
rootCmd.PersistentFlags().BoolVar(&debugFlag, "debug", false, "Enable debug logging")
cobra.OnInitialize(setupLogs)
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(featureCmd.FeatureCmd)
}
setupLogs runs through cobra.OnInitialize rather than at package init, because the flag values it reads are not parsed until Cobra has matched the command.
Execute sets the exit code and prints nothing. Cobra has already written the error to stderr by the time it returns one (cobra@v1.10.2 command.go:1159-1161), so a wrapper that prints it again shows the user the same line twice.
The help and completion commands are hidden so appname --help lists only the commands the tool actually offers.
A root declaring cobra.Groups needs one line more. The hidden help command still counts toward AllChildCommandsHaveGroup (cobra@v1.10.2 command.go:1376-1383), and leaving it ungrouped renders an empty Additional Commands: header:
An ungrouped root needs nothing extra, since the usage template reaches that header only when len .Groups is non-zero.
Root Command, Web Only
package cmd
import (
"io""os""time""github.com/charmbracelet/x/term""github.com/rs/zerolog""github.com/rs/zerolog/log""github.com/spf13/cobra"
)
var AppVersion = "dev-build"var debugFlag boolvar rootCmd = &cobra.Command{
Use: "appname",
Short: "Brief description of the application",
Version: AppVersion,
CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true},
}
funcExecute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
funcsetupLogs() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
var out io.Writer = os.Stdout
if term.IsTerminal(os.Stdout.Fd()) {
out = zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.DateTime}
}
log.Logger = zerolog.New(out).With().Timestamp().Logger()
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if debugFlag {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
funcinit() {
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
rootCmd.PersistentFlags().BoolVar(&debugFlag, "debug", false, "Enable debug logging")
cobra.OnInitialize(setupLogs)
rootCmd.AddCommand(serveCmd)
}
Same logger and same --debug as CLI Only, without the utils import. What a server has no use for is the printers, since nobody is watching a container's stdout for a styled checkmark.
Simple Command
A command without subcommands is defined directly in cmd/, with its own flag struct and init(). Grouping flags in a struct per command keeps two commands from colliding over a variable named output in the shared cmd package.
// cmd/serve.gopackage cmd
import (
"fmt""github.com/spf13/cobra""github.com/[GITHUB_USER]/REPO_NAME/internal/server"
u "github.com/[GITHUB_USER]/REPO_NAME/utils"
)
var serveFlags struct {
port int
host string
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the web server",
Run: func(cmd *cobra.Command, args []string) {
srv := server.New(serveFlags.host, serveFlags.port)
if err := srv.Setup(); err != nil {
u.PrintFatal("Failed to set up server", err)
}
u.PrintInfo(fmt.Sprintf("Starting server on %s:%d", serveFlags.host, serveFlags.port))
if err := srv.Run(); err != nil {
u.PrintFatal("Server error", err)
}
},
}
funcinit() {
serveCmd.Flags().IntVarP(&serveFlags.port, "port", "p", 8080, "Port to listen on")
serveCmd.Flags().StringVarP(&serveFlags.host, "host", "H", "0.0.0.0", "Host to bind to")
}
The same command in a Web Only project swaps u.PrintFatal(msg, err) for log.Fatal().Err(err).Msg(msg) and u.PrintInfo for log.Info(), and drops the utils import.
Run Function Shape
A Run body validates flags, builds a config struct, calls into internal/, and reports the result. Keeping the work in internal/ leaves the command as the only layer that knows about flags and terminals, which is what lets the logic be tested without one.
Run: func(cmd *cobra.Command, args []string) {
if flags.required == "" {
u.PrintFatal("--required flag is required", nil)
}
cfg := internal.Config{Field1: flags.field1, Field2: flags.field2}
result, err := internal.DoThing(cfg)
if err != nil {
u.PrintFatal("Failed to do thing", err)
}
u.PrintSuccess("Thing completed")
u.PrintGeneric(result)
}
Run with u.PrintFatal is the default. RunE is for a command holding something a deferred function has to release, such as a lock file, a temporary directory, or a half-written output file, since PrintFatal calls os.Exit(1) and skips defers.
Everywhere else RunE costs two things. PrintFatal(msg, err) carries a human label and the wrapped error separately, which is what lets the normal tier show the label alone while --debug shows the chain, and a returned error collapses both into one string. Cobra then prints Error: <err> followed by the entire usage block (cobra@v1.10.2 command.go:1159-1167), burying the message under a wall of flags.
A tree that uses RunE anywhere sets both silences on the root and reports the error itself, which arrives back at PrintFatal:
A group of related subcommands gets a package under cmd/. The parent command is exported and has no Run, so invoking it bare prints help instead of doing something arbitrary; the children are unexported and each carry a Run.
// cmd/feature-cmd/feature.gopackage featureCmd
import (
"fmt""github.com/spf13/cobra""github.com/[GITHUB_USER]/REPO_NAME/internal/feature"
u "github.com/[GITHUB_USER]/REPO_NAME/utils"
)
var createFlags struct {
name string
config string
}
var FeatureCmd = &cobra.Command{
Use: "feature",
Short: "Feature management commands",
}
var createCmd = &cobra.Command{
Use: "create",
Short: "Create a new feature",
Run: func(cmd *cobra.Command, args []string) {
cfg := feature.CreateConfig{Name: createFlags.name, Config: createFlags.config}
if err := feature.Create(cfg); err != nil {
u.PrintFatal("Failed to create feature", err)
}
u.PrintSuccess(fmt.Sprintf("Created feature: %s", createFlags.name))
},
}
funcinit() {
FeatureCmd.AddCommand(createCmd)
createCmd.Flags().StringVarP(&createFlags.name, "name", "n", "", "Feature name (required)")
createCmd.MarkFlagRequired("name")
createCmd.Flags().StringVarP(&createFlags.config, "config", "c", "", "Config file path")
}