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" {