From e6546b2ceaa08ff9ece54a399b30fcf491df911f Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Feb 2026 13:36:28 +0000 Subject: [PATCH] cmd/headscale/cli: silence cobra error/usage output and centralise error formatting Set SilenceErrors and SilenceUsage on the root command so that cobra never prints usage text for runtime errors. A SetFlagErrorFunc callback re-enables usage output specifically for flag-parsing errors (the kubectl pattern). Add printError to utils.go and switch Execute() to ExecuteC() so the returned error is formatted as JSON/YAML when --output requests machine-readable output. --- cmd/headscale/cli/root.go | 16 +++++++++++++--- cmd/headscale/cli/utils.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index 7f84fb8a..69c135e3 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -1,7 +1,6 @@ package cli import ( - "fmt" "os" "runtime" "slices" @@ -39,6 +38,14 @@ func init() { StringP("output", "o", "", "Output format. Empty for human-readable, 'json', 'json-line' or 'yaml'") rootCmd.PersistentFlags(). Bool("force", false, "Disable prompts and forces the execution") + + // Re-enable usage output only for flag-parsing errors; runtime errors + // from RunE should never dump usage text. + rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { + cmd.SilenceUsage = false + + return err + }) } func initConfig() { @@ -140,12 +147,15 @@ var rootCmd = &cobra.Command{ headscale is an open source implementation of the Tailscale control server https://github.com/juanfont/headscale`, + SilenceErrors: true, + SilenceUsage: true, } func Execute() { - err := rootCmd.Execute() + cmd, err := rootCmd.ExecuteC() if err != nil { - fmt.Fprintln(os.Stderr, err) + outputFormat, _ := cmd.Flags().GetString("output") + printError(err, outputFormat) os.Exit(1) } } diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 31087569..eef83cc0 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -203,6 +203,34 @@ func ErrorOutput(errResult error, override string, outputFormat string) { os.Exit(1) } +// printError writes err to stderr, formatting it as JSON/YAML when the +// --output flag requests machine-readable output. Used exclusively by +// Execute() so that every error surfaces in the format the caller asked for. +func printError(err error, outputFormat string) { + type errOutput struct { + Error string `json:"error"` + } + + e := errOutput{Error: err.Error()} + + var formatted []byte + + switch outputFormat { + case "json": + formatted, _ = json.MarshalIndent(e, "", "\t") //nolint:errchkjson // errOutput contains only a string field + case "json-line": + formatted, _ = json.Marshal(e) //nolint:errchkjson // errOutput contains only a string field + case "yaml": + formatted, _ = yaml.Marshal(e) + default: + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + + return + } + + fmt.Fprintf(os.Stderr, "%s\n", formatted) +} + func HasMachineOutputFlag() bool { for _, arg := range os.Args { if arg == "json" || arg == "json-line" || arg == "yaml" {