Skip to content

Commit 124f01d

Browse files
authored
feat: Togglable color logging (#594)
* feat: Disable color logging when not a terminal Resolves #462 - Add `NO_COLOR` and `FORCE_COLOR` env vars support - Add TOML `no-color` and `force-color` params - Add `--no-color` and `--force-color` cmdline flags
1 parent 6292f39 commit 124f01d

File tree

16 files changed

+718
-263
lines changed

16 files changed

+718
-263
lines changed

Makefile

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
EXE := tfswitch
2-
PKG := github.com/warrensbox/terraform-switcher
3-
BUILDPATH := build
4-
PATH := $(BUILDPATH):$(PATH)
5-
VER ?= $(shell git ls-remote --tags --sort=version:refname [email protected]:warrensbox/terraform-switcher.git | awk '{if ($$2 ~ "\\^\\{\\}$$") next; print vers[split($$2,vers,"\\/")]}' | tail -1)
1+
EXE := tfswitch
2+
PKG := github.com/warrensbox/terraform-switcher
3+
BUILDPATH := build
4+
PATH := $(BUILDPATH):$(PATH)
5+
VER ?= $(shell git ls-remote --tags --sort=version:refname [email protected]:warrensbox/terraform-switcher.git | awk '{if ($$2 ~ "\\^\\{\\}$$") next; print vers[split($$2,vers,"\\/")]}' | tail -1)
66
# Managing Go installations: Installing multiple Go versions
77
# https://go.dev/doc/manage-install
8-
GOBINARY ?= $(shell (egrep -m1 '^go[[:space:]]+[[:digit:]]+\.' go.mod | tr -d '[:space:]' | xargs which) || echo go)
9-
GOOS ?= $(shell $(GOBINARY) env GOOS)
10-
GOARCH ?= $(shell $(GOBINARY) env GOARCH)
8+
GOBINARY ?= $(shell (egrep -m1 '^go[[:space:]]+[[:digit:]]+\.' go.mod | tr -d '[:space:]' | xargs which) || echo go)
9+
GOOS ?= $(shell $(GOBINARY) env GOOS)
10+
GOARCH ?= $(shell $(GOBINARY) env GOARCH)
11+
CONTAINER_ENGINE ?= $(shell command -v podman || command -v docker || echo "NONE")
1112

1213
$(EXE): version go.mod *.go lib/*.go
1314
mkdir -p "$(BUILDPATH)/"
@@ -54,11 +55,46 @@ install: $(EXE)
5455
mkdir -p ~/bin
5556
mv "$(BUILDPATH)/$(EXE)" ~/bin/
5657

57-
.PHONY: docs
58-
docs:
59-
@#cd docs; bundle install --path vendor/bundler; bundle exec jekyll build -c _config.yml; cd ..
58+
.PHONY: docs-build
59+
docs-build:
60+
cd www && mkdocs build
61+
62+
.PHONY: docs-deploy
63+
docs-deploy:
6064
cd www && mkdocs gh-deploy --force
6165

66+
.PHONY: docs-serve
67+
docs-serve:
68+
cd www && mkdocs serve
69+
70+
.PHONY: docs-serve-public
71+
docs-serve-public:
72+
cd www && mkdocs serve --dev-addr 0.0.0.0:8000
73+
6274
.PHONY: goreleaser-release-snapshot
6375
goreleaser-release-snapshot:
6476
RELEASE_VERSION=$(VER) goreleaser release --config ./.goreleaser.yml --snapshot --clean
77+
78+
.PHONY: lint
79+
lint: super-linter
80+
81+
.PHONY: super-linter
82+
super-linter:
83+
ifeq ($(CONTAINER_ENGINE),NONE)
84+
$(error "No container engine found. Please install Podman or Docker.")
85+
else
86+
# Keep `--env' vars below the `VALIDATE_ALL_CODEBASE' in sync with .github/workflows/super-linter.yml
87+
$(CONTAINER_ENGINE) run \
88+
--name super-linter \
89+
--volume "$(shell git rev-parse --show-toplevel):$(shell git rev-parse --show-toplevel)" \
90+
--volume "$(shell git rev-parse --git-common-dir):$(shell git rev-parse --git-common-dir)" \
91+
--workdir "$(shell git rev-parse --show-toplevel)" \
92+
--env GITHUB_WORKSPACE="$(shell git rev-parse --show-toplevel)" \
93+
--env RUN_LOCAL=true \
94+
--env VALIDATE_ALL_CODEBASE=false \
95+
--env FILTER_REGEX_EXCLUDE='^$$PWD/test-data/' \
96+
--env BASH_EXEC_IGNORE_LIBRARIES=true \
97+
--env VALIDATE_GO=false \
98+
--env VALIDATE_JSCPD=false \
99+
--rm ghcr.io/super-linter/super-linter:slim-latest
100+
endif

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/hashicorp/hcl/v2 v2.23.0
1313
github.com/hashicorp/terraform-config-inspect v0.0.0-20250401063509-d2d12f9a63bb
1414
github.com/manifoldco/promptui v0.9.0
15+
github.com/mattn/go-isatty v0.0.20
1516
github.com/mitchellh/go-homedir v1.1.0
1617
github.com/pborman/getopt v1.1.0
1718
github.com/spf13/viper v1.20.1

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
5555
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
5656
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
5757
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
58+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
59+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
5860
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
5961
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
6062
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
@@ -103,6 +105,7 @@ golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
103105
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
104106
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
105107
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
108+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
106109
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
107110
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
108111
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=

lib/command_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestNewCommand(t *testing.T) {
1515
if reflect.ValueOf(cmd).Kind() == reflect.Ptr {
1616
t.Logf("Value returned is a pointer %v [expected]", cmd)
1717
} else {
18-
t.Errorf("Value returned is not a pointer %v [expected", cmd)
18+
t.Errorf("Value returned is not a pointer %v [unexpected]", cmd)
1919
}
2020
}
2121

lib/logging.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/gookit/color"
77
"github.com/gookit/slog"
88
"github.com/gookit/slog/handler"
9+
"github.com/mattn/go-isatty"
910
)
1011

1112
var (
@@ -19,14 +20,25 @@ var (
1920
TraceLogging = slog.Levels{slog.PanicLevel, slog.FatalLevel, slog.ErrorLevel, slog.WarnLevel, slog.InfoLevel, slog.NoticeLevel, slog.DebugLevel, slog.TraceLevel}
2021
)
2122

23+
func isColorLogging() bool {
24+
if os.Getenv("NO_COLOR") != "" {
25+
return false
26+
} else if color.SupportColor() {
27+
if os.Getenv("FORCE_COLOR") == "" {
28+
return isatty.IsTerminal(os.Stdout.Fd())
29+
}
30+
return true
31+
}
32+
return false
33+
}
34+
2235
func NewStderrConsoleWithLF(lf slog.LevelFormattable) *handler.ConsoleHandler {
2336
h := handler.NewIOWriterWithLF(os.Stderr, lf)
2437

2538
// default use text formatter
2639
f := slog.NewTextFormatter()
2740
// default enable color on console
28-
f.WithEnableColor(color.SupportColor())
29-
41+
f.WithEnableColor(isColorLogging())
3042
h.SetFormatter(f)
3143
return h
3244
}
@@ -37,7 +49,7 @@ func NewStderrConsoleHandler(levels []slog.Level) *handler.ConsoleHandler {
3749

3850
func InitLogger(logLevel string) *slog.Logger {
3951
formatter := slog.NewTextFormatter()
40-
formatter.EnableColor = true
52+
formatter.EnableColor = isColorLogging()
4153
formatter.ColorTheme = slog.ColorTheme
4254
formatter.TimeFormat = "15:04:05.000"
4355

lib/param_parsing/environment.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ func GetParamsFromEnvironment(params Params) Params {
1212
description := envVar.description
1313
env := envVar.env
1414
param := envVar.param
15+
ptype := envVar.ptype
1516
toml := envVar.toml
1617

1718
if len(env) == 0 {
@@ -27,17 +28,32 @@ func GetParamsFromEnvironment(params Params) Params {
2728
}
2829

2930
paramKey := reflect.Indirect(reflectedParams).FieldByName(param)
30-
if paramKey.Kind() != reflect.String {
31-
logger.Warnf("Parameter %q is not a string, skipping assignment from environment variable %q", param, env)
31+
if !paramKey.CanSet() {
32+
logger.Warnf("Parameter %q cannot be set, skipping assignment from environment variable %q", param, env)
3233
continue
3334
}
3435

35-
if envVarValue := os.Getenv(env); envVarValue != "" {
36-
logger.Debugf("%s (%q) from environment variable %q: %q", description, toml, env, envVarValue)
37-
if !paramKey.CanSet() {
38-
logger.Warnf("Parameter %q cannot be set, skipping assignment from environment variable %q", param, env)
39-
}
36+
envVarValue := os.Getenv(env)
37+
if envVarValue == "" {
38+
logger.Tracef("Environment variable %q value is empty for %q parameter, skipping assignment", env, toml)
39+
continue
40+
}
41+
42+
logger.Debugf("%s (%q) from environment variable %q: %q", description, toml, env, envVarValue)
43+
44+
switch ptype {
45+
case reflect.String:
4046
paramKey.SetString(envVarValue)
47+
case reflect.Bool:
48+
// Inherit `gookit/color` lib's behavior: whatever the value is, set it to true
49+
// E.g. NO_COLOR: https://github.com/gookit/color/blob/master/color.go#L49
50+
paramKey.SetBool(true)
51+
default:
52+
logger.Errorf(
53+
"Internal error: unhandled switch case for \"%T\" type of %q parameter (env var %q)",
54+
ptype, param, toml,
55+
)
56+
continue
4157
}
4258
}
4359
return params

lib/param_parsing/environment_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ package param_parsing
33

44
import (
55
"os"
6+
"os/exec"
7+
"regexp"
8+
"strings"
69
"testing"
710

11+
"github.com/gookit/color"
812
"github.com/warrensbox/terraform-switcher/lib"
913
)
1014

@@ -97,3 +101,81 @@ func TestGetParamsFromEnvironment_log_level_from_env(t *testing.T) {
97101
t.Errorf("Determined log level is not matching. Got %q, expected %q", params.LogLevel, expected)
98102
}
99103
}
104+
105+
func TestNoColorEnvVar(t *testing.T) {
106+
envVarName := "NO_COLOR"
107+
_ = os.Setenv(envVarName, "true")
108+
goCommandArgs := []string{"run", "../../main.go", "--dry-run", "1.10.5"}
109+
110+
t.Logf("Testing %q env var", envVarName)
111+
112+
out, err := exec.Command("go", goCommandArgs...).CombinedOutput()
113+
_ = os.Unsetenv(envVarName)
114+
if err != nil {
115+
t.Fatalf("Unexpected failure: \"%v\", output: %q", err, string(out))
116+
}
117+
118+
matched, err := regexp.MatchString(ansiCodesRegex, string(out))
119+
if err != nil {
120+
t.Fatalf("Unexpected failure: \"%v\", output: %q", err, string(out))
121+
}
122+
123+
if matched {
124+
t.Errorf("Expected no ANSI color codes in output, but found some: %q", string(out))
125+
} else {
126+
t.Log("Success: no ANSI color codes in output")
127+
}
128+
}
129+
130+
func TestForceColorEnvVar(t *testing.T) {
131+
envVarName := "FORCE_COLOR"
132+
if !color.SupportColor() {
133+
t.Skipf("Skipping test for %q env var as terminal doesn't support colors", envVarName)
134+
}
135+
136+
_ = os.Setenv(envVarName, "true")
137+
goCommandArgs := []string{"run", "../../main.go", "--dry-run", "1.10.5"}
138+
139+
t.Logf("Testing %q env var", envVarName)
140+
141+
out, err := exec.Command("go", goCommandArgs...).CombinedOutput()
142+
_ = os.Unsetenv(envVarName)
143+
if err != nil {
144+
t.Fatalf("Unexpected failure: \"%v\", output: %q", err, string(out))
145+
}
146+
147+
matched, err := regexp.MatchString(ansiCodesRegex, string(out))
148+
if err != nil {
149+
t.Fatalf("Unexpected failure: \"%v\", output: %q", err, string(out))
150+
}
151+
152+
if !matched {
153+
t.Errorf("Expected ANSI color codes in output, but found none: %q", string(out))
154+
} else {
155+
t.Log("Success: found ANSI color codes in output")
156+
}
157+
}
158+
159+
func TestNoAndForceColorEnvVars(t *testing.T) {
160+
envVarNameForceColor := "FORCE_COLOR"
161+
_ = os.Setenv(envVarNameForceColor, "true")
162+
envVarNameNoColor := "NO_COLOR"
163+
_ = os.Setenv(envVarNameNoColor, "true")
164+
165+
expectedOutput := "FATAL (env) Cannot force color and disable color at the same time. Please choose either of them."
166+
167+
goCommandArgs := []string{"run", "../../main.go", "--dry-run", "1.10.5"}
168+
169+
t.Logf("Testing %q and %q env vars both present", envVarNameForceColor, envVarNameNoColor)
170+
171+
out, _ := exec.Command("go", goCommandArgs...).CombinedOutput() // nolint:errcheck // We want to test the output even if it fails
172+
173+
_ = os.Unsetenv(envVarNameForceColor)
174+
_ = os.Unsetenv(envVarNameNoColor)
175+
176+
if !strings.Contains(string(out), expectedOutput) {
177+
t.Errorf("Expected %q, got: %q", expectedOutput, out)
178+
} else {
179+
t.Logf("Success: %q", expectedOutput)
180+
}
181+
}

0 commit comments

Comments
 (0)