Skip to content

Commit 2c5aa77

Browse files
authored
Preserve stderr color output when redirecting stdout (#65)
1 parent 153990f commit 2c5aa77

File tree

5 files changed

+61
-16
lines changed

5 files changed

+61
-16
lines changed

internal/tiger/cmd/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ func outputTable(w io.Writer, cfg *config.ConfigOutput) error {
206206
if cfg.GatewayURL != nil {
207207
table.Append("gateway_url", *cfg.GatewayURL)
208208
}
209+
if cfg.Color != nil {
210+
table.Append("color", fmt.Sprintf("%t", *cfg.Color))
211+
}
209212
if cfg.Output != nil {
210213
table.Append("output", *cfg.Output)
211214
}

internal/tiger/cmd/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
143143
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
144144
"project_id": "json-project",
145145
"service_id": "",
146+
"color": true,
146147
"output": "json",
147148
"analytics": true,
148149
"password_storage": "none",
@@ -203,6 +204,7 @@ version_check_last_time: ` + now.Format(time.RFC3339) + "\n"
203204
"docs_mcp_url": "https://mcp.tigerdata.com/docs",
204205
"project_id": "yaml-project",
205206
"service_id": "",
207+
"color": true,
206208
"output": "yaml",
207209
"analytics": false,
208210
"password_storage": "keyring",

internal/tiger/cmd/root.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66

7+
"github.com/fatih/color"
78
"github.com/spf13/cobra"
89
"github.com/spf13/viper"
910
"go.uber.org/zap"
@@ -21,6 +22,7 @@ func buildRootCmd() *cobra.Command {
2122
var analytics bool
2223
var passwordStorage string
2324
var skipUpdateCheck bool
25+
var colorFlag bool
2426

2527
cmd := &cobra.Command{
2628
Use: "tiger",
@@ -50,6 +52,10 @@ tiger auth login
5052
zap.Bool("debug", cfg.Debug),
5153
)
5254

55+
if !cfg.Color {
56+
color.NoColor = true
57+
}
58+
5359
return nil
5460
},
5561
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
@@ -61,7 +67,7 @@ tiger auth login
6167
// Skip update check if:
6268
// 1. --skip-update-check flag was provided
6369
// 2. Running "version --check" (version command handles its own check)
64-
isVersionCheck := cmd.Name() == "version" && cmd.Flags().Changed("check")
70+
isVersionCheck := cmd.Name() == "version" && cmd.Flag("check").Changed
6571
if !skipUpdateCheck && !isVersionCheck {
6672
output := cmd.ErrOrStderr()
6773
result := version.PerformCheck(cfg, &output, false)
@@ -92,13 +98,15 @@ tiger auth login
9298
cmd.PersistentFlags().BoolVar(&analytics, "analytics", true, "enable/disable usage analytics")
9399
cmd.PersistentFlags().StringVar(&passwordStorage, "password-storage", config.DefaultPasswordStorage, "password storage method (keyring, pgpass, none)")
94100
cmd.PersistentFlags().BoolVar(&skipUpdateCheck, "skip-update-check", false, "skip checking for updates on startup")
101+
cmd.PersistentFlags().BoolVar(&colorFlag, "color", true, "enable colored output")
95102

96103
// Bind flags to viper
97104
viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug"))
98105
viper.BindPFlag("project_id", cmd.PersistentFlags().Lookup("project-id"))
99106
viper.BindPFlag("service_id", cmd.PersistentFlags().Lookup("service-id"))
100107
viper.BindPFlag("analytics", cmd.PersistentFlags().Lookup("analytics"))
101108
viper.BindPFlag("password_storage", cmd.PersistentFlags().Lookup("password-storage"))
109+
viper.BindPFlag("color", cmd.PersistentFlags().Lookup("color"))
102110

103111
// Note: api_url is intentionally not exposed as a CLI flag.
104112
// It can be configured via:

internal/tiger/config/config.go

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
type Config struct {
1919
APIURL string `mapstructure:"api_url" yaml:"api_url"`
2020
Analytics bool `mapstructure:"analytics" yaml:"analytics"`
21+
Color bool `mapstructure:"color" yaml:"color"`
2122
ConfigDir string `mapstructure:"config_dir" yaml:"-"`
2223
ConsoleURL string `mapstructure:"console_url" yaml:"console_url"`
2324
Debug bool `mapstructure:"debug" yaml:"debug"`
@@ -31,11 +32,13 @@ type Config struct {
3132
ServiceID string `mapstructure:"service_id" yaml:"service_id"`
3233
VersionCheckInterval time.Duration `mapstructure:"version_check_interval" yaml:"version_check_interval"`
3334
VersionCheckLastTime time.Time `mapstructure:"version_check_last_time" yaml:"version_check_last_time"`
35+
viper *viper.Viper `mapstructure:"-" yaml:"-"`
3436
}
3537

3638
type ConfigOutput struct {
3739
APIURL *string `mapstructure:"api_url" json:"api_url,omitempty" yaml:"api_url,omitempty"`
3840
Analytics *bool `mapstructure:"analytics" json:"analytics,omitempty" yaml:"analytics,omitempty"`
41+
Color *bool `mapstructure:"color" json:"color,omitempty" yaml:"color,omitempty"`
3942
ConfigDir *string `mapstructure:"config_dir" json:"config_dir,omitempty" yaml:"config_dir,omitempty"`
4043
ConsoleURL *string `mapstructure:"console_url" json:"console_url,omitempty" yaml:"console_url,omitempty"`
4144
Debug *bool `mapstructure:"debug" json:"debug,omitempty" yaml:"debug,omitempty"`
@@ -52,33 +55,35 @@ type ConfigOutput struct {
5255
}
5356

5457
const (
58+
ConfigFileName = "config.yaml"
5559
DefaultAPIURL = "https://console.cloud.timescale.com/public/api/v1"
60+
DefaultAnalytics = true
61+
DefaultColor = true
5662
DefaultConsoleURL = "https://console.cloud.timescale.com"
57-
DefaultGatewayURL = "https://console.cloud.timescale.com/api"
63+
DefaultDebug = false
5864
DefaultDocsMCP = true
5965
DefaultDocsMCPURL = "https://mcp.tigerdata.com/docs"
66+
DefaultGatewayURL = "https://console.cloud.timescale.com/api"
6067
DefaultOutput = "table"
61-
DefaultAnalytics = true
6268
DefaultPasswordStorage = "keyring"
63-
DefaultDebug = false
6469
DefaultReleasesURL = "https://cli.tigerdata.com"
6570
DefaultVersionCheckInterval = 24 * time.Hour
66-
ConfigFileName = "config.yaml"
6771
)
6872

6973
var defaultValues = map[string]any{
74+
"analytics": DefaultAnalytics,
7075
"api_url": DefaultAPIURL,
76+
"color": DefaultColor,
7177
"console_url": DefaultConsoleURL,
72-
"gateway_url": DefaultGatewayURL,
78+
"debug": DefaultDebug,
7379
"docs_mcp": DefaultDocsMCP,
7480
"docs_mcp_url": DefaultDocsMCPURL,
75-
"project_id": "",
76-
"service_id": "",
81+
"gateway_url": DefaultGatewayURL,
7782
"output": DefaultOutput,
78-
"analytics": DefaultAnalytics,
7983
"password_storage": DefaultPasswordStorage,
80-
"debug": DefaultDebug,
84+
"project_id": "",
8185
"releases_url": DefaultReleasesURL,
86+
"service_id": "",
8287
"version_check_interval": DefaultVersionCheckInterval,
8388
"version_check_last_time": time.Time{},
8489
}
@@ -125,6 +130,7 @@ func SetupViper(configDir string) error {
125130
func FromViper(v *viper.Viper) (*Config, error) {
126131
cfg := &Config{
127132
ConfigDir: filepath.Dir(v.ConfigFileUsed()),
133+
viper: v,
128134
}
129135

130136
if err := v.Unmarshal(cfg); err != nil {
@@ -213,7 +219,7 @@ func UseTestConfig(configDir string, values map[string]any) (*Config, error) {
213219

214220
func (c *Config) Set(key, value string) error {
215221
// Validate and update the field
216-
validated, err := c.updateField(key, value)
222+
validated, err := c.UpdateField(key, value)
217223
if err != nil {
218224
return err
219225
}
@@ -244,10 +250,10 @@ func setBool(key, val string) (bool, error) {
244250
return b, nil
245251
}
246252

247-
// updateField updates the field in the Config struct corresponding to the given key.
253+
// UpdateField updates the field in the Config struct corresponding to the given key.
248254
// It accepts either a string (from user input) or a typed value (string/bool from defaults).
249255
// The function validates the value and updates both the struct field and viper state.
250-
func (c *Config) updateField(key string, value any) (any, error) {
256+
func (c *Config) UpdateField(key string, value any) (any, error) {
251257
var validated any
252258

253259
switch key {
@@ -315,6 +321,22 @@ func (c *Config) updateField(key string, value any) (any, error) {
315321
c.ServiceID = s
316322
validated = s
317323

324+
case "color":
325+
switch v := value.(type) {
326+
case bool:
327+
c.Color = v
328+
validated = v
329+
case string:
330+
b, err := setBool("color", v)
331+
if err != nil {
332+
return nil, err
333+
}
334+
c.Color = b
335+
validated = b
336+
default:
337+
return nil, fmt.Errorf("color must be string or bool, got %T", value)
338+
}
339+
318340
case "output":
319341
s, ok := value.(string)
320342
if !ok {
@@ -433,7 +455,11 @@ func (c *Config) updateField(key string, value any) (any, error) {
433455
return nil, fmt.Errorf("unknown configuration key: %s", key)
434456
}
435457

436-
viper.Set(key, validated)
458+
if c.viper == nil {
459+
viper.Set(key, validated)
460+
} else {
461+
c.viper.Set(key, validated)
462+
}
437463
return validated, nil
438464
}
439465

@@ -465,7 +491,7 @@ func (c *Config) Unset(key string) error {
465491

466492
// Apply the default to the current global viper state
467493
if def, ok := defaultValues[key]; ok {
468-
if _, err := c.updateField(key, def); err != nil {
494+
if _, err := c.UpdateField(key, def); err != nil {
469495
return err
470496
}
471497
}
@@ -497,7 +523,7 @@ func (c *Config) Reset() error {
497523
if key == "project_id" {
498524
continue
499525
}
500-
if _, err := c.updateField(key, value); err != nil {
526+
if _, err := c.UpdateField(key, value); err != nil {
501527
return err
502528
}
503529
}

internal/tiger/version/check.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ func PrintUpdateWarning(result *CheckResult, cfg *config.Config, output *io.Writ
239239
return
240240
}
241241

242+
// need to set color.NoColor correctly for the `output` (stderr)
243+
if cfg.Color && util.IsTerminal(*output) {
244+
original := color.NoColor
245+
defer func() { color.NoColor = original }()
246+
color.NoColor = false
247+
}
242248
fmt.Fprintf(*output, "\n\n%s %s → %s\nTo upgrade: %s\n",
243249
color.YellowString("A new release of tiger-cli is available:"),
244250
color.CyanString(result.CurrentVersion),

0 commit comments

Comments
 (0)